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) {
String _taskFor(String prefix, BuildInfo buildInfo) {
final String buildType = camelCase(buildInfo.modeName);
final String productFlavor = buildInfo.flavor ?? '';
return '$prefix${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
return '$prefix${sentenceCase(productFlavor)}${sentenceCase(buildType)}';
}
/// Returns the task to build an APK.
......@@ -592,7 +592,7 @@ class AndroidGradleBuilder implements AndroidBuilder {
command.addAll(androidBuildInfo.buildInfo.toGradleConfig());
if (buildInfo.dartObfuscation && buildInfo.mode != BuildMode.release) {
_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.',
);
}
......
......@@ -34,11 +34,20 @@ String snakeCase(String str, [ String sep = '_' ]) {
(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) {
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)`).
......
......@@ -236,7 +236,7 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand {
throwToolExit('Building for iOS is only supported on macOS.');
}
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) {
throwToolExit('Cannot analyze code size without performing a full build.');
......
......@@ -179,7 +179,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
for (final BuildInfo buildInfo in buildInfos) {
final String productBundleIdentifier = await _project.ios.productBundleIdentifier(buildInfo);
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);
if (modeDirectory.existsSync()) {
......@@ -446,7 +446,7 @@ end
}
// Always build debug for simulator.
final String simulatorConfiguration = toTitleCase(getNameForBuildMode(BuildMode.debug));
final String simulatorConfiguration = sentenceCase(getNameForBuildMode(BuildMode.debug));
pluginsBuildCommand = <String>[
...globals.xcode.xcrunCommand(),
'xcodebuild',
......
......@@ -10,6 +10,7 @@ import '../base/context.dart';
import '../base/file_system.dart';
import '../base/net.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../convert.dart';
import '../dart/pub.dart';
import '../features.dart';
......@@ -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(
organization: organization,
projectName: projectName,
titleCaseProjectName: titleCaseProjectName,
projectDescription: stringArg('description'),
flutterRoot: flutterRoot,
withPluginHook: generatePlugin,
......
......@@ -317,6 +317,7 @@ abstract class CreateBase extends FlutterCommand {
throwToolExit(error);
}
}
assert(projectName != null);
return projectName;
}
......@@ -325,6 +326,7 @@ abstract class CreateBase extends FlutterCommand {
Map<String, Object> createTemplateContext({
String organization,
String projectName,
String titleCaseProjectName,
String projectDescription,
String androidLanguage,
String iosDevelopmentTeam,
......@@ -361,6 +363,7 @@ abstract class CreateBase extends FlutterCommand {
return <String, Object>{
'organization': organization,
'projectName': projectName,
'titleCaseProjectName': titleCaseProjectName,
'androidIdentifier': androidIdentifier,
'iosIdentifier': appleIdentifier,
'macosIdentifier': appleIdentifier,
......
......@@ -462,7 +462,7 @@ class AppDomain extends Domain {
}) async {
if (!await device.supportsRuntimeMode(options.buildInfo.mode)) {
throw Exception(
'${toTitleCase(options.buildInfo.friendlyModeName)} '
'${sentenceCase(options.buildInfo.friendlyModeName)} '
'mode is not supported for ${device.name}.',
);
}
......
......@@ -587,7 +587,7 @@ class RunCommand extends RunCommandBase {
for (final Device device in devices) {
if (!await device.supportsRuntimeMode(buildMode)) {
throwToolExit(
'${toTitleCase(getFriendlyModeName(buildMode))} '
'${sentenceCase(getFriendlyModeName(buildMode))} '
'mode is not supported by ${device.name}.',
);
}
......
......@@ -435,7 +435,7 @@ class XcodeProjectInfo {
/// The expected scheme for [buildInfo].
@visibleForTesting
static String expectedSchemeFor(BuildInfo? buildInfo) {
return toTitleCase(buildInfo?.flavor ?? 'runner');
return sentenceCase(buildInfo?.flavor ?? 'runner');
}
/// The expected build configuration for [buildInfo] and [scheme].
......
......@@ -119,7 +119,7 @@ Future<void> _runCmake(String buildModeName, Directory sourceDir, Directory buil
await buildDir.create(recursive: true);
final String buildFlag = toTitleCase(buildModeName);
final String buildFlag = sentenceCase(buildModeName);
final bool needCrossBuildOptionsForArm64 = needCrossBuild
&& targetPlatform == TargetPlatform.linux_arm64;
int result;
......
......@@ -154,7 +154,7 @@ class BuildableMacOSApp extends MacOSApp {
getMacOSBuildDirectory(),
'Build',
'Products',
toTitleCase(getNameForBuildMode(buildMode)),
sentenceCase(getNameForBuildMode(buildMode)),
appBundleNameFile.readAsStringSync().trim());
}
......
......@@ -63,7 +63,7 @@ class BuildableWindowsApp extends WindowsApp {
return globals.fs.path.join(
getWindowsBuildDirectory(),
'runner',
toTitleCase(getNameForBuildMode(buildMode)),
sentenceCase(getNameForBuildMode(buildMode)),
'$binaryName.exe',
);
}
......
......@@ -279,7 +279,7 @@ Future<void> _runBuild(
'--build',
buildDir.path,
'--config',
toTitleCase(buildModeName),
sentenceCase(buildModeName),
if (install)
...<String>['--target', 'INSTALL'],
if (globals.logger.isVerbose)
......
......@@ -194,7 +194,7 @@ class WindowsUWPDevice extends Device {
}
final String binaryDir = _fileSystem.path.absolute(
_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
// the single-architecture package.
......
......@@ -485,11 +485,17 @@ class IosProject extends XcodeBasedProject {
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(
target,
<String, Object>{
'ios': true,
'projectName': parent.manifest.appName,
'projectName': projectName,
'titleCaseProjectName': titleCaseProjectName,
'iosIdentifier': iosBundleIdentifier,
'hasIosDevelopmentTeam': iosDevelopmentTeam != null && iosDevelopmentTeam.isNotEmpty,
'iosDevelopmentTeam': iosDevelopmentTeam ?? '',
......
......@@ -4,6 +4,8 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>{{titleCaseProjectName}}</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
......
......@@ -12,6 +12,8 @@
<string>6.0</string>
<key>CFBundleName</key>
<string>{{projectName}}</string>
<key>CFBundleDisplayName</key>
<string>{{titleCaseProjectName}}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
......
......@@ -78,7 +78,7 @@ void main() {
'cmake',
'-G',
'Ninja',
'-DCMAKE_BUILD_TYPE=${toTitleCase(buildMode)}',
'-DCMAKE_BUILD_TYPE=${sentenceCase(buildMode)}',
'-DFLUTTER_TARGET_PLATFORM=linux-$target',
'/linux',
],
......
......@@ -1363,6 +1363,114 @@ void main() {
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 {
Cache.flutterRoot = '../..';
......@@ -2858,3 +2966,10 @@ class LoggingProcessManager extends LocalProcessManager {
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() {
mode: FileMode.writeOnlyAppend);
pluginDirectory.childFile('pubspec.yaml')
..createSync(recursive: true)
..writeAsStringSync(pluginYamlTemplate.replaceAll('PLUGIN_CLASS', toTitleCase(camelCase(name))));
..writeAsStringSync(pluginYamlTemplate.replaceAll('PLUGIN_CLASS', sentenceCase(camelCase(name))));
directories.add(pluginDirectory);
}
return directories;
......
......@@ -69,6 +69,28 @@ baz=qux
expect(snakeCase('ABc'), equals('a_bc'));
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', () {
......
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