blob: c323f783753cdcbcc72215df2892d968f2966ceb [file] [log] [blame]
// 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-simple';
static List<String> legalTemplateIds = [
'console-simple',
'console-full',
'package-simple',
'web-simple'
];
static Iterable<stagehand.Generator> get generators {
return legalTemplateIds.map(retrieveTemplateGenerator);
}
static stagehand.Generator retrieveTemplateGenerator(String templateId) {
return 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) {
String suffix = g.id == defaultTemplateId ? ' (default)' : '';
return ' ${g.id.padLeft(width)}: ${g.description}$suffix';
}).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);
}
}