From 216e0dcca32d3999ff2dfebcfb67ffe802b8bee6 Mon Sep 17 00:00:00 2001 From: Iacopo Ciao Date: Thu, 30 May 2024 12:38:18 +0200 Subject: [PATCH] refactor: refactor brick_command.dart --- lib/cli_brick_configurations.dart | 15 +- lib/src/commands/brick_command.dart | 431 ++++++++++++++++++ .../{models => commands}/brick_context.dart | 0 lib/src/commands/command_runner_factory.dart | 55 ++- lib/src/commands/node_command.dart | 4 + lib/src/models/addon_command.dart | 46 -- lib/src/models/addon_command_definition.dart | 12 + lib/src/models/application_command.dart | 52 --- .../application_command_definition.dart | 11 + lib/src/models/brick_command.dart | 184 -------- lib/src/models/brick_command_definition.dart | 21 + .../models/brick_command_with_questions.dart | 139 ------ lib/src/models/cli_configuration.dart | 16 +- lib/src/models/library_command.dart | 50 -- .../models/library_command_definition.dart | 12 + lib/src/models/models.dart | 12 +- lib/src/models/new_command.dart | 79 ---- lib/src/models/new_definition.dart | 13 + .../models/workspace_command_definition.dart | 12 + lib/src/utilities/process.dart | 62 +++ 20 files changed, 633 insertions(+), 593 deletions(-) create mode 100644 lib/src/commands/brick_command.dart rename lib/src/{models => commands}/brick_context.dart (100%) delete mode 100644 lib/src/models/addon_command.dart create mode 100644 lib/src/models/addon_command_definition.dart delete mode 100644 lib/src/models/application_command.dart create mode 100644 lib/src/models/application_command_definition.dart delete mode 100644 lib/src/models/brick_command.dart create mode 100644 lib/src/models/brick_command_definition.dart delete mode 100644 lib/src/models/brick_command_with_questions.dart delete mode 100644 lib/src/models/library_command.dart create mode 100644 lib/src/models/library_command_definition.dart delete mode 100644 lib/src/models/new_command.dart create mode 100644 lib/src/models/new_definition.dart create mode 100644 lib/src/models/workspace_command_definition.dart create mode 100644 lib/src/utilities/process.dart diff --git a/lib/cli_brick_configurations.dart b/lib/cli_brick_configurations.dart index 39e08e1..bdd92a0 100644 --- a/lib/cli_brick_configurations.dart +++ b/lib/cli_brick_configurations.dart @@ -1,21 +1,22 @@ +import 'package:devmy_cli/src/models/new_definition.dart'; import 'package:mason/mason.dart'; import 'package:devmy_cli/src/devmy_cli.dart'; final cliConfiguration = CliConfiguration( - new$: NewCommand( + new$: NewCommandDefinition( brick: GitPath('https://github.com/acadevmy/scaffold-brick'), description: 'Easily create a Devmy scaffold masterpiece', ), applications: [ - ApplicationCommand( + ApplicationCommandDefinition( name: 'angular', brick: GitPath('https://github.com/acadevmy/angular-application-brick'), description: 'Generate a fully-equipped Angular project in your workspace', aliases: ['ng'], ), - ApplicationCommand( + ApplicationCommandDefinition( name: 'nextjs', brick: GitPath('https://github.com/acadevmy/next-application-brick'), description: 'Generate a fully-equipped Nextjs project in your workspace', @@ -35,13 +36,13 @@ final cliConfiguration = CliConfiguration( ], aliases: ['next'], ), - ApplicationCommand( + ApplicationCommandDefinition( name: "directus", brick: GitPath("https://github.com/acadevmy/directus-application-brick"), description: "Generate a fully-equipped Directus project in your workspace", ), - ApplicationCommand( + ApplicationCommandDefinition( name: "nestjs", brick: GitPath("https://github.com/acadevmy/nestjs-application-brick"), description: @@ -49,12 +50,12 @@ final cliConfiguration = CliConfiguration( aliases: ['nest']), ], addons: [ - AddonCommand( + AddonCommandDefinition( name: 'next/zustand', brick: GitPath("https://github.com/acadevmy/next-zustand-addon-brick"), description: "Integrating Zustand with Next.js", ), - AddonCommand( + AddonCommandDefinition( name: 'next/shadcnui', brick: GitPath("https://github.com/acadevmy/next-shacnui-addon-brick"), description: "Customizable UI for Next.js with ShadcnUI.", diff --git a/lib/src/commands/brick_command.dart b/lib/src/commands/brick_command.dart new file mode 100644 index 0000000..a2b4ad8 --- /dev/null +++ b/lib/src/commands/brick_command.dart @@ -0,0 +1,431 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:chalkdart/chalk.dart'; +import 'package:devmy_cli/src/constants/constants.dart'; +import 'package:interact/interact.dart'; +import 'package:mason/mason.dart'; + +import '../models/models.dart'; +import '../utilities/process.dart'; +import '../utilities/utilities.dart'; +import 'brick_context.dart'; + +class BrickCommand extends Command { + static final String _workspaceDescriptor = 'pnpm-workspace.yaml'; + BrickCommandDefinition brickCommand; + List addons; + + BrickCommand({ + required this.brickCommand, + required this.addons, + }); + + @override + String get description => brickCommand.description; + + @override + String get name => chalk.bold(brickCommand.name); + + @override + List get aliases => [...brickCommand.aliases, brickCommand.name]; + + @override + Future run() async { + final brickCommand = this.brickCommand; + final Directory currentWorkingDirectory = Directory.current; + + if (brickCommand is WorkspaceCommandDefinition) { + return _runWorkspaceBrick(brickCommand); + } + + final Directory workspaceDirectory = + _searchWorkspaceDirectory(currentWorkingDirectory); + + if (brickCommand is ApplicationCommandDefinition) { + return _runApplicationBrick(workspaceDirectory, brickCommand); + } + + if (brickCommand is LibraryCommandDefinition) { + return _runLibraryBrick(workspaceDirectory, brickCommand); + } + + if (brickCommand is AddonCommandDefinition) { + return _runAddonBrick( + brickCommand, workspaceDirectory, currentWorkingDirectory); + } + } + + Future _runWorkspaceBrick( + WorkspaceCommandDefinition brickCommand, + ) async { + final workspaceName = Input( + prompt: 'What is the name of the workspace?', + validator: InteractValidators.isNotBlank, + ).interact(); + + final Directory workspaceDirectory = Directory.fromUri( + Directory.current.uri.resolve(workspaceName.paramCase)); + workspaceDirectory.createSync(recursive: true); + final context = await _loadBrickContext(brickCommand.brick); + final environment = { + kBrickWorkspaceNameEnvironmentVariable: workspaceName, + }; + + await _runBrick( + workspaceDirectory: workspaceDirectory, + targetDirectory: workspaceDirectory, + brickContext: context, + environment: environment, + brickCommand: brickCommand, + ); + + await pnpmInstall(workspaceDirectory); + await initializeGit(workspaceDirectory); + await commitChanges( + workspaceDirectory, + 'chore(${workspaceName.paramCase}): initial commit', + ); + + await _runAddons( + workspaceDirectory: workspaceDirectory, + addonTargetDirectory: workspaceDirectory, + questions: brickCommand.questions, + environment: environment, + ); + + print(chalk.green('🏛️ Scaffold initialized successfully! 🚀')); + print('Next steps:'); + print(chalk.blueBright(' cd ${workspaceName.paramCase}')); + print(chalk.blueBright(' pnpm run start')); + } + + Future _runApplicationBrick( + Directory workspaceDirectory, + ApplicationCommandDefinition brickCommand, + ) async { + final applicationName = Input( + prompt: 'What is the name of the application?', + validator: InteractValidators.isNotBlank, + ).interact(); + + final Directory applicationDirectory = Directory.fromUri(workspaceDirectory + .uri + .resolve('$kApplicationBasePath/${applicationName.paramCase}')); + + applicationDirectory.createSync(recursive: true); + final context = await _loadBrickContext(brickCommand.brick); + final Map environment = { + kBrickApplicationNameEnvironmentVariable: applicationName + }; + + await _runBrick( + workspaceDirectory: workspaceDirectory, + targetDirectory: applicationDirectory, + brickContext: context, + environment: environment, + brickCommand: brickCommand, + ); + + await pnpmInstall(workspaceDirectory); + await commitChanges( + workspaceDirectory, + 'chore(${applicationName.paramCase}): initial scaffold', + ); + + await _runAddons( + workspaceDirectory: workspaceDirectory, + addonTargetDirectory: applicationDirectory, + questions: brickCommand.questions, + environment: environment, + ); + } + + Future _runLibraryBrick(Directory workspaceDirectory, + LibraryCommandDefinition brickCommand) async { + final libraryName = Input( + prompt: 'What is the name of the library?', + validator: InteractValidators.isNotBlank, + ).interact(); + + final Directory libraryDirectory = Directory.fromUri(workspaceDirectory.uri + .resolve('$kLibraryBasePath/${libraryName.paramCase}')); + + libraryDirectory.createSync(recursive: true); + final context = await _loadBrickContext(brickCommand.brick); + final Map environment = { + kBrickLibraryNameEnvironmentVariable: libraryName + }; + + await _runBrick( + workspaceDirectory: workspaceDirectory, + targetDirectory: libraryDirectory, + brickContext: context, + environment: environment, + brickCommand: brickCommand, + ); + + await pnpmInstall(workspaceDirectory); + await commitChanges( + workspaceDirectory, + 'chore(${libraryName.paramCase}): initial scaffold', + ); + + await _runAddons( + workspaceDirectory: workspaceDirectory, + addonTargetDirectory: libraryDirectory, + questions: brickCommand.questions, + environment: environment, + ); + } + + Future _runAddonBrick( + AddonCommandDefinition brickCommand, + Directory workspaceDirectory, + Directory currentWorkingDirectory, + ) async { + final context = await _loadBrickContext(brickCommand.brick); + final Map environment = {}; + final addonTarget = _askAddonTarget(workspaceDirectory); + String targetName = getPackageName(addonTarget); + + await _runBrick( + workspaceDirectory: workspaceDirectory, + targetDirectory: addonTarget, + brickContext: context, + environment: environment, + brickCommand: brickCommand, + ); + + await pnpmInstall(workspaceDirectory); + await commitChanges( + workspaceDirectory, + 'chore($targetName): applied ${brickCommand.name}', + ); + + await _runAddons( + workspaceDirectory: workspaceDirectory, + addonTargetDirectory: currentWorkingDirectory, + questions: brickCommand.questions, + environment: environment, + ); + } + + Future _runBrick({ + required Directory workspaceDirectory, + required Directory targetDirectory, + required BrickContext brickContext, + required Map environment, + required BrickCommandDefinition brickCommand, + }) async { + final target = DirectoryGeneratorTarget(targetDirectory); + + _promptBundleVariables( + bundle: brickContext.bundle, + environment: environment, + ); + + print('Running pre generation scripts...\n'); + await brickContext.generator.hooks.preGen( + vars: environment, + workingDirectory: targetDirectory.path, + onVarsChanged: (nextVars) => _updateEnvironment(environment, nextVars), + ); + + final generated = await brickContext.generator.generate( + target, + fileConflictResolution: brickCommand.fileConflictResolution, + vars: environment, + ); + + for (final gen in generated) { + print('${chalk.green.bold(gen.status.name.toUpperCase())} ${gen.path}'); + } + + print('\nRunning post generation scripts...\n'); + await brickContext.generator.hooks.postGen( + vars: environment, + workingDirectory: targetDirectory.path, + ); + } + + FutureOr _promptBundleVariables({ + required Map environment, + required MasonBundle bundle, + }) async { + final bundleEntries = bundle.vars.entries; + + // Excluding already acquired environment variables + final questions = bundleEntries + .where((entry) => !environment.containsKey(entry.key)) + .toList(growable: false); + + for (final question in questions) { + environment[question.key] = question.value.input(); + } + } + + void _updateEnvironment( + Map environment, + Map updates, + ) { + for (final entry in updates.entries) { + environment.update(entry.key, (_) => entry.value); + } + } + + Future _loadBrickContext(GitPath brickPath) async { + final spinner = Spinner( + icon: '🧱', + leftPrompt: (done) => '', + rightPrompt: (done) => done ? 'Assets loaded 🚀' : 'Loading assets', + ).interact(); + + try { + final brick = await BricksJson.temp().add( + Brick.git(brickPath), + ); + + final path = brick.path; + final bundle = createBundle(Directory(path)); + + MasonGenerator generator = await MasonGenerator.fromBundle(bundle); + spinner.done(); + + return BrickContext(bundle: bundle, generator: generator); + } catch (e) { + reset(); + rethrow; + } + } + + Future _runAddons({ + required Directory workspaceDirectory, + required Directory addonTargetDirectory, + required List questions, + required Map environment, + }) async { + for (final question in questions) { + final commands = _inputConfigurationsFromDependencies(question); + for (final command in commands) { + final commandEnvironment = {...environment}; + + await _runBrick( + workspaceDirectory: workspaceDirectory, + targetDirectory: addonTargetDirectory, + brickContext: await _loadBrickContext(command.brick), + environment: commandEnvironment, + brickCommand: command, + ); + + await pnpmInstall(workspaceDirectory); + await commitChanges( + workspaceDirectory, + 'chore: applied ${command.name}', + ); + + _runAddons( + workspaceDirectory: workspaceDirectory, + addonTargetDirectory: addonTargetDirectory, + questions: command.questions, + environment: commandEnvironment, + ); + } + } + } + + List _inputConfigurationsFromDependencies( + Question question) { + final optionLabels = [...question.addonNames]; + if (question.isMultiple) { + final selections = MultiSelect( + prompt: question.prompt, + options: optionLabels, + ).interact(); + return selections + .map((index) => optionLabels[index]) + .map((name) => addons.singleWhere((addon) => addon.name == name)) + .toList(growable: false); + } + + if (question.isOptional) { + optionLabels.insert(0, 'none'); + } + + int index = Select( + prompt: question.prompt, + options: optionLabels, + ).interact(); + + if (question.isOptional) { + index--; + } + + if (index == -1) { + return []; + } + + return addons + .where((addon) => addon.name == question.addonNames[index]) + .toList(); + } + + Directory _searchWorkspaceDirectory(Directory workingDirectory) { + Directory cursor = workingDirectory; + + while ( + !File.fromUri(cursor.uri.resolve(_workspaceDescriptor)).existsSync()) { + if (cursor.path == cursor.parent.path) { + throw Exception( + 'the command is not executed within a devmy project. Maybe you should get a coffee break ☕️', + ); + } + cursor = cursor.parent; + } + + return cursor; + } + + List _getProjectDirectories(Directory workspaceDirectory) { + final applications = + Directory.fromUri(workspaceDirectory.uri.resolve(kApplicationBasePath)) + .listSync() + .whereType() + .toList(); + + final libraries = + Directory.fromUri(workspaceDirectory.uri.resolve(kLibraryBasePath)) + .listSync() + .whereType() + .toList(); + + return [workspaceDirectory, ...libraries, ...applications]; + } + + Directory _askAddonTarget(Directory workspaceDirectory) { + final targets = _getProjectDirectories(workspaceDirectory); + final options = targets.map((t) => getPackageName(t)).toList(); + + final selected = Select( + prompt: 'where do you want to apply this addon?', + options: options, + ).interact(); + + return targets[selected]; + } + + String getPackageName(Directory directory) { + final packagePath = directory.uri.resolve('package.json').path; + final rawPackage = File(packagePath).readAsStringSync(); + final package = jsonDecode(rawPackage); + final String name = package['name']; + + if (name.isEmpty) { + throw Exception('Package name not provided in $packagePath'); + } + + return package['name']; + } +} diff --git a/lib/src/models/brick_context.dart b/lib/src/commands/brick_context.dart similarity index 100% rename from lib/src/models/brick_context.dart rename to lib/src/commands/brick_context.dart diff --git a/lib/src/commands/command_runner_factory.dart b/lib/src/commands/command_runner_factory.dart index e94bcf2..cf8554c 100644 --- a/lib/src/commands/command_runner_factory.dart +++ b/lib/src/commands/command_runner_factory.dart @@ -1,4 +1,6 @@ import 'package:args/command_runner.dart'; +import 'package:chalkdart/chalkstrings.dart'; +import 'package:devmy_cli/src/commands/brick_command.dart'; import 'package:devmy_cli/src/commands/node_command.dart'; import 'package:devmy_cli/src/models/models.dart'; @@ -10,42 +12,53 @@ class CommandRunnerFactory { suggestionDistanceLimit: 3, ); - commandRunner.addCommand(configuration.new$); + commandRunner.addCommand(BrickCommand( + brickCommand: configuration.new$, addons: configuration.addons)); commandRunner.addCommand(_createGenerateSection(configuration)); return commandRunner; } Command _createGenerateSection(CliConfiguration configuration) { - for (final addon in configuration.addons) { - addon.addons = configuration.addons; - } - for (final application in configuration.applications) { - application.addons = configuration.addons; - } + final addons = configuration.addons; + + final applicationCommands = configuration.applications + .map((brickCommandDefinition) => + BrickCommand(brickCommand: brickCommandDefinition, addons: addons)) + .toList(); + + final libraryCommands = configuration.libraries + .map((brickCommandDefinition) => + BrickCommand(brickCommand: brickCommandDefinition, addons: addons)) + .toList(); + + final addonCommands = configuration.addons + .map((brickCommandDefinition) => + BrickCommand(brickCommand: brickCommandDefinition, addons: addons)) + .toList(); return NodeCommand( - name: 'generate', + name: chalk.bold('generate'), + aliases: ['g', 'gen', 'generate'], description: - "Unlock the power to create! Generate various components of your project with ease using this versatile command. Explore options like applications, addons, presets and libraries to kickstart your development journey.", + "Generate various components to kickstart your development journey", children: [ NodeCommand( - name: 'application', - description: - "Craft your Devmy project's foundation! Use this command to generate different types of applications, whether it's Angular, Next.js, or beyond. Seamlessly set up the backbone of your project with just a few simple commands.", - children: configuration.applications, + name: chalk.bold('application'), + aliases: ['a', 'app'], + description: "Create a workspace application", + children: applicationCommands, ), NodeCommand( - name: 'library', - description: - "Empower your project with reusable components! With this command, generate libraries tailored to your project's needs. Streamline development by creating shareable components that enhance collaboration and maintainability.", - children: configuration.libraries, + name: chalk.bold('library'), + aliases: ['l', 'lib'], + description: "Create a workspace shareable library", + children: libraryCommands, ), NodeCommand( - name: 'addon', - description: - "Enhance your project's capabilities! Utilize this command to seamlessly integrate addons like Themes or State Manager Scaffolds into your applications. Elevate your project with advanced functionalities without breaking a sweat.", - children: configuration.addons, + name: chalk.bold('addon'), + description: "Integrate addons into your projects", + children: addonCommands, ), ]); } diff --git a/lib/src/commands/node_command.dart b/lib/src/commands/node_command.dart index cdab4be..fff4c58 100644 --- a/lib/src/commands/node_command.dart +++ b/lib/src/commands/node_command.dart @@ -10,9 +10,13 @@ class NodeCommand extends Command { @override final String description; + @override + final List aliases; + NodeCommand({ required this.name, required this.description, + this.aliases = const [], List> children = const [], }) { addSubcommands(children); diff --git a/lib/src/models/addon_command.dart b/lib/src/models/addon_command.dart deleted file mode 100644 index 84eb24a..0000000 --- a/lib/src/models/addon_command.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:devmy_cli/src/models/brick_command.dart'; -import 'package:devmy_cli/src/models/brick_command_with_questions.dart'; -import 'package:mason/mason.dart'; - -import '../constants/brick_variables.dart'; - -class AddonCommand extends BrickCommandWithQuestions { - AddonCommand({ - required super.name, - required super.brick, - required super.description, - super.fileConflictResolution, - super.questions = const [], - super.aliases = const [], - }); - - @override - String getCommitMessage({ - required Map environment, - required BrickCommand brickCommand, - }) { - String name = ''; - if (environment.containsKey(kBrickApplicationNameEnvironmentVariable)) { - name = (environment[kBrickApplicationNameEnvironmentVariable] as String) - .paramCase; - } - - if (environment.containsKey(kBrickWorkspaceNameEnvironmentVariable)) { - name = (environment[kBrickWorkspaceNameEnvironmentVariable] as String) - .paramCase; - } - - if (environment.containsKey(kBrickLibraryNameEnvironmentVariable)) { - name = (environment[kBrickLibraryNameEnvironmentVariable] as String) - .paramCase; - } - - if (name.isEmpty) { - throw UnimplementedError( - 'invalid command to run an addon: missing brick environment name', - ); - } - - return 'chore($name): added ${brickCommand.name}'; - } -} diff --git a/lib/src/models/addon_command_definition.dart b/lib/src/models/addon_command_definition.dart new file mode 100644 index 0000000..69dfa44 --- /dev/null +++ b/lib/src/models/addon_command_definition.dart @@ -0,0 +1,12 @@ +import 'brick_command_definition.dart'; + +class AddonCommandDefinition extends BrickCommandDefinition { + AddonCommandDefinition({ + required super.name, + required super.brick, + required super.description, + super.fileConflictResolution, + super.questions = const [], + super.aliases = const [], + }); +} diff --git a/lib/src/models/application_command.dart b/lib/src/models/application_command.dart deleted file mode 100644 index d850339..0000000 --- a/lib/src/models/application_command.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'dart:io'; -import 'package:devmy_cli/src/models/brick_command.dart'; -import 'package:devmy_cli/src/models/brick_command_with_questions.dart'; -import 'package:mason/mason.dart'; - -import 'package:devmy_cli/src/constants/constants.dart'; - -class ApplicationCommand extends BrickCommandWithQuestions { - ApplicationCommand( - {required super.name, - required super.brick, - required super.description, - super.questions = const [], - super.aliases = const []}); - - @override - Directory getWorkingDirectory({ - required Map environment, - }) { - if (!environment.containsKey(kBrickApplicationNameEnvironmentVariable)) { - usageException( - 'Missing $kBrickApplicationNameEnvironmentVariable environment. Please, open an issue to support.', - ); - } - - final String applicationName = - environment[kBrickApplicationNameEnvironmentVariable]; - - final applicationUri = Directory.current.uri.resolve( - '$kApplicationBasePath/${applicationName.paramCase}', - ); - - final directory = Directory.fromUri(applicationUri); - directory.createSync( - recursive: true, - ); - - return directory; - } - - @override - String getCommitMessage({ - required Map environment, - required BrickCommand brickCommand, - }) { - final applicationName = - (environment[kBrickApplicationNameEnvironmentVariable] as String) - .paramCase; - - return 'chore($applicationName): added ${brickCommand.name}'; - } -} diff --git a/lib/src/models/application_command_definition.dart b/lib/src/models/application_command_definition.dart new file mode 100644 index 0000000..9676ce2 --- /dev/null +++ b/lib/src/models/application_command_definition.dart @@ -0,0 +1,11 @@ +import 'brick_command_definition.dart'; + +class ApplicationCommandDefinition extends BrickCommandDefinition { + ApplicationCommandDefinition({ + required super.name, + required super.brick, + required super.description, + super.questions = const [], + super.aliases = const [], + }); +} diff --git a/lib/src/models/brick_command.dart b/lib/src/models/brick_command.dart deleted file mode 100644 index 2c5ead8..0000000 --- a/lib/src/models/brick_command.dart +++ /dev/null @@ -1,184 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:chalkdart/chalk.dart'; -import 'package:devmy_cli/devmy_cli.dart'; -import 'package:devmy_cli/src/models/brick_context.dart'; -import 'package:devmy_cli/src/utilities/utilities.dart'; -import 'package:interact/interact.dart'; -import 'package:mason/mason.dart'; - -/// This class is the basic representation of a Mason brick-based command. -/// It provides the basic functionality to load a brick, -/// retrieve the environment variables it needs and, finally, execute it. -abstract class BrickCommand extends Command { - @override - final String name; - @override - final String description; - final GitPath brick; - final FileConflictResolution fileConflictResolution; - @override - final List aliases; - - BrickCommand({ - required this.description, - required this.name, - required this.brick, - this.fileConflictResolution = FileConflictResolution.overwrite, - this.aliases = const [], - }); - - FutureOr getWorkingDirectory({ - required Map environment, - }) { - return Directory.current; - } - - Future> loadEnvironment() async { - return {}; - } - - FutureOr promptCommandVariables( - {required Map environment}) {} - - FutureOr runBrick({ - required Directory workingDirectory, - required BrickContext brickContext, - required Map environment, - required BrickCommand brickCommand, - }) async { - final target = DirectoryGeneratorTarget(workingDirectory); - - print('Running pre generation scripts...'); - await brickContext.generator.hooks.preGen( - vars: environment, - workingDirectory: workingDirectory.path, - onVarsChanged: (nextVars) => updateEnvironment(environment, nextVars), - ); - - final generated = await brickContext.generator.generate( - target, - fileConflictResolution: brickCommand.fileConflictResolution, - vars: environment, - ); - - for (final gen in generated) { - print('${chalk.green.bold(gen.status.name.toUpperCase())} ${gen.path}'); - } - - print('Running post generation scripts...'); - await brickContext.generator.hooks.postGen( - vars: environment, - workingDirectory: workingDirectory.path, - ); - - await runPnpmInstall(); - - await _commitChanges( - brickCommand: brickCommand, - environment: environment, - ); - } - - FutureOr loadBrickContext(GitPath brickPath) async { - final spinner = Spinner( - icon: '🧱', - leftPrompt: (done) => '', // prompts are optional - rightPrompt: (done) => done ? 'Assets loaded 🚀' : 'Loading assets', - ).interact(); - - try { - final brick = await BricksJson.temp().add( - Brick.git(brickPath), - ); - - final path = brick.path; - final bundle = createBundle(Directory(path)); - - MasonGenerator generator = await MasonGenerator.fromBundle(bundle); - spinner.done(); - - return BrickContext(bundle: bundle, generator: generator); - } catch (e) { - reset(); - rethrow; - } - } - - FutureOr promptBundleVariables({ - required Map environment, - required MasonBundle bundle, - }) async { - final bundleEntries = bundle.vars.entries; - - // Excluding already acquired environment variables - final questions = bundleEntries - .where((entry) => !environment.containsKey(entry.key)) - .toList(growable: false); - - for (final question in questions) { - environment[question.key] = question.value.input(); - } - } - - void updateEnvironment( - Map environment, - Map updates, - ) { - for (final entry in updates.entries) { - environment.update(entry.key, (_) => entry.value); - } - } - - String getCommitMessage({ - required Map environment, - required BrickCommand brickCommand, - }); - - Future runPnpmInstall() async { - print('📦 Running pnpm i'); - try { - await Process.run('pnpm', ['i']); - print(chalk.green('📦 pnpm configured successfully 🚀')); - } catch (_) { - print(chalk.yellowBright('⚠️ failed pnpm i')); - } - } - - Future _commitChanges({ - required Map environment, - required BrickCommand brickCommand, - }) async { - print('📚 Staging initial files...'); - Directory cwd = Directory.current; - String? workspaceName = environment[kBrickWorkspaceNameEnvironmentVariable]; - - if (workspaceName != null) { - cwd = Directory.fromUri(cwd.uri.resolve(workspaceName.paramCase)); - } - - await Process.run( - 'git', - ['add', '.'], - workingDirectory: cwd.path, - ); - - final commitMessage = brickCommand.getCommitMessage( - environment: environment, - brickCommand: brickCommand, - ); - - print( - '📚 Committing "$commitMessage"...', - ); - - await Process.run( - 'git', - ['commit', '-m', '"$commitMessage"'], - ); - - print(chalk.green('📚 Git commited successfully! 🚀')); - } -} diff --git a/lib/src/models/brick_command_definition.dart b/lib/src/models/brick_command_definition.dart new file mode 100644 index 0000000..3713a6a --- /dev/null +++ b/lib/src/models/brick_command_definition.dart @@ -0,0 +1,21 @@ +import 'package:mason/mason.dart'; +import 'package:devmy_cli/src/models/question.dart'; + +/// This class is the basic representation of a Mason brick-based command. +abstract class BrickCommandDefinition { + final String name; + final String description; + final GitPath brick; + final FileConflictResolution fileConflictResolution; + final List aliases; + final List questions; + + BrickCommandDefinition({ + required this.description, + required this.name, + required this.brick, + this.fileConflictResolution = FileConflictResolution.overwrite, + this.aliases = const [], + this.questions = const [], + }); +} diff --git a/lib/src/models/brick_command_with_questions.dart b/lib/src/models/brick_command_with_questions.dart deleted file mode 100644 index 2b9443c..0000000 --- a/lib/src/models/brick_command_with_questions.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:devmy_cli/src/models/question.dart'; -import 'package:interact/interact.dart'; - -import 'package:devmy_cli/src/models/addon_command.dart'; -import 'package:devmy_cli/src/models/brick_command.dart'; - -import 'brick_context.dart'; - -abstract class BrickCommandWithQuestions extends BrickCommand { - final List questions; - late List addons; - - BrickCommandWithQuestions( - {required super.brick, - required super.description, - required super.name, - super.fileConflictResolution, - required this.questions, - super.aliases = const []}); - - @override - FutureOr runBrick({ - required Directory workingDirectory, - required BrickContext brickContext, - required Map environment, - required BrickCommand brickCommand, - }) async { - await super.runBrick( - workingDirectory: workingDirectory, - brickContext: brickContext, - environment: environment, - brickCommand: brickCommand, - ); - - await runQuestions( - workingDirectory: workingDirectory, - brickContext: brickContext, - environment: environment, - brickCommand: brickCommand, - ); - } - - Future runQuestions({ - required Directory workingDirectory, - required BrickContext brickContext, - required Map environment, - required BrickCommand brickCommand, - }) async { - if (brickCommand is! BrickCommandWithQuestions) { - return; - } - - for (final question in brickCommand.questions) { - final commands = _inputConfigurationsFromDependencies(question); - for (final command in commands) { - await runBrick( - workingDirectory: workingDirectory, - brickContext: await loadBrickContext(command.brick), - environment: environment, - brickCommand: command, - ); - } - } - } - - List _inputConfigurationsFromDependencies(Question question) { - final optionLabels = [...question.addonNames]; - if (question.isMultiple) { - final selections = MultiSelect( - prompt: question.prompt, - options: optionLabels, - ).interact(); - return selections - .map((index) => optionLabels[index]) - .map((name) => addons.singleWhere((addon) => addon.name == name)) - .toList(growable: false); - } - - if (question.isOptional) { - optionLabels.insert(0, 'none'); - } - - int index = Select( - prompt: question.prompt, - options: optionLabels, - ).interact(); - - if (question.isOptional) { - index--; - } - - if (index == -1) { - return []; - } - - return addons - .where((addon) => addon.name == question.addonNames[index]) - .toList(); - } - - List askMultiSelect(Question question) { - final selections = MultiSelect( - prompt: question.prompt, - options: question.addonNames, - ).interact(); - return selections - .map((index) => question.addonNames[index]) - .map((name) => addons.singleWhere((addon) => addon.name == name)) - .toList(growable: false); - } - - List askRequiredSelect(Question question) { - int index = Select( - prompt: question.prompt, - options: question.addonNames, - ).interact(); - final addonName = question.addonNames[index]; - - return addons.where((addon) => addon.name == addonName).toList(); - } - - List askOptionalSelect(Question question) { - int index = Select( - prompt: question.prompt, - options: ['none', ...question.addonNames], - ).interact(); - - if (index == 0) { - return []; - } - - final addonName = question.addonNames[index - 1]; - - return addons.where((addon) => addon.name == addonName).toList(); - } -} diff --git a/lib/src/models/cli_configuration.dart b/lib/src/models/cli_configuration.dart index 3525fca..bd74867 100644 --- a/lib/src/models/cli_configuration.dart +++ b/lib/src/models/cli_configuration.dart @@ -1,13 +1,13 @@ -import 'package:devmy_cli/src/models/addon_command.dart'; -import 'package:devmy_cli/src/models/application_command.dart'; -import 'package:devmy_cli/src/models/library_command.dart'; -import 'package:devmy_cli/src/models/new_command.dart'; +import 'package:devmy_cli/src/models/addon_command_definition.dart'; +import 'package:devmy_cli/src/models/application_command_definition.dart'; +import 'package:devmy_cli/src/models/library_command_definition.dart'; +import 'package:devmy_cli/src/models/new_definition.dart'; class CliConfiguration { - final List applications; - final List libraries; - final List addons; - final NewCommand new$; + final List applications; + final List libraries; + final List addons; + final NewCommandDefinition new$; const CliConfiguration({ this.applications = const [], diff --git a/lib/src/models/library_command.dart b/lib/src/models/library_command.dart deleted file mode 100644 index bd923b7..0000000 --- a/lib/src/models/library_command.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'dart:io'; - -import 'package:devmy_cli/src/models/brick_command.dart'; - -import 'package:devmy_cli/src/constants/constants.dart'; -import 'package:mason/mason.dart'; - -class LibraryCommand extends BrickCommand { - LibraryCommand({ - required super.description, - required super.name, - required super.brick, - }); - - @override - Directory getWorkingDirectory({ - required Map environment, - }) { - if (!environment.containsKey(kBrickLibraryNameEnvironmentVariable)) { - usageException( - 'Missing $kBrickLibraryNameEnvironmentVariable environment. Please, open an issue to support.', - ); - } - - final String libraryName = - environment[kBrickLibraryNameEnvironmentVariable]; - - final libraryUri = Directory.current.uri.resolve( - '$kLibraryBasePath/${libraryName.paramCase}', - ); - - final directory = Directory.fromUri(libraryUri); - directory.createSync( - recursive: true, - ); - - return directory; - } - - @override - String getCommitMessage({ - required Map environment, - required BrickCommand brickCommand, - }) { - final libraryName = - (environment[kBrickLibraryNameEnvironmentVariable] as String).paramCase; - - return 'chore($libraryName): added ${brickCommand.name}'; - } -} diff --git a/lib/src/models/library_command_definition.dart b/lib/src/models/library_command_definition.dart new file mode 100644 index 0000000..c935aea --- /dev/null +++ b/lib/src/models/library_command_definition.dart @@ -0,0 +1,12 @@ +import 'package:devmy_cli/src/models/brick_command_definition.dart'; + +class LibraryCommandDefinition extends BrickCommandDefinition { + LibraryCommandDefinition({ + required super.name, + required super.brick, + required super.description, + super.fileConflictResolution, + super.questions = const [], + super.aliases = const [], + }); +} diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index dde0a81..5fd5862 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,9 +1,7 @@ -export 'package:devmy_cli/src/models/addon_command.dart'; -export 'package:devmy_cli/src/models/application_command.dart'; -export 'package:devmy_cli/src/models/brick_command.dart'; -export 'package:devmy_cli/src/models/brick_command_with_questions.dart'; +export 'package:devmy_cli/src/models/addon_command_definition.dart'; +export 'package:devmy_cli/src/models/application_command_definition.dart'; +export 'package:devmy_cli/src/models/brick_command_definition.dart'; export 'package:devmy_cli/src/models/cli_configuration.dart'; -export 'package:devmy_cli/src/models/library_command.dart'; -export 'package:devmy_cli/src/models/new_command.dart'; +export 'package:devmy_cli/src/models/library_command_definition.dart'; +export 'package:devmy_cli/src/models/workspace_command_definition.dart'; export 'package:devmy_cli/src/models/question.dart'; -export 'package:devmy_cli/src/models/brick_context.dart'; diff --git a/lib/src/models/new_command.dart b/lib/src/models/new_command.dart deleted file mode 100644 index 5463f7a..0000000 --- a/lib/src/models/new_command.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:mason/mason.dart'; - -import 'package:devmy_cli/src/models/brick_command.dart'; - -import 'package:devmy_cli/src/constants/constants.dart'; - -class NewCommand extends BrickCommand { - NewCommand({ - required String description, - required GitPath brick, - }) : super( - description: description, - name: 'new', - brick: brick, - ); - - @override - FutureOr run() async { - final brickContext = await loadBrickContext(brick); - - final environment = await loadEnvironment(); - - print('prompt command variables'); - await promptBundleVariables( - environment: environment, - bundle: brickContext.bundle, - ); - print('prompt command variables'); - await promptCommandVariables(environment: environment); - - Directory workingDirectory = - await getWorkingDirectory(environment: environment); - - await runBrick( - workingDirectory: workingDirectory, - brickContext: brickContext, - environment: environment, - brickCommand: this, - ); - } - - @override - FutureOr getWorkingDirectory({ - required Map environment, - }) { - if (!environment.containsKey(kBrickWorkspaceNameEnvironmentVariable)) { - usageException( - 'Missing $kBrickWorkspaceNameEnvironmentVariable environment. Please, open an issue to support.', - ); - } - - final String workspaceName = - environment[kBrickWorkspaceNameEnvironmentVariable]; - - final workspaceUri = Directory.current.uri.resolve(workspaceName.paramCase); - - final directory = Directory.fromUri(workspaceUri); - directory.createSync( - recursive: true, - ); - - return directory; - } - - @override - String getCommitMessage({ - required Map environment, - required BrickCommand brickCommand, - }) { - final workspaceName = - (environment[kBrickWorkspaceNameEnvironmentVariable] as String) - .paramCase; - - return 'chore($workspaceName): added ${brickCommand.name}'; - } -} diff --git a/lib/src/models/new_definition.dart b/lib/src/models/new_definition.dart new file mode 100644 index 0000000..053bd2b --- /dev/null +++ b/lib/src/models/new_definition.dart @@ -0,0 +1,13 @@ +import 'package:mason/mason.dart'; +import 'package:devmy_cli/src/models/workspace_command_definition.dart'; + +class NewCommandDefinition extends WorkspaceCommandDefinition { + NewCommandDefinition({ + required String description, + required GitPath brick, + }) : super( + description: description, + name: 'new', + brick: brick, + ); +} diff --git a/lib/src/models/workspace_command_definition.dart b/lib/src/models/workspace_command_definition.dart new file mode 100644 index 0000000..a5ea543 --- /dev/null +++ b/lib/src/models/workspace_command_definition.dart @@ -0,0 +1,12 @@ +import 'package:devmy_cli/src/models/brick_command_definition.dart'; + +class WorkspaceCommandDefinition extends BrickCommandDefinition { + WorkspaceCommandDefinition({ + required super.name, + required super.brick, + required super.description, + super.fileConflictResolution, + super.questions = const [], + super.aliases = const [], + }); +} diff --git a/lib/src/utilities/process.dart b/lib/src/utilities/process.dart new file mode 100644 index 0000000..a1bdf2d --- /dev/null +++ b/lib/src/utilities/process.dart @@ -0,0 +1,62 @@ +import 'dart:io'; + +import 'package:chalkdart/chalk.dart'; + +Future pnpmInstall(Directory workspaceDirectory) async { + print('📦 run pnpm install'); + try { + await Process.run( + 'corepack', + ['pnpm', 'i'], + workingDirectory: workspaceDirectory.path, + ); + } catch (_) { + print(chalk.yellowBright('⚠️ failed pnpm i')); + } +} + +Future initializeGit(Directory workspaceDirectory) async { + try { + await Process.run( + 'git', + ['init', '--initial-branch=main'], + workingDirectory: workspaceDirectory.path, + ); + + print( + '📚 initialize Git', + ); + } catch (_) { + print(chalk.yellowBright('⚠️ failed git initialization')); + } +} + +Future commitChanges( + Directory workspaceDirectory, + String message, +) async { + try { + await Process.run( + 'git', + ['add', '.'], + workingDirectory: workspaceDirectory.path, + ); + + print( + '📚 Commit "$message"', + ); + + await Process.run( + 'git', + [ + 'commit', + '-m', + '"$message"', + "--no-verify", + ], + workingDirectory: workspaceDirectory.path, + ); + } catch (_) { + print(chalk.yellowBright('⚠️ failed git commit')); + } +}