| // Copyright (c) 2024, 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' hide json; |
| import 'dart:io'; |
| |
| import 'package:collection/collection.dart'; |
| import 'package:crypto/crypto.dart' show sha256; |
| |
| import 'encoded_asset.dart'; |
| import 'extension.dart'; |
| import 'hooks/syntax.g.dart'; |
| import 'metadata.dart'; |
| import 'user_defines.dart'; |
| import 'utils/datetime.dart'; |
| |
| /// The input for `hook/build.dart` or `hook/link.dart`. |
| /// |
| /// The shared properties of a [LinkInput] and a [BuildInput]. This abstraction |
| /// makes it easier to design APIs intended for both kinds of build hooks, |
| /// building and linking. |
| sealed class HookInput { |
| /// The underlying json configuration of this [HookInput]. |
| Map<String, Object?> get json => _syntax.json; |
| |
| /// The directory in which output and intermediate artifacts that are unique |
| /// to the [config] can be placed. |
| /// |
| /// This directory is unique per hook and per [config]. |
| /// |
| /// The contents of this directory will not be modified by anything else than |
| /// the hook itself. |
| /// |
| /// The invoker of the the hook will ensure concurrent invocations wait on |
| /// each other. |
| Uri get outputDirectory { |
| if (_cachedOutputDirectory != null) { |
| return _cachedOutputDirectory!.uri; |
| } |
| final checksum = config.computeChecksum(); |
| final directory = Directory.fromUri( |
| outputDirectoryShared.resolve('$checksum/'), |
| ); |
| if (!directory.existsSync()) { |
| directory.createSync(recursive: true); |
| } |
| _cachedOutputDirectory = directory; |
| return directory.uri; |
| } |
| |
| Directory? _cachedOutputDirectory; |
| |
| /// The directory in which shared output and intermediate artifacts can be |
| /// placed. |
| /// |
| /// This directory is unique per hook. |
| /// |
| /// The contents of this directory will not be modified by anything else than |
| /// the hook itself. |
| /// |
| /// The invoker of the the hook will ensure concurrent invocations wait on |
| /// each other. |
| Uri get outputDirectoryShared => _syntax.outDirShared; |
| |
| /// The file to write the [HookOutput] to at the end of a hook invocation. |
| Uri get outputFile; |
| |
| /// The name of the package the assets are built for. |
| String get packageName => _syntax.packageName; |
| |
| /// The root of the package the assets are built for. |
| /// |
| /// Often a package's assets are built because a package is a dependency of |
| /// another. For this it is convenient to know the packageRoot. |
| Uri get packageRoot => _syntax.packageRoot; |
| |
| final HookInputSyntax _syntax; |
| |
| HookInput(Map<String, Object?> json) |
| : _syntax = HookInputSyntax.fromJson(json) { |
| // Trigger validation, remove with cleanup. |
| outputDirectory; |
| outputDirectoryShared; |
| } |
| |
| @override |
| String toString() => const JsonEncoder.withIndent(' ').convert(json); |
| |
| HookConfig get config => HookConfig._(this); |
| |
| /// The user-defines for this [packageName]. |
| HookInputUserDefines get userDefines => HookInputUserDefines._(this); |
| } |
| |
| /// The user-defines in [HookInput.userDefines]. |
| final class HookInputUserDefines { |
| final HookInput _input; |
| |
| HookInputUserDefines._(this._input); |
| |
| /// The value for the user-define for [key] for this package. |
| /// |
| /// This can be arbitrary JSON/YAML if provided from the SDK from such source. |
| /// If it's provided from command-line arguments, it's likely a string. |
| Object? operator [](String key) { |
| final syntaxNode = _input._syntax.userDefines; |
| if (syntaxNode == null) { |
| return null; |
| } |
| final packageUserDefines = PackageUserDefinesSyntaxExtension.fromSyntax( |
| syntaxNode, |
| ); |
| final pubspecSource = packageUserDefines.workspacePubspec; |
| return pubspecSource?.defines[key]; |
| } |
| |
| /// The absolute path for user-defines for [key] for this package.key |
| /// |
| /// The relative path passed as user-define is resolved against the base path. |
| /// For user-defines originating from a JSON/YAML, the base path is this |
| /// JSON/YAML. For user-defines originating from command-line aruments, the |
| /// base path is the working directory of the command-line invocation. |
| /// |
| /// If the user-define is `null` or not a [String], returns `null`. |
| Uri? path(String key) { |
| final syntaxNode = _input._syntax.userDefines; |
| if (syntaxNode == null) { |
| return null; |
| } |
| final packageUserDefines = PackageUserDefinesSyntaxExtension.fromSyntax( |
| syntaxNode, |
| ); |
| final pubspecSource = packageUserDefines.workspacePubspec; |
| final sources = <PackageUserDefinesSource>[]; |
| if (pubspecSource != null) { |
| sources.add(pubspecSource); |
| } |
| // TODO(https://github.com/dart-lang/native/issues/2215): Add commandline |
| // arguments. |
| for (final source in sources) { |
| final relativepath = source.defines[key]; |
| if (relativepath is String) { |
| return source.basePath.resolve(relativepath); |
| } |
| } |
| return null; |
| } |
| } |
| |
| /// The builder for [HookInput]. |
| sealed class HookInputBuilder { |
| final _syntax = HookInputSyntax.fromJson({}) |
| ..config = ConfigSyntax(buildAssetTypes: [], extensions: null); |
| |
| Map<String, Object?> get json => _syntax.json; |
| |
| void setupShared({ |
| required Uri packageRoot, |
| required String packageName, |
| required Uri outputDirectoryShared, |
| required Uri outputFile, |
| PackageUserDefines? userDefines, |
| }) { |
| _syntax.packageRoot = packageRoot; |
| _syntax.packageName = packageName; |
| _syntax.outDirShared = outputDirectoryShared; |
| _syntax.outFile = outputFile; |
| _syntax.userDefines = userDefines?.toSyntax(); |
| } |
| |
| /// Constructs a checksum for a [BuildInput]. |
| /// |
| /// This can be used to construct an output directory name specific to the |
| /// [HookConfig] being built with this builder. It is therefore assumed the |
| /// output directory has not been set yet. |
| String computeChecksum() => _jsonChecksum(_syntax.config.json); |
| |
| HookConfigBuilder get config => HookConfigBuilder._(this); |
| |
| /// Adds the protocol extension to this hook input. |
| /// |
| /// This will [HookConfigBuilder.addBuildAssetTypes] and add the extensions' |
| /// config to [config]. |
| void addExtension(ProtocolExtension extension); |
| } |
| |
| String _jsonChecksum(Map<String, Object?> json) { |
| final hash = sha256 |
| .convert(const JsonEncoder().fuse(const Utf8Encoder()).convert(json)) |
| .toString() |
| // 256 bit hashes lead to 64 hex character strings. |
| // To avoid overflowing file paths limits, only use 32. |
| // Using 16 hex characters would also be unlikely to have collisions. |
| .substring(0, 32); |
| return hash; |
| } |
| |
| /// The input for `hook/build.dart`. |
| final class BuildInput extends HookInput { |
| @override |
| Uri get outputFile => _syntax.outFile; |
| |
| final BuildInputSyntax _syntaxBuildInput; |
| |
| BuildInput(super.json) : _syntaxBuildInput = BuildInputSyntax.fromJson(json); |
| |
| @override |
| BuildConfig get config => BuildConfig._(this); |
| |
| /// The assets emitted by `hook/build.dart` of direct dependencies with [ToBuildHooks]. |
| BuildInputAssets get assets => BuildInputAssets._(this); |
| |
| /// The metadata emitted by dependent build hooks. |
| BuildInputMetadata get metadata => BuildInputMetadata._(this); |
| } |
| |
| /// The metadata in [BuildInput.metadata]. |
| final class BuildInputMetadata { |
| final BuildInput _input; |
| |
| BuildInputMetadata._(this._input); |
| |
| /// The metadata emitted by the build hook of package [packageName]. |
| PackageMetadata operator [](String packageName) => PackageMetadata( |
| (_input.assets.encodedAssets[packageName] ?? []) |
| .where((e) => e.isMetadataAsset) |
| .map((e) => e.asMetadataAsset) |
| .toList(), |
| ); |
| } |
| |
| /// The metadata from a specific package, available in [BuildInput.metadata]. |
| final class PackageMetadata { |
| PackageMetadata(this._metadata); |
| |
| final List<MetadataAsset> _metadata; |
| |
| Object? operator [](String key) => |
| _metadata.firstWhereOrNull((e) => e.key == key)?.value; |
| } |
| |
| /// The assets in [BuildInput.assets]. |
| final class BuildInputAssets { |
| final BuildInput _input; |
| |
| BuildInputAssets._(this._input); |
| |
| Map<String, List<EncodedAsset>> get encodedAssets => { |
| for (final MapEntry(:key, :value) |
| in (_input._syntaxBuildInput.assets ?? {}).entries) |
| key: _parseAssets(value), |
| }; |
| |
| List<EncodedAsset> operator [](String packageName) => |
| encodedAssets[packageName] ?? []; |
| } |
| |
| /// The builder for [BuildInput]. |
| final class BuildInputBuilder extends HookInputBuilder { |
| @override |
| BuildInputSyntax get _syntax => BuildInputSyntax.fromJson(super._syntax.json); |
| |
| void setupBuildInput({Map<String, List<EncodedAsset>>? assets}) { |
| _syntax.setup( |
| assets: assets == null |
| ? null |
| : { |
| for (final MapEntry(:key, :value) in assets.entries) |
| key: [ |
| for (final asset in value) |
| AssetSyntax.fromJson(asset.toJson()), |
| ], |
| }, |
| ); |
| } |
| |
| @override |
| BuildConfigBuilder get config => BuildConfigBuilder._(this); |
| |
| @override |
| void addExtension(ProtocolExtension extension) => |
| extension.setupBuildInput(this); |
| |
| BuildInput build() => BuildInput(json); |
| } |
| |
| /// The builder for [HookConfig]. |
| final class HookConfigBuilder { |
| final HookInputBuilder builder; |
| |
| HookConfigBuilder._(this.builder); |
| |
| ConfigSyntax get _syntax => builder._syntax.config; |
| |
| Map<String, Object?> get json => _syntax.json; |
| |
| void addBuildAssetTypes(Iterable<String> assetTypes) { |
| _syntax.buildAssetTypes.addAll(assetTypes); |
| } |
| } |
| |
| /// The builder for [BuildConfig]. |
| final class BuildConfigBuilder extends HookConfigBuilder { |
| @override |
| late final BuildConfigSyntax _syntax = BuildConfigSyntax.fromJson( |
| super._syntax.json, |
| ); |
| |
| BuildConfigBuilder._(super.builder) : super._(); |
| |
| void setupBuild({required bool linkingEnabled}) { |
| _syntax.setup(linkingEnabled: linkingEnabled); |
| } |
| } |
| |
| /// The input for a `hook/link.dart`. |
| final class LinkInput extends HookInput { |
| List<EncodedAsset> get _encodedAssets { |
| final assets = _syntaxLinkInput.assets; |
| return _parseAssets(assets); |
| } |
| |
| Uri? get recordedUsagesFile => _syntaxLinkInput.resourceIdentifiers; |
| |
| @override |
| Uri get outputFile => _syntax.outFile; |
| |
| final LinkInputSyntax _syntaxLinkInput; |
| |
| LinkInput(super.json) : _syntaxLinkInput = LinkInputSyntax.fromJson(json) { |
| // Run validation. |
| _encodedAssets; |
| } |
| |
| @override |
| LinkConfig get config => LinkConfig._(this); |
| |
| LinkInputAssets get assets => LinkInputAssets._(this); |
| } |
| |
| /// The assets in [LinkInput.assets]; |
| final class LinkInputAssets { |
| final LinkInput _input; |
| |
| LinkInputAssets._(this._input); |
| |
| List<EncodedAsset> get encodedAssets => _input._encodedAssets; |
| } |
| |
| /// The builder for [LinkInput]. |
| final class LinkInputBuilder extends HookInputBuilder { |
| @override |
| LinkInputSyntax get _syntax => LinkInputSyntax.fromJson(super._syntax.json); |
| |
| void setupLink({ |
| required List<EncodedAsset> assets, |
| required Uri? recordedUsesFile, |
| }) { |
| _syntax.setup( |
| assets: [ |
| for (final asset in assets) AssetSyntax.fromJson(asset.toJson()), |
| ], |
| resourceIdentifiers: recordedUsesFile, |
| ); |
| } |
| |
| @override |
| void addExtension(ProtocolExtension extension) => |
| extension.setupLinkInput(this); |
| |
| @override |
| LinkConfigBuilder get config => LinkConfigBuilder._(this); |
| |
| LinkInput build() => LinkInput(json); |
| } |
| |
| /// The builder for [BuildConfig]. |
| final class LinkConfigBuilder extends HookConfigBuilder { |
| @override |
| late final BuildConfigSyntax _syntax = BuildConfigSyntax.fromJson( |
| super._syntax.json, |
| ); |
| |
| LinkConfigBuilder._(super.builder) : super._(); |
| } |
| |
| List<EncodedAsset> _parseAssets(List<AssetSyntax>? assets) => assets == null |
| ? [] |
| : [ |
| for (final asset in assets) |
| EncodedAsset.fromJson(asset.json, asset.path), |
| ]; |
| |
| /// The output from a `hook/build.dart` or `hook/link.dart`. |
| /// |
| /// The shared properties of a [BuildOutput] and a [LinkOutput]. This |
| /// abstraction makes it easier to design APIs intended for both kinds of build |
| /// hooks, building and linking. |
| sealed class HookOutput { |
| /// The underlying json configuration of this [HookOutput]. |
| Map<String, Object?> get json => _syntax.json; |
| |
| /// Start time for the build of this output. |
| /// |
| /// The [timestamp] is rounded down to whole seconds, because |
| /// [File.lastModified] is rounded to whole seconds and caching logic compares |
| /// these timestamps. |
| DateTime get timestamp => DateTime.parse(_syntax.timestamp); |
| |
| /// The files used by this build. |
| /// |
| /// If any of the files in [dependencies] are modified after [timestamp], the |
| /// build will be re-run. |
| /// |
| /// The (transitive) Dart sources do not have to be added to these |
| /// dependencies, only non-Dart files. (Note that old Dart and Flutter SDKs |
| /// do not automatically add the Dart sources. So builds get wrongly cached, |
| /// try updating to the latest release.) |
| List<Uri> get dependencies => _syntax.dependencies ?? []; |
| |
| /// The assets produced by this build. |
| List<EncodedAsset> get _encodedAssets => _parseAssets(_syntax.assets); |
| |
| HookOutputSyntax get _syntax; |
| |
| HookOutput._(Map<String, Object?> json); |
| |
| @override |
| String toString() => const JsonEncoder.withIndent(' ').convert(json); |
| } |
| |
| /// The builder for [HookOutput]. |
| sealed class HookOutputBuilder { |
| final _syntax = HookOutputSyntax( |
| timestamp: DateTime.now().roundDownToSeconds().toString(), |
| assets: null, |
| dependencies: null, |
| ); |
| |
| Map<String, Object?> get json => _syntax.json; |
| |
| HookOutputBuilder(); |
| |
| /// Adds file used by this build. |
| /// |
| /// If any of the files are modified after [BuildOutput.timestamp], the |
| // build will be re-run. |
| void addDependency(Uri uri) { |
| final dependencies = _syntax.dependencies ?? []; |
| dependencies.add(uri); |
| _syntax.dependencies = dependencies; |
| } |
| |
| /// Adds files used by this build. |
| /// |
| /// If any of the files are modified after [BuildOutput.timestamp], the |
| // build will be re-run. |
| void addDependencies(Iterable<Uri> uris) { |
| final dependencies = _syntax.dependencies ?? []; |
| dependencies.addAll(uris); |
| _syntax.dependencies = dependencies; |
| } |
| } |
| |
| /// The output from a `hook/build.dart`. |
| final class BuildOutput extends HookOutput { |
| /// The assets produced by this build which should be linked. |
| /// |
| /// Every key in the map is a package name. These assets in the values are not |
| /// bundled with the application, but are sent to the link hook of the package |
| /// specified in the key, which can decide if they are bundled or not. |
| Map<String, List<EncodedAsset>> get _encodedAssetsForLinking => { |
| for (final MapEntry(:key, :value) |
| in (_syntax.assetsForLinking ?? {}).entries) |
| key: _parseAssets(value), |
| }; |
| |
| List<EncodedAsset> get _encodedAssetsForBuild => |
| _parseAssets(_syntax.assetsForBuild ?? []); |
| |
| @override |
| final BuildOutputSyntax _syntax; |
| |
| /// Creates a [BuildOutput] from the given [json]. |
| BuildOutput(super.json) |
| : _syntax = BuildOutputSyntax.fromJson(json), |
| super._(); |
| |
| /// The assets produced by this build. |
| BuildOutputAssets get assets => BuildOutputAssets._(this); |
| } |
| |
| /// The assets in [BuildOutput.assets]. |
| final class BuildOutputAssets { |
| final BuildOutput _output; |
| |
| BuildOutputAssets._(this._output); |
| |
| /// The assets produced by this build. |
| List<EncodedAsset> get encodedAssets => _output._encodedAssets; |
| |
| /// The assets produced by this build which should be linked. |
| /// |
| /// Every key in the map is a package name. These assets in the values are not |
| /// bundled with the application, but are sent to the link hook of the package |
| /// specified in the key, which can decide if they are bundled or not. |
| Map<String, List<EncodedAsset>> get encodedAssetsForLinking => |
| _output._encodedAssetsForLinking; |
| |
| List<EncodedAsset> get encodedAssetsForBuild => |
| _output._encodedAssetsForBuild; |
| } |
| |
| /// The builder for [BuildOutput]. |
| /// |
| /// There are various Dart extensions on this [BuildOutputBuilder] that allow |
| /// adding specific asset types - which should be used by normal hook authors. |
| /// For example |
| /// |
| /// ```dart |
| /// main(List<String> arguments) async { |
| /// await build((input, output) { |
| /// output.assets.code.add(CodeAsset(...)); |
| /// output.assets.data.add(DataAsset(...)); |
| /// }); |
| /// } |
| /// ``` |
| final class BuildOutputBuilder extends HookOutputBuilder { |
| BuildOutputMetadataBuilder get metadata => BuildOutputMetadataBuilder._(this); |
| |
| BuildOutputAssetsBuilder get assets => BuildOutputAssetsBuilder._(this); |
| |
| @override |
| BuildOutputSyntax get _syntax => |
| BuildOutputSyntax.fromJson(super._syntax.json); |
| |
| BuildOutput build() => BuildOutput(json); |
| } |
| |
| /// The builder for [BuildOutputBuilder.metadata]. |
| final class BuildOutputMetadataBuilder { |
| final BuildOutputBuilder _output; |
| |
| BuildOutputMetadataBuilder._(this._output); |
| |
| void operator []=(String key, Object value) { |
| _output.assets.addEncodedAsset( |
| MetadataAsset(key: key, value: value).encode(), |
| routing: const ToBuildHooks(), |
| ); |
| } |
| |
| void addAll(Map<String, Object> metadata) { |
| for (final MapEntry(:key, :value) in metadata.entries) { |
| this[key] = value; |
| } |
| } |
| } |
| |
| /// The destination for assets in the [BuildOutput]. |
| /// |
| /// Currently supported routings: |
| /// * [ToBuildHooks]: From build hook to all dependent builds hooks. |
| /// * [ToLinkHook]: From build hook to a specific link hook. |
| /// * [ToAppBundle]: From build or link hook to the application Bundle. |
| sealed class AssetRouting { |
| const AssetRouting(); |
| } |
| |
| /// Assets with this [AssetRouting] in the [BuildOutput] will be sent to the SDK |
| /// to be bundled with the app. |
| final class ToAppBundle extends AssetRouting { |
| const ToAppBundle(); |
| } |
| |
| /// Assets with this [AssetRouting] in the [BuildOutput] will be sent to build |
| /// hooks. |
| /// |
| /// The assets are only available for build hooks of packages that have a direct |
| /// dependency on the package emitting the asset with this routing. |
| /// |
| /// The assets will not be bundled in the final application unless also added |
| /// with [ToAppBundle]. Prefer bundling the asset in the sending hook, otherwise |
| /// multiple receivers might try to bundle the asset leading to duplicate assets |
| /// in the app bundle. |
| /// |
| /// The receiver will know about sender package (it must be a direct |
| /// dependency), the sender does not know about the receiver. Hence this routing |
| /// is a broadcast with 0-N receivers. |
| final class ToBuildHooks extends AssetRouting { |
| const ToBuildHooks(); |
| } |
| |
| /// Assets with this [AssetRouting] in the [BuildOutput] will be sent to the |
| /// link hook of [packageName]. |
| /// |
| /// The assets are only available to the link hook of [packageName]. |
| /// |
| /// The assets will not be bundled in the final application unless added with |
| /// [ToAppBundle] in the link hook of [packageName]. |
| /// |
| /// The receiver will not know about the sender package. The sender knows about |
| /// the receiver package. Hence, the receiver must be specified and there is |
| /// exactly one receiver. |
| final class ToLinkHook extends AssetRouting { |
| final String packageName; |
| |
| const ToLinkHook(this.packageName); |
| } |
| |
| /// The builder for [BuildOutputAssets]. |
| final class BuildOutputAssetsBuilder { |
| final BuildOutputBuilder _output; |
| |
| BuildOutputAssetsBuilder._(this._output); |
| |
| /// Adds [EncodedAsset]s produced by this build. |
| /// |
| /// The asset is routed according to [routing]. |
| /// |
| /// Note to hook writers. Prefer using the `.add` method on the extension for |
| /// the specific asset type being added: |
| /// |
| /// ```dart |
| /// main(List<String> arguments) async { |
| /// await build((input, output) { |
| /// output.assets.code.add(CodeAsset(...)); |
| /// output.assets.data.add(DataAsset(...)); |
| /// }); |
| /// } |
| /// ``` |
| void addEncodedAsset( |
| EncodedAsset asset, { |
| AssetRouting routing = const ToAppBundle(), |
| }) { |
| switch (routing) { |
| case ToAppBundle(): |
| final assets = _syntax.assets ?? []; |
| assets.add(AssetSyntax.fromJson(asset.toJson())); |
| _syntax.assets = assets; |
| case ToBuildHooks(): |
| final assets = _syntax.assetsForBuild ?? []; |
| assets.add(AssetSyntax.fromJson(asset.toJson())); |
| _syntax.assetsForBuild = assets; |
| case ToLinkHook(): |
| final packageName = routing.packageName; |
| final assetsForLinking = _syntax.assetsForLinking ?? {}; |
| assetsForLinking[packageName] ??= []; |
| assetsForLinking[packageName]!.add( |
| AssetSyntax.fromJson(asset.toJson()), |
| ); |
| _syntax.assetsForLinking = assetsForLinking; |
| } |
| } |
| |
| /// Adds [EncodedAsset]s produced by this build. |
| /// |
| /// The asset is routed according to [routing]. |
| /// |
| /// Note to hook writers. Prefer using the `.addAll` method on the extension |
| /// for the specific asset type being added: |
| /// |
| /// ```dart |
| /// main(List<String> arguments) async { |
| /// await build((input, output) { |
| /// output.assets.code.addAll([CodeAsset(...), ...]); |
| /// output.assets.data.addAll([DataAsset(...), ...]); |
| /// }); |
| /// } |
| /// ``` |
| void addEncodedAssets( |
| Iterable<EncodedAsset> assets, { |
| AssetRouting routing = const ToAppBundle(), |
| }) { |
| switch (routing) { |
| case ToAppBundle(): |
| final list = _syntax.assets ?? []; |
| for (final asset in assets) { |
| list.add(AssetSyntax.fromJson(asset.toJson())); |
| } |
| _syntax.assets = list; |
| case ToBuildHooks(): |
| final list = _syntax.assetsForBuild ?? []; |
| for (final asset in assets) { |
| list.add(AssetSyntax.fromJson(asset.toJson())); |
| } |
| _syntax.assetsForBuild = list; |
| case ToLinkHook(): |
| final linkInPackage = routing.packageName; |
| final assetsForLinking = _syntax.assetsForLinking ?? {}; |
| final list = assetsForLinking[linkInPackage] ??= []; |
| for (final asset in assets) { |
| list.add(AssetSyntax.fromJson(asset.toJson())); |
| } |
| _syntax.assetsForLinking = assetsForLinking; |
| } |
| } |
| |
| BuildOutputSyntax get _syntax => |
| BuildOutputSyntax.fromJson(_output._syntax.json); |
| } |
| |
| /// The output for a `hook/link.dart`. |
| final class LinkOutput extends HookOutput { |
| /// Creates a [BuildOutput] from the given [json]. |
| LinkOutput(super.json) : _syntax = LinkOutputSyntax.fromJson(json), super._(); |
| |
| LinkOutputAssets get assets => LinkOutputAssets._(this); |
| |
| @override |
| final LinkOutputSyntax _syntax; |
| } |
| |
| /// The assets in [LinkOutput.assets]. |
| final class LinkOutputAssets { |
| final LinkOutput _output; |
| |
| LinkOutputAssets._(this._output); |
| |
| /// The assets produced by this build. |
| List<EncodedAsset> get encodedAssets => _output._encodedAssets; |
| } |
| |
| /// The builder for [LinkOutput]. |
| /// |
| /// There are various Dart extensions on this [LinkOutputBuilder] that allow |
| /// adding specific asset types - which should be used by normal hook authors. |
| /// For example |
| /// |
| /// ```dart |
| /// main(List<String> arguments) async { |
| /// await build((input, output) { |
| /// output.assets.code.add(CodeAsset(...)); |
| /// output.assets.data.add(DataAsset(...)); |
| /// }); |
| /// } |
| /// ``` |
| final class LinkOutputBuilder extends HookOutputBuilder { |
| LinkOutputAssetsBuilder get assets => LinkOutputAssetsBuilder._(this); |
| |
| LinkOutput build() => LinkOutput(json); |
| } |
| |
| /// The builder for [LinkOutput.assets] in [LinkOutputBuilder]. |
| final class LinkOutputAssetsBuilder { |
| final LinkOutputBuilder _builder; |
| |
| LinkOutputAssetsBuilder._(this._builder); |
| |
| /// Adds [EncodedAsset]s produced by this build. |
| /// |
| /// Note to hook writers. Prefer using the `.add` method on the extension for |
| /// the specific asset type being added: |
| /// |
| /// ```dart |
| /// main(List<String> arguments) async { |
| /// await build((input, output) { |
| /// output.assets.code.add(CodeAsset(...)); |
| /// output.assets.data.add(DataAsset(...)); |
| /// }); |
| /// } |
| /// ``` |
| void addEncodedAsset(EncodedAsset asset) { |
| final list = _syntax.assets ?? []; |
| list.add(AssetSyntax.fromJson(asset.toJson())); |
| _syntax.assets = list; |
| } |
| |
| /// Adds [EncodedAsset]s produced by this build. |
| /// |
| /// Note to hook writers. Prefer using the `.addAll` method on the extension |
| /// for the specific asset type being added: |
| /// |
| /// ```dart |
| /// main(List<String> arguments) async { |
| /// await build((input, output) { |
| /// output.assets.code.addAll([CodeAsset(...), ...]); |
| /// output.assets.data.addAll([DataAsset(...), ...]); |
| /// }); |
| /// } |
| /// ``` |
| void addEncodedAssets(Iterable<EncodedAsset> assets) { |
| final list = _syntax.assets ?? []; |
| for (final asset in assets) { |
| list.add(AssetSyntax.fromJson(asset.toJson())); |
| } |
| _syntax.assets = list; |
| } |
| |
| LinkOutputSyntax get _syntax => |
| LinkOutputSyntax.fromJson(_builder._syntax.json); |
| } |
| |
| /// The configuration in [HookInput.config]. |
| final class HookConfig { |
| Map<String, Object?> get json => _syntax.json; |
| |
| List<Object> get path => _syntax.path; |
| |
| final ConfigSyntax _syntax; |
| |
| /// The asset types that should be built by an invocation of a hook. |
| /// |
| /// The invoker of a hook may, and in most cases will, invoke the hook |
| /// separately for different asset types. |
| /// |
| /// This means that hooks should be written in a way that they are a no-op if |
| /// they are invoked for an asset type that is not emitted by the hook. Most |
| /// asset extensions provide a to check [buildAssetTypes] for their own asset |
| /// type. For example, `CodeAsset`s can be used as follows: |
| /// |
| /// ```dart |
| /// if (input.config.buildCodeAssets) { |
| /// // Emit code asset. |
| /// } |
| /// ``` |
| List<String> get buildAssetTypes => _syntax.buildAssetTypes; |
| |
| HookConfig._(HookInput input) : _syntax = input._syntax.config; |
| |
| /// Constructs a checksum for this hook config. |
| /// |
| /// This can be used to construct an output directory name specific to the |
| /// [HookConfig] being built with this builder. It is therefore assumed the |
| /// output directory has not been set yet. |
| String computeChecksum() => _jsonChecksum(_syntax.json); |
| } |
| |
| /// The configuration in [BuildInput.config]. |
| final class BuildConfig extends HookConfig { |
| @override |
| // ignore: overridden_fields |
| final BuildConfigSyntax _syntax; |
| |
| bool get linkingEnabled => _syntax.linkingEnabled; |
| |
| BuildConfig._(super.input) |
| : _syntax = BuildConfigSyntax.fromJson( |
| input._syntax.config.json, |
| path: input._syntax.config.path, |
| ), |
| super._(); |
| } |
| |
| /// The configuration in [LinkInput.config]. |
| final class LinkConfig extends HookConfig { |
| LinkConfig._(super.input) : super._(); |
| } |