installMissingCloudflarePackages function

Future<void> installMissingCloudflarePackages({
  1. required String npm,
  2. required Iterable<String> packages,
})

Installs only Node packages that are not already declared.

Reinstalling a declared package without a version can rewrite an exact dependency to npm's configured save prefix, even when nothing changed.

Each entry of packages is a plain name or a versioned spec such as @scope/name@^1.2.3. A versioned spec is passed to npm install as is, so npm does not fall back to an older release (for example when min-release-age hides latest). A declared package whose locked version is below the requested range is reinstalled with the spec. Other declared dependencies are never changed by Katana itself. After installation the locked versions are verified, so a resolution outside the range fails explicitly instead of continuing with an older release.

Implementation

Future<void> installMissingCloudflarePackages({
  required String npm,
  required Iterable<String> packages,
}) async {
  if (isLocalApply) {
    validateLocalCloudflarePackages(packages);
    return;
  }
  final declared = <String>{};
  final packageJson = File("cloudflare/package.json");
  if (packageJson.existsSync()) {
    final decoded = jsonDecode(await packageJson.readAsString());
    if (decoded is Map) {
      for (final section in ["dependencies", "devDependencies"]) {
        final values = decoded[section];
        if (values is Map) {
          declared.addAll(values.keys.map((key) => key.toString()));
        }
      }
    }
  }
  final lockedBefore = _readCloudflareLockedVersions();
  final targets = <String>[];
  final versioned = <({String name, String range})>[];
  for (final spec in packages) {
    final (:name, :range) = parseCloudflarePackageSpec(spec);
    if (range != null) {
      versioned.add((name: name, range: range));
    }
    if (!declared.contains(name)) {
      targets.add(spec);
      continue;
    }
    final locked = lockedBefore[name];
    if (range != null &&
        locked != null &&
        !cloudflarePackageVersionSatisfies(locked, range)) {
      label("Update $name from $locked to satisfy $range");
      targets.add(spec);
    }
  }
  if (targets.isEmpty) {
    return;
  }
  await command(
    "Package installation.",
    [npm, "install", ...targets],
    workingDirectory: "cloudflare",
    runInShell: true,
  );
  final lockedAfter = _readCloudflareLockedVersions();
  for (final (:name, :range) in versioned) {
    if (!targets.any((spec) => parseCloudflarePackageSpec(spec).name == name)) {
      continue;
    }
    final locked = lockedAfter[name];
    if (locked == null || !cloudflarePackageVersionSatisfies(locked, range)) {
      throw StateError(
        "Cloudflare npm 依存を要求範囲で導入できませんでした: $name ${locked ?? "未導入"}(要求: $range)。npm install のエラー(min-release-age・before・registry)を確認してください。",
      );
    }
  }
}