blob: 1d8ca4d2503d6b0cb3faf46784774fc362935e85 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/util/resolve_input_uri.dart'
as front_end;
import 'package:args/args.dart';
import 'package:build_integration/file_system/multi_root.dart'
as front_end
show MultiRootFileSystem;
import 'package:front_end/src/api_unstable/vm.dart'
as front_end
show FileSystem;
import 'package:meta/meta.dart' show visibleForTesting;
import 'package:vm/kernel_front_end.dart' show createFrontEndFileSystem;
/// Configuration options for `shadowSources` and [SourceShadowTransformer].
class ShadowOptions {
/// Dynamic module entrypoint script path.
final Uri scriptUri;
/// package_config.json path.
final Uri packagesUri;
/// dynamic_interface.yaml path.
final Uri dynamicInterfaceUri;
/// Used for reading sources and multi-root virtual file system.
final front_end.FileSystem frontendFs;
/// Prefix to apply to shadowed package names.
final String prefix;
/// Output directory for shadowed sources.
final Uri outputDir;
/// Whether to enable relative library URIs in the dynamic interface.
///
/// Relative URIs without a scheme in a dynamic interface are resolved using
/// the [dynamicInterfaceUri]. Relative URIs without a scheme in entrypoint
/// script directives are resolved using the [scriptUri].
///
/// This flag makes sure that we can compare these resolution results (i.e.
/// both [dynamicInterfaceUri] and [scriptUri] are absolute and in the same
/// scheme).
final bool enableRelativeDynamicInterfaceLibraries;
/// Whether to enable relative directive URIs in the entrypoint [scriptUri].
///
/// These relative URIs are resolved using the [scriptUri], while a package
/// config resolves package: URIs according to the [packagesUri] and the
/// package config contents.
///
/// To make sure that we can compare resolution results (i.e. figure out that
/// `import 'foo.dart'` and `import 'package:bar/foo.dart'` refer to the same
/// library), [scriptUri] and [packagesUri] must both be absolute and in
/// the same scheme.
final bool enableRelativeEntrypointScriptDirectives;
ShadowOptions._({
required this.scriptUri,
required this.packagesUri,
required this.dynamicInterfaceUri,
required this.frontendFs,
required this.prefix,
required this.outputDir,
}) : enableRelativeDynamicInterfaceLibraries =
dynamicInterfaceUri.scheme == scriptUri.scheme &&
dynamicInterfaceUri.isAbsolute &&
scriptUri.isAbsolute,
enableRelativeEntrypointScriptDirectives =
scriptUri.scheme == packagesUri.scheme &&
scriptUri.isAbsolute &&
packagesUri.isAbsolute;
/// Convenience wrapper for parsing command line arguments.
///
/// Does minimal validation and conversion of arguments, relying on
/// [ShadowOptions.create].
static ShadowOptions fromArgs(List<String> args) {
final results = _parser.parse(args);
final frontendFs = createFrontEndFileSystem(
results.option('filesystem-scheme'),
results.multiOption('filesystem-root'),
);
return ShadowOptions.create(
scriptUri: results.option('script')!,
packagesUri: results.option('packages')!,
dynamicInterfaceUri: results.option('dynamic-interface')!,
frontendFs: frontendFs,
prefix: results.option('prefix')!,
outputDir: results.option('output-dir')!,
);
}
/// Creates and validates [ShadowOptions].
static ShadowOptions create({
required String scriptUri,
required String packagesUri,
required String dynamicInterfaceUri,
required front_end.FileSystem frontendFs,
required String prefix,
required String outputDir,
}) {
validatePrefix(prefix);
final isMultiRoot = frontendFs is front_end.MultiRootFileSystem;
final multiRootFilesystemScheme = isMultiRoot
? frontendFs.markerScheme
: null;
final resolvedScriptUri = front_end.resolveInputUri(scriptUri);
final resolvedPackagesUri = front_end.resolveInputUri(packagesUri);
final resolvedDynamicInterfaceUri = front_end.resolveInputUri(
dynamicInterfaceUri,
);
var resolvedOutputDir = front_end.resolveInputUri(outputDir);
if (!resolvedOutputDir.path.endsWith('/')) {
resolvedOutputDir = resolvedOutputDir.replace(
path: '${resolvedOutputDir.path}/',
);
}
if (isMultiRoot && resolvedOutputDir.scheme == multiRootFilesystemScheme) {
throw ArgumentError.value(
outputDir,
'outputDir',
'Cannot write to multi-root filesystem. '
'Please use file: URI or a path.',
);
}
return ShadowOptions._(
scriptUri: resolvedScriptUri,
packagesUri: resolvedPackagesUri,
dynamicInterfaceUri: resolvedDynamicInterfaceUri,
frontendFs: frontendFs,
prefix: prefix,
outputDir: resolvedOutputDir,
);
}
}
/// Validates that [prefix] consists of valid prefix characters [a-z0-9_.].
@visibleForTesting
void validatePrefix(String prefix) {
if (!_validPrefixRegExp.hasMatch(prefix)) {
throw ArgumentError.value(
prefix,
'prefix',
'Prefix must contain valid Dart package name characters [a-z0-9_.]',
);
}
}
final _validPrefixRegExp = RegExp(r'^[a-z0-9_.]+$');
final _parser = ArgParser()
..addOption(
'script',
abbr: 's',
mandatory: true,
help: 'URI to the entrypoint script file.',
)
..addOption(
'packages',
abbr: 'p',
mandatory: true,
help: 'Path or URI to input packages config JSON file.',
)
..addOption(
'dynamic-interface',
abbr: 'd',
mandatory: true,
help: 'Path or URI to dynamic_interface.yaml file.',
)
..addMultiOption(
'filesystem-root',
abbr: 'r',
help: 'Filesystem root directories.',
)
..addOption(
'filesystem-scheme',
help: 'The URI scheme for the multi-root virtual filesystem.',
)
..addOption(
'prefix',
abbr: 'i',
mandatory: true,
help: 'Import prefix string to disambiguate shadowed dependencies.',
)
..addOption(
'output-dir',
abbr: 'o',
mandatory: true,
help: 'Output directory for shadowed sources.',
);