Problem
After pantry install, npm dependencies declared in package.json are downloaded and extracted into pantry/<name>/. However, bun run X, bun --bun X, and any bun build invocation use Bun's module resolver, which only walks node_modules/ (plus whatever bunfig.toml's linker setting points to — which is also node_modules/). pantry/<name>/ is invisible to Bun.
The result is that swapping bun install for pantry install in a Bun/npm project leaves the on-disk state in a configuration where Bun cannot find the packages it just "installed."
The build pipeline runs on Bun
This isn't just about apps that happen to import 'lodash' at runtime — the entire build/test/lint/typecheck pipeline used by these projects is a Bun-runtime pipeline. The relevant scripts in a typical bunpress-style package.json:
{
"scripts": {
"build:bunpress": "cd packages/bunpress && bun run build",
"build": "bun --bun build.ts",
"typecheck": "bun --bun tsc --noEmit",
"lint": "bunx --bun pickier .",
"test": "bun test"
}
}
Every one of these invocations is Bun loading a JS/TS file as a Bun program. Inside build.ts you typically see things like:
import { dts } from 'bun-plugin-dtsx' // ← Bun build plugin
import { stx } from 'bun-plugin-stx' // ← Bun build plugin
import { CLI } from '@stacksjs/clapp'
await Bun.build({
entrypoints: ['./src/index.ts'],
plugins: [dts(), stx()],
})
The plugins themselves (bun-plugin-dtsx, bun-plugin-stx, bun-plugin-tailwindcss, etc.) are defined as Bun plugins — they exist solely to extend the Bun runtime/build system, are loaded by Bun's module resolver, and are registered via Bun.build({ plugins }) which calls back into Bun internals. They have no meaning outside Bun. Same goes for the broader toolchain: bunx --bun pickier, bun test, bun --bun tsc, bun packages/bunpress/bin/cli.ts dev — all Bun-runtime invocations resolving npm packages and binaries through Bun's module resolution.
So when pantry install puts bun-plugin-dtsx in pantry/bun-plugin-dtsx/ instead of node_modules/bun-plugin-dtsx/, the failure cascades through the entire downstream pipeline:
bun --bun build.ts → Could not resolve: "bun-plugin-dtsx" → build fails
bun --bun tsc --noEmit → Script not found "tsc" (no node_modules/typescript/bin/tsc) → typecheck fails
bunx --bun pickier . → has to fall back to fetching pickier from registry, or fails outright depending on cache state
- any
bun run <script> whose body imports an npm pkg → fails the moment Bun hits the import
There is no Bun-side knob that can fix this. bunfig.toml's linker only controls Bun's own installer layout (hoisted vs isolated under node_modules/); it does not point Bun's resolver at pantry/. Bun's plugin API does not accept a custom resolution root either.
Reproduction
A standard Bun project (e.g. stacksjs/bunpress) with:
- a
package.json that declares Bun-runtime npm deps (e.g. bun-plugin-dtsx, bun-plugin-stx, bunfig, @stacksjs/clapp, ts-syntax-highlighter, typescript)
- a
bunfig.toml with linker = "hoisted"
- a
build.ts that calls Bun.build({ plugins: [dts(), stx()] })
Steps:
rm -rf node_modules/
pantry install — completes successfully, populates pantry/<name>/ for every dep, no node_modules/ is created
pantry run build (or bun run build, or bun --bun build.ts)
Observed:
error: Could not resolve: "bun-plugin-dtsx". Maybe you need to "bun install"?
at .../build.ts
error: Could not resolve: "@stacksjs/clapp". ...
error: Could not resolve: "ts-syntax-highlighter". ...
error: Could not resolve: "bunfig". ...
The same pattern hits typecheck (bun --bun tsc --noEmit → Script not found "tsc" because node_modules/typescript/bin/tsc doesn't exist) and any bunx/bun --bun invocation that resolves an npm binary.
Why it happens
- Pantry's workspace install places each dep at
<workspace_root>/<modules_dir>/<name>/ where modules_dir defaults to pantry.
- Bun's resolver does not consult
pantry/ — it only looks at node_modules/ (and ancestor node_modules/ directories per Node-style resolution).
- Bun's plugin loader uses the same resolver, so Bun-build plugins (
bun-plugin-*) are subject to the same lookup.
bunfig.toml's linker option controls how Bun installs (hoisted vs isolated layout under node_modules/); it does not redirect Bun's resolver to a different directory.
- So packages that are 100% present on disk under
pantry/ are unresolvable from Bun's perspective. There is no Bun-side configuration that closes the gap.
Impact
This affects every project that:
- has a
package.json with npm deps, and
- runs anything via
bun build, bun --bun <bin>, bunx, bun test, or bun run <script> where the script (or anything it imports) reaches for an npm package
…which is essentially every Bun/TypeScript project today, and especially every project whose build is itself a Bun program loading Bun plugins. Migrating CI from bun install to pantry install produces a broken build with no diagnostic from pantry — the install step succeeds silently, and the failure surfaces later in whichever step first imports a JS dep.
Visible in CI for repos that already attempted the migration:
Notes
Problem
After
pantry install, npm dependencies declared inpackage.jsonare downloaded and extracted intopantry/<name>/. However,bun run X,bun --bun X, and anybun buildinvocation use Bun's module resolver, which only walksnode_modules/(plus whateverbunfig.toml's linker setting points to — which is alsonode_modules/).pantry/<name>/is invisible to Bun.The result is that swapping
bun installforpantry installin a Bun/npm project leaves the on-disk state in a configuration where Bun cannot find the packages it just "installed."The build pipeline runs on Bun
This isn't just about apps that happen to
import 'lodash'at runtime — the entire build/test/lint/typecheck pipeline used by these projects is a Bun-runtime pipeline. The relevant scripts in a typical bunpress-stylepackage.json:{ "scripts": { "build:bunpress": "cd packages/bunpress && bun run build", "build": "bun --bun build.ts", "typecheck": "bun --bun tsc --noEmit", "lint": "bunx --bun pickier .", "test": "bun test" } }Every one of these invocations is Bun loading a JS/TS file as a Bun program. Inside
build.tsyou typically see things like:The plugins themselves (
bun-plugin-dtsx,bun-plugin-stx,bun-plugin-tailwindcss, etc.) are defined as Bun plugins — they exist solely to extend the Bun runtime/build system, are loaded by Bun's module resolver, and are registered viaBun.build({ plugins })which calls back into Bun internals. They have no meaning outside Bun. Same goes for the broader toolchain:bunx --bun pickier,bun test,bun --bun tsc,bun packages/bunpress/bin/cli.ts dev— all Bun-runtime invocations resolving npm packages and binaries through Bun's module resolution.So when
pantry installputsbun-plugin-dtsxinpantry/bun-plugin-dtsx/instead ofnode_modules/bun-plugin-dtsx/, the failure cascades through the entire downstream pipeline:bun --bun build.ts→Could not resolve: "bun-plugin-dtsx"→ build failsbun --bun tsc --noEmit→Script not found "tsc"(nonode_modules/typescript/bin/tsc) → typecheck failsbunx --bun pickier .→ has to fall back to fetching pickier from registry, or fails outright depending on cache statebun run <script>whose body imports an npm pkg → fails the moment Bun hits the importThere is no Bun-side knob that can fix this.
bunfig.toml'slinkeronly controls Bun's own installer layout (hoistedvsisolatedundernode_modules/); it does not point Bun's resolver atpantry/. Bun's plugin API does not accept a custom resolution root either.Reproduction
A standard Bun project (e.g. stacksjs/bunpress) with:
package.jsonthat declares Bun-runtime npm deps (e.g.bun-plugin-dtsx,bun-plugin-stx,bunfig,@stacksjs/clapp,ts-syntax-highlighter,typescript)bunfig.tomlwithlinker = "hoisted"build.tsthat callsBun.build({ plugins: [dts(), stx()] })Steps:
rm -rf node_modules/pantry install— completes successfully, populatespantry/<name>/for every dep, nonode_modules/is createdpantry run build(orbun run build, orbun --bun build.ts)Observed:
The same pattern hits typecheck (
bun --bun tsc --noEmit→Script not found "tsc"becausenode_modules/typescript/bin/tscdoesn't exist) and anybunx/bun --buninvocation that resolves an npm binary.Why it happens
<workspace_root>/<modules_dir>/<name>/wheremodules_dirdefaults topantry.pantry/— it only looks atnode_modules/(and ancestornode_modules/directories per Node-style resolution).bun-plugin-*) are subject to the same lookup.bunfig.toml'slinkeroption controls how Bun installs (hoisted vs isolated layout undernode_modules/); it does not redirect Bun's resolver to a different directory.pantry/are unresolvable from Bun's perspective. There is no Bun-side configuration that closes the gap.Impact
This affects every project that:
package.jsonwith npm deps, andbun build,bun --bun <bin>,bunx,bun test, orbun run <script>where the script (or anything it imports) reaches for an npm package…which is essentially every Bun/TypeScript project today, and especially every project whose build is itself a Bun program loading Bun plugins. Migrating CI from
bun installtopantry installproduces a broken build with no diagnostic from pantry — the install step succeeds silently, and the failure surfaces later in whichever step first imports a JS dep.Visible in CI for repos that already attempted the migration:
pantry install(build.tsimportsbun-plugin-dtsxandbun-plugin-stx)Notes
pantry install's on-disk contract is for projects whose runtime (Bun) only knows how to resolve fromnode_modules/, and whose build pipeline runs entirely on top of that runtime via Bun plugins.node_modules/, install intonode_modules/directly when apackage.jsonis present, symlink scheme, etc.) is a design call.