Skip to content
This repository has been archived by the owner on Sep 24, 2024. It is now read-only.

Commit

Permalink
Feature/setup tool (#64)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
purplenoodlesoop authored Jun 16, 2022
1 parent be5e634 commit 6bf14ff
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 94 deletions.
1 change: 0 additions & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@ analyzer:
- drift
exclude:
- 'tool/google_localizer/**'
- 'tool/setup_clone/**'
- 'lib/generated_plugin_registrant.dart'
- '**/*.select.dart'
10 changes: 5 additions & 5 deletions automation/makefile/format.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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)
32 changes: 0 additions & 32 deletions test/unit_test.dart

This file was deleted.

13 changes: 7 additions & 6 deletions tool/setup_clone/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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.
256 changes: 208 additions & 48 deletions tool/setup_clone/main.dart
Original file line number Diff line number Diff line change
@@ -1,38 +1,81 @@
// ignore_for_file: avoid-ignoring-return-values

import 'dart:async';

import 'dart:io';

Future<void> main(List<String> 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';

String get appWidgetPath => _appFileName + '.dart';
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<R>({
required Environment environment,
required R Function() body,
Expand All @@ -41,68 +84,185 @@ class Environment {
body,
zoneValues: {_key: environment},
);
}

static Environment get current => (Zone.current[_key] as Environment?)!;
Environment createEnvironment(List<String> 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<String> args) => Environment(
originalName: const NameBundle.original(),
newName: NameBundle(packageName: args.first),
);
Future<void> replaceInCodebase(
String Function(NameBundle nameBundle) select,
Stream<File> files,
) async {
final environment = Environment.current();

final originalName = select(environment.originalName);
final newName = select(environment.newName);

Future<void> 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<void> renamePackage() => rename(
inDirectory: './',
select: (nameBundle) => nameBundle.packageName,
Future<void> replaceInDirectory(
String Function(NameBundle nameBundle) select, {
String path = './',
}) =>
replaceInCodebase(
select,
Directory(path)
.list(recursive: true)
.where((event) => event is File)
.cast(),
);

Future<void> replaceInFile(
String path,
String Function(NameBundle nameBundle) select,
) =>
replaceInCodebase(
select,
Stream.value(
File(path),
),
);

Future<void> renameAppWidgetName() => rename(
inDirectory: './lib/feature/app/',
select: (nameBundle) => nameBundle.appWidgetName,
Future<void> renameAppTitle() => replaceInFile(
'./lib/src/core/l10n/app_en.arb',
(nameBundle) => nameBundle.appTitle,
);

Future<void> renameWidgetFile() async {}
Future<void> renamePackageDescription() => replaceInFile(
'./pubspec.yaml',
(nameBundle) => nameBundle.packageDescription,
);

Future<void> renamePackage() => replaceInDirectory(
(nameBundle) => nameBundle.packageName,
);

Future<void> moveReadmeToSetup() async {}
Future<void> createEmptyReadme() async {}
Future<void> selfDestruct() async {}
Future<void> renameAppWidgetName() => replaceInDirectory(
(nameBundle) => nameBundle.appWidgetName,
);

String appWidgetPath(
NameBundle bundle,
) =>
'./lib/src/feature/app/${bundle.appWidgetPath}';

Future<void> renameWidgetFile() async {
final environment = Environment.current();

await File(appWidgetPath(environment.originalName)).rename(
appWidgetPath(environment.newName),
);
}

Future<void> renameReadmeFile() async {
await File('./README.md').rename('./STARTER.md');
}

Future<void> createEmptyReadme() async {
final file = File('./README.md');

await file.create();

final sink = file.openWrite()
..write('# ')
..writeln(Environment.current().newName.packageName);

await sink.close();
}

Future<bool> runProcessConcealing(
String command,
List<String> arguments,
) async {
try {
await Process.run(command, arguments);

return true;
} on Object {
return false;
}
}

Future<void> 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<void> seq(List<Future<void> Function()> actions) async {
Future<void> seq(List<FutureOr<void> Function()> actions) async {
for (final action in actions) {
await action();
}
}

Future<void> performRenaming() => seq(const [
Future<void> performSetup() => seq(const [
renameAppTitle,
renamePackageDescription,
renamePackage,
renameAppWidgetName,
renameWidgetFile,
moveReadmeToSetup,
renameReadmeFile,
createEmptyReadme,
selfDestruct,
createFlutterRunners,
printResultMessage,
]);
4 changes: 2 additions & 2 deletions tool/setup_clone/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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


0 comments on commit 6bf14ff

Please sign in to comment.