Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions packages/bun-plugin/__regression__/css-emit.bun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'

import { expect, it } from 'bun:test'

const pluginEntry = resolve(import.meta.dir, '..', 'dist', 'index.mjs')

// Separate processes keep the real WASM sheet and plugin hooks isolated from
// the root suite's mocks. Every process starts without a df directory.
it.each(['empty theme', 'configured theme', 'extracted styles'])(
'emits an importable stylesheet from a clean checkout: %s',
(scenario) => {
const cwd = mkdtempSync(join(tmpdir(), 'devup-css-emit-'))
try {
writeFileSync(join(cwd, 'bunfig.toml'), '')
if (scenario === 'configured theme') {
writeFileSync(
join(cwd, 'devup.json'),
JSON.stringify({
theme: { colors: { default: { primary: '#123456' } } },
}),
)
}
const widths = Array.from({ length: 8 }, (_, i) => 101 + i)
for (const width of widths) {
writeFileSync(
join(cwd, `fixture-${width}.ts`),
`import { css } from '@devup-ui/react'
export const cls = css({ width: '${width}px' })
`,
)
}
writeFileSync(
join(cwd, 'check.ts'),
`import { existsSync, readFileSync } from 'node:fs'
import { expect } from 'bun:test'

expect(existsSync('df')).toBe(false)
await import(${JSON.stringify(pluginEntry.replaceAll('\\', '/'))})
const cssPath = './df/devup-ui/devup-ui.css'
${
scenario === 'extracted styles'
? `const widths = ${JSON.stringify(widths)}
const modules = await Promise.all(widths.map(width => import('./fixture-' + width + '.ts')))
for (const mod of modules) expect(mod.cls).toBeTruthy()
const css = readFileSync(cssPath, 'utf-8')
for (const width of widths) expect(css).toContain('width:' + width + 'px')`
: `expect(existsSync(cssPath)).toBe(true)
${scenario === 'configured theme' ? "expect(readFileSync(cssPath, 'utf-8')).toContain('--primary:#123456')" : ''}`
}
await import(cssPath)
`,
)
expect(existsSync(join(cwd, 'df'))).toBe(false)
const result = Bun.spawnSync([process.execPath, 'run', 'check.ts'], {
cwd,
stdout: 'pipe',
stderr: 'pipe',
env: { ...process.env, BUN_RUNTIME_TRANSPILER_CACHE_PATH: '0' },
})
expect(
result.exitCode,
result.stdout.toString() + result.stderr.toString(),
).toBe(0)
} finally {
rmSync(cwd, { recursive: true, force: true })
}
},
)
62 changes: 62 additions & 0 deletions packages/bun-plugin/__regression__/css-emit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Bun CSS emission regression

The source at `6f9079ec` already resolves injected CSS imports to a path-free
virtual module (`src/css-id.ts`) and loads it as an empty JavaScript module.
That fixes Bun runtime loading and cross-worktree transpiler-cache isolation,
but `writeDataFiles()` only creates the CSS directory and `loadSourceFile()`
discards extracted CSS. No stylesheet is written.

The other plugins have different bundler lifecycles:

- Next initializes `devup-ui.css` with `getCss(null, false)` and refreshes CSS
through its loaders/coordinator.
- Webpack writes initial CSS in watch mode, writes extracted CSS in its source
loader, serves CSS through its CSS loader, and writes the final base sheet
after a successful build.
- Vite creates the directory before its initial base stylesheet, materializes
CSS imports during transformation, and serves/finalizes CSS from `getCss`.
- Rsbuild materializes stylesheet imports during transformation and serves
their contents through its CSS transform.

Bun uses `singleCss = true`, so its artifact is `df/devup-ui/devup-ui.css`.
Initialize it after directory creation with actual theme CSS, then refresh it
from `getCss(null, false)` after extraction. A placeholder alone would discard
the styles, since Bun's virtual runtime module intentionally contains no CSS.
Keep virtual resolution for cache isolation. No shared utility is needed for
this small change: the other plugins' emission timing and loaders differ.

