[BUG] /design-sync story previews ignore package.json "browser": false, so builds fail on Node built-ins

Status Open
Reported on v2.1.282
Maintainer reply None cached
Activity 0 comments · opened Sep 25, 2026

Summary

/design-sync story previews ignore "browser": false entries in a dependency's package.json, so a story whose import graph reaches such a package fails to compile:

! preview build failed: <Component>: Could not resolve "stream" (node_modules/@saulx/utils/dist/src/readStream.js:1:28)

Plain esbuild with the same options builds the same import fine.

Claude Code 2.1.282, macOS, storybook shape.

Cause

@saulx/utils declares:

"browser": { "./dist/src/readStream.js": false }

esbuild honours that and resolves the file to an empty module. In lib/story-imports.mjs, however, the ds-import-policy plugin resolves every import itself with b.resolve(...) and then, for third-party files, returns that result:

if (p.includes('/node_modules/')) return r;           // third-party stays put

The returned object carries only path and namespace, so esbuild's "disabled" marker is lost and the real readStream.js is bundled. That file does import { PassThrough } from 'stream', which fails for platform: 'browser'.

Verified fix

I bundled a one-line story (import * as B from '@based', which reaches @saulx/utils through the Based SDK client) with the converter's own storyImportPlugins:

| Plugins | Result |
|---|---|
| Stock story-imports.mjs | Could not resolve "stream" |
| Same file, with that line changed to return null; | builds |
| Plain esbuild, no plugins | builds |

Returning null for node_modules paths hands resolution back to esbuild, which already keeps third-party files bundled as-is, and preserves the browser field handling. The same pattern is worth checking wherever a plugin returns a raw b.resolve() result.

cfg.storyImports offers no workaround that keeps the module's real contents. shim can only redirect the file to the design system's global.

View original on GitHub ↗