npmDependencies in the manifest, installed once for every plugin
replaces the per-plugin package.json from earlier tonight. the owner's call, and the reason is one i had not weighed: sharp is ~50 MB of platform binaries and the obvious dependency for anything touching images, so a second and third plugin wanting it would have meant a second and third copy with no version they could converge on. so every plugin's `npmDependencies` merge into ONE generated plugins/package.json, installed into plugins/node_modules/. bun resolves node_modules up the tree, so that directory serves every plugin and none of the platform's own code. ranges intersect rather than alternate. two plugins asking for ^0.33.5 and ^0.33.9 produce "^0.33.5 ^0.33.9", which npm and bun read as AND — the resolved version must satisfy both. `||` would have meant "either", which is the silent version of this failure: one plugin quietly getting a version the other cannot import. incompatible requirements now fail the install with bun's own message, and both askers are named in the steps either way. generated from the plugins ON DISK, like generatePluginSchemas, and never edited — so a package cannot appear there without a plugin having declared it. what this gives up, knowingly: a committed lockfile. ranges are the whole point and a range resolves per machine, so plugin packages are resolved at install time like osDependencies are, and unlike the platform's own. CLAUDE.md is emphatic about frozen lockfiles and 2026-08-04, so it is named in the manifest type rather than left to be discovered. the generated plugins/package.json and lockfile are gitignored — /plugins/*/ only ignores directories, so files at that level needed naming explicitly.
This commit is contained in:
@@ -24,7 +24,7 @@ import { generatePluginSchemas, pushPluginSchema } from './schema';
|
||||
import { declaredTables, dropTables, existingTables } from './tables';
|
||||
import type { OnLog } from './run-logged';
|
||||
import { installDependencies, installRelease, manualInstallHint, reportDependencies, stillMissing } from './os-deps';
|
||||
import { installNpmDependencies, npmManifestFor, npmWouldBlockUpdates, verifyNpmDependencies } from './npm-deps';
|
||||
import { anyPluginWantsPackages, installNpmDependencies, unresolvableDependencies } from './npm-deps';
|
||||
import { mountPrefix, type DiscoveredPlugin } from './manifest';
|
||||
|
||||
// The install runner: the four verbs, each as a short ordered list of effects.
|
||||
@@ -269,44 +269,48 @@ export async function installPlugin(
|
||||
await step(steps, onStep, `dependencies: ${deps.dependencies.map((d) => d.binary).join(', ')} already present`);
|
||||
}
|
||||
|
||||
// ── npm packages, in the plugin's own directory ──
|
||||
// ── npm packages, shared across every plugin ──
|
||||
//
|
||||
// Beside the OS dependencies above and verified the same way, because the same failure is available:
|
||||
// an install that exits 0 and still leaves nothing the sidecar can import. Atelier hit exactly that
|
||||
// on 2026-08-20 — `sharp` in the PLATFORM's package.json, alpha never told to `bun install`, and the
|
||||
// an install that exits 0 and still leaves nothing the sidecar can import. Atelier hit exactly that on
|
||||
// 2026-08-20 — `sharp` in the PLATFORM's package.json, alpha never told to `bun install`, and the
|
||||
// sidecar crash-looped on a plugin that had reported a clean install.
|
||||
if (npmManifestFor(plugin)) {
|
||||
await step(steps, onStep, "packages: installing from the plugin's own package.json");
|
||||
const npm = await installNpmDependencies(plugin);
|
||||
if (!npm.ok) {
|
||||
return { ok: false, appName, steps, error: `package install failed: ${npm.output || 'no output'}` };
|
||||
}
|
||||
//
|
||||
// Generated from every plugin ON DISK rather than from this one, so a shared `sharp` is installed once
|
||||
// and two plugins wanting it converge instead of carrying a copy each.
|
||||
{
|
||||
const { plugins: onDisk } = await discoverPlugins();
|
||||
if (anyPluginWantsPackages(onDisk)) {
|
||||
await step(steps, onStep, 'packages: resolving npm dependencies declared by plugins');
|
||||
const npm = await installNpmDependencies(onDisk);
|
||||
|
||||
// Resolved, not assumed. `sharp` resolves to a platform-specific binary package, so a lockfile that
|
||||
// installs cleanly on one machine can still leave nothing importable on another — which is the
|
||||
// shape of the failure this whole block exists to stop shipping.
|
||||
const unresolvable = await verifyNpmDependencies(plugin);
|
||||
if (unresolvable.length) {
|
||||
return {
|
||||
ok: false,
|
||||
appName,
|
||||
steps,
|
||||
error: `packages installed, but ${unresolvable.join(', ')} cannot be resolved from ${plugin.dir}`,
|
||||
};
|
||||
}
|
||||
await step(steps, onStep, 'packages: installed and resolvable');
|
||||
for (const [name, askers] of Object.entries(npm.conflicts)) {
|
||||
// Named, not silently intersected. Two plugins disagreeing about a version is a thing their
|
||||
// authors need to know, and the install either resolves it or fails on the next line.
|
||||
await step(
|
||||
steps,
|
||||
onStep,
|
||||
`packages: ${name} requested as ${askers.map((a) => `${a.range} (${a.appName})`).join(', ')} — intersected`,
|
||||
);
|
||||
}
|
||||
|
||||
// Said now, while there is something to be done about it. `pullPlugin` refuses on any uncommitted
|
||||
// change, so an unignored `node_modules/` turns the Update button into a permanent "N uncommitted
|
||||
// changes" about a directory the platform created — and the person seeing that message has no
|
||||
// reason to connect it to a package.json shipped three versions ago.
|
||||
if (await npmWouldBlockUpdates(plugin)) {
|
||||
await step(
|
||||
steps,
|
||||
onStep,
|
||||
'packages: WARNING — this plugin does not ignore node_modules/, so Update will refuse as dirty. ' +
|
||||
'Add `node_modules/` to its .gitignore.',
|
||||
);
|
||||
if (!npm.ok) {
|
||||
return { ok: false, appName, steps, error: `package install failed: ${npm.output || 'no output'}` };
|
||||
}
|
||||
|
||||
// Resolved, not assumed. `sharp` resolves to a platform-specific binary package, so an install
|
||||
// that succeeds on one machine can still leave nothing importable on another — which is the shape
|
||||
// of the failure this whole block exists to stop shipping.
|
||||
const unresolvable = unresolvableDependencies(plugin);
|
||||
if (unresolvable.length) {
|
||||
return {
|
||||
ok: false,
|
||||
appName,
|
||||
steps,
|
||||
error: `packages installed, but ${unresolvable.join(', ')} cannot be resolved from ${plugin.dir}`,
|
||||
};
|
||||
}
|
||||
await step(steps, onStep, 'packages: installed and resolvable');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user