Unverified Commit 42eb9032 authored by Chris Yang's avatar Chris Yang Committed by GitHub

[flutter_tools] iOS: display name defaults to the Title Case of flutter project name. (#91529)

parent a6e91c36
...@@ -69,7 +69,7 @@ Directory getRepoDirectory(Directory buildDirectory) { ...@@ -69,7 +69,7 @@ Directory getRepoDirectory(Directory buildDirectory) {
String _taskFor(String prefix, BuildInfo buildInfo) { String _taskFor(String prefix, BuildInfo buildInfo) {
final String buildType = camelCase(buildInfo.modeName); final String buildType = camelCase(buildInfo.modeName);
final String productFlavor = buildInfo.flavor ?? ''; final String productFlavor = buildInfo.flavor ?? '';
return '$prefix${toTitleCase(productFlavor)}${toTitleCase(buildType)}'; return '$prefix${sentenceCase(productFlavor)}${sentenceCase(buildType)}';
} }
/// Returns the task to build an APK. /// Returns the task to build an APK.
...@@ -592,7 +592,7 @@ class AndroidGradleBuilder implements AndroidBuilder { ...@@ -592,7 +592,7 @@ class AndroidGradleBuilder implements AndroidBuilder {
command.addAll(androidBuildInfo.buildInfo.toGradleConfig()); command.addAll(androidBuildInfo.buildInfo.toGradleConfig());
if (buildInfo.dartObfuscation && buildInfo.mode != BuildMode.release) { if (buildInfo.dartObfuscation && buildInfo.mode != BuildMode.release) {
_logger.printStatus( _logger.printStatus(
'Dart obfuscation is not supported in ${toTitleCase(buildInfo.friendlyModeName)}' 'Dart obfuscation is not supported in ${sentenceCase(buildInfo.friendlyModeName)}'
' mode, building as un-obfuscated.', ' mode, building as un-obfuscated.',
); );
} }
......
...@@ -34,11 +34,20 @@ String snakeCase(String str, [ String sep = '_' ]) { ...@@ -34,11 +34,20 @@ String snakeCase(String str, [ String sep = '_' ]) {
(Match m) => '${m.start == 0 ? '' : sep}${m[0]!.toLowerCase()}'); (Match m) => '${m.start == 0 ? '' : sep}${m[0]!.toLowerCase()}');
} }
String toTitleCase(String str) { /// Converts `fooBar` to `FooBar`.
///
/// This uses [toBeginningOfSentenceCase](https://pub.dev/documentation/intl/latest/intl/toBeginningOfSentenceCase.html),
/// with the input and return value of non-nullable.
String sentenceCase(String str, [String? locale]) {
if (str.isEmpty) { if (str.isEmpty) {
return str; return str;
} }
return str.substring(0, 1).toUpperCase() + str.substring(1); return toBeginningOfSentenceCase(str, locale)!;
}
/// Converts `foo_bar` to `Foo Bar`.
String snakeCaseToTitleCase(String snakeCaseString) {
return snakeCaseString.split('_').map(camelCase).map(sentenceCase).join(' ');
} }
/// Return the plural of the given word (`cat(s)`). /// Return the plural of the given word (`cat(s)`).
......
...@@ -236,7 +236,7 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand { ...@@ -236,7 +236,7 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand {
throwToolExit('Building for iOS is only supported on macOS.'); throwToolExit('Building for iOS is only supported on macOS.');
} }
if (environmentType == EnvironmentType.simulator && !buildInfo.supportsSimulator) { if (environmentType == EnvironmentType.simulator && !buildInfo.supportsSimulator) {
throwToolExit('${toTitleCase(buildInfo.friendlyModeName)} mode is not supported for simulators.'); throwToolExit('${sentenceCase(buildInfo.friendlyModeName)} mode is not supported for simulators.');
} }
if (configOnly && buildInfo.codeSizeDirectory != null) { if (configOnly && buildInfo.codeSizeDirectory != null) {
throwToolExit('Cannot analyze code size without performing a full build.'); throwToolExit('Cannot analyze code size without performing a full build.');
......
...@@ -179,7 +179,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { ...@@ -179,7 +179,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
for (final BuildInfo buildInfo in buildInfos) { for (final BuildInfo buildInfo in buildInfos) {
final String productBundleIdentifier = await _project.ios.productBundleIdentifier(buildInfo); final String productBundleIdentifier = await _project.ios.productBundleIdentifier(buildInfo);
globals.printStatus('Building frameworks for $productBundleIdentifier in ${getNameForBuildMode(buildInfo.mode)} mode...'); globals.printStatus('Building frameworks for $productBundleIdentifier in ${getNameForBuildMode(buildInfo.mode)} mode...');
final String xcodeBuildConfiguration = toTitleCase(getNameForBuildMode(buildInfo.mode)); final String xcodeBuildConfiguration = sentenceCase(getNameForBuildMode(buildInfo.mode));
final Directory modeDirectory = outputDirectory.childDirectory(xcodeBuildConfiguration); final Directory modeDirectory = outputDirectory.childDirectory(xcodeBuildConfiguration);
if (modeDirectory.existsSync()) { if (modeDirectory.existsSync()) {
...@@ -446,7 +446,7 @@ end ...@@ -446,7 +446,7 @@ end
} }
// Always build debug for simulator. // Always build debug for simulator.
final String simulatorConfiguration = toTitleCase(getNameForBuildMode(BuildMode.debug)); final String simulatorConfiguration = sentenceCase(getNameForBuildMode(BuildMode.debug));
pluginsBuildCommand = <String>[ pluginsBuildCommand = <String>[
...globals.xcode.xcrunCommand(), ...globals.xcode.xcrunCommand(),
'xcodebuild', 'xcodebuild',
......
...@@ -10,6 +10,7 @@ import '../base/context.dart'; ...@@ -10,6 +10,7 @@ import '../base/context.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/net.dart'; import '../base/net.dart';
import '../base/terminal.dart'; import '../base/terminal.dart';
import '../base/utils.dart';
import '../convert.dart'; import '../convert.dart';
import '../dart/pub.dart'; import '../dart/pub.dart';
import '../features.dart'; import '../features.dart';
...@@ -247,9 +248,13 @@ class CreateCommand extends CreateBase { ...@@ -247,9 +248,13 @@ class CreateCommand extends CreateBase {
); );
} }
// The dart project_name is in snake_case, this variable is the Title Case of the Project Name.
final String titleCaseProjectName = snakeCaseToTitleCase(projectName);
final Map<String, Object> templateContext = createTemplateContext( final Map<String, Object> templateContext = createTemplateContext(
organization: organization, organization: organization,
projectName: projectName, projectName: projectName,
titleCaseProjectName: titleCaseProjectName,
projectDescription: stringArg('description'), projectDescription: stringArg('description'),
flutterRoot: flutterRoot, flutterRoot: flutterRoot,
withPluginHook: generatePlugin, withPluginHook: generatePlugin,
......
...@@ -317,6 +317,7 @@ abstract class CreateBase extends FlutterCommand { ...@@ -317,6 +317,7 @@ abstract class CreateBase extends FlutterCommand {
throwToolExit(error); throwToolExit(error);
} }
} }
assert(projectName != null);
return projectName; return projectName;
} }
...@@ -325,6 +326,7 @@ abstract class CreateBase extends FlutterCommand { ...@@ -325,6 +326,7 @@ abstract class CreateBase extends FlutterCommand {
Map<String, Object> createTemplateContext({ Map<String, Object> createTemplateContext({
String organization, String organization,
String projectName, String projectName,
String titleCaseProjectName,
String projectDescription, String projectDescription,
String androidLanguage, String androidLanguage,
String iosDevelopmentTeam, String iosDevelopmentTeam,
...@@ -361,6 +363,7 @@ abstract class CreateBase extends FlutterCommand { ...@@ -361,6 +363,7 @@ abstract class CreateBase extends FlutterCommand {
return <String, Object>{ return <String, Object>{
'organization': organization, 'organization': organization,
'projectName': projectName, 'projectName': projectName,
'titleCaseProjectName': titleCaseProjectName,
'androidIdentifier': androidIdentifier, 'androidIdentifier': androidIdentifier,
'iosIdentifier': appleIdentifier, 'iosIdentifier': appleIdentifier,
'macosIdentifier': appleIdentifier, 'macosIdentifier': appleIdentifier,
......
...@@ -462,7 +462,7 @@ class AppDomain extends Domain { ...@@ -462,7 +462,7 @@ class AppDomain extends Domain {
}) async { }) async {
if (!await device.supportsRuntimeMode(options.buildInfo.mode)) { if (!await device.supportsRuntimeMode(options.buildInfo.mode)) {
throw Exception( throw Exception(
'${toTitleCase(options.buildInfo.friendlyModeName)} ' '${sentenceCase(options.buildInfo.friendlyModeName)} '
'mode is not supported for ${device.name}.', 'mode is not supported for ${device.name}.',
); );
} }
......
...@@ -587,7 +587,7 @@ class RunCommand extends RunCommandBase { ...@@ -587,7 +587,7 @@ class RunCommand extends RunCommandBase {
for (final Device device in devices) { for (final Device device in devices) {
if (!await device.supportsRuntimeMode(buildMode)) { if (!await device.supportsRuntimeMode(buildMode)) {
throwToolExit( throwToolExit(
'${toTitleCase(getFriendlyModeName(buildMode))} ' '${sentenceCase(getFriendlyModeName(buildMode))} '
'mode is not supported by ${device.name}.', 'mode is not supported by ${device.name}.',
); );
} }
......
...@@ -435,7 +435,7 @@ class XcodeProjectInfo { ...@@ -435,7 +435,7 @@ class XcodeProjectInfo {
/// The expected scheme for [buildInfo]. /// The expected scheme for [buildInfo].
@visibleForTesting @visibleForTesting
static String expectedSchemeFor(BuildInfo? buildInfo) { static String expectedSchemeFor(BuildInfo? buildInfo) {
return toTitleCase(buildInfo?.flavor ?? 'runner'); return sentenceCase(buildInfo?.flavor ?? 'runner');
} }
/// The expected build configuration for [buildInfo] and [scheme]. /// The expected build configuration for [buildInfo] and [scheme].
......
...@@ -119,7 +119,7 @@ Future<void> _runCmake(String buildModeName, Directory sourceDir, Directory buil ...@@ -119,7 +119,7 @@ Future<void> _runCmake(String buildModeName, Directory sourceDir, Directory buil
await buildDir.create(recursive: true); await buildDir.create(recursive: true);
final String buildFlag = toTitleCase(buildModeName); final String buildFlag = sentenceCase(buildModeName);
final bool needCrossBuildOptionsForArm64 = needCrossBuild final bool needCrossBuildOptionsForArm64 = needCrossBuild
&& targetPlatform == TargetPlatform.linux_arm64; && targetPlatform == TargetPlatform.linux_arm64;
int result; int result;
......
...@@ -154,7 +154,7 @@ class BuildableMacOSApp extends MacOSApp { ...@@ -154,7 +154,7 @@ class BuildableMacOSApp extends MacOSApp {
getMacOSBuildDirectory(), getMacOSBuildDirectory(),
'Build', 'Build',
'Products', 'Products',
toTitleCase(getNameForBuildMode(buildMode)), sentenceCase(getNameForBuildMode(buildMode)),
appBundleNameFile.readAsStringSync().trim()); appBundleNameFile.readAsStringSync().trim());
} }
......
...@@ -63,7 +63,7 @@ class BuildableWindowsApp extends WindowsApp { ...@@ -63,7 +63,7 @@ class BuildableWindowsApp extends WindowsApp {
return globals.fs.path.join( return globals.fs.path.join(
getWindowsBuildDirectory(), getWindowsBuildDirectory(),
'runner', 'runner',
toTitleCase(getNameForBuildMode(buildMode)), sentenceCase(getNameForBuildMode(buildMode)),
'$binaryName.exe', '$binaryName.exe',
); );
} }
......
...@@ -279,7 +279,7 @@ Future<void> _runBuild( ...@@ -279,7 +279,7 @@ Future<void> _runBuild(
'--build', '--build',
buildDir.path, buildDir.path,
'--config', '--config',
toTitleCase(buildModeName), sentenceCase(buildModeName),
if (install) if (install)
...<String>['--target', 'INSTALL'], ...<String>['--target', 'INSTALL'],
if (globals.logger.isVerbose) if (globals.logger.isVerbose)
......
...@@ -194,7 +194,7 @@ class WindowsUWPDevice extends Device { ...@@ -194,7 +194,7 @@ class WindowsUWPDevice extends Device {
} }
final String binaryDir = _fileSystem.path.absolute( final String binaryDir = _fileSystem.path.absolute(
_fileSystem.path.join('build', 'winuwp', 'runner_uwp', 'AppPackages', binaryName)); _fileSystem.path.join('build', 'winuwp', 'runner_uwp', 'AppPackages', binaryName));
final String config = toTitleCase(getNameForBuildMode(_buildMode ?? BuildMode.debug)); final String config = sentenceCase(getNameForBuildMode(_buildMode ?? BuildMode.debug));
// If a multi-architecture package exists, install that; otherwise install // If a multi-architecture package exists, install that; otherwise install
// the single-architecture package. // the single-architecture package.
......
...@@ -485,11 +485,17 @@ class IosProject extends XcodeBasedProject { ...@@ -485,11 +485,17 @@ class IosProject extends XcodeBasedProject {
terminal: globals.terminal, terminal: globals.terminal,
); );
final String projectName = parent.manifest.appName;
// The dart project_name is in snake_case, this variable is the Title Case of the Project Name.
final String titleCaseProjectName = snakeCaseToTitleCase(projectName);
template.render( template.render(
target, target,
<String, Object>{ <String, Object>{
'ios': true, 'ios': true,
'projectName': parent.manifest.appName, 'projectName': projectName,
'titleCaseProjectName': titleCaseProjectName,
'iosIdentifier': iosBundleIdentifier, 'iosIdentifier': iosBundleIdentifier,
'hasIosDevelopmentTeam': iosDevelopmentTeam != null && iosDevelopmentTeam.isNotEmpty, 'hasIosDevelopmentTeam': iosDevelopmentTeam != null && iosDevelopmentTeam.isNotEmpty,
'iosDevelopmentTeam': iosDevelopmentTeam ?? '', 'iosDevelopmentTeam': iosDevelopmentTeam ?? '',
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>{{titleCaseProjectName}}</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
......
...@@ -12,6 +12,8 @@ ...@@ -12,6 +12,8 @@
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>{{projectName}}</string> <string>{{projectName}}</string>
<key>CFBundleDisplayName</key>
<string>{{titleCaseProjectName}}</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
......
...@@ -78,7 +78,7 @@ void main() { ...@@ -78,7 +78,7 @@ void main() {
'cmake', 'cmake',
'-G', '-G',
'Ninja', 'Ninja',
'-DCMAKE_BUILD_TYPE=${toTitleCase(buildMode)}', '-DCMAKE_BUILD_TYPE=${sentenceCase(buildMode)}',
'-DFLUTTER_TARGET_PLATFORM=linux-$target', '-DFLUTTER_TARGET_PLATFORM=linux-$target',
'/linux', '/linux',
], ],
......
...@@ -1363,6 +1363,114 @@ void main() { ...@@ -1363,6 +1363,114 @@ void main() {
ProcessManager: () => fakeProcessManager, ProcessManager: () => fakeProcessManager,
}); });
testUsingContext('display name is Title Case for objc iOS project.', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--template=app', '--no-pub', '--org', 'com.foo.bar','--ios-language=objc', '--project-name=my_project', projectDir.path]);
final String plistPath = globals.fs.path.join('ios', 'Runner', 'Info.plist');
final File plistFile = globals.fs.file(globals.fs.path.join(projectDir.path, plistPath));
expect(plistFile, exists);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project');
});
testUsingContext('display name is Title Case for swift iOS project.', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--template=app', '--no-pub', '--org', 'com.foo.bar','--ios-language=swift', '--project-name=my_project', projectDir.path]);
final String plistPath = globals.fs.path.join('ios', 'Runner', 'Info.plist');
final File plistFile = globals.fs.file(globals.fs.path.join(projectDir.path, plistPath));
expect(plistFile, exists);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project');
});
testUsingContext('display name is Title Case for objc iOS module.', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--template=module', '--offline', '--org', 'com.foo.bar','--ios-language=objc', '--project-name=my_project', projectDir.path]);
final String plistPath = globals.fs.path.join('.ios', 'Runner', 'Info.plist');
final File plistFile = globals.fs.file(globals.fs.path.join(projectDir.path, plistPath));
expect(plistFile, exists);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project');
}, overrides: <Type, Generator>{
Pub: () => Pub(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
),
});
testUsingContext('display name is Title Case for swift iOS module.', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--template=module', '--offline', '--org', 'com.foo.bar','--ios-language=swift', '--project-name=my_project', projectDir.path]);
final String plistPath = globals.fs.path.join('.ios', 'Runner', 'Info.plist');
final File plistFile = globals.fs.file(globals.fs.path.join(projectDir.path, plistPath));
expect(plistFile, exists);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project');
}, overrides: <Type, Generator>{
Pub: () => Pub(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
),
});
testUsingContext('display name is Title Case for swift iOS plugin.', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--template=plugin', '--no-pub', '--org', 'com.foo.bar', '--platforms=ios', '--ios-language=swift', '--project-name=my_project', projectDir.path]);
final String plistPath = globals.fs.path.join('example', 'ios', 'Runner', 'Info.plist');
final File plistFile = globals.fs.file(globals.fs.path.join(projectDir.path, plistPath));
expect(plistFile, exists);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project');
});
testUsingContext('display name is Title Case for objc iOS plugin.', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--template=plugin', '--no-pub', '--org', 'com.foo.bar', '--platforms=ios', '--ios-language=objc', '--project-name=my_project', projectDir.path]);
final String plistPath = globals.fs.path.join('example', 'ios', 'Runner', 'Info.plist');
final File plistFile = globals.fs.file(globals.fs.path.join(projectDir.path, plistPath));
expect(plistFile, exists);
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
expect(displayName, 'My Project');
});
testUsingContext('has correct content and formatting with macOS app template', () async { testUsingContext('has correct content and formatting with macOS app template', () async {
Cache.flutterRoot = '../..'; Cache.flutterRoot = '../..';
...@@ -2858,3 +2966,10 @@ class LoggingProcessManager extends LocalProcessManager { ...@@ -2858,3 +2966,10 @@ class LoggingProcessManager extends LocalProcessManager {
commands.clear(); commands.clear();
} }
} }
String _getStringValueFromPlist({File plistFile, String key}) {
final List<String> plist = plistFile.readAsLinesSync().map((String line) => line.trim()).toList();
final int keyIndex = plist.indexOf('<key>$key</key>');
assert(keyIndex > 0);
return plist[keyIndex+1].replaceAll('<string>', '').replaceAll('</string>', '');
}
...@@ -170,7 +170,7 @@ void main() { ...@@ -170,7 +170,7 @@ void main() {
mode: FileMode.writeOnlyAppend); mode: FileMode.writeOnlyAppend);
pluginDirectory.childFile('pubspec.yaml') pluginDirectory.childFile('pubspec.yaml')
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync(pluginYamlTemplate.replaceAll('PLUGIN_CLASS', toTitleCase(camelCase(name)))); ..writeAsStringSync(pluginYamlTemplate.replaceAll('PLUGIN_CLASS', sentenceCase(camelCase(name))));
directories.add(pluginDirectory); directories.add(pluginDirectory);
} }
return directories; return directories;
......
...@@ -69,6 +69,28 @@ baz=qux ...@@ -69,6 +69,28 @@ baz=qux
expect(snakeCase('ABc'), equals('a_bc')); expect(snakeCase('ABc'), equals('a_bc'));
expect(snakeCase('ABC'), equals('a_b_c')); expect(snakeCase('ABC'), equals('a_b_c'));
}); });
testWithoutContext('sentenceCase', () async {
expect(sentenceCase('abc'), equals('Abc'));
expect(sentenceCase('ab_c'), equals('Ab_c'));
expect(sentenceCase('a b c'), equals('A b c'));
expect(sentenceCase('a B c'), equals('A B c'));
expect(sentenceCase('Abc'), equals('Abc'));
expect(sentenceCase('ab_c'), equals('Ab_c'));
expect(sentenceCase('a_bc'), equals('A_bc'));
expect(sentenceCase('a_b_c'), equals('A_b_c'));
});
testWithoutContext('snakeCaseToTitleCase', () async {
expect(snakeCaseToTitleCase('abc'), equals('Abc'));
expect(snakeCaseToTitleCase('ab_c'), equals('Ab C'));
expect(snakeCaseToTitleCase('a_b_c'), equals('A B C'));
expect(snakeCaseToTitleCase('a_B_c'), equals('A B C'));
expect(snakeCaseToTitleCase('Abc'), equals('Abc'));
expect(snakeCaseToTitleCase('ab_c'), equals('Ab C'));
expect(snakeCaseToTitleCase('a_bc'), equals('A Bc'));
expect(snakeCaseToTitleCase('a_b_c'), equals('A B C'));
});
}); });
group('text wrapping', () { group('text wrapping', () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment