| // 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 'package:record_use/record_use.dart'; | |
| import 'api/build_and_link.dart'; | |
| 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 directory is | |
| /// nested inside [outputDirectoryShared] and has a short checksum to avoid | |
| /// running out of path length on Windows. | |
| /// | |
| /// Prefer using a sub directory in [outputDirectoryShared] with a | |
| /// checksum of the fields on the [config] that influence your build. Reusing | |
| /// a precise subdirectory only dependent on what influences your build avoids | |
| /// cache misses for [config]s that differ in fields irrelevant for your | |
| /// build. | |
| /// | |
| /// 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. Use a sub directory of | |
| /// [outputDirectoryShared] with a checksum of the parts on the [config] that | |
| /// influence your assets. Ensure your checksum is relatively short to avoid | |
| /// running out of path lengths on Windows. Reusing a precise subdirectory | |
| /// only dependent on what influences your build avoids cache misses for | |
| /// [config]s that differ in fields irrelevant for your build. | |
| /// | |
| /// 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); | |
| /// The configuration for this hook input. | |
| HookConfig get config => HookConfig._(this); | |
| /// Custom configurations specified in the root package `pubspec.yaml` | |
| /// (or root package pub workspace `pubspec.yaml` if using a workspace) under | |
| /// the `hooks.user_defines` block. | |
| /// | |
| /// These are used to pass custom parameters or local file paths to build and | |
| /// link hooks from the project's build environment. | |
| /// | |
| /// ### Workspace Scope | |
| /// The SDK hook runner reads user-defines from the root package's | |
| /// `pubspec.yaml` (or the root package pub workspace's `pubspec.yaml` if | |
| /// using a workspace). As a result, only end-users (the authors of the root | |
| /// app/package consuming the dependencies) can configure user-defines. | |
| /// Dependencies cannot supply their own default user-defines. | |
| /// | |
| /// ### Caching | |
| /// Hook user-defines are workspace-wide. If any user-defines inside the | |
| /// root package `pubspec.yaml` (or the root package pub workspace | |
| /// `pubspec.yaml` if using a workspace) change, the hook is re-run. | |
| /// However, if the user-defines for the package are identical, the hook is | |
| /// not re-run. | |
| /// | |
| /// ### Supported Types | |
| /// Configured values inside `pubspec.yaml` can be any JSON-compatible type, | |
| /// such as booleans, strings, numbers, nested maps, or lists: | |
| /// ```yaml | |
| /// hooks: | |
| /// user_defines: | |
| /// my_package: | |
| /// supported_archs: | |
| /// - arm64 | |
| /// - x64 | |
| /// ``` | |
| /// | |
| /// ### Package Filtering | |
| /// User-defines are filtered per package. A hook inside `my_package` can only | |
| /// access keys configured under `hooks.user_defines.my_package`. It cannot | |
| /// access defines of other packages. | |
| /// | |
| /// ### Common Use Cases | |
| /// End-users can configure user-defines to: | |
| /// - Select whether to download a prebuilt binary or build from source: | |
| /// ```yaml | |
| /// hooks: | |
| /// user_defines: | |
| /// my_package: | |
| /// local_build: false | |
| /// ``` | |
| /// - Enable/disable debug options or configure a custom compiler flag: | |
| /// ```yaml | |
| /// hooks: | |
| /// user_defines: | |
| /// my_package: | |
| /// debug_mode: true | |
| /// ``` | |
| /// | |
| /// ### Example Hook Usage | |
| /// In `pubspec.yaml`: | |
| /// ```yaml | |
| /// hooks: | |
| /// user_defines: | |
| /// my_package: | |
| /// enable_experimental: true | |
| /// custom_lib: assets/libnative.so | |
| /// ``` | |
| /// | |
| /// In `hook/build.dart`: | |
| /// <!-- file://./../../example/api/config_snippet_6.dart --> | |
| /// ```dart | |
| /// import 'dart:io'; | |
| /// import 'package:hooks/hooks.dart'; | |
| /// | |
| /// void main(List<String> args) async { | |
| /// await build(args, (input, output) async { | |
| /// // Access raw user-defines value | |
| /// final debugLogging = input.userDefines['enable_debug_logging']; | |
| /// if (debugLogging is! bool?) { | |
| /// throw const FormatException( | |
| /// 'hooks.user_defines.my_package.enable_debug_logging must be a ' | |
| /// 'boolean (or omitted)', | |
| /// ); | |
| /// } | |
| /// if (debugLogging == true) { | |
| /// print('Debug logging is enabled.'); | |
| /// } | |
| /// | |
| /// // Resolve relative path against pubspec.yaml base path | |
| /// final customAssetUri = input.userDefines.path('custom_asset'); | |
| /// if (customAssetUri != null) { | |
| /// final file = File.fromUri(customAssetUri); | |
| /// output.dependencies.add(file.uri); // Declare cache dependency | |
| /// // Use the file... | |
| /// } | |
| /// }); | |
| /// } | |
| /// ``` | |
| 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 if provided from the SDK from such source. | |
| /// If it's provided from command-line arguments, it's likely a string. | |
| /// | |
| /// For example, if a project's `pubspec.yaml` contains: | |
| /// ```yaml | |
| /// hooks: | |
| /// user_defines: | |
| /// my_package: | |
| /// enable_experimental_features: true | |
| /// optimization_level: "O3" | |
| /// ``` | |
| /// Then: | |
| /// - `input.userDefines['enable_experimental_features']` returns `true`. | |
| /// - `input.userDefines['optimization_level']` returns `"O3"`. | |
| Object? operator [](String key) => _findDefine([key])?.$1; | |
| /// An absolute [Uri] that can be used to interpret user-defines as paths. | |
| /// | |
| /// This uses the [keyPathDefine] to traverse available options, starting at | |
| /// the root. If the key path contains a string, this enters a YAML map. For | |
| /// an integer element, this enters a YAML sequence. Any other key path | |
| /// element is invalid and throws. | |
| /// If no element exists for any position in the key path, null is returned. | |
| /// | |
| /// This is a generalized variant of [path] returning the base URI for all | |
| /// options under [keyPathDefine]. To resolve a top-level string option as a | |
| /// path, use [path] directly. | |
| Uri? baseUri(List<Object /* String|int */> keyPathDefine) => | |
| _findDefine(keyPathDefine)?.$2.basePath; | |
| /// Resolves the path provided in the user-define for [key] to an absolute | |
| /// [Uri] pointing to the file or directory on the host filesystem. | |
| /// | |
| /// Absolute paths are converted to a file [Uri] without further | |
| /// resolution. Relative paths are resolved against the directory | |
| /// containing the `pubspec.yaml` where the user-define was declared (or | |
| /// the command-line working directory if provided via command-line | |
| /// arguments). | |
| /// | |
| /// If the user-define is `null` or not a [String], returns `null`. | |
| /// | |
| /// > If the hook reads the resolved file or directory, the hook author | |
| /// > **must** register it as a dependency in | |
| /// > [HookOutputBuilder.dependencies] (e.g. using | |
| /// > `output.dependencies.add(resolvedUri)`) to ensure the build cache is | |
| /// > invalidated and the hook is re-run when the file changes. | |
| /// | |
| /// For example, if a project's `pubspec.yaml` contains: | |
| /// ```yaml | |
| /// hooks: | |
| /// user_defines: | |
| /// my_package: | |
| /// prebuilt_assets_dir: assets/prebuilt/ | |
| /// ``` | |
| /// The resolved path can be accessed and registered as a dependency: | |
| /// <!-- file://./../../example/api/config_snippet_7.dart --> | |
| /// ```dart | |
| /// final assetsUri = input.userDefines.path('prebuilt_assets_dir'); | |
| /// if (assetsUri != null) { | |
| /// output.dependencies.add(assetsUri); | |
| /// // Read assets from the directory... | |
| /// } | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// - [baseUri], which returns the equivalent of [Uri.base] for the source | |
| /// defining a key (e.g. the directory containing a workspace pubspec with | |
| /// a `hooks` entry). Unlike this method, which directly interprets a | |
| /// value as a path, the base uri can be used to [Uri.resolve] arbitrary | |
| /// path values. | |
| Uri? path(String key) { | |
| if (_findDefine([key]) case (final String path, final source)?) { | |
| if (File(path).isAbsolute) { | |
| return Uri.file(path); | |
| } | |
| return source.basePath.resolve(path); | |
| } | |
| return null; | |
| } | |
| (Object, PackageUserDefinesSource)? _findDefine(List<Object> keyPathDefine) { | |
| 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) { | |
| Object? options = source.defines; | |
| for (final (i, key) in keyPathDefine.indexed) { | |
| if (key is String) { | |
| if (options is Map) { | |
| options = options[key]; | |
| } else { | |
| return null; | |
| } | |
| } else if (key is int) { | |
| if (options is List) { | |
| options = options[key]; | |
| } else { | |
| return null; | |
| } | |
| } else { | |
| throw ArgumentError.value( | |
| keyPathDefine, | |
| 'keyPathDefine', | |
| 'Must contain only strings or ints (found `$key` at index $i).', | |
| ); | |
| } | |
| } | |
| if (options != null) { | |
| return (options, source); | |
| } | |
| } | |
| return null; | |
| } | |
| } | |
| /// The builder for [HookInput]. | |
| sealed class HookInputBuilder { | |
| final _syntax = HookInputSyntax.fromJson({}) | |
| ..config = ConfigSyntax(buildAssetTypes: [], extensions: null); | |
| /// The JSON representation of this hook input builder. | |
| Map<String, Object?> get json => _syntax.json; | |
| /// Sets up the hook input. | |
| 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); | |
| /// The configuration for this hook input. | |
| 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 on Windows, only use 10. 10 hex characters with 1000 | |
| // different configs leads to a one in a million collision chance. On | |
| // collision, we'd get a cache miss and rerun the hooks and throw away the | |
| // cache for the colliding configuration. | |
| .substring(0, 10); | |
| return hash; | |
| } | |
| /// The input for `hook/build.dart`. | |
| final class BuildInput extends HookInput { | |
| @override | |
| Uri get outputFile => _syntax.outFile; | |
| final BuildInputSyntax _syntaxBuildInput; | |
| ///// Creates a [BuildInput] from the given [json]. | |
| 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; | |
| /// Retrieves the metadata value for the given [key]. | |
| 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); | |
| /// The encoded assets from direct dependencies. | |
| Map<String, List<EncodedAsset>> get encodedAssets => { | |
| for (final MapEntry(:key, :value) | |
| in (_input._syntaxBuildInput.assets ?? {}).entries) | |
| key: EncodedAssetSyntax._fromSyntax(value), | |
| }; | |
| /// The encoded assets from the direct dependency [packageName]. | |
| 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); | |
| /// Sets up the build input with the given [assets]. | |
| 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) asset.toSyntax()], | |
| }, | |
| ); | |
| } | |
| @override | |
| BuildConfigBuilder get config => BuildConfigBuilder._(this); | |
| @override | |
| void addExtension(ProtocolExtension extension) => | |
| extension.setupBuildInput(this); | |
| /// Builds the [BuildInput]. | |
| BuildInput build() => BuildInput(json); | |
| } | |
| /// The builder for [HookConfig]. | |
| final class HookConfigBuilder { | |
| ///// The build for the parent (the hook input). | |
| final HookInputBuilder builder; | |
| HookConfigBuilder._(this.builder); | |
| ConfigSyntax get _syntax => builder._syntax.config; | |
| /// The JSON representation of this config. | |
| Map<String, Object?> get json => _syntax.json; | |
| /// Adds asset types to this hook configuration. | |
| 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._(); | |
| /// Sets up the build configuration. | |
| 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 => assets.encodedAssets; | |
| /// The file containing recorded usages, if any. | |
| @Deprecated('Use recordedUses instead') | |
| Uri? get recordedUsagesFile => _syntaxLinkInput.resourceIdentifiers; | |
| /// The recorded usages, if any. | |
| late final Recordings? recordedUses = () { | |
| final file = _syntaxLinkInput.resourceIdentifiers; | |
| if (file == null) return null; | |
| final content = File.fromUri(file).readAsStringSync(); | |
| final json = jsonDecode(content) as Map<String, Object?>; | |
| return Recordings.fromJson(json); | |
| }(); | |
| @override | |
| Uri get outputFile => _syntax.outFile; | |
| final LinkInputSyntax _syntaxLinkInput; | |
| /// Creates a [LinkInput] from the given [json]. | |
| LinkInput(super.json) : _syntaxLinkInput = LinkInputSyntax.fromJson(json) { | |
| // Run validation. | |
| _encodedAssets; | |
| } | |
| @override | |
| LinkConfig get config => LinkConfig._(this); | |
| /// The assets passed to `hook/link.dart`. | |
| LinkInputAssets get assets => LinkInputAssets._(this); | |
| /// The metadata sent to this link hook by dependent link hooks. | |
| Map<String, Object?> get metadata => Map.fromEntries( | |
| assets.assetsFromLinking | |
| .where((e) => e.isMetadataAsset) | |
| .map((e) => e.asMetadataAsset) | |
| .map((e) => MapEntry(e.key, e.value)), | |
| ); | |
| } | |
| /// The assets in [LinkInput.assets]; | |
| final class LinkInputAssets { | |
| final LinkInput _input; | |
| LinkInputAssets._(this._input); | |
| /// The encoded assets passed to `hook/link.dart`. | |
| List<EncodedAsset> get encodedAssets => | |
| EncodedAssetSyntax._fromSyntax(_input._syntaxLinkInput.assets); | |
| /// The encoded assets from direct dependencies. | |
| List<EncodedAsset> get assetsFromLinking => | |
| EncodedAssetSyntax._fromSyntax(_input._syntaxLinkInput.assetsFromLinking); | |
| } | |
| /// The builder for [LinkInput]. | |
| final class LinkInputBuilder extends HookInputBuilder { | |
| @override | |
| LinkInputSyntax get _syntax => LinkInputSyntax.fromJson(super._syntax.json); | |
| /// Sets up the link input. | |
| void setupLink({ | |
| required List<EncodedAsset> assets, | |
| required List<EncodedAsset> assetsFromLinking, | |
| required Uri? recordedUsesFile, | |
| }) { | |
| _syntax.setup( | |
| assets: [for (final asset in assets) asset.toSyntax()], | |
| assetsFromLinking: [ | |
| for (final asset in assetsFromLinking) asset.toSyntax(), | |
| ], | |
| resourceIdentifiers: recordedUsesFile, | |
| ); | |
| } | |
| @override | |
| void addExtension(ProtocolExtension extension) => | |
| extension.setupLinkInput(this); | |
| @override | |
| LinkConfigBuilder get config => LinkConfigBuilder._(this); | |
| /// Builds the [LinkInput]. | |
| 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._(); | |
| } | |
| /// Extension methods for [EncodedAsset] to convert to and from the syntax | |
| /// model. | |
| extension EncodedAssetSyntax on List<EncodedAsset> { | |
| static List<EncodedAsset> _fromSyntax(List<AssetSyntax>? assets) { | |
| if (assets == null) { | |
| return []; | |
| } | |
| return [ | |
| 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 => | |
| EncodedAssetSyntax._fromSyntax(_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, | |
| status: .success, | |
| failureDetails: null, | |
| assetsForLinking: {}, | |
| ); | |
| /// The JSON representation of this hook output builder. | |
| 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. | |
| @Deprecated('Use dependencies.add() instead.') | |
| void addDependency(Uri uri) { | |
| dependencies.add(uri); | |
| } | |
| /// Adds files used by this build. | |
| /// | |
| /// If any of the files are modified after [BuildOutput.timestamp], the | |
| // build will be re-run. | |
| @Deprecated('Use dependencies.addAll() instead.') | |
| void addDependencies(Iterable<Uri> uris) { | |
| dependencies.addAll(uris); | |
| } | |
| /// The dependencies builder for this hook output. | |
| HookOutputDependenciesBuilder get dependencies => | |
| HookOutputDependenciesBuilder._(this); | |
| /// Sets the failure of this output. | |
| void setFailure(FailureType value) { | |
| _syntax.status = .failure; | |
| _syntax.failureDetails = FailureSyntax( | |
| type: switch (value) { | |
| FailureType.build => .build, | |
| FailureType.infra => .infra, | |
| FailureType.uncategorized => .uncategorized, | |
| _ => .uncategorized, | |
| }, | |
| ); | |
| } | |
| } | |
| /// The builder for [HookOutput.dependencies]. | |
| class HookOutputDependenciesBuilder { | |
| final HookOutputBuilder _output; | |
| HookOutputDependenciesBuilder._(this._output); | |
| /// Adds file used by this build. | |
| /// | |
| /// If any of the files are modified after [BuildOutput.timestamp], the | |
| // build will be re-run. | |
| void add(Uri uri) { | |
| final dependencies = _output._syntax.dependencies ?? []; | |
| dependencies.add(uri); | |
| _output._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 addAll(Iterable<Uri> uris) { | |
| final dependencies = _output._syntax.dependencies ?? []; | |
| dependencies.addAll(uris); | |
| _output._syntax.dependencies = dependencies; | |
| } | |
| } | |
| /// The output from a `hook/build.dart` on success. | |
| /// | |
| /// See [BuildOutputFailure] for failure. | |
| final class BuildOutput extends HookOutput implements BuildOutputMaybeFailure { | |
| /// 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: EncodedAssetSyntax._fromSyntax(value), | |
| }; | |
| List<EncodedAsset> get _encodedAssetsForBuild => | |
| EncodedAssetSyntax._fromSyntax(_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; | |
| /// The assets produced by this build which should be available to subsequent | |
| /// build hooks. | |
| 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 | |
| /// | |
| /// <!-- file://./../../example/api/config_snippet_1.dart --> | |
| /// ```dart | |
| /// import 'package:code_assets/code_assets.dart'; | |
| /// import 'package:data_assets/data_assets.dart'; | |
| /// import 'package:hooks/hooks.dart'; | |
| /// | |
| /// void main(List<String> arguments) async { | |
| /// await build(arguments, (input, output) async { | |
| /// output.assets.code.add( | |
| /// CodeAsset( | |
| /// name: 'my_code', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// linkMode: DynamicLoadingBundled(), | |
| /// ), | |
| /// ); | |
| /// output.assets.data.add( | |
| /// DataAsset( | |
| /// name: 'my_data', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// ), | |
| /// ); | |
| /// }); | |
| /// } | |
| /// ``` | |
| /// The builder for [BuildOutput]. | |
| final class BuildOutputBuilder extends HookOutputBuilder { | |
| /// The metadata builder for this build output. | |
| BuildOutputMetadataBuilder get metadata => BuildOutputMetadataBuilder._(this); | |
| /// The assets builder for this build output. | |
| BuildOutputAssetsBuilder get assets => BuildOutputAssetsBuilder._(this); | |
| @override | |
| BuildOutputSyntax get _syntax => | |
| BuildOutputSyntax.fromJson(super._syntax.json); | |
| /// Builds the [BuildOutput]. | |
| BuildOutput build() => BuildOutput(json); | |
| } | |
| /// The builder for [BuildOutputBuilder.metadata]. | |
| final class BuildOutputMetadataBuilder { | |
| final BuildOutputBuilder _output; | |
| BuildOutputMetadataBuilder._(this._output); | |
| /// Sets the metadata [value] for the given [key]. | |
| void operator []=(String key, Object value) { | |
| _output.assets.addEncodedAsset( | |
| MetadataAsset(key: key, value: value).encode(), | |
| routing: const ToBuildHooks(), | |
| ); | |
| } | |
| /// Adds all entries from [metadata]. | |
| void addAll(Map<String, Object> metadata) { | |
| for (final MapEntry(:key, :value) in metadata.entries) { | |
| this[key] = value; | |
| } | |
| } | |
| } | |
| /// The builder for [LinkOutputBuilder.metadata]. | |
| final class LinkOutputMetadataBuilder { | |
| final LinkOutputBuilder _output; | |
| LinkOutputMetadataBuilder._(this._output); | |
| /// Sets the metadata [value] for the given [key] to be sent to [packageName]. | |
| void add(String packageName, String key, Object value) { | |
| _output.assets.addEncodedAsset( | |
| MetadataAsset(key: key, value: value).encode(), | |
| routing: ToLinkHook(packageName), | |
| ); | |
| } | |
| /// Adds all entries from [metadata] to be sent to [packageName]. | |
| void addAll(String packageName, Map<String, Object> metadata) { | |
| metadata.forEach((key, value) => add(packageName, 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 hook to the application Bundle. | |
| sealed class AssetRouting {} | |
| /// The destination for assets in the [LinkOutput]. | |
| /// | |
| /// An asset can be either sent to other link hooks with [ToLinkHook] or | |
| /// directly to the application Bundle with [ToAppBundle]. | |
| /// | |
| /// Currently supported routings: | |
| /// * [ToLinkHook]: From link hook to another depending link hook. | |
| /// * [ToAppBundle]: From link hook to the application Bundle. | |
| sealed class LinkAssetRouting {} | |
| /// Assets with this [AssetRouting] in the [HookOutput] will be sent to the SDK | |
| /// to be bundled with the app. | |
| final class ToAppBundle implements AssetRouting, LinkAssetRouting { | |
| /// Creates a [ToAppBundle]. | |
| 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 implements AssetRouting { | |
| /// Creates a [ToBuildHooks]. | |
| const ToBuildHooks(); | |
| } | |
| /// Assets with this [AssetRouting] in the [HookOutput] 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 implements AssetRouting, LinkAssetRouting { | |
| /// The name of the package that contains the `hook/link.dart` to which assets | |
| /// should be sent. | |
| final String packageName; | |
| /// Creates a [ToLinkHook] with the given [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: | |
| /// | |
| /// <!-- file://./../../example/api/config_snippet_1.dart --> | |
| /// ```dart | |
| /// import 'package:code_assets/code_assets.dart'; | |
| /// import 'package:data_assets/data_assets.dart'; | |
| /// import 'package:hooks/hooks.dart'; | |
| /// | |
| /// void main(List<String> arguments) async { | |
| /// await build(arguments, (input, output) async { | |
| /// output.assets.code.add( | |
| /// CodeAsset( | |
| /// name: 'my_code', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// linkMode: DynamicLoadingBundled(), | |
| /// ), | |
| /// ); | |
| /// output.assets.data.add( | |
| /// DataAsset( | |
| /// name: 'my_data', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// ), | |
| /// ); | |
| /// }); | |
| /// } | |
| /// ``` | |
| void addEncodedAsset( | |
| EncodedAsset asset, { | |
| AssetRouting routing = const ToAppBundle(), | |
| }) { | |
| switch (routing) { | |
| case ToAppBundle(): | |
| final assets = _syntax.assets ?? []; | |
| assets.add(asset.toSyntax()); | |
| _syntax.assets = assets; | |
| case ToBuildHooks(): | |
| final assets = _syntax.assetsForBuild ?? []; | |
| assets.add(asset.toSyntax()); | |
| _syntax.assetsForBuild = assets; | |
| case ToLinkHook(): | |
| final packageName = routing.packageName; | |
| final assetsForLinking = _syntax.assetsForLinking ?? {}; | |
| assetsForLinking[packageName] ??= []; | |
| assetsForLinking[packageName]!.add(asset.toSyntax()); | |
| _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: | |
| /// | |
| /// <!-- file://./../../example/api/config_snippet_3.dart --> | |
| /// ```dart | |
| /// import 'package:code_assets/code_assets.dart'; | |
| /// import 'package:data_assets/data_assets.dart'; | |
| /// import 'package:hooks/hooks.dart'; | |
| /// | |
| /// void main(List<String> arguments) async { | |
| /// await build(arguments, (input, output) async { | |
| /// output.assets.code.addAll([ | |
| /// CodeAsset( | |
| /// name: 'my_code', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// linkMode: DynamicLoadingBundled(), | |
| /// ), | |
| /// ]); | |
| /// output.assets.data.addAll([ | |
| /// DataAsset( | |
| /// name: 'my_data', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// ), | |
| /// ]); | |
| /// }); | |
| /// } | |
| /// ``` | |
| void addEncodedAssets( | |
| Iterable<EncodedAsset> assets, { | |
| AssetRouting routing = const ToAppBundle(), | |
| }) { | |
| switch (routing) { | |
| case ToAppBundle(): | |
| final list = _syntax.assets ?? []; | |
| for (final asset in assets) { | |
| list.add(asset.toSyntax()); | |
| } | |
| _syntax.assets = list; | |
| case ToBuildHooks(): | |
| final list = _syntax.assetsForBuild ?? []; | |
| for (final asset in assets) { | |
| list.add(asset.toSyntax()); | |
| } | |
| _syntax.assetsForBuild = list; | |
| case ToLinkHook(): | |
| final linkInPackage = routing.packageName; | |
| final assetsForLinking = _syntax.assetsForLinking ?? {}; | |
| final list = assetsForLinking[linkInPackage] ??= []; | |
| for (final asset in assets) { | |
| list.add(asset.toSyntax()); | |
| } | |
| _syntax.assetsForLinking = assetsForLinking; | |
| } | |
| } | |
| BuildOutputSyntax get _syntax => | |
| BuildOutputSyntax.fromJson(_output._syntax.json); | |
| } | |
| /// The output for a `hook/link.dart` on success. | |
| /// | |
| /// See [LinkOutputFailure] for failure. | |
| final class LinkOutput extends HookOutput implements LinkOutputMaybeFailure { | |
| /// The assets produced by this link hook which are routed to link hooks in | |
| /// other packages. | |
| /// | |
| /// These can only be the packages which are direct dependencies of the | |
| /// current package. | |
| /// 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 what to do with them. | |
| Map<String, List<EncodedAsset>> get _encodedAssetsForLink => { | |
| for (final MapEntry(:key, :value) | |
| in (_syntax.assetsForLinking ?? {}).entries) | |
| key: EncodedAssetSyntax._fromSyntax(value), | |
| }; | |
| /// Creates a [LinkOutput] from the given [json]. | |
| LinkOutput(super.json) : _syntax = LinkOutputSyntax.fromJson(json), super._(); | |
| /// The assets produced by this link hook. | |
| 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 assets produced by this link hook sent to a specific link hook. | |
| /// | |
| /// The key of the map is the package name of the destination link hook. | |
| Map<String, List<EncodedAsset>> get encodedAssetsForLink => | |
| _output._encodedAssetsForLink; | |
| } | |
| /// 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 | |
| /// | |
| /// <!-- file://./../../example/api/config_snippet_2.dart --> | |
| /// ```dart | |
| /// import 'package:code_assets/code_assets.dart'; | |
| /// import 'package:data_assets/data_assets.dart'; | |
| /// import 'package:hooks/hooks.dart'; | |
| /// | |
| /// void main(List<String> arguments) async { | |
| /// await link(arguments, (input, output) async { | |
| /// output.assets.code.add( | |
| /// CodeAsset( | |
| /// name: 'my_code', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// linkMode: DynamicLoadingBundled(), | |
| /// ), | |
| /// ); | |
| /// output.assets.data.add( | |
| /// DataAsset( | |
| /// name: 'my_data', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// ), | |
| /// ); | |
| /// }); | |
| /// } | |
| /// ``` | |
| final class LinkOutputBuilder extends HookOutputBuilder { | |
| /// The metadata builder for this link output. | |
| LinkOutputMetadataBuilder get metadata => LinkOutputMetadataBuilder._(this); | |
| /// The assets builder for this link output. | |
| LinkOutputAssetsBuilder get assets => LinkOutputAssetsBuilder._(this); | |
| /// Builds the [LinkOutput]. | |
| 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: | |
| /// | |
| /// <!-- file://./../../example/api/config_snippet_2.dart --> | |
| /// ```dart | |
| /// import 'package:code_assets/code_assets.dart'; | |
| /// import 'package:data_assets/data_assets.dart'; | |
| /// import 'package:hooks/hooks.dart'; | |
| /// | |
| /// void main(List<String> arguments) async { | |
| /// await link(arguments, (input, output) async { | |
| /// output.assets.code.add( | |
| /// CodeAsset( | |
| /// name: 'my_code', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// linkMode: DynamicLoadingBundled(), | |
| /// ), | |
| /// ); | |
| /// output.assets.data.add( | |
| /// DataAsset( | |
| /// name: 'my_data', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// ), | |
| /// ); | |
| /// }); | |
| /// } | |
| /// ``` | |
| void addEncodedAsset( | |
| EncodedAsset asset, { | |
| LinkAssetRouting routing = const ToAppBundle(), | |
| }) { | |
| switch (routing) { | |
| case ToAppBundle(): | |
| final assets = _syntax.assets ?? []; | |
| assets.add(asset.toSyntax()); | |
| _syntax.assets = assets; | |
| case ToLinkHook(): | |
| final packageName = routing.packageName; | |
| final assetsForLinking = _syntax.assetsForLinking ?? {}; | |
| assetsForLinking[packageName] ??= []; | |
| assetsForLinking[packageName]!.add(asset.toSyntax()); | |
| _syntax.assetsForLinking = assetsForLinking; | |
| } | |
| } | |
| /// 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: | |
| /// | |
| /// <!-- file://./../../example/api/config_snippet_4.dart --> | |
| /// ```dart | |
| /// import 'package:code_assets/code_assets.dart'; | |
| /// import 'package:data_assets/data_assets.dart'; | |
| /// import 'package:hooks/hooks.dart'; | |
| /// | |
| /// void main(List<String> arguments) async { | |
| /// await link(arguments, (input, output) async { | |
| /// output.assets.code.addAll([ | |
| /// CodeAsset( | |
| /// name: 'my_code', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// linkMode: DynamicLoadingBundled(), | |
| /// ), | |
| /// ]); | |
| /// output.assets.data.addAll([ | |
| /// DataAsset( | |
| /// name: 'my_data', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// ), | |
| /// ]); | |
| /// }); | |
| /// } | |
| /// ``` | |
| void addEncodedAssets( | |
| Iterable<EncodedAsset> assets, { | |
| LinkAssetRouting routing = const ToAppBundle(), | |
| }) { | |
| switch (routing) { | |
| case ToAppBundle(): | |
| final list = _syntax.assets ?? []; | |
| for (final asset in assets) { | |
| list.add(asset.toSyntax()); | |
| } | |
| _syntax.assets = list; | |
| case ToLinkHook(): | |
| final linkInPackage = routing.packageName; | |
| final assetsForLinking = _syntax.assetsForLinking ?? {}; | |
| final list = assetsForLinking[linkInPackage] ??= []; | |
| for (final asset in assets) { | |
| list.add(asset.toSyntax()); | |
| } | |
| _syntax.assetsForLinking = assetsForLinking; | |
| } | |
| } | |
| LinkOutputSyntax get _syntax => | |
| LinkOutputSyntax.fromJson(_builder._syntax.json); | |
| } | |
| /// The configuration in [HookInput.config]. | |
| final class HookConfig { | |
| /// The JSON representation of this config. | |
| Map<String, Object?> get json => _syntax.json; | |
| /// The JSON path to this config inside the input. | |
| /// | |
| /// This is a public member such that [ProtocolExtension]s can access it. | |
| 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: | |
| /// | |
| /// <!-- file://./../../example/api/config_snippet_5.dart --> | |
| /// ```dart | |
| /// import 'package:code_assets/code_assets.dart'; | |
| /// import 'package:hooks/hooks.dart'; | |
| /// | |
| /// void main(List<String> arguments) async { | |
| /// await build(arguments, (input, output) async { | |
| /// if (input.config.buildCodeAssets) { | |
| /// output.assets.code.add( | |
| /// CodeAsset( | |
| /// name: 'my_code', | |
| /// file: Uri.file('path/to/file'), | |
| /// package: input.packageName, | |
| /// linkMode: DynamicLoadingBundled(), | |
| /// ), | |
| /// ); | |
| /// } | |
| /// }); | |
| /// } | |
| /// ``` | |
| 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; | |
| /// Whether linking is enabled for this build. | |
| 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._(); | |
| } | |
| /// A type of failure that occurred during a hook execution. | |
| class FailureType { | |
| /// The name of this failure type. | |
| final String name; | |
| const FailureType._(this.name); | |
| /// A failure that occurred due to a problem in the build logic of the hook. | |
| static const build = FailureType._('build'); | |
| /// A failure that occurred due to an infrastructure issue. | |
| /// | |
| /// For example, a network issue, or a tool not being available. | |
| /// | |
| /// Typically fixed by investigating infra reliability. | |
| static const infra = FailureType._('infra'); | |
| /// A failure that is not categorized as [build] or [infra]. | |
| /// | |
| /// Typically treated as [build]. | |
| static const uncategorized = FailureType._('uncategorized'); | |
| @override | |
| String toString() => name; | |
| } | |
| /// The output of a hook that has failed. | |
| final class HookOutputFailure { | |
| /// The JSON representation of this failure. | |
| Map<String, Object?> get json => _syntax.json; | |
| final HookOutputSyntax _syntax; | |
| HookOutputFailure._(Map<String, Object?> json) | |
| : _syntax = HookOutputSyntax.fromJson(json); | |
| /// The type of failure. | |
| /// | |
| /// This helps in categorizing the error and determining the appropriate | |
| /// response or fix. | |
| FailureType get type => switch (_syntax.failureDetails?.type) { | |
| .build => FailureType.build, | |
| .infra => FailureType.infra, | |
| .uncategorized => FailureType.uncategorized, | |
| _ => FailureType.uncategorized, | |
| }; | |
| } | |
| /// The output from a `hook/build.dart` on failure. | |
| /// | |
| /// See [BuildOutput] for success. | |
| final class BuildOutputFailure extends HookOutputFailure | |
| implements BuildOutputMaybeFailure { | |
| BuildOutputFailure._(super.json) : super._(); | |
| } | |
| /// The output from a `hook/link.dart` on failure. | |
| /// | |
| /// See [LinkOutput] for success. | |
| final class LinkOutputFailure extends HookOutputFailure | |
| implements LinkOutputMaybeFailure { | |
| LinkOutputFailure._(super.json) : super._(); | |
| } | |
| /// Either a successful [BuildOutput] or a [BuildOutputFailure]. | |
| sealed class BuildOutputMaybeFailure { | |
| /// The JSON representation of this output. | |
| Map<String, Object?> get json; | |
| factory BuildOutputMaybeFailure(Map<String, Object?> json) { | |
| final syntax = HookOutputSyntax.fromJson(json); | |
| final status = syntax.status; | |
| switch (status) { | |
| case null: // backwards compatibility. | |
| case .success: | |
| return BuildOutput(json); | |
| case .failure: | |
| return BuildOutputFailure._(json); | |
| } | |
| throw StateError('Unknown status: $status.'); | |
| } | |
| } | |
| /// Either a successful [LinkOutput] or a [LinkOutputFailure]. | |
| sealed class LinkOutputMaybeFailure { | |
| /// The JSON representation of this output. | |
| Map<String, Object?> get json; | |
| factory LinkOutputMaybeFailure(Map<String, Object?> json) { | |
| final syntax = HookOutputSyntax.fromJson(json); | |
| final status = syntax.status; | |
| switch (status) { | |
| case null: // backwards compatibility. | |
| case .success: | |
| return LinkOutput(json); | |
| case .failure: | |
| return LinkOutputFailure._(json); | |
| } | |
| throw StateError('Unknown status: $status.'); | |
| } | |
| } | |
| /// Base class for errors that can be thrown during a [build] or [link] | |
| /// invocation. | |
| /// | |
| /// Throwing these errors in [build] or [link] will automatically set the | |
| /// [HookOutputBuilder.setFailure] and exit the process with the exit code | |
| /// belonging to that error type. | |
| abstract class HookError extends Error { | |
| /// The error message. | |
| final String message; | |
| /// An optional underlying exception that caused this error. | |
| final Object? wrappedException; | |
| /// An optional stack trace associated with the [wrappedException]. | |
| final StackTrace? wrappedTrace; | |
| /// Creates a [HookError] with the given [message]. | |
| HookError({required this.message, this.wrappedException, this.wrappedTrace}); | |
| /// The exit code that should be used if the process terminates due to this | |
| /// error. | |
| int get exitCode; | |
| /// The [FailureType] associated with this error. | |
| FailureType get failureType; | |
| } | |
| /// An error indicating a problem with the build logic within a hook. | |
| /// | |
| /// Throwing this error in [build] or [link] will automatically set the | |
| /// [HookOutputBuilder.setFailure] and exit the process with [exitCode]. | |
| /// | |
| /// This typically means something went wrong during the asset generation or | |
| /// transformation process. | |
| final class BuildError extends HookError { | |
| /// Creates a [BuildError] with the given [message]. | |
| BuildError({ | |
| required super.message, | |
| super.wrappedException, | |
| super.wrappedTrace, | |
| }); | |
| @override | |
| int get exitCode => 1; | |
| /// The failure type for build errors is [FailureType.build]. | |
| @override | |
| FailureType get failureType => FailureType.build; | |
| } | |
| /// An error indicating an infrastructure-related problem during hook execution. | |
| /// | |
| /// Throwing this error in [build] or [link] will automatically set the | |
| /// [HookOutputBuilder.setFailure] and exit the process with [exitCode]. | |
| /// | |
| /// This could be due to issues like network problems. | |
| final class InfraError extends HookError { | |
| /// Creates a [InfraError] with the given [message]. | |
| InfraError({ | |
| required super.message, | |
| super.wrappedException, | |
| super.wrappedTrace, | |
| }); | |
| @override | |
| int get exitCode => 2; | |
| /// The failure type for infrastructure errors is [FailureType.infra]. | |
| @override | |
| FailureType get failureType => FailureType.infra; | |
| } |