| // 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/cli.dart'; | |
| 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 = [ | |
| CliGenerator(), | |
| 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>', | |
| ...?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( | |
| super.id, | |
| super.label, | |
| super.description, { | |
| super.alternateId, | |
| super.categories, | |
| super.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]!, | |
| ); | |
| } |