installMissingCloudflarePackages function
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)を確認してください。",
);
}
}
}