Files
suaveplan/openspec/archive/2026-05-19-fix-storage-uri-plugin-bundle-node-fetch-emit/proposal.md
2026-05-27 23:29:53 +01:00

66 lines
4.4 KiB
Markdown

# Fix `storage-uri-plugin-bundle` so the `./node-fetch` subpath emits its `.js`
**Package**: `@suaveplan/storage-uri-plugin-bundle`
**Status**: PROPOSED
**Type**: bug-fix
## Why
The shipped `storage-uri-plugin-bundle` package declares a `./node-fetch` subpath export in its `package.json`:
```json
"./node-fetch": {
"import": {
"types": "./dist/node-fetch/node-fetch.d.ts",
"default": "./dist/node-fetch/node-fetch.js"
}
}
```
…and the existing spec (`openspec/specs/storage-uri-plugin-bundle/spec.md`, requirement "Server-tier `./node-fetch` subpath for `file://` roots on Node") requires this entry point to exist and behave correctly. The build, however, only emits `dist/node-fetch/node-fetch.d.ts` — the `.js` is **never written**. Cause:
- `vite.config.ts` declares `lib: { entry: "src/index.ts" }` (single entry).
- `src/index.ts` does NOT re-export `nodeFileFetch` (correctly — the package contract is universal-tier at the root barrel; the node-fetch surface is server-tier and lives behind the subpath).
- Rollup, walking only from `src/index.ts`, never sees `src/node-fetch/node-fetch.ts` and tree-shakes it. `vite-plugin-dts` walks the source tree independently, so the `.d.ts` survives — masking the defect from type-only consumers.
Node-tier consumers that import `@suaveplan/storage-uri-plugin-bundle/node-fetch` get `MODULE_NOT_FOUND` at runtime. Both Stage-3 audio-ir room packages (`club`, `pool`) hit this and worked around it by inlining a local `nodeFileFetch` adapter. The fix is mechanical: add `src/node-fetch/node-fetch.ts` as a second rollup entry so it's emitted as a top-level chunk.
## What changes
- MODIFIED: `vite.config.ts` — replace `lib: { entry: "src/index.ts" }` with a multi-entry build that produces both `dist/index.js` and `dist/node-fetch/node-fetch.js`. Use `lib: { entry: { index: "src/index.ts", "node-fetch/node-fetch": "src/node-fetch/node-fetch.ts" }, formats: ["es"] }`. `preserveModules: true` is already set, so per-file chunking continues to work.
- ADDED requirement: a build-time invariant in the package's capability spec. The runtime contract requirement already exists; this one explicitly mandates that the build artifact ships, so the same regression cannot recur silently.
## Out of scope
- Any runtime behavior change. `nodeFileFetch`'s implementation under `src/node-fetch/node-fetch.ts` is unchanged.
- Any change to the root barrel (`src/index.ts`) — it stays universal-tier with no `node:*` imports.
- Audio-IR consumer cleanups. Removing the inline shims in `audio-ir-room-plugin-club` and `audio-ir-room-plugin-pool` is a separate, follow-up change once this fix lands. Not bundled here per AGENTS §193 (one change per package).
## Source mapping
In-repo fix. No upstream reference. The capability spec already documents the desired behavior; this change adds an additional ADDED requirement covering the build artifact.
### Requirements with no source line
| Requirement | Justification |
|---|---|
| Build-time emission invariant | Caught only by Stage-3 consumers; not previously stated as a spec requirement |
## Verification approach
- `tests/build/node-fetch-emit.test.ts` — after `bunx turbo build --filter=@suaveplan/storage-uri-plugin-bundle`, the test reads `<package-root>/dist/node-fetch/node-fetch.js` via `node:fs.statSync`, asserts the file exists, is non-empty, and is a `.js` file (not a `.d.ts`).
- `tests/build/node-fetch-runtime-import.test.ts` — dynamically `import()`s `<package-root>/dist/node-fetch/node-fetch.js` directly and asserts the export `nodeFileFetch` is a function — proves the emitted file is actually runnable, not just present.
- Re-runs of all existing node-fetch source-level tests (`src/node-fetch/node-fetch.test.ts`) — must remain green; this change does not modify the runtime.
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/storage-uri-plugin-bundle`. Today:
- `@suaveplan/audio-ir-room-plugin-club` — [x] no action needed (reason: inline shim under integration tests does not depend on the subpath emit; the follow-up cleanup change will adopt the shipped subpath)
- `@suaveplan/audio-ir-room-plugin-pool` — [x] no action needed (same reason)
- `@suaveplan/audio-ir-loader` — [x] no action needed (consumer doesn't import the subpath)
All three remain green at 100/100/100/100 after this fix. The shipped subpath becomes usable for new consumers immediately.