[dartdev] add a dartdev 'create' command

Change-Id: I95625a9c422335ba5de92c887afce9eb564d6a04
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/133460
Commit-Queue: Devon Carew <devoncarew@google.com>
Reviewed-by: Jaime Wren <jwren@google.com>
This commit is contained in:
Devon Carew 2020-02-02 02:23:13 +00:00 committed by commit-bot@chromium.org
parent ed441e48d0
commit c8ed304e97
14 changed files with 308 additions and 13 deletions

View file

@ -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

3
DEPS
View file

@ -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"),

View file

@ -8,3 +8,6 @@ pubspec.lock
# Directory created by dartdoc
doc/api/
# Directory created by pub
.dart_tool/

View file

@ -4,4 +4,4 @@ A command-line utility for Dart development.
## Docs
This tool is currently under active development.
This tool is currently under active development.

View file

@ -9,7 +9,7 @@ import 'package:dartdev/dartdev.dart';
/// The entry point for dartdev.
main(List<String> args) async {
final runner = DartdevRunner();
final runner = DartdevRunner(args);
try {
dynamic result = await runner.run(args);
exit(result is int ? result : 0);

View file

@ -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<int> extends CommandRunner {
static const String dartdevDescription =
'A command-line utility for Dart development';
DartdevRunner() : super('dartdev', '$dartdevDescription.') {
DartdevRunner(List<String> 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<int> 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);

View file

@ -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<String> legalTemplateIds = [
'console-full',
'package-simple',
'web-simple'
];
static Iterable<stagehand.Generator> 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} <directory>';
@override
FutureOr<int> 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<int> 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);
}
}

View file

@ -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<int> 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)

View file

@ -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<int> extends Command {
final String _name;
final String _description;

View file

@ -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
pedantic: ^1.8.0

View file

@ -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}');
});
}
}

View file

@ -13,7 +13,9 @@ void main() {
void help() {
TestProject p;
tearDown(() => p?.dispose());
test('--help', () {
p = project();

View file

@ -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();

View file

@ -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