blob: 1b3d4d62c7d748ab93fda3d3b50fc2af0d4557ca [file] [log] [blame] [edit]
// 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:meta/meta.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 Winddows.
///
/// 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);
/// The user-defines for this hook input.
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);
/// 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.
///
/// Experimental: The record uses feature needs to be enabled as experiment.
/// The experiment is only available in the Dart SDK, not in Flutter. We
/// reserve the right to break this API at any point without respecting
/// semantic versioning of this package.
@experimental
Uri? get recordedUsagesFile => _syntaxLinkInput.resourceIdentifiers;
@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: OutputStatusSyntax.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 = OutputStatusSyntax.failure;
_syntax.failureDetails = FailureSyntax(
type: switch (value) {
FailureType.build => FailureTypeSyntax.build,
FailureType.infra => FailureTypeSyntax.infra,
FailureType.uncategorized => FailureTypeSyntax.uncategorized,
_ => FailureTypeSyntax.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) {
FailureTypeSyntax.build => FailureType.build,
FailureTypeSyntax.infra => FailureType.infra,
FailureTypeSyntax.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 OutputStatusSyntax.success:
return BuildOutput(json);
case OutputStatusSyntax.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 OutputStatusSyntax.success:
return LinkOutput(json);
case OutputStatusSyntax.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;
}