#2072 · Plugin frontends cannot typecheck imports of BB-shimmed packages (sonner, vaul, most radix families)
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
A plugin author scaffolds a frontend plugin with bb plugin new toasty --app, follows the documentation and writes
import { toast } from "sonner", and then sees npx tsc --noEmit fail with
TS2307: Cannot find module 'sonner' while bb plugin build succeeds — even when sonner is not
installed at all. This is exactly what happens on the base commit: the build never resolves shimmed packages from
node_modules (esbuild intercepts the 21 shimmed specifiers and synthesises a module from a generated export
manifest), whereas tsc uses ordinary node resolution and nothing in the scaffold or in the
@get-bb/plugin-sdk declarations supplies types for them. The scaffold only installs type packages for the
shimmed specifiers the four starter components happen to import (@radix-ui/react-dialog, clsx,
tailwind-merge, class-variance-authority); 13 of the 21 documented "import freely" specifiers
(sonner, vaul, @pierre/diffs(+/react), nine radix families) are unresolvable
out of the box. The only documented guidance is "import freely", so the fix — a devDependency on a package that is
never bundled — is undiscoverable. A secondary problem: the natural workaround npm i -D sonner installs sonner
2.0.8 while the host ships 1.7.4, and nothing (bb plugin types included) keeps those type pins matched to the host.
2. Claims vs findings
| Claim from the issue | Status | Evidence |
|---|---|---|
import { toast } from "sonner" in a fresh bb plugin new --app scaffold fails tsc --noEmit with TS2307 but bb plugin build succeeds. | Verified | §4 steps 3–4; tsc-sonner.log, build-sonner.log. The import alone suffices. |
The build does not need the package: it succeeds with sonner absent from node_modules and emits a correct shim. | Verified | ls node_modules/sonner → "No such file or directory" before the build; dist/app.js contains globalThis.__bbPluginRuntime … N.sonner … {Toaster, toast, useSonner} = x (§4 step 4). |
Does not reproduce for react or @radix-ui/react-dialog because the scaffold pre-installs their types. | Verified | Scaffold devDependencies contain @types/react, @types/react-dom, @radix-ui/react-dialog (scaffold-package.json); the all-shims typecheck lists neither. |
Uncovered: sonner, vaul, @pierre/diffs (+/react), nine radix families. | Verified | Exactly those 13 specifiers fail in tsc-allshims.log (plus the app.tsx sonner line = 14 errors); the same bundle builds with all 21 slots referenced (build-allshims.log). |
Shim list is a static map and export lists come from a generated manifest, not node_modules. | Verified | RUNTIME_SLOT_BY_SPECIFIER, SHIM_FILTER + onResolve, shimExportsOf → RUNTIME_EXPORT_MANIFEST[specifier]. |
grep -ci sonner …/bundled-types/bb-plugin-sdk-app.d.ts → 0: the SDK ships no declarations for shimmed specifiers. | Verified | 0 matches for sonner/vaul in every file of packages/plugin-sdk/bundled-types/ built at base. build-bundled-dts.mjs EXTERNAL keeps real npm packages external and the SDK source never imports sonner. |
bb plugin types --check is clean, so it is not a stale-pin problem. | Verified (by code) | bb plugin types only repins @get-bb/plugin-sdk (plugin-scaffold.ts); it knows nothing about shimmed packages. |
Scaffold puts clsx, tailwind-merge, class-variance-authority in dependencies. | True for 0.38.0, already fixed on main | 0.38.0's committed generated file put them in PLUGIN_STARTER_DEPENDENCIES; since #1943 (2026-08-19, after the 0.38.0 tag) the generator classifies them as type-only devDependencies (scaffold-package.json at base shows them under devDependencies). |
Putting a shimmed package in dependencies "silently bloats a plugin bundle". | Refuted | After npm install --save sonner (sonner 2.0.8 on disk) dist/app.js is byte-identical in size (33,614 B) and still reads __bbPluginRuntime.sonner: the esbuild onResolve filter matches by specifier before node resolution. The real cost of the mistake is an unneeded download for git installs (npm install --omit=dev), not bundle size. |
Reproduces on bb 0.38.0 and on a dev build at 6be45053b. | Verified at fcada5a3b (later) | No commit between 6be45053b and origin/main touches the scaffold type deps or SDK declarations for this. |
3. Environment
- bb source at
fcada5a3b88302acb9944aa74b11db4ecaa215a0(main, 2026-08-21); dev CLI reportsbbVersion 0.39.0,pluginSdkVersion 0.4.11(not yet on npm — 0.4.10 is latest — so the scaffold's SDK pin was satisfied with annpm packtarball ofpackages/plugin-sdk, the workaround the CLI itself prints). - macOS 26.5.2 (Darwin 25.5.0), Node v22.23.1, npm 10.9.8, TypeScript 5.9.3 inside the scaffold (scaffold pins
^5.7.0). - No running server was needed:
bb plugin newandbb plugin buildare offline CLI commands. The build was run withBB_DATA_DIR=/tmp/bb-2072-dataso the build toolchain download (esbuild@0.28.1, tailwind 4.3.0) landed in an isolated data dir. No dev instance, no ports. - Scratch plugin:
/tmp/bb-2072-scratch/bb-plugin-toasty; unit repro:packages/templates/test/plugin-scaffold-shim-types.test.ts(in-worktree, uncommitted).
4. Minimal reproduction
A. CLI-driven (what the reporter did)
- Scaffold a frontend plugin with the dev CLI (equivalent to
bb plugin new toasty --app):cd /tmp/bb-2072-scratch NODE_ENV=development node <worktree>/apps/cli/dist/index.js plugin new toasty --app Created bb-plugin-toasty/ (bb-plugin-toasty). Warning: @get-bb/plugin-sdk 0.4.11 — this bb's SDK version — was not found on npm. `npm install` in the new plugin will fail until that version publishes. To work around it, pack the SDK from a bb checkout and point the devDependency at the tarball: (cd <bb-repo>/packages/plugin-sdk && npm pack) npm pkg set devDependencies.@get-bb/plugin-sdk="file:/abs/path/to/get-bb-plugin-sdk-0.4.11.tgz" Could not run npm install — run it in the plugin directory before `bb plugin build`. Next steps: cd bb-plugin-toasty npm install --include=dev bb plugin install .Because 0.4.11 is unpublished, satisfy the pin with a tarball (this is what the CLI suggests; with a published SDKnpm installjust works):(cd <worktree>/packages/plugin-sdk && npm pack --pack-destination /tmp/bb-2072-scratch) cd bb-plugin-toasty npm pkg set 'devDependencies.@get-bb/plugin-sdk=file:/tmp/bb-2072-scratch/get-bb-plugin-sdk-0.4.11.tgz' npm install --include=dev npx tsc --noEmit # exit 0 — the untouched scaffold typechecks (control)
- Add the documented toast import to
app.tsx(the import alone is enough; the call just makes it realistic):import { useState } from "react"; import { toast } from "sonner"; // <- added … onClick={() => { toast.success("hi"); // <- added - Typecheck:
$ npx tsc --noEmit expected: (no output, exit 0) actual: app.tsx(13,23): error TS2307: Cannot find module 'sonner' or its corresponding type declarations. tsc exit=2
- Build, with
sonnerabsent fromnode_modules:$ ls node_modules/sonner ls: node_modules/sonner: No such file or directory $ BB_DATA_DIR=/tmp/bb-2072-data NODE_ENV=development node <worktree>/apps/cli/dist/index.js plugin build Downloading the plugin build toolchain (one time)… esbuild@0.28.1, @tailwindcss/node@4.3.0, @tailwindcss/oxide@4.3.0, tailwindcss@4.3.0 Toolchain ready (0.9s) dist/server.js dist/server.js.map dist/server.meta.json dist/app.js dist/app.css dist/app.meta.json build exit=0 $ grep -o '.{60}sonner.{80}' dist/app.js | head -1 sion:Je}=R;var N=globalThis.__bbPluginRuntime;if(N==null||N.sonner==null)throw new Error('Cannot load "sonner": this bundle must be loaded by the …var x=N.sonner,ut="default"in x?x.default:x,{Toaster:dt,toast:J,useSonner:ct}=x;Expected: typecheck and build agree. Actual: the build emits a correct shim for a module the typechecker cannot find. - Scope — import every shimmed specifier (AllShims.tsx, wired into
app.tsx):$ npx tsc --noEmit app.tsx(13,23): error TS2307: Cannot find module 'sonner' or its corresponding type declarations. components/AllShims.tsx(5,30): error TS2307: Cannot find module '@pierre/diffs' or its corresponding type declarations. components/AllShims.tsx(6,35): error TS2307: Cannot find module '@pierre/diffs/react' or its corresponding type declarations. components/AllShims.tsx(7,30): error TS2307: Cannot find module '@radix-ui/react-alert-dialog' or its corresponding type declarations. components/AllShims.tsx(8,30): error TS2307: Cannot find module '@radix-ui/react-context-menu' or its corresponding type declarations. components/AllShims.tsx(10,31): error TS2307: Cannot find module '@radix-ui/react-dropdown-menu' or its corresponding type declarations. components/AllShims.tsx(11,28): error TS2307: Cannot find module '@radix-ui/react-hover-card' or its corresponding type declarations. components/AllShims.tsx(12,26): error TS2307: Cannot find module '@radix-ui/react-menubar' or its corresponding type declarations. components/AllShims.tsx(13,33): error TS2307: Cannot find module '@radix-ui/react-navigation-menu' or its corresponding type declarations. components/AllShims.tsx(14,26): error TS2307: Cannot find module '@radix-ui/react-popover' or its corresponding type declarations. components/AllShims.tsx(15,25): error TS2307: Cannot find module '@radix-ui/react-select' or its corresponding type declarations. components/AllShims.tsx(16,26): error TS2307: Cannot find module '@radix-ui/react-tooltip' or its corresponding type declarations. components/AllShims.tsx(17,25): error TS2307: Cannot find module 'sonner' or its corresponding type declarations. components/AllShims.tsx(18,23): error TS2307: Cannot find module 'vaul' or its corresponding type declarations. tsc exit=2 $ … plugin build dist/server.js dist/server.js.map dist/server.meta.json dist/app.js dist/app.css dist/app.meta.json build exit=0 $ grep -o '[A-Za-z]\.[a-zA-Z]*==null' dist/app.js | sed 's/^[A-Za-z]\.//; s/==null//' | sort -u | tr '\n' ' ' classVarianceAuthority clsx jsxRuntime pierreDiffs pierreDiffsReact pluginSdkApp radixAlertDialog radixContextMenu radixDialog radixDropdownMenu radixHoverCard radixMenubar radixNavigationMenu radixPopover radixSelect radixTooltip react reactDom reactDomClient sonner tailwindMerge vaul
13 of 21 shimmed specifiers fail to typecheck; all 21 build into runtime-slot shims. - The workaround and the "wrong section" experiment:
npm install --save sonner(puts sonner 2.0.8 independencies). The'sonner'TS2307 disappears (tsc-after-sonner-dep.log has only the other 12), and the rebuiltdist/app.jsis the same 33,614 bytes and still shims sonner — so the bundle is not bloated; the package is merely an unnecessary install. Note the version: host runs sonner^1.7.4(apps/app/package.json), the author now typechecks against 2.0.8.
B. Unit-level repro (fails on base, passes with the prototype fix)
packages/templates/test/plugin-scaffold-shim-types.test.ts scaffolds with
scaffoldPlugin({app: true}), symlinks exactly the packages the scaffold's package.json declares
(what npm install --include=dev leaves on disk; @get-bb/plugin-sdk → the workspace package, whose
bundled-types/ turbo built), and runs tsc --project tsconfig.json. Run from packages/templates with
pnpm exec vitest run test/plugin-scaffold-shim-types.test.ts.
RUN v4.1.1 /Users/sawyerhood/.bb-machines/bee.getbb.app/checkouts/bb/.claude/worktrees/wf_21e66a79-f02-12/packages/templates
❯ @bb/templates test/plugin-scaffold-shim-types.test.ts (3 tests | 2 failed) 15560ms
× `import { toast } from "sonner"` typechecks out of the box 5732ms
× every runtime-shimmed specifier typechecks out of the box 4467ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
FAIL @bb/templates test/plugin-scaffold-shim-types.test.ts > scaffold typechecks documented shimmed imports (#2072) > `import { toast } from "sonner"` typechecks out of the box
AssertionError: app.tsx(13,23): error TS2307: Cannot find module 'sonner' or its corresponding type declarations.
: expected 'app.tsx(13,23): error TS2307: Cannot …' not to contain 'TS2307'
- Expected
+ Received
- TS2307
+ app.tsx(13,23): error TS2307: Cannot find module 'sonner' or its corresponding type declarations.
+
❯ test/plugin-scaffold-shim-types.test.ts:187:46
185| // app.tsx(13,23): error TS2307: Cannot find module 'sonner' or …
186| // corresponding type declarations.
187| expect(result.output, result.output).not.toContain("TS2307");
| ^
188| expect(result.ok).toBe(true);
189| }, 120_000);
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
FAIL @bb/templates test/plugin-scaffold-shim-types.test.ts > scaffold typechecks documented shimmed imports (#2072) > every runtime-shimmed specifier typechecks out of the box
AssertionError: components/all-shims.ts(5,21): error TS2307: Cannot find module '@pierre/diffs' or its corresponding type declarations.
components/all-shims.ts(6,21): error TS2307: Cannot find module '@pierre/diffs/react' or its corresponding type declarations.
components/all-shims.ts(7,21): error TS2307: Cannot find module '@radix-ui/react-alert-dialog' or its corresponding type declarations.
components/all-shims.ts(8,21): error TS2307: Cannot find module '@radix-ui/react-context-menu' or its corresponding type declarations.
components/all-shims.ts(10,21): error TS2307: Cannot find module '@radix-ui/react-dropdown-menu' or its corresponding type declarations.
components/all-shims.ts(11,22): error TS2307: Cannot find module '@radix-ui/react-hover-card' or its corresponding type declarations.
components/all-shims.ts(12,22): error TS2307: Cannot find module '@radix-ui/react-menubar' or its corresponding type declarations.
components/all-shims.ts(13,22): error TS2307: Cannot find module '@radix-ui/react-navigation-menu' or its corresponding type declarations.
components/all-shims.ts(14,22): error TS2307: Cannot find module '@radix-ui/react-popover' or its corresponding type declarations.
components/all-shims.ts(15,22): error TS2307: Cannot find module '@radix-ui/react-select' or its corresponding type declarations.
components/all-shims.ts(16,22): error TS2307: Cannot find module '@radix-ui/react-tooltip' or its corresponding type declarations.
components/all-shims.ts(17,22): error TS2307: Cannot find module 'sonner' or its corresponding type declarations.
components/all-shims.ts(18,22): error TS2307: Cannot find module 'vaul' or its corresponding type declarations.
: expected [ '@pierre/diffs', …(12) ] to deeply equal []
- Expected
+ Received
- []
+ [
+ "@pierre/diffs",
+ "@pierre/diffs/react",
+ "@radix-ui/react-alert-dialog",
+ "@radix-ui/react-context-menu",
+ "@radix-ui/react-dropdown-menu",
+ "@radix-ui/react-hover-card",
+ "@radix-ui/react-menubar",
+ "@radix-ui/react-navigation-menu",
+ "@radix-ui/react-popover",
+ "@radix-ui/react-select",
+ "@radix-ui/react-tooltip",
+ "sonner",
+ "vaul",
+ ]
❯ test/plugin-scaffold-shim-types.test.ts:208:36
206| // On main: 13 of the 21 specifiers are unresolvable (all but reac…
207| // @radix-ui/react-dialog, clsx, tailwind-merge, class-variance-au…
208| expect(missing, result.output).toEqual([]);
| ^
209| expect(result.ok).toBe(true);
210| }, 120_000);
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
Test Files 1 failed (1)
Tests 2 failed | 1 passed (3)
Start at 08:53:27
Duration 16.12s (transform 320ms, setup 0ms, import 477ms, tests 15.56s, environment 0ms)
The control ("untouched scaffold typechecks") passes; the two assertions that a documented import resolves fail with the exact TS2307 text. Full test source:
// Repro for get-bb/bb#2072: a fresh `bb plugin new --app` scaffold cannot
// typecheck the documented `import { toast } from "sonner"` (nor most other
// BB-shimmed specifiers) even though `bb plugin build` bundles it fine.
//
// The test scaffolds a plugin, materialises node_modules the way `npm install`
// would (every package the scaffold's package.json declares, symlinked from
// this workspace so no network is needed), writes an app.tsx that imports a
// shimmed package, and runs tsc with the scaffold's own tsconfig.
import { readFileSync } from "node:fs";
import {
mkdir,
mkdtemp,
readFile,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import { execFile } from "node:child_process";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { scaffoldPlugin } from "../src/plugin-scaffold.js";
const execFileAsync = promisify(execFile);
const repoRoot = resolve(process.cwd(), "../..");
const appRoot = join(repoRoot, "apps", "app");
const pluginSdkRoot = join(repoRoot, "packages", "plugin-sdk");
const requireFromApp = createRequire(join(appRoot, "package.json"));
const requireFromRoot = createRequire(join(repoRoot, "package.json"));
const requireFromSdk = createRequire(join(pluginSdkRoot, "package.json"));
/**
* Mirror of RUNTIME_SLOT_BY_SPECIFIER in
* packages/plugin-build/src/build-plugin-app.ts, minus the SDK's own
* specifiers and the shared-ui icon module. Every one of these is documented
* as "import freely, never bundled" in the bb-plugin-authoring skill.
*/
const SHIMMED_SPECIFIERS = [
"react",
"react-dom",
"react-dom/client",
"react/jsx-runtime",
"@pierre/diffs",
"@pierre/diffs/react",
"@radix-ui/react-alert-dialog",
"@radix-ui/react-context-menu",
"@radix-ui/react-dialog",
"@radix-ui/react-dropdown-menu",
"@radix-ui/react-hover-card",
"@radix-ui/react-menubar",
"@radix-ui/react-navigation-menu",
"@radix-ui/react-popover",
"@radix-ui/react-select",
"@radix-ui/react-tooltip",
"sonner",
"vaul",
"clsx",
"tailwind-merge",
"class-variance-authority",
] as const;
function packageRoot(name: string): string {
// pnpm lays each dependency out as node_modules/<name> (a symlink into the
// store); prefer that over require.resolve, which ESM-only packages with a
// strict exports map (e.g. @pierre/diffs) refuse.
for (const base of [appRoot, repoRoot, pluginSdkRoot]) {
const candidate = join(base, "node_modules", name);
try {
readFileSync(join(candidate, "package.json"), "utf8");
return candidate;
} catch {
// not here
}
}
for (const req of [requireFromApp, requireFromRoot, requireFromSdk]) {
try {
return dirname(req.resolve(`${name}/package.json`));
} catch {
// package.json may not be exported; walk up from the entry instead.
try {
let current = dirname(req.resolve(name));
while (true) {
try {
const manifest = JSON.parse(
readFileSync(join(current, "package.json"), "utf8"),
) as { name?: string };
if (manifest.name === name) return current;
} catch {
// keep walking
}
const parent = dirname(current);
if (parent === current) break;
current = parent;
}
} catch {
// try the next resolver
}
}
}
throw new Error(`package root not found in workspace: ${name}`);
}
/**
* What `npm install --include=dev` leaves on disk for the scaffold: exactly the
* packages its package.json declares, nothing more. @get-bb/plugin-sdk links
* to the workspace package (turbo's build:types has filled bundled-types/).
*/
async function installDeclaredDependencies(targetDir: string): Promise<void> {
const manifest = JSON.parse(
await readFile(join(targetDir, "package.json"), "utf8"),
) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
const names = new Set([
...Object.keys(manifest.dependencies ?? {}),
...Object.keys(manifest.devDependencies ?? {}),
]);
for (const name of names) {
const target = join(targetDir, "node_modules", name);
await mkdir(dirname(target), { recursive: true });
const source =
name === "@get-bb/plugin-sdk" ? pluginSdkRoot : packageRoot(name);
await symlink(source, target, "dir");
}
}
async function runTsc(
targetDir: string,
): Promise<{ ok: boolean; output: string }> {
const typescriptRoot = packageRoot("typescript");
try {
const { stdout, stderr } = await execFileAsync(
process.execPath,
[join(typescriptRoot, "lib", "tsc.js"), "--project", "tsconfig.json"],
{ cwd: targetDir },
);
return { ok: true, output: `${stdout}${stderr}` };
} catch (error) {
const failed = error as { stderr?: string; stdout?: string };
return { ok: false, output: `${failed.stdout ?? ""}${failed.stderr ?? ""}` };
}
}
describe("scaffold typechecks documented shimmed imports (#2072)", () => {
let workDir: string;
let targetDir: string;
beforeEach(async () => {
workDir = await mkdtemp(join(tmpdir(), "bb-scaffold-shims-"));
targetDir = join(workDir, "bb-plugin-toasty");
await scaffoldPlugin({
targetDir,
packageName: "bb-plugin-toasty",
bbVersion: "0.39.0",
app: true,
});
await installDeclaredDependencies(targetDir);
});
afterEach(async () => {
await rm(workDir, { recursive: true, force: true });
});
it("the untouched scaffold typechecks (control)", async () => {
const result = await runTsc(targetDir);
expect(result.output, result.output).toBe("");
expect(result.ok).toBe(true);
}, 120_000);
it('`import { toast } from "sonner"` typechecks out of the box', async () => {
const appPath = join(targetDir, "app.tsx");
const app = await readFile(appPath, "utf8");
await writeFile(
appPath,
app.replace(
'import { useState } from "react";',
'import { useState } from "react";\nimport { toast } from "sonner";\ntoast.success("hi");',
),
);
const result = await runTsc(targetDir);
// On main this fails with:
// app.tsx(13,23): error TS2307: Cannot find module 'sonner' or its
// corresponding type declarations.
expect(result.output, result.output).not.toContain("TS2307");
expect(result.ok).toBe(true);
}, 120_000);
it("every runtime-shimmed specifier typechecks out of the box", async () => {
const lines = SHIMMED_SPECIFIERS.map(
(specifier, i) => `import * as m${i} from "${specifier}";`,
);
lines.push(
`export const all = [${SHIMMED_SPECIFIERS.map((_, i) => `m${i}`).join(", ")}];`,
);
await writeFile(
join(targetDir, "components", "all-shims.ts"),
`${lines.join("\n")}\n`,
);
const result = await runTsc(targetDir);
const missing = [
...result.output.matchAll(/Cannot find module '([^']+)'/g),
].map((m) => m[1]);
// On main: 13 of the 21 specifiers are unresolvable (all but react*,
// @radix-ui/react-dialog, clsx, tailwind-merge, class-variance-authority).
expect(missing, result.output).toEqual([]);
expect(result.ok).toBe(true);
}, 120_000);
});
Repro files: 2072/repro/
5. Root cause
Two resolvers, one of which never looks at node_modules.
-
bb plugin buildregisters an esbuildonResolvehook whose filter is derived from the shim table and routes every matching specifier into a synthetic namespace, before esbuild ever tries node resolution (build-plugin-app.ts#L242-L262):const SHIM_FILTER = new RegExp( `^(${Object.keys(RUNTIME_SLOT_BY_SPECIFIER) .map((specifier) => specifier.replace(/[/@.-]/g, "\\$&")) .join("|")})$`, ); … build.onResolve({ filter: SHIM_FILTER }, (args) => ({ path: args.path, namespace: SHIM_NAMESPACE, }));The module body is generated by shimModuleSource fromRUNTIME_EXPORT_MANIFEST[specifier](shimExportsOf), a table produced at bb build time byscripts/generate-runtime-export-manifest.mjsfrom the host app's installed packages. The plugin's ownnode_modulesis irrelevant to the build for these specifiers — which is why the build "genuinely does not need the package". -
tscresolves the same specifiers withmoduleResolution: "bundler"from the scaffold's tsconfig (plugin-scaffold.ts#L1240-L1266) through the plugin'snode_modules. Three places could supply declarations and none does for 13 of the 21 specifiers:- The scaffold's devDependencies. scaffoldPlugin writes
PLUGIN_STARTER_TYPE_DEPENDENCIES, which generate-plugin-scaffold.mjs fills only with shimmed packages that a starter registry item (button, card, input, dialog) lists in itsdependencies. Coverage is therefore an accident of the starter component set:@radix-ui/react-dialog,clsx,tailwind-merge,class-variance-authorityget typed;sonnerand the rest do not. The generator's hand-copiedSHIMMED_SPECIFIERS(L29-L52, "Keep in sync with RUNTIME_SLOT_BY_SPECIFIER") has already drifted — it lacks@pierre/diffsand@pierre/diffs/react. - The SDK declarations. build-bundled-dts.mjs keeps every real npm package external and the SDK source never imports sonner/vaul/radix, so
bundled-types/*.d.tscontain nodeclare module "sonner"(grep: 0 hits). - Docs. The authoring skill says "Never bundled (runtime-shimmed, import freely)" (SKILL.md#L2164-L2177), the guide repeats it (bb-guide-plugins.md#L575-L579), and the scaffold README (plugin-scaffold.ts#L1297-L1304) names
sonneras provided at runtime. None says "add it to devDependencies for types" or which major to pin.
- The scaffold's devDependencies. scaffoldPlugin writes
Why the symptom follows: the import compiles under esbuild because esbuild is told to fabricate the module, and fails under tsc because tsc is told nothing. The first-party plugins in this repo work around it exactly as the reporter did — plugins/github, plugins/docs, plugins/memory, plugins/automations, plugins/side-chat all carry "sonner": "^1.7.4" (and vaul) in devDependencies — which confirms the devDependency is the intended mechanism and that it simply was never propagated to the scaffold or the docs.
Underlying issue (version drift): whichever package an author installs for types is unmanaged. npm i -D sonner today yields 2.0.8 against a 1.7.4 host runtime (sonner 2 adds toast.getToasts()/getHistory and onAutoClose typings that do not exist at runtime). bb plugin types repins only @get-bb/plugin-sdk (plugin-scaffold.ts#L599-L620), and the docs only state majors for clsx/tailwind-merge/cva. The runtime export manifest already encodes the host's exact surface but is never surfaced to the typechecker.
6. Proposed fix (first principles)
Confidence: high that the mechanism is as described; the prototype below makes the repro test pass.
- Make the scaffold's type-only devDependencies the full shim list, not the starter-component subset. In
packages/templates/scripts/generate-plugin-scaffold.mjs, add every shimmed package tostarterTypeOnlyDeps, versions mirrored fromapps/app/package.json(the existingversionedDeps()already does this and already has every package available — verified: all 16 packages are inapps/appdependencies). Prototype diff (makes all three repro assertions pass, vitest-shims-prototype.log):diff --git a/packages/templates/scripts/generate-plugin-scaffold.mjs b/packages/templates/scripts/generate-plugin-scaffold.mjs index 85aaee761..f08ad124a 100644 --- a/packages/templates/scripts/generate-plugin-scaffold.mjs +++ b/packages/templates/scripts/generate-plugin-scaffold.mjs @@ -86,6 +86,16 @@ const starterTypeOnlyDeps = new Set(); ); } } +// #2072 prototype: every shimmed package is documented as "import freely", +// so every one of them must typecheck out of the box — not only the ones the +// starter components happen to import. Types only (devDependencies); the +// build never bundles them. +for (const specifier of SHIMMED_SPECIFIERS) { + starterTypeOnlyDeps.add(specifier); +} +// @pierre/diffs is shimmed too but not in SHIMMED_SPECIFIERS above (the set +// predates it); add it explicitly so the drift is visible in this prototype. +starterTypeOnlyDeps.add("@pierre/diffs"); starterFiles.sort((a, b) => { if (a.target < b.target) return -1; if (a.target > b.target) return 1;Better than the prototype's hand-maintained set: move the slot map to one data module that bothbuild-plugin-app.tsand the generator read (e.g.packages/plugin-build/runtime-slots.mjsexportingRUNTIME_SLOT_BY_SPECIFIER, imported by the TS source), so the generator cannot drift again (it already lost@pierre/diffs).react*stays covered by@types/react(-dom);@pierre/diffs/reactresolves once@pierre/diffsis installed. - Let
bb plugin typesmaintain those pins. ExtendsyncPluginTypes/planManifestinpackages/templates/src/plugin-scaffold.tsto (re)write the shimmed packages' devDependency ranges to the host's versions alongside the SDK pin, and have--checkreport drift. This fixes both "I added a shimmed import later" and the sonner 1.x/2.x mismatch, and it is the one place that already knows "the bb you actually run". - Document it where authors look (AGENTS.md requires the CLI/guide/skill surfaces to move together): in
bb-plugin-authoring/SKILL.md§"Never bundled",bb-guide-plugins.md, and the scaffold README: "shimmed packages need adevDependencyfor types only — at the host's version;bb plugin typeskeeps them matched; never put them independencies". - Guard it. Keep a test asserting
PLUGIN_STARTER_TYPE_DEPENDENCIES ⊇ RUNTIME_SLOT_BY_SPECIFIER(minus SDK/react/icon slots), or adopt the repro test from §4B, which exercises the real tsc path without network.
On the issue's option 1 (ambient declarations shipped by the SDK): not recommended. The export manifest only knows names, so generated
ambient modules would be any-typed (worse than an error); bundling the real radix/sonner/vaul declarations into
bb-plugin-sdk-app.d.ts via rollup-plugin-dts is possible but creates a second copy of third-party types that the author's own installed
copies (shadcn add @bb/drawer installs vaul as a dependency today) would shadow or conflict with, and it balloons the SDK
declaration bundle. The devDependency route is what every in-repo plugin already uses.
What could go wrong: adding ~12 devDependencies slows bb plugin new's npm install slightly and the
radix packages pull peer react (already present). Pinning with carets mirrored from apps/app keeps types in the host's
major; an exact pin would be stricter but would need the same release-time bump discipline as the SDK pin.
7. PR review
No open pull request is linked to this issue (searched 2072 in:body and "sonner types"; none).
8. Related issues
- #1943 "Stop committing generated modules; turbo generates them" — moved clsx/tailwind-merge/cva to type-only devDependencies (the reporter's side note), landed 2026-08-19, after the 0.38.0 tag.
- #1133 (closed) "bb plugin new --app omits required dev dependencies under NODE_ENV=production" — same area (scaffold install vs. devDependencies).
- #1850 (closed) "Make plugin SDK bundled declaration generation byte-deterministic" — the SDK declaration bundle this report shows contains no shimmed-package types.
- #2026 (merged) — unrelated
fileOpenerfixes from the same authoring session.
9. Appendix
Scaffold package.json at base (before edits)
{
"name": "bb-plugin-toasty",
"version": "0.1.0",
"type": "module",
"engines": {
"bb": ">=0.39",
"bbPluginSdk": ">=0.4.11"
},
"bb": {
"name": "Toasty",
"description": "A BB plugin.",
"branding": {
"icon": "Zap"
},
"server": "./server.ts",
"app": "./app.tsx"
},
"dependencies": {
"@hugeicons/core-free-icons": "^4.1.3",
"@hugeicons/react": "^1.1.6",
"@radix-ui/react-slot": "^1.3.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@get-bb/plugin-sdk": "file:/tmp/bb-2072-scratch/get-bb-plugin-sdk-0.4.11.tgz",
"@radix-ui/react-dialog": "^1.1.19",
"@types/better-sqlite3": "^7.6.12",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"better-sqlite3": "^12.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"hono": "^4.11.9",
"tailwind-merge": "^3.4.0",
"typescript": "^5.7.0"
}
}
AllShims.tsx
// Every specifier in RUNTIME_SLOT_BY_SPECIFIER (packages/plugin-build/src/build-plugin-app.ts)
import * as React from "react";
import * as ReactDom from "react-dom";
import * as ReactDomClient from "react-dom/client";
import * as PierreDiffs from "@pierre/diffs";
import * as PierreDiffsReact from "@pierre/diffs/react";
import * as AlertDialog from "@radix-ui/react-alert-dialog";
import * as ContextMenu from "@radix-ui/react-context-menu";
import * as Dialog from "@radix-ui/react-dialog";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as HoverCard from "@radix-ui/react-hover-card";
import * as Menubar from "@radix-ui/react-menubar";
import * as NavigationMenu from "@radix-ui/react-navigation-menu";
import * as Popover from "@radix-ui/react-popover";
import * as Select from "@radix-ui/react-select";
import * as Tooltip from "@radix-ui/react-tooltip";
import * as Sonner from "sonner";
import * as Vaul from "vaul";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cva } from "class-variance-authority";
export const all = [
React,
ReactDom,
ReactDomClient,
PierreDiffs,
PierreDiffsReact,
AlertDialog,
ContextMenu,
Dialog,
DropdownMenu,
HoverCard,
Menubar,
NavigationMenu,
Popover,
Select,
Tooltip,
Sonner,
Vaul,
clsx,
twMerge,
cva,
];
Prototype run (all three tests green)
RUN v4.1.1 /Users/sawyerhood/.bb-machines/bee.getbb.app/checkouts/bb/.claude/worktrees/wf_21e66a79-f02-12/packages/templates
Test Files 1 passed (1)
Tests 3 passed (3)
Start at 08:53:08
Duration 9.59s (transform 174ms, setup 0ms, import 252ms, tests 9.27s, environment 0ms)
Commands run (in order)
git checkout fcada5a3b
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
mkdir -p /tmp/bb-2072-scratch && cd /tmp/bb-2072-scratch
NODE_ENV=development node <worktree>/apps/cli/dist/index.js plugin new toasty --app
(cd <worktree>/packages/plugin-sdk && npm pack --pack-destination /tmp/bb-2072-scratch)
cd bb-plugin-toasty
npm pkg set 'devDependencies.@get-bb/plugin-sdk=file:/tmp/bb-2072-scratch/get-bb-plugin-sdk-0.4.11.tgz'
npm install --include=dev
npx tsc --noEmit # exit 0 (control)
# add `import { toast } from "sonner"` + toast.success("hi") to app.tsx
npx tsc --noEmit # exit 2, TS2307 sonner
ls node_modules/sonner # does not exist
BB_DATA_DIR=/tmp/bb-2072-data NODE_ENV=development node <worktree>/apps/cli/dist/index.js plugin build # exit 0
# write components/AllShims.tsx, import it from app.tsx
npx tsc --noEmit # 14 TS2307 errors
… plugin build # exit 0, all 21 slots shimmed
npm install --save sonner # 2.0.8
npx tsc --noEmit # sonner error gone, 12 remain
… plugin build; wc -c dist/app.js # 33614 both times
cd <worktree>/packages/templates
pnpm exec vitest run test/plugin-scaffold-shim-types.test.ts # 2 failed, 1 passed (base)
# apply prototype diff; node packages/templates/scripts/generate-plugin-scaffold.mjs
pnpm exec vitest run test/plugin-scaffold-shim-types.test.ts # 3 passed
git checkout -- packages/templates/scripts/generate-plugin-scaffold.mjs; regenerate
git fetch origin main; git log fcada5a3b..origin/main -- packages/templates packages/plugin-build packages/plugin-sdk # empty
Note on isolation: the first plugin build ran without BB_DATA_DIR and cached the build toolchain under ~/.bb/plugins/toolchain-0.28.1-4.3.0-4.3.0-4.3.0 (a pure npm-package cache, created 08:47:13, no other file touched). It was removed immediately and every later build used /tmp/bb-2072-data.