Skip to content

fix(bun-plugin,rsbuild-plugin): stop baking absolute CSS paths into transformed code - #657

Merged
owjs3901 merged 2 commits into
mainfrom
owjs3901/du1-worktree-css-path-leak
Sep 9, 2026
Merged

fix(bun-plugin,rsbuild-plugin): stop baking absolute CSS paths into transformed code#657
owjs3901 merged 2 commits into
mainfrom
owjs3901/du1-worktree-css-path-leak

Conversation

@owjs3901

@owjs3901 owjs3901 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Machine-absolute CSS paths escape from two plugins into transformed module text. In bun-plugin this breaks tests outright; in rsbuild-plugin it makes the default build output depend on where the repository is checked out. Both are fixed here by the rule the other three plugins already follow: the stylesheet import must be relative to the importer, and nothing derived from process.cwd() may reach emitted output.

Symptom

Developing one repository in several checkouts at once — git worktree, a CI matrix, sibling clones — makes every checkout but the first fail every bun test that touches a Devup UI component. Found on a project running 9 worktrees in parallel: three of them reported 468, 468 and 314 failing tests, in worktrees where only Rust or Markdown had been edited, or nothing at all. git stash did not change the result, so nobody could tell whether their own changes were broken.

$ bun test "apps/front/src/app/(auth)/alarm/setting/__test__/page.test.tsx"
bun test v1.4.1 (4661e494f)
error: Cannot find module
  'C:\...\workspaces\girok-space\wquw-156-charcount\df\devup-ui\devup-ui.css'
from
  'C:\...\workspaces\girok-space\wquw-168-companion-default-public\apps\front\src\components\pages\settring\SettingList.tsx'
 0 pass  1 fail

The importing file is in one worktree; the stylesheet it is told to import is an absolute path into a different, unrelated worktree.

Reproduction

Two directories with a byte-identical source file, bunfig.toml preloading @devup-ui/bun-plugin, run one after the other:

$ cd wtA && bun test
 1 pass  0 fail

$ cd wtB && bun test
error: Cannot find module 'C:\...\du1-repro\wtA\df\devup-ui\devup-ui.css'
  from 'C:\...\du1-repro\wtB\src\Comp.tsx'
 0 pass  1 fail

This is now packages/bun-plugin/__regression__/worktree-isolation.bun.ts.

Root cause

