← reports

#2072 · Plugin frontends cannot typecheck imports of BB-shimmed packages (sonner, vaul, most radix families)

Bug Priority: Medium Effort: small plugins open on GitHub 2026-08-21 · base fcada5a3b

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 issueStatusEvidence
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.Verifiedls 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.VerifiedScaffold 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.VerifiedExactly 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.VerifiedRUNTIME_SLOT_BY_SPECIFIER, SHIM_FILTER + onResolve, shimExportsOfRUNTIME_EXPORT_MANIFEST[specifier].
grep -ci sonner …/bundled-types/bb-plugin-sdk-app.d.ts → 0: the SDK ships no declarations for shimmed specifiers.Verified0 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 main0.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".RefutedAfter 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

4. Minimal reproduction

A. CLI-driven (what the reporter did)

  1. 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 SDK npm install just 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)
  2. 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
  3. 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
  4. Build, with sonner absent from node_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.
  5. 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.
  6. The workaround and the "wrong section" experiment: npm install --save sonner (puts sonner 2.0.8 in dependencies). The 'sonner' TS2307 disappears (tsc-after-sonner-dep.log has only the other 12), and the rebuilt dist/app.js is 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.

  1. bb plugin build registers an esbuild onResolve hook 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 from RUNTIME_EXPORT_MANIFEST[specifier] (shimExportsOf), a table produced at bb build time by scripts/generate-runtime-export-manifest.mjs from the host app's installed packages. The plugin's own node_modules is irrelevant to the build for these specifiers — which is why the build "genuinely does not need the package".
  2. tsc resolves the same specifiers with moduleResolution: "bundler" from the scaffold's tsconfig (plugin-scaffold.ts#L1240-L1266) through the plugin's node_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 its dependencies. Coverage is therefore an accident of the starter component set: @radix-ui/react-dialog, clsx, tailwind-merge, class-variance-authority get typed; sonner and the rest do not. The generator's hand-copied SHIMMED_SPECIFIERS (L29-L52, "Keep in sync with RUNTIME_SLOT_BY_SPECIFIER") has already drifted — it lacks @pierre/diffs and @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.ts contain no declare 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 sonner as provided at runtime. None says "add it to devDependencies for types" or which major to pin.

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.

  1. 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 to starterTypeOnlyDeps, versions mirrored from apps/app/package.json (the existing versionedDeps() already does this and already has every package available — verified: all 16 packages are in apps/app dependencies). 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 both build-plugin-app.ts and the generator read (e.g. packages/plugin-build/runtime-slots.mjs exporting RUNTIME_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/react resolves once @pierre/diffs is installed.
  2. Let bb plugin types maintain those pins. Extend syncPluginTypes/planManifest in packages/templates/src/plugin-scaffold.ts to (re)write the shimmed packages' devDependency ranges to the host's versions alongside the SDK pin, and have --check report 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".
  3. 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 a devDependency for types only — at the host's version; bb plugin types keeps them matched; never put them in dependencies".
  4. 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

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.