| // 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 'package:args/args.dart'; |
| import 'package:collection/collection.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:yaml/yaml.dart'; |
| import 'package:yaml_edit/yaml_edit.dart'; |
| |
| import '../command.dart'; |
| import '../entrypoint.dart'; |
| import '../exceptions.dart'; |
| import '../git.dart'; |
| import '../io.dart'; |
| import '../log.dart' as log; |
| import '../package.dart'; |
| import '../package_name.dart'; |
| import '../pubspec.dart'; |
| import '../solver.dart'; |
| import '../source/git.dart'; |
| import '../source/hosted.dart'; |
| import '../source/path.dart'; |
| import '../utils.dart'; |
| |
| /// Handles the `add` pub command. Adds a dependency to `pubspec.yaml` and gets |
| /// the package. The user may pass in a git constraint, host url, or path as |
| /// requirements. If no such options are passed in, this command will do a |
| /// resolution to find the latest version of the package that is compatible with |
| /// the other dependencies in `pubspec.yaml`, and then enter that as the lower |
| /// bound in a ^x.y.z constraint. |
| /// |
| /// The descriptor used to be given with args like --path, --sdk, |
| /// --git-<option>. |
| /// |
| /// We still support these arguments, but now the documented way to give the |
| /// descriptor is to give a yaml-descriptor as in pubspec.yaml. |
| class AddCommand extends PubCommand { |
| @override |
| String get name => 'add'; |
| @override |
| String get description => r''' |
| Add dependencies to `pubspec.yaml`. |
| |
| Invoking `dart pub add foo bar` will add `foo` and `bar` to `pubspec.yaml` |
| with a default constraint derived from latest compatible version. |
| |
| Add to dev_dependencies by prefixing with "dev:". |
| |
| Add packages with specific constraints or other sources by giving a descriptor |
| after a colon. |
| |
| For example: |
| * Add a hosted dependency at newest compatible stable version: |
| `$topLevelProgram pub add foo` |
| * Add a hosted dev dependency at newest compatible stable version: |
| `$topLevelProgram pub add dev:foo` |
| * Add a hosted dependency with the given constraint |
| `$topLevelProgram pub add foo:^1.2.3` |
| * Add multiple dependencies: |
| `$topLevelProgram pub add foo dev:bar` |
| * Add a path dependency: |
| `$topLevelProgram pub add 'foo{"path":"../foo"}'` |
| * Add a hosted dependency: |
| `$topLevelProgram pub add 'foo{"hosted":"my-pub.dev"}'` |
| * Add an sdk dependency: |
| `$topLevelProgram pub add 'foo{"sdk":"flutter"}'` |
| * Add a git dependency: |
| `$topLevelProgram pub add 'foo{"git":"https://github.com/foo/foo"}'` |
| * Add a git dependency with a path and ref specified: |
| `$topLevelProgram pub add \ |
| 'foo{"git":{"url":"../foo.git","ref":"branch","path":"subdir"}}'`'''; |
| |
| @override |
| String get argumentsDescription => |
| '[options] [dev:]<package>[:descriptor] [[dev:]<package>[:descriptor] ...]'; |
| @override |
| String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-add'; |
| |
| AddCommand() { |
| argParser.addFlag( |
| 'dev', |
| abbr: 'd', |
| negatable: false, |
| help: 'Adds to the development dependencies instead.', |
| hide: true, |
| ); |
| |
| // Following options are hidden/deprecated in favor of the new syntax: [dev:]<package>[:descriptor] ... |
| // To avoid breaking changes we keep supporting them, but hide them from --help to discourage |
| // further use. Combining these with new syntax will fail. |
| argParser.addOption( |
| 'git-url', |
| help: 'Git URL of the package', |
| hide: true, |
| ); |
| argParser.addOption( |
| 'git-ref', |
| help: 'Git branch or commit to be retrieved', |
| hide: true, |
| ); |
| argParser.addOption( |
| 'git-path', |
| help: 'Path of git package in repository', |
| hide: true, |
| ); |
| argParser.addOption( |
| 'hosted-url', |
| help: 'URL of package host server', |
| hide: true, |
| ); |
| argParser.addOption( |
| 'path', |
| help: 'Add package from local path', |
| hide: true, |
| ); |
| argParser.addOption( |
| 'sdk', |
| help: 'add package from SDK source', |
| allowed: ['flutter'], |
| valueHelp: '[flutter]', |
| hide: true, |
| ); |
| |
| argParser.addFlag('offline', |
| help: 'Use cached packages instead of accessing the network.'); |
| |
| argParser.addFlag('dry-run', |
| abbr: 'n', |
| negatable: false, |
| help: "Report what dependencies would change but don't change any."); |
| |
| argParser.addFlag('precompile', |
| help: 'Build executables in immediate dependencies.'); |
| argParser.addOption('directory', |
| abbr: 'C', help: 'Run this in the directory <dir>.', valueHelp: 'dir'); |
| argParser.addFlag( |
| 'example', |
| help: |
| 'Also update dependencies in `example/` after modifying pubspec.yaml in the root package (if it exists).', |
| hide: true, |
| ); |
| } |
| |
| @override |
| Future<void> runProtected() async { |
| if (argResults.rest.length > 1) { |
| if (argResults.gitUrl != null) { |
| usageException(''' |
| --git-url cannot be used with multiple packages. |
| Specify multiple git packages with descriptors.'''); |
| } else if (argResults.path != null) { |
| usageException(''' |
| --path cannot be used with multiple packages. |
| Specify multiple path packages with descriptors.'''); |
| } else if (argResults.sdk != null) { |
| usageException(''' |
| --sdk cannot be used with multiple packages. |
| Specify multiple sdk packages with descriptors.'''); |
| } |
| } |
| if (argResults.rest.isEmpty) { |
| usageException('Must specify at least one package to be added.'); |
| } |
| |
| final updates = |
| argResults.rest.map((p) => _parsePackage(p, argResults)).toList(); |
| |
| var updatedPubSpec = entrypoint.root.pubspec; |
| for (final update in updates) { |
| /// Perform version resolution in-memory. |
| updatedPubSpec = await _addPackageToPubspec(updatedPubSpec, update); |
| } |
| |
| late SolveResult solveResult; |
| |
| try { |
| /// Use [SolveType.UPGRADE] to solve for the highest version of [package] |
| /// in case [package] was already a transitive dependency. In the case |
| /// where the user specifies a version constraint, this serves to ensure |
| /// that a resolution exists before we update pubspec.yaml. |
| // TODO(sigurdm): We should really use a spinner here. |
| solveResult = await resolveVersions( |
| SolveType.upgrade, cache, Package.inMemory(updatedPubSpec)); |
| } on GitException { |
| final name = updates.first.ref.name; |
| dataError('Unable to resolve package "$name" with the given ' |
| 'git parameters.'); |
| } on SolveFailure catch (e) { |
| dataError(e.message); |
| } on WrappedException catch (e) { |
| /// [WrappedException]s may appear if an invalid [hostUrl] is passed in. |
| dataError(e.message); |
| } |
| |
| /// Verify the results for each package. |
| for (final update in updates) { |
| final ref = update.ref; |
| final name = ref.name; |
| final resultPackage = solveResult.packages |
| .firstWhere((packageId) => packageId.name == name); |
| |
| /// Assert that [resultPackage] is within the original user's expectations. |
| final constraint = update.constraint; |
| if (constraint != null && !constraint.allows(resultPackage.version)) { |
| final dependencyOverrides = updatedPubSpec.dependencyOverrides; |
| if (dependencyOverrides.isNotEmpty) { |
| dataError('"$name" resolved to "${resultPackage.version}" which ' |
| 'does not satisfy constraint "$constraint". This could be ' |
| 'caused by "dependency_overrides".'); |
| } |
| } |
| } |
| if (argResults.isDryRun) { |
| /// Even if it is a dry run, run `acquireDependencies` so that the user |
| /// gets a report on the other packages that might change version due |
| /// to this new dependency. |
| final newRoot = Package.inMemory(updatedPubSpec); |
| |
| await Entrypoint.inMemory(newRoot, cache, |
| solveResult: solveResult, lockFile: entrypoint.lockFile) |
| .acquireDependencies( |
| SolveType.get, |
| dryRun: true, |
| precompile: argResults.shouldPrecompile, |
| analytics: analytics, |
| ); |
| } else { |
| /// Update the `pubspec.yaml` before calling [acquireDependencies] to |
| /// ensure that the modification timestamp on `pubspec.lock` and |
| /// `.dart_tool/package_config.json` is newer than `pubspec.yaml`, |
| /// ensuring that [entrypoint.assertUptoDate] will pass. |
| _updatePubspec( |
| solveResult.packages, |
| updates, |
| ); |
| |
| /// Create a new [Entrypoint] since we have to reprocess the updated |
| /// pubspec file. |
| final updatedEntrypoint = Entrypoint(directory, cache); |
| await updatedEntrypoint.acquireDependencies( |
| SolveType.get, |
| precompile: argResults.shouldPrecompile, |
| analytics: analytics, |
| ); |
| |
| if (argResults.example && entrypoint.example != null) { |
| await entrypoint.example!.acquireDependencies( |
| SolveType.get, |
| precompile: argResults.shouldPrecompile, |
| onlyReportSuccessOrFailure: true, |
| analytics: analytics, |
| ); |
| } |
| } |
| |
| if (isOffline) { |
| log.warning('Warning: Packages added when offline may not resolve to ' |
| 'the latest compatible version available.'); |
| } |
| } |
| |
| /// Creates a new in-memory [Pubspec] by adding [package] to the |
| /// dependencies of [original]. |
| Future<Pubspec> _addPackageToPubspec( |
| Pubspec original, |
| _ParseResult package, |
| ) async { |
| final name = package.ref.name; |
| final dependencies = [...original.dependencies.values]; |
| var devDependencies = [...original.devDependencies.values]; |
| final dependencyNames = dependencies.map((dependency) => dependency.name); |
| final devDependencyNames = |
| devDependencies.map((devDependency) => devDependency.name); |
| final range = |
| package.ref.withConstraint(package.constraint ?? VersionConstraint.any); |
| if (package.isDev) { |
| if (devDependencyNames.contains(name)) { |
| log.message('"$name" is already in "dev_dependencies". ' |
| 'Will try to update the constraint.'); |
| devDependencies.removeWhere((element) => element.name == name); |
| } |
| |
| /// If package is originally in dependencies and we wish to add it to |
| /// dev_dependencies, this is a redundant change, and we should not |
| /// remove the package from dependencies, since it might cause the user's |
| /// code to break. |
| if (dependencyNames.contains(name)) { |
| dataError('"$name" is already in "dependencies". ' |
| 'Use "pub remove $name" to remove it before adding it ' |
| 'to "dev_dependencies"'); |
| } |
| |
| devDependencies.add(range); |
| } else { |
| if (dependencyNames.contains(name)) { |
| log.message( |
| '"$name" is already in "dependencies". Will try to update the constraint.'); |
| dependencies.removeWhere((element) => element.name == name); |
| } |
| |
| /// If package is originally in dev_dependencies and we wish to add it to |
| /// dependencies, we remove the package from dev_dependencies, since it is |
| /// now redundant. |
| if (devDependencyNames.contains(name)) { |
| log.message('"$name" was found in dev_dependencies. ' |
| 'Removing "$name" and adding it to dependencies instead.'); |
| devDependencies.removeWhere((element) => element.name == name); |
| } |
| |
| dependencies.add(range); |
| } |
| |
| return Pubspec( |
| original.name, |
| version: original.version, |
| sdkConstraints: original.sdkConstraints, |
| dependencies: dependencies, |
| devDependencies: devDependencies, |
| dependencyOverrides: original.dependencyOverrides.values, |
| ); |
| } |
| |
| /// Split [arg] on ':' and interpret it with the flags in [argResult] either as |
| /// an old-style or a new-style descriptor to produce a PackageRef]. |
| _ParseResult _parsePackage(String arg, ArgResults argResults) { |
| var isDev = argResults['dev'] as bool; |
| if (arg.startsWith('dev:')) { |
| if (argResults.isDev) { |
| usageException("Cannot combine 'dev:' with --dev"); |
| } |
| isDev = true; |
| arg = arg.substring('dev:'.length); |
| } |
| final nextColon = arg.indexOf(':'); |
| final packageName = nextColon == -1 ? arg : arg.substring(0, nextColon); |
| if (!packageNameRegExp.hasMatch(packageName)) { |
| usageException('Not a valid package name: "$packageName"'); |
| } |
| final descriptor = nextColon == -1 ? null : arg.substring(nextColon + 1); |
| |
| final _PartialParseResult partial; |
| if (argResults.hasOldStyleOptions) { |
| partial = _parseDescriptorOldStyleArgs( |
| packageName, |
| descriptor, |
| argResults, |
| ); |
| } else { |
| partial = _parseDescriptorNewStyle(packageName, descriptor); |
| } |
| |
| return _ParseResult(partial.ref, partial.constraint, isDev: isDev); |
| } |
| |
| /// Parse [descriptor] to return the corresponding [_ParseResult] using the |
| /// arguments given in [argResults] to configure the description. |
| /// |
| /// [descriptor] should be a constraint as parsed by |
| /// [VersionConstraint.parse]. If it fails to parse as a version constraint |
| /// but could parse with [_parseDescriptorNewStyle()] a specific usage |
| /// description is issued. |
| /// |
| /// Examples: |
| /// ``` |
| /// retry |
| /// retry:2.0.0 |
| /// retry:^2.0.0 |
| /// retry:'>=2.0.0' |
| /// retry:'>2.0.0 <3.0.1' |
| /// 'retry:>2.0.0 <3.0.1' |
| /// retry:any |
| /// ``` |
| /// |
| /// If a version constraint is provided when the `--path` or any of the |
| /// `--git-<option>` options are used, a [UsageException] will be thrown. |
| /// |
| /// Packages must either be a git, hosted, sdk, or path package. Mixing of |
| /// options is not allowed and will cause a [UsageException] to be thrown. |
| /// |
| /// If any of the other git options are defined when `--git-url` is not |
| /// defined, an error will be thrown. |
| /// |
| /// The returned [_PartialParseResult] will always have `ref!=null`. |
| _PartialParseResult _parseDescriptorOldStyleArgs( |
| String packageName, |
| String? descriptor, |
| ArgResults argResults, |
| ) { |
| final conflictingFlagSets = [ |
| ['git-url', 'git-ref', 'git-path'], |
| ['hosted-url'], |
| ['path'], |
| ['sdk'], |
| ]; |
| |
| for (final flag |
| in conflictingFlagSets.expand((s) => s).where(argResults.wasParsed)) { |
| final conflictingFlag = conflictingFlagSets |
| .where((s) => !s.contains(flag)) |
| .expand((s) => s) |
| .firstWhereOrNull(argResults.wasParsed); |
| if (conflictingFlag != null) { |
| usageException( |
| 'Packages can only have one source, "pub add" flags "--$flag" and ' |
| '"--$conflictingFlag" are conflicting.'); |
| } |
| } |
| |
| /// We want to allow for [constraint] to take on a `null` value here to |
| /// preserve the fact that the user did not specify a constraint. |
| VersionConstraint? constraint; |
| try { |
| constraint = |
| descriptor == null ? null : VersionConstraint.parse(descriptor); |
| } on FormatException catch (e) { |
| var couldParseAsNewStyle = true; |
| try { |
| _parseDescriptorNewStyle(packageName, descriptor); |
| // If parsing the descriptor as a new-style descriptor succeeds we |
| // can give this more specific error message. |
| } catch (_) { |
| couldParseAsNewStyle = false; |
| } |
| if (couldParseAsNewStyle) { |
| usageException( |
| '--dev, --path, --sdk, --git-url, --git-path and --git-ref cannot be combined with a descriptor.'); |
| } else { |
| usageException('Invalid version constraint: ${e.message}'); |
| } |
| } |
| |
| /// The package to be added. |
| late final PackageRef ref; |
| final path = argResults.path; |
| if (argResults.hasGitOptions) { |
| final gitUrl = argResults.gitUrl; |
| if (gitUrl == null) { |
| usageException('The `--git-url` is required for git dependencies.'); |
| } |
| Uri parsed; |
| try { |
| parsed = Uri.parse(gitUrl); |
| } on FormatException catch (e) { |
| usageException('The --git-url must be a valid url: ${e.message}.'); |
| } |
| |
| /// Process the git options to return the simplest representation to be |
| /// added to the pubspec. |
| |
| ref = PackageRef( |
| packageName, |
| GitDescription( |
| url: parsed.toString(), |
| containingDir: p.current, |
| ref: argResults.gitRef, |
| path: argResults.gitPath, |
| ), |
| ); |
| } else if (path != null) { |
| ref = PackageRef( |
| packageName, PathDescription(p.absolute(path), p.isRelative(path))); |
| } else if (argResults.sdk != null) { |
| ref = cache.sdk.parseRef(packageName, argResults.sdk); |
| } else { |
| ref = PackageRef( |
| packageName, |
| HostedDescription( |
| packageName, |
| argResults.hostedUrl ?? cache.hosted.defaultUrl, |
| ), |
| ); |
| } |
| return _PartialParseResult(ref, constraint); |
| } |
| |
| /// Parse [package] to return the corresponding [_ParseResult]. |
| /// |
| /// [package] must be written in the format |
| /// `<package-name>[:descriptor>]`, where quotations should be used if |
| /// necessary. |
| /// |
| /// `descriptor` is what you would put in a pubspec.yaml in the dependencies |
| /// section. |
| /// |
| /// Assumes that none of '--git-url', '--git-ref', '--git-path', '--path' and |
| /// '--sdk' are present in [argResults]. |
| /// |
| /// |
| /// Examples: |
| /// ``` |
| /// retry |
| /// retry:2.0.0 |
| /// dev:retry:^2.0.0 |
| /// retry:'>=2.0.0' |
| /// retry:'>2.0.0 <3.0.1' |
| /// 'retry:>2.0.0 <3.0.1' |
| /// retry:any |
| /// 'retry:{"path":"../foo"}' |
| /// 'retry:{"git":{"url":"../foo","ref":"branchname"},"version":"^1.2.3"}' |
| /// 'retry:{"sdk":"flutter"}' |
| /// 'retry:{"hosted":"mypub.dev"}' |
| /// ``` |
| /// |
| /// The --path --sdk and --git-<option> arguments cannot be combined with a |
| /// non-string descriptor. |
| /// |
| /// If a version constraint is provided when the `--path` or any of the |
| /// `--git-<option>` options are used, a [PackageParseError] will be thrown. |
| /// |
| /// Packages must either be a git, hosted, sdk, or path package. Mixing of |
| /// options is not allowed and will cause a [PackageParseError] to be thrown. |
| /// |
| /// If any of the other git options are defined when `--git-url` is not |
| /// defined, an error will be thrown. |
| /// |
| /// Returns a `ref` of `null` if the descriptor did not specify a source. |
| /// Then the source will be determined by the old-style arguments. |
| _PartialParseResult _parseDescriptorNewStyle( |
| String packageName, |
| String? descriptor, |
| ) { |
| /// We want to allow for [constraint] to take on a `null` value here to |
| /// preserve the fact that the user did not specify a constraint. |
| VersionConstraint? constraint; |
| |
| /// The package to be added. |
| PackageRef? ref; |
| |
| if (descriptor != null) { |
| try { |
| // An unquoted version constraint is not always valid yaml. |
| // But we want to allow it here anyways. |
| constraint = VersionConstraint.parse(descriptor); |
| } on FormatException { |
| final parsedDescriptor = loadYaml(descriptor); |
| // Use the pubspec parsing mechanism for parsing the descriptor. |
| final Pubspec dummyPubspec; |
| try { |
| dummyPubspec = Pubspec.fromMap({ |
| 'dependencies': { |
| packageName: parsedDescriptor, |
| } |
| }, cache.sources, |
| // Resolve relative paths relative to current, not where the pubspec.yaml is. |
| location: p.toUri(p.join(p.current, 'descriptor'))); |
| } on FormatException catch (e) { |
| usageException('Failed parsing package specification: ${e.message}'); |
| } |
| final range = dummyPubspec.dependencies[packageName]!; |
| if (parsedDescriptor is String) { |
| // Ref will be constructed by the default behavior below. |
| ref = null; |
| } else { |
| ref = range.toRef(); |
| } |
| final hasExplicitConstraint = parsedDescriptor is String || |
| (parsedDescriptor is Map && |
| parsedDescriptor.containsKey('version')); |
| // If the descriptor has an explicit constraint, use that. Otherwise we |
| // infer it. |
| if (hasExplicitConstraint) { |
| constraint = range.constraint; |
| } |
| } |
| } |
| return _PartialParseResult( |
| ref ?? |
| PackageRef( |
| packageName, |
| HostedDescription( |
| packageName, |
| argResults.hostedUrl ?? cache.hosted.defaultUrl, |
| ), |
| ), |
| constraint, |
| ); |
| } |
| |
| /// Writes the changes to the pubspec file. |
| void _updatePubspec( |
| List<PackageId> resultPackages, |
| List<_ParseResult> updates, |
| ) { |
| final yamlEditor = YamlEditor(readTextFile(entrypoint.pubspecPath)); |
| log.io('Reading ${entrypoint.pubspecPath}.'); |
| log.fine('Contents:\n$yamlEditor'); |
| |
| for (final update in updates) { |
| final dependencyKey = update.isDev ? 'dev_dependencies' : 'dependencies'; |
| final constraint = update.constraint; |
| final ref = update.ref; |
| final name = ref.name; |
| final resultId = resultPackages.firstWhere((id) => id.name == name); |
| var description = ref.description; |
| final versionConstraintString = |
| constraint == null ? '^${resultId.version}' : constraint.toString(); |
| late Object? pubspecInformation; |
| if (description is HostedDescription && |
| description.url == cache.hosted.defaultUrl) { |
| pubspecInformation = versionConstraintString; |
| } else { |
| pubspecInformation = { |
| ref.source.name: ref.description.serializeForPubspec( |
| containingDir: entrypoint.root.dir, |
| languageVersion: entrypoint.root.pubspec.languageVersion), |
| if (description is HostedDescription || constraint != null) |
| 'version': versionConstraintString |
| }; |
| } |
| |
| if (yamlEditor.parseAt( |
| [dependencyKey], |
| orElse: () => YamlScalar.wrap(null), |
| ).value == |
| null) { |
| // Handle the case where [dependencyKey] does not already exist. |
| // We ensure it is in Block-style by default. |
| yamlEditor.update( |
| [dependencyKey], |
| wrapAsYamlNode({name: pubspecInformation}, |
| collectionStyle: CollectionStyle.BLOCK)); |
| } else { |
| final packagePath = [dependencyKey, name]; |
| |
| yamlEditor.update(packagePath, pubspecInformation); |
| } |
| |
| /// Remove the package from dev_dependencies if we are adding it to |
| /// dependencies. Refer to [_addPackageToPubspec] for additional discussion. |
| if (!update.isDev) { |
| final devDependenciesNode = yamlEditor |
| .parseAt(['dev_dependencies'], orElse: () => YamlScalar.wrap(null)); |
| |
| if (devDependenciesNode is YamlMap && |
| devDependenciesNode.containsKey(name)) { |
| if (devDependenciesNode.length == 1) { |
| yamlEditor.remove(['dev_dependencies']); |
| } else { |
| yamlEditor.remove(['dev_dependencies', name]); |
| } |
| |
| log.fine('Removed $name from "dev_dependencies".'); |
| } |
| } |
| } |
| |
| /// Windows line endings are already handled by [yamlEditor] |
| writeTextFile(entrypoint.pubspecPath, yamlEditor.toString()); |
| } |
| } |
| |
| class _PartialParseResult { |
| final PackageRef ref; |
| final VersionConstraint? constraint; |
| _PartialParseResult(this.ref, this.constraint); |
| } |
| |
| class _ParseResult { |
| final PackageRef ref; |
| final VersionConstraint? constraint; |
| final bool isDev; |
| _ParseResult(this.ref, this.constraint, {required this.isDev}); |
| } |
| |
| extension on ArgResults { |
| bool get isDev => this['dev']; |
| bool get isDryRun => this['dry-run']; |
| String? get gitUrl => this['git-url']; |
| String? get gitPath => this['git-path']; |
| String? get gitRef => this['git-ref']; |
| String? get hostedUrl => this['hosted-url']; |
| String? get path => this['path']; |
| String? get sdk => this['sdk']; |
| bool get hasOldStyleOptions => |
| hasGitOptions || |
| path != null || |
| sdk != null || |
| hostedUrl != null || |
| isDev; |
| bool get shouldPrecompile => this['precompile']; |
| bool get example => this['example']; |
| bool get hasGitOptions => gitUrl != null || gitRef != null || gitPath != null; |
| } |