plugins can declare their own npm packages
osDependencies covered binaries on PATH — ffmpeg, radicale, tea — and there was nothing for a javascript package. so a plugin needing one had to add it to the PLATFORM's package.json, which is what atelier did with sharp, carefully, with the frozen-lockfile dance documented in BLOCKERS.md. it still broke on the first machine that was not this one. alpha pulled the package.json, nobody ran bun install, and officer-atelier crash-looped on "Cannot find package 'sharp'" — after the install flow had reported success. three things were wrong with that shape. the platform carried a dependency only one plugin needs, and uninstalling the plugin would not have removed it. installing a plugin could not install it, so "installed" and "can start" were different facts with nothing checking the second. and it forced the platform's lockfile open, which is precisely the signal frozenLockfile exists to keep rare. now: a plugin ships its own package.json and lockfile, committed in its own repository, and the install flow runs `bun install --frozen-lockfile` in the plugin's directory. bun resolves node_modules up the tree, so plugins/<name>/node_modules serves that plugin and nothing else, and uninstall takes the packages with the directory. the presence of the file is the declaration — no manifest field, same reason sidecar/index.ts has none: a second place to say it is a second place to be wrong. verified by RESOLVING, not by the exit code. sharp resolves to a platform-specific binary package, so a lockfile that installs cleanly on one machine can leave nothing importable on another — which is the exact failure being fixed, and checking the exit code would have reproduced it one layer up. sharp is off the platform's package.json. the lockfile diff is that removal plus @img/sharp-libvips-* re-resolving 1.0.x → 1.2.4, because the platform no longer pins sharp; nothing else moved, checked line by line. the platform still reaches sharp transitively through next, which is where it came from.
This commit is contained in:
@@ -24,6 +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, verifyNpmDependencies } from './npm-deps';
|
||||
import { mountPrefix, type DiscoveredPlugin } from './manifest';
|
||||
|
||||
// The install runner: the four verbs, each as a short ordered list of effects.
|
||||
@@ -268,6 +269,34 @@ 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 ──
|
||||
//
|
||||
// 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
|
||||
// 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'}` };
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
if (plugin.schema) {
|
||||
// The barrel first, then the push. It is generated from the DIRECTORIES on disk rather than from
|
||||
// what is installed — see schema.ts for why that difference is the whole safety property.
|
||||
|
||||
Reference in New Issue
Block a user