From 6bf14ff0a893666f0dbfd23ea6a12dde7c45647d Mon Sep 17 00:00:00 2001 From: Yakov K Date: Fri, 17 Jun 2022 00:45:53 +0300 Subject: [PATCH] Feature/setup tool (#64) * Implement basic renaming * Add Stats and expand replacement for all codebase * Add app title renaming * Abstract out different reanmings * Rename package description * Rename main widget file * Implement Flutter runners creation * Manage README in setup * Update setup tool readme and uncomment it in Makefile --- analysis_options.yaml | 1 - automation/makefile/format.mk | 10 +- test/unit_test.dart | 32 ----- tool/setup_clone/README.md | 13 +- tool/setup_clone/main.dart | 256 +++++++++++++++++++++++++++------- tool/setup_clone/pubspec.yaml | 4 +- 6 files changed, 222 insertions(+), 94 deletions(-) delete mode 100644 test/unit_test.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index c8bc195..31d75ea 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -5,6 +5,5 @@ analyzer: - drift exclude: - 'tool/google_localizer/**' - - 'tool/setup_clone/**' - 'lib/generated_plugin_registrant.dart' - '**/*.select.dart' diff --git a/automation/makefile/format.mk b/automation/makefile/format.mk index 7ba8919..80b2898 100644 --- a/automation/makefile/format.mk +++ b/automation/makefile/format.mk @@ -22,8 +22,8 @@ format: @dart fix --apply . @dart format -l 80 --fix . -#setup: -# @echo "* Getting dependencies for setup tool *" -# fvm dart pub get --directory=./tool/setup_clone -# @echo "* Setting up the project *" -# @fvm dart ./tool/setup_clone/main.dart $(NAME) +setup: + @echo "* Getting dependencies for setup tool *" + fvm dart pub get --directory=./tool/setup_clone + @echo "* Setting up the project *" + @fvm dart ./tool/setup_clone/main.dart $(NAME) diff --git a/test/unit_test.dart b/test/unit_test.dart deleted file mode 100644 index 857287e..0000000 --- a/test/unit_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -// ignore_for_file: unnecessary_lambdas, prefer_const_constructors - -import 'package:flutter/widgets.dart'; -import 'package:purple_starter/runner_stub.dart' as runner_stub; -import 'package:purple_starter/src/core/error/parsing_exception.dart'; -import 'package:purple_starter/src/core/error/unknown_host_platform_error.dart'; -import 'package:purple_starter/src/core/router/app_router.dart'; - -import 'package:test/test.dart'; - -void main() { - group('Smoke unit test', () { - test('placeholder', () { - expectLater(runner_stub.run, throwsA(isA())); - expect(() => PlaceholderPage(), returnsNormally); - expect(PlaceholderPage(), isA()); - expect(PlaceholderPage().key, equals(PlaceholderPage().key)); - expect( - PlaceholderPage( - key: ValueKey('key'), - ).key, - isNot(equals(PlaceholderPage().key)), - ); - expect( - PlaceholderPage().runtimeType, - same(PlaceholderPage().runtimeType), - ); - expect(ParsingException(0).toString(), isA()); - expect(() => ParsingException(0), returnsNormally); - }); - }); -} diff --git a/tool/setup_clone/README.md b/tool/setup_clone/README.md index 1028427..84a9a3f 100644 --- a/tool/setup_clone/README.md +++ b/tool/setup_clone/README.md @@ -3,9 +3,10 @@ Run this tool after cloning the newly created repository from the template. This tool: - 1. Renames the package to a given name. - 2. Renames the localized app title to a given name. - 3. Renames the app widget to a given name. - 4. Moves the root `README.md` to a `SETUP.md`. - 5. Creates an empty `README.md` with a given name in the title. - 6. Self-destructs. \ No newline at end of file + 1. Renames localized App title. + 2. Renames package description. + 3. Renames root App Widget name. + 4. Renames root App Widget file name. + 5. Renames README.md to STARTER.md + 6. Creates an empty README.md. + 7. Creates Flutter Runners for all available platforms. \ No newline at end of file diff --git a/tool/setup_clone/main.dart b/tool/setup_clone/main.dart index f76b51d..cfe5f0c 100644 --- a/tool/setup_clone/main.dart +++ b/tool/setup_clone/main.dart @@ -1,20 +1,37 @@ +// ignore_for_file: avoid-ignoring-return-values + import 'dart:async'; import 'dart:io'; Future main(List args) => Environment.run( environment: createEnvironment(args), - body: performRenaming, + body: performSetup, ); String capitalized(String source) => - '${source[0].toUpperCase()}${source.substring(1)}'; + source[0].toUpperCase() + source.substring(1); class NameBundle { + static const String _originalPackageName = 'purple_starter'; + final String packageName; + final String appTitle; + final String packageDescription; + + const NameBundle({ + required this.packageName, + required this.appTitle, + required this.packageDescription, + }); - const NameBundle({required this.packageName}); - const NameBundle.original() : this(packageName: 'purple_starter'); + const NameBundle.original() + : this( + packageName: _originalPackageName, + appTitle: 'An app created from `$_originalPackageName` app template.', + packageDescription: + 'A fresh Flutter project created using Purple Starter.', + ); String get _appFileName => packageName + '_app'; @@ -22,17 +39,43 @@ class NameBundle { String get appWidgetName => _appFileName.split('_').map(capitalized).join(); } +class SetupStats { + late final Stopwatch _stopwatch = Stopwatch(); + + int _replaced = 0; + + int get replaced => _replaced; + + void incrementReplaced() { + _replaced++; + } + + void startTimer() { + _stopwatch.start(); + } + + Duration stopTimer() { + _stopwatch.stop(); + + return _stopwatch.elapsed; + } +} + class Environment { - static const _key = Object(); + static final _key = Object(); final NameBundle originalName; final NameBundle newName; + final SetupStats stats; Environment({ required this.originalName, required this.newName, + required this.stats, }); + factory Environment.current() => Zone.current[_key] as Environment; + static R run({ required Environment environment, required R Function() body, @@ -41,68 +84,185 @@ class Environment { body, zoneValues: {_key: environment}, ); +} - static Environment get current => (Zone.current[_key] as Environment?)!; +Environment createEnvironment(List args) { + final packageName = args.first; + final appTitle = capitalized(packageName.replaceAll('_', ' ')); + + return Environment( + originalName: const NameBundle.original(), + newName: NameBundle( + packageName: packageName, + appTitle: appTitle, + packageDescription: '$appTitle Flutter app', + ), + stats: SetupStats()..startTimer(), + ); } -Environment createEnvironment(List args) => Environment( - originalName: const NameBundle.original(), - newName: NameBundle(packageName: args.first), - ); +Future replaceInCodebase( + String Function(NameBundle nameBundle) select, + Stream files, +) async { + final environment = Environment.current(); + + final originalName = select(environment.originalName); + final newName = select(environment.newName); -Future rename({ - required String inDirectory, - required String Function(NameBundle nameBundle) select, -}) async { - final environment = Environment.current; - - final from = select(environment.originalName); - final to = select(environment.newName); - - await Process.run('find', [ - inDirectory, - '( -type d -name .git -prune )', - '-o', - '-type', - 'f', - '-print0', - '|', - 'xargs', - '-0', - 'sed', - '-i', - "''", - "'s/$from/$to/g'", - ]); + await for (final file in files) { + try { + final contents = await file.readAsString(); + final replaced = contents.replaceAll(originalName, newName); + if (contents != replaced) { + await file.writeAsString(replaced); + environment.stats.incrementReplaced(); + } + } on Object { + // ignore + } + } } -Future renamePackage() => rename( - inDirectory: './', - select: (nameBundle) => nameBundle.packageName, +Future replaceInDirectory( + String Function(NameBundle nameBundle) select, { + String path = './', +}) => + replaceInCodebase( + select, + Directory(path) + .list(recursive: true) + .where((event) => event is File) + .cast(), + ); + +Future replaceInFile( + String path, + String Function(NameBundle nameBundle) select, +) => + replaceInCodebase( + select, + Stream.value( + File(path), + ), ); -Future renameAppWidgetName() => rename( - inDirectory: './lib/feature/app/', - select: (nameBundle) => nameBundle.appWidgetName, +Future renameAppTitle() => replaceInFile( + './lib/src/core/l10n/app_en.arb', + (nameBundle) => nameBundle.appTitle, ); -Future renameWidgetFile() async {} +Future renamePackageDescription() => replaceInFile( + './pubspec.yaml', + (nameBundle) => nameBundle.packageDescription, + ); + +Future renamePackage() => replaceInDirectory( + (nameBundle) => nameBundle.packageName, + ); -Future moveReadmeToSetup() async {} -Future createEmptyReadme() async {} -Future selfDestruct() async {} +Future renameAppWidgetName() => replaceInDirectory( + (nameBundle) => nameBundle.appWidgetName, + ); + +String appWidgetPath( + NameBundle bundle, +) => + './lib/src/feature/app/${bundle.appWidgetPath}'; + +Future renameWidgetFile() async { + final environment = Environment.current(); + + await File(appWidgetPath(environment.originalName)).rename( + appWidgetPath(environment.newName), + ); +} + +Future renameReadmeFile() async { + await File('./README.md').rename('./STARTER.md'); +} + +Future createEmptyReadme() async { + final file = File('./README.md'); + + await file.create(); + + final sink = file.openWrite() + ..write('# ') + ..writeln(Environment.current().newName.packageName); + + await sink.close(); +} + +Future runProcessConcealing( + String command, + List arguments, +) async { + try { + await Process.run(command, arguments); + + return true; + } on Object { + return false; + } +} + +Future createFlutterRunners() async { + final packageName = Environment.current().newName.packageName; + final sharedCommands = [ + 'create', + '--project-name', + packageName, + '--org', + 'com.$packageName', + '.', + ]; + + final hasRunFvm = await runProcessConcealing( + 'fvm', + ['flutter', ...sharedCommands], + ); + + if (!hasRunFvm) { + final hasRunFlutter = await runProcessConcealing('flutter', sharedCommands); + if (!hasRunFlutter) { + throw Exception('Failed to create Flutter runners.'); + } + } +} + +String assembleMessage(String newName, int replaced, Duration duration) => + 'Setup complete! ' + 'Replaced to $newName-derived names ' + '$replaced times. Took $duration.'; + +void printResultMessage() { + final environment = Environment.current(); + final stats = environment.stats; + + stdout.write( + assembleMessage( + environment.newName.packageName, + stats.replaced, + stats.stopTimer(), + ), + ); +} -Future seq(List Function()> actions) async { +Future seq(List Function()> actions) async { for (final action in actions) { await action(); } } -Future performRenaming() => seq(const [ +Future performSetup() => seq(const [ + renameAppTitle, + renamePackageDescription, renamePackage, renameAppWidgetName, renameWidgetFile, - moveReadmeToSetup, + renameReadmeFile, createEmptyReadme, - selfDestruct, + createFlutterRunners, + printResultMessage, ]); diff --git a/tool/setup_clone/pubspec.yaml b/tool/setup_clone/pubspec.yaml index b819930..4f0b611 100644 --- a/tool/setup_clone/pubspec.yaml +++ b/tool/setup_clone/pubspec.yaml @@ -5,7 +5,7 @@ environment: sdk: ">=2.14.0 <3.0.0" dev_dependencies: - # Analyzer extensions - purple_lints: ^0.2.0 + dart_code_metrics: ^4.13.0 + purple_metrics_lints: ^0.2.0