ReviewOS

pantry-pm/pantry

pantry install + bun run build is incompatible: deps land in pantry/ but bun resolves only from node_modules/

#201
Open glennmichael123 opened this 24 days ago · 0 comments
24 days ago

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.tsCould not resolve: "bun-plugin-dtsx" → build fails
  • bun --bun tsc --noEmitScript 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:

  1. rm -rf node_modules/
  2. pantry install — completes successfully, populates pantry/<name>/ for every dep, no node_modules/ is created
  3. 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 --noEmitScript 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:

  • stacksjs/bunpress — typecheck, build, and test jobs all fail post-pantry install (build.ts imports bun-plugin-dtsx and bun-plugin-stx)
  • previously reported as the symptom of #200, but the underlying cause is the layout mismatch, not a missing install step

Notes

  • Filing this as the architectural problem behind #200. #200 frames it as "pantry install should also run bun install," but that's one possible response — the deeper question is what pantry install's on-disk contract is for projects whose runtime (Bun) only knows how to resolve from node_modules/, and whose build pipeline runs entirely on top of that runtime via Bun plugins.
  • Not proposing a fix in this issue; the right answer (mirror into node_modules/, install into node_modules/ directly when a package.json is present, symlink scheme, etc.) is a design call.

Sign in to comment on this issue.