buildPackage function
Future<Directory>
buildPackage({
- required Uri library,
- required Directory output,
- required Map<
String, Object?> metadata, - Map<
String, File> assets = const {}, - String? packagesPath,
- ExternalLibrary? external,
- BuildTarget target = BuildTarget.wasm,
- Set<
BuildTarget> ? targets, - Directory? nativeRuntime,
Build and assemble an npm package, or atomically rebuild a managed package.
Metadata is validated before source analysis, compilation or staging. Node
is not required to validate package names. Native builds obtain a pinned
precompiled runtime, or use the explicit offline nativeRuntime override.
Wasm is the default. Intermediate build files are
cleaned up; a postcommit cleanup failure reports the retained path without
changing the successful result. Managed output can be rebuilt on
qualified macOS and Linux filesystems; legacy or unsupported output is
preserved.
With targets, include both backends in one package; target chooses the
root entrypoint. Explicit native/wasm subpaths never fall back to one another.
Implementation
Future<Directory> buildPackage({
required Uri library,
required Directory output,
required Map<String, Object?> metadata,
Map<String, File> assets = const {},
String? packagesPath,
ExternalLibrary? external,
BuildTarget target = BuildTarget.wasm,
Set<BuildTarget>? targets,
Directory? nativeRuntime,
}) async {
final selected = Set<BuildTarget>.of(targets ?? {target});
final supplied = packageMetadata(metadata);
final nativeOnly =
selected.length == 1 && selected.contains(BuildTarget.native);
packageMetadata(supplied, native: nativeOnly);
if (selected.isEmpty || !selected.contains(target)) {
throw ArgumentError('Package targets must contain the root target.');
}
if (!selected.contains(BuildTarget.native) && nativeRuntime != null) {
throw ArgumentError(
'nativeRuntime is only valid when the package includes native.',
);
}
final destination = output.absolute;
final packages = libraryPackagesPath(packagesPath, external);
await checkOutputInputs(destination, [
if (library.scheme == 'file') File.fromUri(library),
if (packages != null) File(packages),
?nativeRuntime,
...assets.values,
]);
await _checkAssets(assets);
// Reject unsupported existing output before analysis. Fresh dual contracts
// still fail before creating an output parent or a stable lock inode.
OutputUpdate? update;
if (await FileSystemEntity.type(destination.path, followLinks: false) !=
FileSystemEntityType.notFound) {
update = await OutputUpdate.open(destination);
}
Directory? staging;
var committed = false;
try {
packageMetadata({
...?update?.previous?.metadata,
...supplied,
}, native: nativeOnly);
final bindings = <BuildTarget, BindingModule>{};
String? declarations;
for (final backend in BuildTarget.values.where(selected.contains)) {
bindings[backend] = await readLibraryBindings(
library: library,
packagesPath: packages,
external: external,
target: backend,
);
}
// External package: entrypoints resolve to physical files in the existing
// frontend. Reuse that result instead of duplicating package resolution.
await checkOutputInputs(destination, [
for (final binding in bindings.values) File.fromUri(binding.source),
]);
if (selected.length > 1) {
declarations = validatePackageContracts(
bindings[BuildTarget.wasm]!,
bindings[BuildTarget.native]!,
);
} else if (selected.contains(BuildTarget.native)) {
declarations = emitNativeDeclarations(
validateNative(bindings[BuildTarget.native]!),
);
}
Directory? runtime;
if (selected.contains(BuildTarget.native)) {
final bundle = await resolveNativeRuntime(
runtime: nativeRuntime,
output: destination,
);
runtime = bundle.directory;
if (nativeOnly) {
packageMetadata({
...?update?.previous?.metadata,
...supplied,
}, nativePlatform: bundle.platform);
}
}
update ??= await OutputUpdate.open(destination);
await destination.parent.create(recursive: true);
staging = await destination.parent.createTemp('.napi-package-build-');
final modules = <BuiltModule>[];
for (final backend in BuildTarget.values.where(selected.contains)) {
final module = await buildModule(
bindings: bindings[backend]!,
output: Directory('${staging.path}/${backend.name}'),
packagesPath: packages,
target: backend,
nativeRuntime: backend == BuildTarget.native ? runtime : null,
);
if (declarations != null) {
final name = backend == BuildTarget.native
? 'module.d.mts'
: 'module.d.wasm.ts';
final actual = await File('${module.directory.path}/$name')
.readAsString();
if (actual != declarations) {
throw StateError(
'Built $backend declarations changed after validation.',
);
}
}
modules.add(module);
}
final result = await _assemblePackage(
modules: modules,
rootTarget: target,
output: destination,
metadata: supplied,
assets: assets,
update: update,
);
committed = true;
return result;
} finally {
try {
if (staging != null) {
await cleanBuildStaging(staging, committed: committed);
}
} finally {
update?.close();
}
}
}