blob: 2df9ad10d381c5d0f815a117dc74ae8ad3e0aa17 [file] [log] [blame]
// Copyright (c) 2021, 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' show utf8;
import 'package:collection/collection.dart' show IterableExtension;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'templates/console.dart';
import 'templates/console_simple.dart';
import 'templates/package.dart';
import 'templates/server_shelf.dart';
import 'templates/web.dart';
final _substituteRegExp = RegExp(r'__([a-zA-Z]+)__');
final _nonValidSubstituteRegExp = RegExp(r'[^a-zA-Z]');
final List<Generator> generators = [
ConsoleGenerator(),
PackageGenerator(),
ServerShelfGenerator(),
WebGenerator(),
// Deprecated generators:
ConsoleSimpleGenerator(),
];
Generator? getGenerator(String id) {
Generator? result;
result = generators.firstWhereOrNull((g) => g.id == id);
result ??= generators.firstWhereOrNull((g) => g.alternateId == id);
return result;
}
/// An abstract class which both defines a template generator and can generate a
/// user project based on this template.
abstract class Generator implements Comparable<Generator> {
final String id;
final String label;
final String description;
final String? alternateId;
final List<String> categories;
final bool deprecated;
final List<TemplateFile> files = [];
TemplateFile? _entrypoint;
/// Lazily initialized cache for lower-case if [id].
///
/// Used by [compareTo].
String? _lowerCaseId;
Generator(
this.id,
this.label,
this.description, {
this.categories = const [],
this.alternateId,
this.deprecated = false,
});
String get lowerCaseId => _lowerCaseId ??= id.toLowerCase();
/// The entrypoint of the application; the main file for the project, which an
/// IDE might open after creating the project.
TemplateFile? get entrypoint => _entrypoint;
TemplateFile addFile(String path, String contents) {
return addTemplateFile(TemplateFile(path, contents));
}
/// Add a new template file.
TemplateFile addTemplateFile(TemplateFile file) {
files.add(file);
return file;
}
/// Return the template file wih the given [path].
TemplateFile? getFile(String path) =>
files.firstWhereOrNull((file) => file.path == path);
/// Set the main entrypoint of this template. This is the 'most important'
/// file of this template. An IDE might use this information to open this file
/// after the user's project is generated.
void setEntrypoint(TemplateFile entrypoint) {
if (_entrypoint != null) throw StateError('entrypoint already set');
_entrypoint = entrypoint;
}
void generate(
String projectName,
GeneratorTarget target, {
Map<String, String>? additionalVars,
}) {
final vars = {
'projectName': projectName,
'description': description,
'year': DateTime.now().year.toString(),
'author': '<your name>',
if (additionalVars != null) ...additionalVars,
};
for (TemplateFile file in files) {
final resultFile = file.runSubstitution(vars);
final filePath = resultFile.path;
target.createFile(filePath, resultFile.content);
}
}
@override
int compareTo(Generator other) => lowerCaseId.compareTo(other.lowerCaseId);
/// Return some user facing instructions about how to finish installation of
/// the template.
///
/// [directory] is the directory of the generated project.
///
/// [scriptPath] is the path of the default target script
/// (e.g., bin/foo.dart) **without** an extension. If null, the implicit run
/// command will be output by default (e.g., dart run).
String getInstallInstructions(
String directory, {
String? scriptPath,
}) {
final buffer = StringBuffer();
buffer.writeln(' cd ${p.relative(directory)}');
if (scriptPath != null) {
buffer.write(' dart run $scriptPath.dart');
} else {
buffer.write(' dart run');
}
return buffer.toString();
}
@override
String toString() => '[$id: $description]';
}
/// An abstract implementation of a [Generator].
abstract class DefaultGenerator extends Generator {
DefaultGenerator(
String id,
String label,
String description, {
String? alternateId,
List<String> categories = const [],
bool deprecated = false,
}) : super(
id,
label,
description,
categories: categories,
alternateId: alternateId,
deprecated: deprecated,
);
}
/// A target for a [Generator]. This class knows how to create files given a
/// path for the file (relative to the particular [GeneratorTarget] instance),
/// and the binary content for the file.
abstract class GeneratorTarget {
/// Create a file at the given path with the given contents.
void createFile(String path, List<int> contents);
}
/// This class represents a file in a generator template. The contents could
/// either be binary or text. If text, the contents may contain mustache
/// variables that can be substituted (`__myVar__`).
class TemplateFile {
final String path;
final String content;
TemplateFile(this.path, this.content);
FileContents runSubstitution(Map<String, String> parameters) {
if (path == 'pubspec.yaml' && parameters['author'] == '<your name>') {
parameters = Map.from(parameters);
parameters['author'] = 'Your Name';
}
final newPath = substituteVars(path, parameters);
final newContents = _createContent(parameters);
return FileContents(newPath, newContents);
}
List<int> _createContent(Map<String, String> vars) {
return utf8.encode(substituteVars(content, vars));
}
}
class FileContents {
final String path;
final List<int> content;
FileContents(this.path, this.content);
}
/// Given a `String` [str] with mustache templates, and a [Map] of String key /
/// value pairs, substitute all instances of `__key__` for `value`. I.e.,
///
/// ```
/// Foo __projectName__ baz.
/// ```
///
/// and
///
/// ```
/// {'projectName': 'bar'}
/// ```
///
/// becomes:
///
/// ```
/// Foo bar baz.
/// ```
///
/// A key value can only be an ASCII string made up of letters: A-Z, a-z.
/// No whitespace, numbers, or other characters are allowed.
@visibleForTesting
String substituteVars(String str, Map<String, String> vars) {
if (vars.keys.any((element) => element.contains(_nonValidSubstituteRegExp))) {
throw ArgumentError.value(
vars, 'vars', 'vars.keys can only contain letters.');
}
return str.replaceAllMapped(
_substituteRegExp, (match) => vars[match[1]!] ?? match[0]!);
}