Files
bun.sh/test/bundler/css/css-module-scripts.test.ts
Claude Bot 70f4fe90c5 feat(bundler): add CSS Module Scripts support
Implement CSS Module Scripts (https://web.dev/articles/css-module-scripts)
for Bun's bundler. When importing CSS with `{ type: 'css' }` attribute:

```javascript
import sheet from './styles.css' with { type: 'css' };
// or dynamically:
const module = await import('./styles.css', { with: { type: 'css' } });
```

The import now returns a CSSStyleSheet object that can be used with
`document.adoptedStyleSheets`, instead of the previous behavior of
returning an empty object or file path.

Changes:
- Add `__cssModuleScript` runtime helper that creates CSSStyleSheet
- Add `is_css_module_script` flag to ImportRecord
- Track CSS Module Script files in LinkerGraph
- Generate `__cssModuleScript(cssContent)` for CSS imports with type assertion
- Handle both static and dynamic imports correctly
- Preserve existing CSS Modules behavior (class name mappings) for
  imports without the type assertion

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 20:45:19 +00:00

148 lines
4.9 KiB
TypeScript

import { itBundled } from "../expectBundled";
// Tests for CSS Module Scripts - https://web.dev/articles/css-module-scripts
// When importing CSS with `with { type: 'css' }`, the import should return a CSSStyleSheet object
describe("css-module-scripts", () => {
// Mock CSSStyleSheet for testing since we're running in Bun, not a browser
const env = {
...process.env,
// Inject a mock CSSStyleSheet constructor
BUN_DEBUG_QUIET_LOGS: "1",
};
itBundled("css-module-scripts/StaticImportWithTypeCSS", {
files: {
"/entry.js": /* js */ `
import sheet from './styles.css' with { type: 'css' };
console.log('sheet type:', typeof sheet);
console.log('sheet instanceof CSSStyleSheet:', sheet instanceof CSSStyleSheet);
console.log('cssRules length:', sheet.cssRules.length);
`,
"/styles.css": `.foo { color: red; }`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
target: "browser",
format: "esm",
onAfterBundle(api) {
// Verify the output contains __cssModuleScript call
const content = api.readFile("/out/entry.js");
expect(content).toContain("__cssModuleScript");
expect(content).toContain(".foo { color: red; }");
},
});
itBundled("css-module-scripts/DynamicImportWithTypeCSS", {
files: {
"/entry.js": /* js */ `
const module = await import('./styles.css', { with: { type: 'css' } });
const sheet = module.default;
console.log('sheet type:', typeof sheet);
console.log('sheet instanceof CSSStyleSheet:', sheet instanceof CSSStyleSheet);
console.log('cssRules length:', sheet.cssRules.length);
`,
"/styles.css": `.bar { color: blue; }`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
target: "browser",
format: "esm",
onAfterBundle(api) {
// Verify the output contains __cssModuleScript call with CSS content
const content = api.readFile("/out/entry.js");
expect(content).toContain("__cssModuleScript");
expect(content).toContain(".bar { color: blue; }");
},
});
itBundled("css-module-scripts/DynamicImportWithAssertTypeCSS", {
// Test the older `assert` syntax for backwards compatibility
files: {
"/entry.js": /* js */ `
const module = await import('./styles.css', { assert: { type: 'css' } });
const sheet = module.default;
console.log('sheet type:', typeof sheet);
`,
"/styles.css": `.baz { color: green; }`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
target: "browser",
format: "esm",
onAfterBundle(api) {
// Verify the output contains __cssModuleScript call
const content = api.readFile("/out/entry.js");
expect(content).toContain("__cssModuleScript");
},
});
itBundled("css-module-scripts/CSSModuleWithTypeCSS", {
// CSS Modules (*.module.css) should still work with type: 'css'
// but return a CSSStyleSheet instead of the class name mapping
files: {
"/entry.js": /* js */ `
import sheet from './styles.module.css' with { type: 'css' };
console.log('sheet instanceof CSSStyleSheet:', sheet instanceof CSSStyleSheet);
`,
"/styles.module.css": `.myClass { color: purple; }`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
target: "browser",
format: "esm",
onAfterBundle(api) {
const content = api.readFile("/out/entry.js");
expect(content).toContain("__cssModuleScript");
},
});
itBundled("css-module-scripts/PlainCSSImportWithoutType", {
// Plain CSS imports without type should NOT return CSSStyleSheet
// (existing behavior - either side-effect or object with class names)
files: {
"/entry.js": /* js */ `
import './styles.css';
console.log('CSS imported as side effect');
`,
"/styles.css": `.plain { color: black; }`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
target: "browser",
format: "esm",
onAfterBundle(api) {
const content = api.readFile("/out/entry.js");
// Should NOT contain CSSStyleSheet for plain imports
expect(content).not.toContain("new CSSStyleSheet");
},
});
itBundled("css-module-scripts/MultipleRules", {
files: {
"/entry.js": /* js */ `
import sheet from './styles.css' with { type: 'css' };
console.log('rules:', sheet.cssRules.length);
`,
"/styles.css": /* css */ `
.a { color: red; }
.b { color: blue; }
.c { color: green; }
@media (min-width: 768px) {
.a { color: darkred; }
}
`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
target: "browser",
format: "esm",
onAfterBundle(api) {
const content = api.readFile("/out/entry.js");
expect(content).toContain("__cssModuleScript");
// The CSS content should be included as a string
expect(content).toContain(".a");
expect(content).toContain("color");
},
});
});