From c8ed304e979a283f10e28a4104e0da31a3f114ff Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Sun, 2 Feb 2020 02:23:13 +0000 Subject: [PATCH] [dartdev] add a dartdev 'create' command Change-Id: I95625a9c422335ba5de92c887afce9eb564d6a04 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/133460 Commit-Queue: Devon Carew Reviewed-by: Jaime Wren --- .packages | 1 + DEPS | 3 + pkg/dartdev/.gitignore | 3 + pkg/dartdev/README.md | 2 +- pkg/dartdev/bin/dartdev.dart | 2 +- pkg/dartdev/lib/dartdev.dart | 14 +- pkg/dartdev/lib/src/commands/create.dart | 180 +++++++++++++++++++++ pkg/dartdev/lib/src/commands/format.dart | 7 +- pkg/dartdev/lib/src/core.dart | 3 +- pkg/dartdev/pubspec.yaml | 3 +- pkg/dartdev/test/commands/create_test.dart | 97 +++++++++++ pkg/dartdev/test/commands/flag_test.dart | 2 + pkg/dartdev/test/test_all.dart | 2 + pkg/pkg.status | 2 +- 14 files changed, 308 insertions(+), 13 deletions(-) create mode 100644 pkg/dartdev/lib/src/commands/create.dart create mode 100644 pkg/dartdev/test/commands/create_test.dart diff --git a/.packages b/.packages index 673f9a71206..b6c666c492e 100644 --- a/.packages +++ b/.packages @@ -94,6 +94,7 @@ sourcemap_testing:pkg/sourcemap_testing/lib source_maps:third_party/pkg/source_maps/lib source_span:third_party/pkg/source_span/lib stack_trace:third_party/pkg/stack_trace/lib +stagehand:third_party/pkg/stagehand/lib status_file:pkg/status_file/lib stream_channel:third_party/pkg/stream_channel/lib string_scanner:third_party/pkg/string_scanner/lib diff --git a/DEPS b/DEPS index 191816bb2e3..d261291b265 100644 --- a/DEPS +++ b/DEPS @@ -134,6 +134,7 @@ vars = { "source_maps_tag": "8af7cc1a1c3a193c1fba5993ce22a546a319c40e", "source_span_tag": "1.5.5", "stack_trace_tag": "1.9.3", + "stagehand_tag": "v3.3.6", "stream_channel_tag": "2.0.0", "string_scanner_tag": "1.0.3", "test_descriptor_tag": "1.1.1", @@ -380,6 +381,8 @@ deps = { "@" + Var("source_map_stack_trace_tag"), Var("dart_root") + "/third_party/pkg/stack_trace": Var("dart_git") + "stack_trace.git" + "@" + Var("stack_trace_tag"), + Var("dart_root") + "/third_party/pkg/stagehand": + Var("dart_git") + "stagehand.git" + "@" + Var("stagehand_tag"), Var("dart_root") + "/third_party/pkg/stream_channel": Var("dart_git") + "stream_channel.git" + "@" + Var("stream_channel_tag"), diff --git a/pkg/dartdev/.gitignore b/pkg/dartdev/.gitignore index 96cf44bc4f2..0bd726bc407 100644 --- a/pkg/dartdev/.gitignore +++ b/pkg/dartdev/.gitignore @@ -8,3 +8,6 @@ pubspec.lock # Directory created by dartdoc doc/api/ + +# Directory created by pub +.dart_tool/ diff --git a/pkg/dartdev/README.md b/pkg/dartdev/README.md index c1aeeeb17b5..3d88cff33cf 100644 --- a/pkg/dartdev/README.md +++ b/pkg/dartdev/README.md @@ -4,4 +4,4 @@ A command-line utility for Dart development. ## Docs -This tool is currently under active development. \ No newline at end of file +This tool is currently under active development. diff --git a/pkg/dartdev/bin/dartdev.dart b/pkg/dartdev/bin/dartdev.dart index db8472912ae..796805643a2 100644 --- a/pkg/dartdev/bin/dartdev.dart +++ b/pkg/dartdev/bin/dartdev.dart @@ -9,7 +9,7 @@ import 'package:dartdev/dartdev.dart'; /// The entry point for dartdev. main(List args) async { - final runner = DartdevRunner(); + final runner = DartdevRunner(args); try { dynamic result = await runner.run(args); exit(result is int ? result : 0); diff --git a/pkg/dartdev/lib/dartdev.dart b/pkg/dartdev/lib/dartdev.dart index 84227995870..e07346431dd 100644 --- a/pkg/dartdev/lib/dartdev.dart +++ b/pkg/dartdev/lib/dartdev.dart @@ -6,25 +6,29 @@ import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:cli_util/cli_logging.dart'; +import 'src/commands/create.dart'; import 'src/commands/format.dart'; import 'src/core.dart'; -class DartdevRunner extends CommandRunner { +class DartdevRunner extends CommandRunner { static const String dartdevDescription = 'A command-line utility for Dart development'; - DartdevRunner() : super('dartdev', '$dartdevDescription.') { + DartdevRunner(List args) : super('dartdev', '$dartdevDescription.') { + final bool verbose = args.contains('-v') || args.contains('--verbose'); + argParser.addFlag('verbose', abbr: 'v', negatable: false, help: 'Show verbose output.'); - // The list of currently supported commands: - addCommand(FormatCommand()); + addCommand(CreateCommand(verbose: verbose)); + addCommand(FormatCommand(verbose: verbose)); } @override - Future runCommand(ArgResults results) async { + Future runCommand(ArgResults results) async { isVerbose = results['verbose']; + final Ansi ansi = Ansi(Ansi.terminalSupportsAnsi); log = isVerbose ? Logger.verbose(ansi: ansi) : Logger.standard(ansi: ansi); return await super.runCommand(results); diff --git a/pkg/dartdev/lib/src/commands/create.dart b/pkg/dartdev/lib/src/commands/create.dart new file mode 100644 index 00000000000..513bf6563bc --- /dev/null +++ b/pkg/dartdev/lib/src/commands/create.dart @@ -0,0 +1,180 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; +import 'dart:math' as math; + +import 'package:path/path.dart' as path; +import 'package:stagehand/stagehand.dart' as stagehand; + +import '../core.dart'; +import '../sdk.dart'; + +/// A command to create a new project from a set of templates. +class CreateCommand extends DartdevCommand { + static String defaultTemplateId = 'console-full'; + + static List legalTemplateIds = [ + 'console-full', + 'package-simple', + 'web-simple' + ]; + + static Iterable get generators => + stagehand.generators.where((g) => legalTemplateIds.contains(g.id)); + + static stagehand.Generator retrieveTemplateGenerator(String templateId) => + stagehand.getGenerator(templateId); + + CreateCommand({bool verbose = false}) + : super('create', 'Create a new project.') { + argParser.addOption( + 'template', + allowed: legalTemplateIds, + help: 'The project template to use.', + defaultsTo: defaultTemplateId, + ); + argParser.addFlag('pub', + defaultsTo: true, + help: "Whether to run 'pub get' after the project has been created."); + argParser.addFlag( + 'list-templates', + negatable: false, + hide: !verbose, + help: 'List the available templates in JSON format.', + ); + argParser.addFlag( + 'force', + negatable: false, + help: + 'Force project generation, even if the target directory already exists.', + ); + } + + @override + String get invocation => '${super.invocation} '; + + @override + FutureOr run() async { + if (argResults['list-templates']) { + log.stdout(_availableTemplatesJson()); + return 0; + } + + if (argResults.rest.isEmpty) { + printUsage(); + return 1; + } + + String templateId = argResults['template']; + + String dir = argResults.rest.first; + var targetDir = io.Directory(dir); + if (targetDir.existsSync() && !(argResults['force'])) { + log.stderr( + "Directory '$dir' already exists (use '--force' to force project generation)."); + return 73; + } + + log.stdout( + 'Creating ${log.ansi.emphasized(path.absolute(dir))} using template $templateId...'); + log.stdout(''); + + var generator = retrieveTemplateGenerator(templateId); + await generator.generate( + path.basename(dir), + DirectoryGeneratorTarget(generator, io.Directory(dir)), + ); + + if (argResults['pub']) { + log.stdout(''); + var progress = log.progress('Running pub get'); + var process = await startProcess( + sdk.pub, + ['get', '--no-precompile'], + cwd: dir, + ); + + // Run 'pub get'. We display output from the pub command, but keep the + // output terse. This is to give the user a sense of the work that pub + // did without scrolling the previous stdout sections off the screen. + var buffer = StringBuffer(); + routeToStdout( + process, + logToTrace: true, + listener: (str) { + // Filter lines like '+ multi_server_socket 1.0.2'. + if (!str.startsWith('+ ')) { + buffer.writeln(' $str'); + } + }, + ); + int code = await process.exitCode; + if (code != 0) return code; + progress.finish(showTiming: true); + log.stdout(buffer.toString().trimRight()); + } + + log.stdout(''); + log.stdout('Created project $dir! In order to get started, type:'); + log.stdout(''); + log.stdout(log.ansi.emphasized(' cd ${path.relative(dir)}')); + // TODO(devoncarew): Once we have a 'run' command, print out here how to run + // the app. + log.stdout(''); + + return 0; + } + + @override + String get usageFooter { + int width = legalTemplateIds.map((s) => s.length).reduce(math.max); + String desc = generators + .map((g) => ' ${g.id.padLeft(width)}: ${g.description}') + .join('\n'); + return '\nAvailable templates:\n$desc'; + } + + String _availableTemplatesJson() { + var items = generators.map((stagehand.Generator generator) { + var m = { + 'name': generator.id, + 'label': generator.label, + 'description': generator.description, + 'categories': generator.categories + }; + + if (generator.entrypoint != null) { + m['entrypoint'] = generator.entrypoint.path; + } + + return m; + }); + + JsonEncoder encoder = JsonEncoder.withIndent(' '); + return encoder.convert(items.toList()); + } +} + +class DirectoryGeneratorTarget extends stagehand.GeneratorTarget { + final stagehand.Generator generator; + final io.Directory dir; + + DirectoryGeneratorTarget(this.generator, this.dir) { + dir.createSync(); + } + + @override + Future createFile(String filePath, List contents) async { + io.File file = io.File(path.join(dir.path, filePath)); + + String name = path.relative(file.path, from: dir.path); + log.stdout(' $name'); + + await file.create(recursive: true); + await file.writeAsBytes(contents); + } +} diff --git a/pkg/dartdev/lib/src/commands/format.dart b/pkg/dartdev/lib/src/commands/format.dart index 97c2d7dd63b..32f02062411 100644 --- a/pkg/dartdev/lib/src/commands/format.dart +++ b/pkg/dartdev/lib/src/commands/format.dart @@ -2,16 +2,19 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:async'; + import '../core.dart'; import '../sdk.dart'; class FormatCommand extends DartdevCommand { - FormatCommand() : super('format', 'Format one or more Dart files.') { + FormatCommand({bool verbose = false}) + : super('format', 'Format one or more Dart files.') { // TODO(jwren) add all options and flags } @override - run() async { + FutureOr run() async { // TODO(jwren) implement verbose in dart_style // dartfmt doesn't have '-v' or '--verbose', so remove from the argument list var args = List.from(argResults.arguments) diff --git a/pkg/dartdev/lib/src/core.dart b/pkg/dartdev/lib/src/core.dart index b937f198754..8e0e3d85cb2 100644 --- a/pkg/dartdev/lib/src/core.dart +++ b/pkg/dartdev/lib/src/core.dart @@ -8,11 +8,10 @@ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:cli_util/cli_logging.dart'; -Ansi ansi = Ansi(Ansi.terminalSupportsAnsi); Logger log; bool isVerbose = false; -abstract class DartdevCommand extends Command { +abstract class DartdevCommand extends Command { final String _name; final String _description; diff --git a/pkg/dartdev/pubspec.yaml b/pkg/dartdev/pubspec.yaml index 65aaee20d59..57ba7ac5017 100644 --- a/pkg/dartdev/pubspec.yaml +++ b/pkg/dartdev/pubspec.yaml @@ -10,9 +10,10 @@ dependencies: cli_util: ^0.1.0 intl: ^0.16.0 path: ^1.6.2 + stagehand: 3.3.6 watcher: ^0.9.7+13 yaml: ^2.2.0 dev_dependencies: test: ^1.0.0 - pedantic: ^1.8.0 \ No newline at end of file + pedantic: ^1.8.0 diff --git a/pkg/dartdev/test/commands/create_test.dart b/pkg/dartdev/test/commands/create_test.dart new file mode 100644 index 00000000000..6a63cf7bddb --- /dev/null +++ b/pkg/dartdev/test/commands/create_test.dart @@ -0,0 +1,97 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:dartdev/src/commands/create.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import '../utils.dart'; + +void main() { + group('create', defineCreate); +} + +void defineCreate() { + TestProject p; + + setUp(() => p = null); + + tearDown(() => p?.dispose()); + + test('default template exists', () { + expect(CreateCommand.legalTemplateIds, + contains(CreateCommand.defaultTemplateId)); + }); + + test('all templates exist', () { + for (String templateId in CreateCommand.legalTemplateIds) { + expect(CreateCommand.legalTemplateIds, contains(templateId)); + } + }); + + test('list templates', () { + p = project(); + + ProcessResult result = p.runSync('create', ['--list-templates']); + expect(result.exitCode, 0); + + String output = result.stdout.toString(); + var parsedResult = jsonDecode(output); + expect(parsedResult, hasLength(CreateCommand.legalTemplateIds.length)); + expect(parsedResult[0]['name'], isNotNull); + expect(parsedResult[0]['label'], isNotNull); + expect(parsedResult[0]['description'], isNotNull); + }); + + test('no directory given', () { + p = project(); + + ProcessResult result = p.runSync('create'); + expect(result.exitCode, 1); + }); + + test('directory already exists', () { + p = project(); + + ProcessResult result = p.runSync('create', [ + '--no-pub', + '--template', + CreateCommand.defaultTemplateId, + p.dir.path + ]); + expect(result.exitCode, 73); + }); + + test('bad template id', () { + p = project(); + + ProcessResult result = + p.runSync('create', ['--no-pub', '--template', 'foo-bar', p.dir.path]); + expect(result.exitCode, isNot(0)); + }); + + // Create tests for each template. + for (String templateId in CreateCommand.legalTemplateIds) { + test('create $templateId', () { + p = project(); + + ProcessResult result = p.runSync('create', + ['--force', '--no-pub', '--template', templateId, p.dir.path]); + expect(result.exitCode, 0); + + String projectName = path.basename(p.dir.path); + + String entry = + CreateCommand.retrieveTemplateGenerator(templateId).entrypoint.path; + entry = entry.replaceAll('__projectName__', projectName); + File entryFile = File(path.join(p.dir.path, entry)); + + expect(entryFile.existsSync(), true, + reason: 'File not found: ${entryFile.path}'); + }); + } +} diff --git a/pkg/dartdev/test/commands/flag_test.dart b/pkg/dartdev/test/commands/flag_test.dart index 1e6caddfd17..809f3e98f34 100644 --- a/pkg/dartdev/test/commands/flag_test.dart +++ b/pkg/dartdev/test/commands/flag_test.dart @@ -13,7 +13,9 @@ void main() { void help() { TestProject p; + tearDown(() => p?.dispose()); + test('--help', () { p = project(); diff --git a/pkg/dartdev/test/test_all.dart b/pkg/dartdev/test/test_all.dart index 9cef9d364f0..9b3a5d90830 100644 --- a/pkg/dartdev/test/test_all.dart +++ b/pkg/dartdev/test/test_all.dart @@ -4,12 +4,14 @@ import 'package:test/test.dart'; +import 'commands/create_test.dart' as create; import 'commands/flag_test.dart' as flag; import 'commands/format_test.dart' as format; import 'utils_test.dart' as utils; main() { group('dartdev', () { + create.main(); flag.main(); format.main(); utils.main(); diff --git a/pkg/pkg.status b/pkg/pkg.status index d5de3970487..53d5295aca9 100644 --- a/pkg/pkg.status +++ b/pkg/pkg.status @@ -73,7 +73,7 @@ analyzer_plugin/test/*: SkipByDesign # Only meant to run on vm analyzer_plugin/tool/*: SkipByDesign # Only meant to run on vm build_integration/test/*: SkipByDesign # Only meant to run on vm, most use dart:mirrors and dart:io compiler/tool/*: SkipByDesign # Only meant to run on vm -dartdev/test/command_test: SkipByDesign # Only meant to run on vm (uses dart:io) +dartdev/test/*: SkipByDesign # Only meant to run on vm dartfix/test/*: SkipByDesign # Only meant to run on vm front_end/test/*: SkipByDesign # Only meant to run on vm, most use dart:mirrors and dart:io front_end/tool/*: SkipByDesign # Only meant to run on vm