assemblePackageSet function

Future<List<Directory>> assemblePackageSet({
  1. required List<BuiltModule> modules,
  2. required Directory output,
  3. required Map<String, Object?> metadata,
  4. BuildTarget target = BuildTarget.native,
  5. bool wasmFallback = false,
  6. Map<String, File> assets = const {},
})

Assemble a lightweight entry package and optional native platform packages.

Native packages use the root name plus the platform suffix and the same version. npm selects them using os/cpu/libc. Wasm, when supplied, stays in the entry package. The returned directories put native packages first and the entry package last, ready for publication in that order.

output must not exist. The complete set is committed together; use a new directory for each preparation. assemblePackage retains all platforms in one package for offline distribution. wasmFallback applies only to root ESM imports and requires both backends with a native target.

Implementation

Future<List<Directory>> assemblePackageSet({
  required List<BuiltModule> modules,
  required Directory output,
  required Map<String, Object?> metadata,
  BuildTarget target = BuildTarget.native,
  bool wasmFallback = false,
  Map<String, File> assets = const {},
}) async {
  final selected = List<BuiltModule>.of(modules);
  _checkWasmFallback(
    selected.map((value) => value.target.name).toSet(),
    target,
    wasmFallback,
  );
  final supplied = packageMetadata(metadata);
  final destination = output.absolute;
  await checkOutputInputs(destination, [
    for (final module in selected) module.directory,
    ...assets.values,
  ]);
  await _checkAssets(assets);
  if (await FileSystemEntity.type(destination.path, followLinks: false) !=
      FileSystemEntityType.notFound) {
    throw ArgumentError(
      'Package set output already exists: ${destination.path}',
    );
  }
  final native = <NativePlatform, BuiltModule>{};
  final ownership = <BuiltModule, OutputOwnership>{};
  final packages = <NativePlatform, String>{};
  for (final module in selected) {
    final record = await OutputOwnership.read(
      module.directory,
      kind: OutputKind.module,
    );
    if (record == null) {
      throw ArgumentError(
        'Built module does not exist: ${module.directory.path}',
      );
    }
    ownership[module] = record;
    if (module.target != BuildTarget.native) continue;
    final bundle = await NativeRuntimeBundle.read(
      module.directory,
      built: true,
    );
    if (native.containsKey(bundle.platform)) {
      throw ArgumentError(
        'Duplicate native platform: ${bundle.platform.packageId}',
      );
    }
    final name = '${supplied['name']}-${bundle.platform.packageId}';
    packageMetadata({'name': name, 'version': supplied['version']});
    for (final field in [
      'dependencies',
      'optionalDependencies',
      'peerDependencies',
      'devDependencies',
    ]) {
      final dependencies = supplied[field];
      if (dependencies is Map && dependencies.containsKey(name)) {
        throw ArgumentError(
          'Package metadata $field conflicts with generated package $name.',
        );
      }
    }
    native[bundle.platform] = module;
    packages[bundle.platform] = name;
  }
  if (native.isEmpty) {
    throw ArgumentError(
      'A platform package set requires at least one native module.',
    );
  }
  final platforms = native.keys.toList()
    ..sort((a, b) => a.packageId.compareTo(b.packageId));
  await destination.parent.create(recursive: true);
  final staging = await destination.parent.createTemp('.napi-package-set-');
  var committed = false;
  try {
    // Validate the shared contract before copying any native payloads.
    await _assemblePackage(
      modules: selected,
      rootTarget: target,
      output: Directory('${staging.path}/package'),
      metadata: supplied,
      assets: assets,
      platformSelection: true,
      nativePackages: packages,
      wasmFallback: wasmFallback,
    );
    for (final platform in platforms) {
      final payload = Directory('${staging.path}/${platform.packageId}');
      await assembleNativePackage(
        module: native[platform]!,
        output: payload,
        metadata: {
          for (final field in [
            'description',
            'license',
            'repository',
            'homepage',
            'bugs',
            'author',
            'contributors',
            'keywords',
            'engines',
            'publishConfig',
            'private',
          ])
            if (supplied.containsKey(field)) field: supplied[field],
          'name': packages[platform],
          'version': supplied['version'],
        },
        assets: {
          for (final entry in assets.entries)
            if (entry.key.startsWith('LICENSE') ||
                entry.key.startsWith('NOTICE'))
              entry.key: entry.value,
        },
      );
      await ownership[native[platform]]!.verifyArtifacts(payload);
    }
    for (final entry in ownership.entries) {
      await entry.value.verifyArtifacts(entry.key.directory);
    }
    publishDirectory(staging, destination);
    committed = true;
    return [
      for (final platform in platforms)
        Directory('${destination.path}/${platform.packageId}'),
      Directory('${destination.path}/package'),
    ];
  } finally {
    if (!committed) await cleanBuildStaging(staging, committed: false);
  }
}