bun-plugin

  1. plugin.ts froze cssDir = resolve('df', 'devup-ui') — an absolute path derived from process.cwd() at module load — and onResolve answered the injected stylesheet import with { path: join(cssDir, fileName) }.

  2. Bun persists transpiled modules in a machine-wide on-disk cache (<bun cache>/@t@/*.pile) keyed by module contents only, with plugin-resolved import specifiers already baked in. Neither the cwd nor the importing file is part of the key. A poisoned entry looks like this:

    @t@/00bb51e95221ba90.pile
      import"C:\\...\\girok-space\\wquw-151-bonyeon\\apps\\front\\df\\devup-ui\\devup-ui.css";
    
  3. Checkouts of one repository hold byte-identical sources, so they collapse onto one cache entry, and every checkout after the first imports the first one's absolute CSS path.

The extractor was never at fault. Given a relative css_dir it emits exactly that (libs/extractor/src/lib.rs, main_css_path); the absolute path was created in JS, in onResolve:

$ bun run probe.ts
--- emitted code ---
import "../df/devup-ui/devup-ui.css";
export const fade = "a";

Two things made this worse: the shared package directories are hardlinked across worktrees by bun install, guaranteeing identical inputs; and BUN_RUNTIME_TRANSPILER_CACHE_DIR is ignored in Bun 1.4.1, so the cache cannot be isolated per checkout.

rsbuild-plugin

The same defect class, found while auditing the other plugins. The non-atom path passed the absolute cssDir straight to codeExtract, so every transformed module carried an absolute specifier — with mixed separators on Windows:

import "C:\\...\\<checkout>\\df\\devup-ui/devup-ui-0.css";

Non-atom with singleCss: false is the default configuration, so this is what rsbuild users get out of the box. Byte-identical sources in two checkouts therefore produce different transform output, which is precisely what makes a content-addressed or relocated build cache unsafe. rspack caches per project, so this did not manifest as the loud bun test failure — but the underlying property is the same one that broke Bun.

Fix

bun-plugin — resolve the stylesheet onto a virtual namespaced id ({ path: 'devup-ui.css', namespace: 'devup-ui' }) in the new packages/bun-plugin/src/css-id.ts, and serve it from an onLoad in that namespace. Nothing derived from the cwd can reach a cache entry any more, so sharing one is harmless. The module is empty on purpose: Bun's runtime has no CSS loader (onLoad accepts only the script/data loaders) and the stylesheet is a bundler-side build artifact.

Resolution is decided on the shape of the resolved directory (<distDir>/devup-ui/) rather than on equality with this checkout's absolute cssDir. That keeps the function independent of process.cwd() and, importantly, repairs cache entries an older plugin version already poisoned — no cache wipe required. Verified with the poisoned pile left on disk:

@t@/e8ea24ce4f0d9ce6.pile -> C:\...\du1-repro\wtA\df\devup-ui\devup-ui.css   (still present)
$ cd wtA && bun test   -> 1 pass
$ cd wtB && bun test   -> 1 pass

rsbuild-plugin — always compute relative(dirname(resourcePath), cssDir), as next/webpack/vite already do. Atom mode's POSIX-normalized extraction filename is kept (it exists to match the absolute-keyed canonical map / FILE_ROUTES, which is unrelated), and cssFile is consumed via basename() exactly as in webpack and next, so file numbering and write locations are unchanged.

extractCssDir = ./../df/devup-ui
import "./../df/devup-ui/devup-ui-0.css";

Regression tests

All were verified failing before the change and passing after.

  • packages/bun-plugin/__regression__/worktree-isolation.bun.ts — drives two checkouts through two sequential bun test child processes over the shared cache and asserts each stays on its own stylesheet. Restoring the old onResolve reproduces the exact production error. Wired into bun run --filter @devup-ui/bun-plugin test:regression.

  • packages/bun-plugin/src/__tests__/css-id.test.ts — two checkouts yield the same path-free id, a foreign absolute path is repaired, stylesheets outside the dist css dir keep Bun's normal resolution.

  • packages/rsbuild-plugin/src/__tests__/checkout-isolation.test.ts — drives the plugin's transform from two checkouts and asserts they hand the extractor the same, checkout-independent specifier. Before the fix:

    Expected: "\repos\app\worktree-b\df\devup-ui"
    Received: "\repos\app\worktree-a\df\devup-ui"
    

Scope — all plugins audited

Plugin css_dir passed to codeExtract Status
bun-plugin relative, but onResolve returned an absolute path Fixed
rsbuild-plugin (src/plugin.ts:255) absolute in non-atom mode (the default) Fixed
next-plugin (src/loader.ts:221) relative Already correct
webpack-plugin (src/loader.ts:57) relative Already correct
vite-plugin (src/plugin.ts:465) relative Already correct

vite-plugin's resolveId also returns an absolute path (src/plugin.ts:436), but that is an in-memory Rollup module id that never reaches emitted module text, and the comment there shows it is deliberately stable to keep build hashes identical. Different class — left alone.

Verification

  • bun test — 5180 pass / 0 fail (also re-run by the pre-commit hook on both commits)
  • bun run --filter @devup-ui/bun-plugin test:regression — 2 pass / 0 fail
  • packages/rsbuild-plugin/src/plugin.ts and packages/bun-plugin/src/css-id.ts at 100% coverage
  • eslint clean, cargo fmt --all -- --check and cargo clippy --all-targets --all-features -- -D warnings exit 0

No Rust source was changed. bun.lock carries a workspace-version refresh produced by bun install.

Developing one repository in several checkouts at once (git worktrees, a
CI matrix, sibling clones) made every checkout but the first fail every
`bun test` that touches a Devup UI component:

    error: Cannot find module '<other checkout>/df/devup-ui/devup-ui.css'
      from '<this checkout>/src/Component.tsx'

Root cause: `onResolve` answered the injected stylesheet import with
`join(cssDir, fileName)`, an absolute path derived from `process.cwd()`
at module load. Bun persists transpiled modules in a machine-wide
on-disk cache (`<bun cache>/@t@`) keyed by module contents only, with
plugin-resolved import specifiers already baked in; the key covers
neither the cwd nor the importing file. Checkouts of one repository hold
byte-identical sources, so the second checkout reuses the first one's
cache entry and imports the first one's absolute CSS path. The extractor
was never at fault: it emits the relative specifier it is handed.

Resolve the stylesheet onto a virtual namespaced id instead, so nothing
derived from the cwd can reach a cache entry, and serve it from an
`onLoad` in that namespace (Bun's runtime has no CSS loader, and the
stylesheet is a bundler-side build artifact). Deciding on the *shape* of
the resolved directory rather than on equality with this checkout's
absolute cssDir also repairs cache entries an older plugin version
already poisoned, so no cache wipe is needed.

Regression tests, both verified failing before this change and passing
after: `__regression__/worktree-isolation.bun.ts` drives two checkouts
through two sequential `bun test` child processes over the shared cache
and reproduces the exact error above on the old code, and
`src/__tests__/css-id.test.ts` locks the resolution invariants.

Scope: next-plugin, webpack-plugin and vite-plugin pass a *relative*
cssDir into codeExtract and are unaffected. rsbuild-plugin's non-atom
path (packages/rsbuild-plugin/src/plugin.ts:255) does bake an absolute
cssDir into transformed code and has the same latent defect, but rspack
caches per project so no cross-checkout harm could be reproduced; left
unfixed and recorded rather than changed on a guess.

Verified: bun test 5178 pass / 0 fail, regression suite 2/2, eslint
clean, cargo fmt --check and cargo clippy -D warnings exit 0.
Same defect class as the bun-plugin fix: a machine-absolute path baked
into transformed module text.

The non-atom path passed the absolute `cssDir` straight to `codeExtract`,
so every transformed module carried

    import "C:\\...\\<checkout>\\df\\devup-ui/devup-ui-0.css";

Non-atom with `singleCss: false` is the default configuration, so this is
what rsbuild users get out of the box. It makes the transform output
depend on where the repository happens to be checked out: byte-identical
sources in two checkouts produce different code, which breaks any
content-addressed or relocated build cache exactly the way Bun's
machine-wide transpiler cache broke `bun test`. It also mixed path
separators in a single specifier on Windows.

next-plugin, webpack-plugin and vite-plugin already pass
`relative(dirname(id), cssDir)`; rsbuild was the only one that did not,
and only in the branch users hit by default. Always compute the relative
specifier, and keep atom mode's POSIX-normalized extraction filename
(that one exists to match the absolute-keyed canonical map / FILE_ROUTES,
which is unrelated). `cssFile` is consumed via `basename()`, as in
webpack and next, so file numbering and write locations are unchanged.

    extractCssDir = ./../df/devup-ui
    import "./../df/devup-ui/devup-ui-0.css";

Regression test `src/__tests__/checkout-isolation.test.ts` drives the
plugin's transform from two checkouts and asserts they hand the extractor
the same, checkout-independent specifier. Verified failing before this
change:

    Expected: "\repos\app\worktree-b\df\devup-ui"
    Received: "\repos\app\worktree-a\df\devup-ui"

vite-plugin's `resolveId` also returns an absolute path, but that is an
in-memory Rollup module id that never reaches emitted module text, so it
is left alone.

Verified: bun test 5180 pass / 0 fail, rsbuild-plugin/src/plugin.ts at
100% coverage, eslint clean, cargo fmt --check exit 0.
@owjs3901 owjs3901 changed the title fix(bun-plugin): stop leaking one checkout's CSS path into another fix(bun-plugin,rsbuild-plugin): stop baking absolute CSS paths into transformed code Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Changepacks

@devup-ui/bun-plugin@1.0.16 - packages/bun-plugin/package.json

Maybe you forgot to write the following files to the latest version

@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
packages/bun-plugin/src/css-id.ts 100.00% <100.00%> (ø)
packages/rsbuild-plugin/src/plugin.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@owjs3901
owjs3901 merged commit a6ca784 into main Sep 9, 2026
6 of 7 checks passed
@owjs3901
owjs3901 deleted the owjs3901/du1-worktree-css-path-leak branch September 9, 2026 01:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant