buildPackage function

Future<Directory> buildPackage({
  1. required Uri library,
  2. required Directory output,
  3. required Map<String, Object?> metadata,
  4. Map<String, File> assets = const {},
  5. String? packagesPath,
  6. ExternalLibrary? external,
  7. BuildTarget target = BuildTarget.wasm,
  8. Set<BuildTarget>? targets,
  9. 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();
    }
  }
}