Run `bun run build`, then
`bun run --filter @devup-ui/bun-plugin test:regression`.
The CSS regression runs isolated Bun processes in temporary directories without
`df`, using the built plugin and real WASM. It checks initial emission with and
without a theme, parallel extraction of eight distinct styles, and CSS imports.
It disables the transpiler cache for these cold-start cases; the existing
worktree-isolation regression continues to exercise the shared cache.

## Verification (Windows, Bun 1.4.1)

Before implementation:

- Initial `bun test`: 0 pass, 66 fail/errors because workspace build artifacts
were missing. Initial `bun run lint`: Rust checks passed; ESLint could not
load the unbuilt local ESLint plugin.
- Initial `bun run test`: Rust tests passed, coverage 98.21% (8572/8728 lines);
the Bun phase failed because build artifacts were still missing.
- `bun run build`: passed.
- Prepared `bun test`: 5180 pass, 0 fail; 87 snapshots; 100% function/line
coverage under the existing repository configuration.
- Prepared `bun run lint`: passed (format, Clippy, ESLint).
- Regression RED, before production edits: 2 existing tests passed; all 3 new
cases failed because the stylesheet did not exist (`false` / `ENOENT`).

After implementation:

- `bun run --filter @devup-ui/bun-plugin build`: passed.
- `bun run --filter @devup-ui/bun-plugin test:regression`: 5 pass, 0 fail,
including the unchanged preload and worktree-cache regressions.
- `bun run test`: passed. Rust coverage remains 98.21% (8572/8728 lines,
+0.00%); Bun remains 5180 pass, 0 fail, 87 snapshots and 100% function/line
coverage. No coverage configuration or existing assertions were changed.
- Running Tarpaulin and Clippy concurrently caused transient missing `target`
fingerprint files on Windows. Validation commands were rerun sequentially.
- Sequential `bun run lint`: passed, matching the prepared baseline.
2 changes: 1 addition & 1 deletion packages/bun-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"scripts": {
"lint": "eslint",
"build": "bun ../../node_modules/@typescript/native/bin/tsc && bun build --target node src/index.cjs.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external && bun build --target node src/register.ts --production --env=disable --outfile dist/register.cjs --format cjs --packages external && bun build --target node src/register.ts --production --env=disable --outfile dist/register.mjs --format esm --packages external",
"test:regression": "cd __regression__ && bun test ./preload-race.bun.ts ./worktree-isolation.bun.ts"
"test:regression": "cd __regression__ && bun test ./preload-race.bun.ts ./worktree-isolation.bun.ts ./css-emit.bun.ts"
},
"publishConfig": {
"access": "public"
Expand Down
8 changes: 7 additions & 1 deletion packages/bun-plugin/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync } from 'node:fs'
import { existsSync, writeFileSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { dirname, join, relative, resolve } from 'node:path'

Expand All @@ -10,6 +10,7 @@ import {
} from '@devup-ui/plugin-utils'
import {
codeExtract,
getCss,
getThemeInterface,
hasDevupUI,
registerShorthands,
Expand Down Expand Up @@ -51,6 +52,7 @@ async function writeDataFiles() {
if (!existsSync(cssDir)) {
await mkdir(cssDir, { recursive: true })
}
await writeFile(join(cssDir, 'devup-ui.css'), getCss(null, false), 'utf-8')
}

async function initialize({ shorthands }: DevupUIBunPluginOptions = {}) {
Expand Down Expand Up @@ -89,6 +91,10 @@ async function loadSourceFile(filePath: string) {
false,
importAliases,
)
// singleCss stores every extracted style in the base sheet. Finish the
// write before returning the injected import; synchronous writes also keep
// concurrent source loads from overwriting a newer sheet with an older one.
writeFileSync(join(cssDir, 'devup-ui.css'), getCss(null, false), 'utf-8')
return { contents: code.code, loader }
}
return { contents, loader }
Expand Down
Loading