Files
bun.sh/test/bundler/esbuild/css.test.ts

2211 lines
68 KiB
TypeScript

import { describe } from "bun:test";
import { join } from "node:path";
import { itBundled } from "../expectBundled";
// Tests ported from:
// https://github.com/evanw/esbuild/blob/main/internal/bundler_tests/bundler_css_test.go
// For debug, all files are written to $TEMP/bun-bundle-tests/css
describe("bundler", () => {
itBundled("css/CSSEntryPoint", {
files: {
"/entry.css": /* css */ `
body {
background: white;
color: black }
`,
},
outfile: "/out.js",
onAfterBundle(api) {
api.expectFile("/out.js").toEqualIgnoringWhitespace(`
/* entry.css */
body {
color: #000;
background: #fff;
}`);
},
});
itBundled("css/CSSEntryPointEmpty", {
files: {
"/entry.css": /* css */ `\n`,
},
outfile: "/out.js",
onAfterBundle(api) {
api.expectFile("/out.js").toEqualIgnoringWhitespace(`
/* entry.css */`);
},
});
itBundled("css/CSSNesting", {
target: "bun",
files: {
"/entry.css": /* css */ `
body {
h1 {
color: white;
}
}`,
},
outfile: "/out.js",
onAfterBundle(api) {
api.expectFile("/out.js").toEqualIgnoringWhitespace(`
/* entry.css */
body {
&h1 {
color: #fff;
}
}
`);
},
});
itBundled("css/CSSAtImportMissing", {
files: {
"/entry.css": `@import "./missing.css";`,
},
bundleErrors: {
"/entry.css": ['Could not resolve: "./missing.css"'],
},
});
itBundled("css/CSSAtImportSimple", {
// GENERATED
files: {
"/entry.css": /* css */ `
@import "./internal.css";
`,
"/internal.css": /* css */ `
.before { color: red }
`,
},
outfile: "/out.css",
onAfterBundle(api) {
api.expectFile("/out.css").toEqualIgnoringWhitespace(`
/* internal.css */
.before {
color: red;
}
/* entry.css */
`);
},
});
itBundled("css/CSSAtImportDiamond", {
// GENERATED
files: {
"/a.css": /* css */ `
@import "./b.css";
@import "./c.css";
.last { color: red }
`,
"/b.css": /* css */ `
@import "./d.css";
.first { color: red }
`,
"/c.css": /* css */ `
@import "./d.css";
.third { color: red }
`,
"/d.css": /* css */ `
.second { color: red }
`,
},
outfile: "/out.css",
onAfterBundle(api) {
api.expectFile("/out.css").toEqualIgnoringWhitespace(`
/* b.css */
.first {
color: red;
}
/* d.css */
.second {
color: red;
}
/* c.css */
.third {
color: red;
}
/* a.css */
.last {
color: red;
}
`);
},
});
itBundled("css/CSSAtImportCycle", {
files: {
"/a.css": /* css */ `
@import "./a.css";
.hehe { color: red }
`,
},
outfile: "/out.css",
onAfterBundle(api) {
api.expectFile("/out.css").toEqualIgnoringWhitespace(`
/* a.css */
.hehe {
color: red;
}
`);
},
});
itBundled("css/CSSUrlImport", {
files: {
"/a.css": /* css */ `
.hello {
background-image: url(./hi.svg)
}
`,
"/hi.svg": /* svg */ `
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" fill="blue" />
</svg>
`,
},
outdir: "/out",
onAfterBundle(api) {
api.expectFile("/out/a.css").toEqualIgnoringWhitespace(`
/* a.css */
.hello {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgZmlsbD0iYmx1ZSIgLz4KPC9zdmc+");
}
`);
},
});
// TODO: re-enable these tests when we do minify local css identifiers
// itBundled("css/TestImportLocalCSSFromJSMinifyIdentifiersAvoidGlobalNames", {
// files: {
// "/entry.js": /* js */ `
// import "./global.css";
// import "./local.module.css";
// `,
// "/global.css": /* css */ `
// :is(.a, .b, .c, .d, .e, .f, .g, .h, .i, .j, .k, .l, .m, .n, .o, .p, .q, .r, .s, .t, .u, .v, .w, .x, .y, .z),
// :is(.A, .B, .C, .D, .E, .F, .G, .H, .I, .J, .K, .L, .M, .N, .O, .P, .Q, .R, .S, .T, .U, .V, .W, .X, .Y, .Z),
// ._ { color: red }
// `,
// "/local.module.css": /* css */ `
// .rename-this { color: blue }
// `,
// },
// entryPoints: ["/entry.js"],
// outdir: "/out",
// minifyIdentifiers: true,
// });
// // See: https://github.com/evanw/esbuild/issues/3295
// itBundled("css/ImportLocalCSSFromJSMinifyIdentifiersMultipleEntryPoints", {
// files: {
// "/a.js": /* js */ `
// import { foo, bar } from "./a.module.css";
// console.log(foo, bar);
// `,
// "/a.module.css": /* css */ `
// .foo { color: #001; }
// .bar { color: #002; }
// `,
// "/b.js": /* js */ `
// import { foo, bar } from "./b.module.css";
// console.log(foo, bar);
// `,
// "/b.module.css": /* css */ `
// .foo { color: #003; }
// .bar { color: #004; }
// `,
// },
// entryPoints: ["/a.js", "/b.js"],
// outdir: "/out",
// minifyIdentifiers: true,
// });
// TODO: some classes are commented out bc we don't support :global or :local yet
itBundled("css/ImportCSSFromJSComposes", {
files: {
"/entry.js": /* js */ `
import styles from "./styles.module.css"
console.log(styles)
`,
"/global.css": /* css */ `
.GLOBAL1 {
color: black;
}
`,
"/styles.module.css": /* css */ `
@import "global.css";
/* .local0 {
composes: local1;
:global {
composes: GLOBAL1 GLOBAL2;
}
} */
.local0 {
composes: GLOBAL2 GLOBAL3 from global;
composes: local1 local2;
background: green;
}
/* .local0 :global {
composes: GLOBAL4;
} */
.local3 {
border: 1px solid black;
composes: local4;
}
.local4 {
opacity: 0.5;
}
.local1 {
color: red;
composes: local3;
}
.fromOtherFile {
composes: local0 from "other1.module.css";
composes: local0 from "other2.module.css";
}
`,
"/other1.module.css": /* css */ `
.local0 {
composes: base1 base2 from "base.module.css";
color: blue;
}
`,
"/other2.module.css": /* css */ `
.local0 {
composes: base1 base3 from "base.module.css";
background: purple;
}
`,
"/base.module.css": /* css */ `
.base1 {
cursor: pointer;
}
.base2 {
display: inline;
}
.base3 {
float: left;
}
`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
bundleErrors: {
"/styles.module.css": [
'The name "local2" never appears in "styles.module.css" as a CSS modules locally scoped class name. Note that "composes" only works with single class selectors.',
],
},
onAfterBundle(api) {
api.expectFile("/out/entry.js").toMatchInlineSnapshot(`
"// styles.module.css
var styles_module_default = {
local0: "GLOBAL2 GLOBAL3 local4_-MSaAA local3_-MSaAA local1_-MSaAA local0_-MSaAA",
local3: "local4_-MSaAA local3_-MSaAA",
local4: "local4_-MSaAA",
local1: "local4_-MSaAA local3_-MSaAA local1_-MSaAA",
fromOtherFile: "base1_1Cz41w base2_1Cz41w local0_qwJuwA base3_1Cz41w local0_AgBO5Q fromOtherFile_-MSaAA"
};
// entry.js
console.log(styles_module_default);
"
`);
api.expectFile("/out/entry.css").toMatchInlineSnapshot(`
"/* global.css */
.GLOBAL1 {
color: #000;
}
/* other1.module.css */
.local0_qwJuwA {
color: #00f;
}
/* base.module.css */
.base1_1Cz41w {
cursor: pointer;
}
.base2_1Cz41w {
display: inline;
}
.base3_1Cz41w {
float: left;
}
/* other2.module.css */
.local0_AgBO5Q {
background: purple;
}
/* styles.module.css */
.local0_-MSaAA {
background: green;
}
.local3_-MSaAA {
border: 1px solid #000;
}
.local4_-MSaAA {
opacity: .5;
}
.local1_-MSaAA {
color: red;
}
.fromOtherFile_-MSaAA {
}
"
`);
},
});
itBundled("css/ImportCSSFromJSComposesFromMissingImport", {
files: {
"/entry.js": `
import styles from "./styles.module.css"
console.log(styles)
`,
"/styles.module.css": `
.foo {
composes: x from "file.module.css";
composes: y from "file.module.css";
composes: z from "file.module.css";
composes: x from "file.css";
}
`,
"/file.module.css": `
.x {
color: red;
}
:global(.y) {
color: blue;
}
`,
"/file.css": `
.x {
color: red;
}
`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
bundleErrors: {
"/styles.module.css": [
// TODO: renable when we support :local and :global
// 'Cannot use global name "y" with "composes"',
// 'Cannot use global name "x" with "composes"',
],
"/file.module.css": ['The name "z" never appears in "file.module.css"'],
"/file.css": ['The name "x" never appears in "file.css"'],
},
bundleWarnings: {
"/styles.module.css": [
// TODO: renable when we support :local and :global
// 'The global name "y" is defined in file.module.css. Use the ":local" selector to change "y" into a local name.',
// 'The global name "x" is defined in file.css. Use the "local-css" loader for "file.css" to enable local names.',
],
},
});
itBundled("css/ImportCSSFromJSComposesFromNotCSS", {
files: {
"/entry.js": `
import styles from "./styles.module.css"
console.log(styles)
`,
"/styles.module.css": `
.foo {
composes: bar from "file.txt";
}
`,
"/file.txt": `
.bar {
color: red;
}
`,
},
loader: {
".txt": "text",
},
entryPoints: ["/entry.js"],
outdir: "/out",
bundleErrors: {
"/styles.module.css": ['Cannot use the "composes" property with the "file.txt" file (it is not a CSS file)'],
},
});
itBundled("css/ImportCSSFromJSComposesCircular", {
files: {
"/entry.js": `
import styles from "./styles.module.css"
console.log(styles)
`,
"/styles.module.css": `
.foo {
composes: bar;
}
.bar {
composes: foo;
}
.baz {
composes: baz;
}
`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
onAfterBundle(api) {
api.expectFile("/out/entry.js").toMatchInlineSnapshot(`
"// styles.module.css
var styles_module_default = {
foo: "bar_-MSaAA foo_-MSaAA",
bar: "foo_-MSaAA bar_-MSaAA",
baz: "baz_-MSaAA"
};
// entry.js
console.log(styles_module_default);
"
`);
api.expectFile("/out/entry.css").toMatchInlineSnapshot(`
"/* styles.module.css */
.foo_-MSaAA {
}
.bar_-MSaAA {
}
.baz_-MSaAA {
}
"
`);
},
});
itBundled("css/ImportCSSFromJSComposesFromCircular", {
files: {
"/entry.js": `
import styles from "./styles.module.css"
console.log(styles)
`,
"/styles.module.css": `
.foo {
composes: bar from "other.module.css";
}
.bar {
composes: bar from "styles.module.css";
}
`,
"/other.module.css": `
.bar {
composes: foo from "styles.module.css";
}
`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
onAfterBundle(api) {
api.expectFile("/out/entry.js").toMatchInlineSnapshot(`
"// styles.module.css
var styles_module_default = {
foo: "bar_NlEjJA foo_-MSaAA",
bar: "bar_-MSaAA"
};
// entry.js
console.log(styles_module_default);
"
`);
api.expectFile("/out/entry.css").toMatchInlineSnapshot(`
"/* other.module.css */
.bar_NlEjJA {
}
/* styles.module.css */
.foo_-MSaAA {
}
.bar_-MSaAA {
}
"
`);
},
});
// Define all the invalid cases
const invalidComposesTests = [
{
name: "IDSelector",
cssContent: `
/* Invalid: ID selector */
.withId {
composes: #invalid;
color: red;
}
`,
expectedError: "Invalid declaration",
},
{
name: "ElementSelector",
cssContent: `
/* Invalid: Element selector */
.withElement {
composes: div;
color: blue;
}
`,
expectedError:
'The name "div" never appears in "styles.module.css" as a CSS modules locally scoped class name. Note that "composes" only works with single class selectors.',
},
{
name: "CompoundSelector",
cssContent: `
/* Invalid: Compound selector */
.withCompound {
composes: .valid.invalid;
color: green;
}
`,
expectedError: "Invalid declaration",
},
{
name: "ComplexSelector",
cssContent: `
/* Invalid: Complex selector */
.withComplex {
composes: .parent > .child;
color: purple;
}
`,
expectedError: "Invalid declaration",
},
{
name: "PseudoClass",
cssContent: `
/* Invalid: Pseudo-class */
.withPseudo {
composes: .valid:hover;
color: orange;
}
`,
expectedError: "Invalid declaration",
},
{
name: "AttributeSelector",
cssContent: `
/* Invalid: Attribute selector */
.withAttribute {
composes: [disabled];
color: yellow;
}
`,
expectedError: "Invalid declaration",
},
{
name: "ImportedNonClass",
cssContent: `
/* Invalid: Imported non-class */
.withImportedNonClass {
composes: element from "other.module.css";
color: magenta;
}
`,
expectedError:
'The name "element" never appears in "other.module.css" as a CSS modules locally scoped class name. Note that "composes" only works with single class selectors.',
},
];
// Create a separate test for each invalid case
for (const testCase of invalidComposesTests) {
itBundled(`css/ImportCSSFromJSComposes${testCase.name}`, {
files: {
"/entry.js": /* js */ `
import styles from "./styles.module.css"
console.log(styles)
`,
"/styles.module.css": /* css */ testCase.cssContent,
"/other.module.css": /* css */ `
/* Non-class selectors in another file */
div {
color: brown;
}
#id {
color: teal;
}
.valid {
color: pink;
}
element {
color: gray;
}
`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
bundleErrors: testCase.expectedError
? testCase.name === "ImportedNonClass"
? { "/other.module.css": [testCase.expectedError] }
: { "/styles.module.css": [testCase.expectedError] }
: undefined,
bundleWarnings: testCase.expectedWarning ? { "/styles.module.css": [testCase.expectedWarning] } : undefined,
onAfterBundle(api) {
// Check that the output files were generated correctly
api.expectFile("/out/entry.js").toMatchSnapshot();
api.expectFile("/out/entry.css").toMatchSnapshot();
},
});
}
itBundled("css/ComposesWithSharedPropertiesError", {
files: {
"/entry.js": `
import styles from "./styles.module.css"
console.log(styles)
`,
"/styles.module.css": `
.button {
color: blue;
composes: otherButton from "./other.module.css";
}
`,
"/other.module.css": `
.otherButton {
color: red;
font-size: 16px;
}
`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
bundleWarnings: true,
onAfterBundle(api) {
// Check that the output files were generated correctly
api.expectFile("/out/entry.js").toMatchSnapshot();
api.expectFile("/out/entry.css").toMatchSnapshot();
},
});
itBundled("css/ComposesSameFile", {
files: {
"/entry.js": `
import styles from "./styles.module.css"
console.log(styles)
`,
"/styles.module.css": `
.button {
color: blue;
composes: otherButton from "./styles.module.css";
}
.otherButton {
color: red;
font-size: 16px;
}
`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
bundleWarnings: true,
onAfterBundle(api) {
// Check that the output files were generated correctly
api.expectFile("/out/entry.js").toMatchInlineSnapshot(`
"// styles.module.css
var styles_module_default = {
button: "otherButton_-MSaAA button_-MSaAA",
otherButton: "otherButton_-MSaAA"
};
// entry.js
console.log(styles_module_default);
"
`);
api.expectFile("/out/entry.css").toMatchInlineSnapshot(`
"/* styles.module.css */
.button_-MSaAA {
color: #00f;
}
.otherButton_-MSaAA {
color: red;
font-size: 16px;
}
"
`);
},
});
itBundled("css/ComposesSameFileSameClass", {
files: {
"/entry.js": `
import styles from "./styles.module.css"
console.log(styles)
`,
"/styles.module.css": `
.button {
color: blue;
composes: button from "./styles.module.css";
}
`,
},
entryPoints: ["/entry.js"],
outdir: "/out",
bundleWarnings: true,
onAfterBundle(api) {
// Check that the output files were generated correctly
api.expectFile("/out/entry.js").toMatchInlineSnapshot(`
"// styles.module.css
var styles_module_default = {
button: "button_-MSaAA"
};
// entry.js
console.log(styles_module_default);
"
`);
api.expectFile("/out/entry.css").toMatchInlineSnapshot(`
"/* styles.module.css */
.button_-MSaAA {
color: #00f;
}
"
`);
},
});
});
describe("esbuild-bundler", () => {
itBundled("css/CSSEntryPoint", {
// GENERATED
files: {
"/entry.css": /* css */ `
body {
background: white;
color: black }
`,
},
});
itBundled("css/CSSAtImportMissing", {
files: {
"/entry.css": `@import "./missing.css";`,
},
bundleErrors: {
"/entry.css": ['Could not resolve: "./missing.css"'],
},
});
itBundled("css/CSSAtImportExternal", {
external: ["./external1.css", "./external2.css", "./external3.css", "./external4.css", "./external5.css"],
// GENERATED
files: {
"/entry.css": /* css */ `
@import "./internal.css";
@import "./external1.css";
@import "./external2.css";
@import "./charset1.css";
@import "./charset2.css";
@import "./external5.css" screen;
`,
"/internal.css": /* css */ `
@import "./external5.css" print;
.before { color: red }
`,
"/charset1.css": /* css */ `
@charset "UTF-8";
@import "./external3.css";
@import "./external4.css";
@import "./external5.css";
@import "https://www.example.com/style1.css";
@import "https://www.example.com/style2.css";
@import "https://www.example.com/style3.css" print;
.middle { color: green }
`,
"/charset2.css": /* css */ `
@charset "UTF-8";
@import "./external3.css";
@import "./external5.css" screen;
@import "https://www.example.com/style1.css";
@import "https://www.example.com/style3.css";
.after { color: blue }
`,
},
outfile: "/out/out.css",
onAfterBundle(api) {
api.expectFile("/out/out.css").toEqualIgnoringWhitespace(/* css */ `@import "./external1.css";
@import "./external2.css";
@import "./external4.css";
@import "./external5.css";
@import "https://www.example.com/style2.css";
@import "./external3.css";
@import "https://www.example.com/style1.css";
@import "https://www.example.com/style3.css";
@import "./external5.css" screen;
/* internal.css */
.before {
color: red;
}
/* charset1.css */
.middle {
color: green;
}
/* charset2.css */
.after {
color: #00f;
}
/* entry.css */`);
},
});
itBundled("css/CSSAtImport", {
// GENERATED
files: {
"/entry.css": /* css */ `
@import "./a.css";
@import "./b.css";
.entry { color: red }
`,
"/a.css": /* css */ `
@import "./shared.css";
.a { color: green }
`,
"/b.css": /* css */ `
@import "./shared.css";
.b { color: blue }
`,
"/shared.css": `.shared { color: black }`,
},
});
itBundled("css/CSSFromJSMissingImport", {
// GENERATED
files: {
"/entry.js": /* js */ `
import {missing} from "./a.css"
console.log(missing)
`,
"/a.css": `.a { color: red }`,
},
bundleErrors: {
"/entry.js": ['No matching export in "a.css" for import "missing"'],
},
});
itBundled("css/CSSFromJSMissingStarImport", {
outdir: "/out",
files: {
"/entry.js": /* js */ `
import * as ns from "./a.css"
console.log(ns.missing)
`,
"/a.css": `.a { color: red }`,
},
bundleWarnings: {
"/entry.js": ['Import "missing" will always be undefined because there is no matching export in "a.css"'],
},
onAfterBundle(api) {
api.expectFile("/out/entry.css").toEqualIgnoringWhitespace(/* css */ `/* a.css */
.a{
color: red;
}`);
},
});
itBundled("css/ImportCSSFromJS", {
outdir: "/out",
files: {
"/entry.js": /* js */ `
import "./a.js"
import "./b.js"
`,
"/a.js": /* js */ `
import "./a.css";
console.log('a')
`,
"/a.css": `.a { color: red }`,
"/b.js": /* js */ `
import "./b.css";
console.log('b')
`,
"/b.css": `.b { color: blue }`,
},
});
// itBundled("css/ImportCSSFromJSWriteToStdout", {
// files: {
// "/entry.js": `import "./entry.css"`,
// "/entry.css": `.entry { color: red }`,
// },
// bundleErrors: {
// "/entry.js": ['Cannot import "entry.css" into a JavaScript file without an output path configured'],
// },
// });
itBundled("css/ImportJSFromCSS", {
outdir: "/out",
files: {
"/entry.ts": `export default 123`,
"/entry.css": `@import "./entry.ts";`,
},
entryPoints: ["/entry.css"],
bundleErrors: {
"/entry.css": ['Cannot import a ".ts" file into a CSS file'],
},
});
itBundled("css/ImportJSONFromCSS", {
// GENERATED
files: {
"/entry.json": `{}`,
"/entry.css": `@import "./entry.json";`,
},
entryPoints: ["/entry.css"],
bundleErrors: {
"/entry.css": ['Cannot import a ".json" file into a CSS file'],
},
});
itBundled("css/MissingImportURLInCSS", {
// GENERATED
files: {
"/src/entry.css": /* css */ `
a { background: url(./one.png); }
b { background: url("./two.png"); }
`,
},
bundleErrors: {
"/src/entry.css": ['Could not resolve: "./one.png"', 'Could not resolve: "./two.png"'],
},
});
// Skipping for now
itBundled("css/ExternalImportURLInCSS", {
files: {
"/src/entry.css": /* css */ `
div:after {
content: 'If this is recognized, the path should become "../src/external.png"';
background: url(./external.png);
}
/* These URLs should be external automatically */
a { background: url(http://example.com/images/image.png) }
b { background: url(https://example.com/images/image.png) }
c { background: url(//example.com/images/image.png) }
d { background: url(data:image/png;base64,iVBORw0KGgo=) }
path { fill: url(#filter) }
`,
},
external: ["./src/external.png"],
});
itBundled("css/InvalidImportURLInCSS", {
// GENERATED
files: {
"/entry.css": /* css */ `
a {
background: url(./js.js);
background: url("./jsx.jsx");
background: url(./ts.ts);
background: url('./tsx.tsx');
background: url(./json.json);
background: url(./css.css);
}
`,
"/js.js": `export default 123`,
"/jsx.jsx": `export default 123`,
"/ts.ts": `export default 123`,
"/tsx.tsx": `export default 123`,
"/json.json": `{ "test": true }`,
"/css.css": `a { color: red }`,
},
bundleErrors: {
"/entry.css": [
'Cannot import a ".jsx" file into a CSS file',
'Cannot import a ".jsx" file into a CSS file',
'Cannot import a ".ts" file into a CSS file',
'Cannot import a ".tsx" file into a CSS file',
'Cannot import a ".json" file into a CSS file',
],
},
/* TODO FIX expectedScanLog: `entry.css: ERROR: Cannot use "js.js" as a URL
NOTE: You can't use a "url()" token to reference the file "js.js" because it was loaded with the "js" loader, which doesn't provide a URL to embed in the resulting CSS.
entry.css: ERROR: Cannot use "jsx.jsx" as a URL
NOTE: You can't use a "url()" token to reference the file "jsx.jsx" because it was loaded with the "jsx" loader, which doesn't provide a URL to embed in the resulting CSS.
entry.css: ERROR: Cannot use "ts.ts" as a URL
NOTE: You can't use a "url()" token to reference the file "ts.ts" because it was loaded with the "ts" loader, which doesn't provide a URL to embed in the resulting CSS.
entry.css: ERROR: Cannot use "tsx.tsx" as a URL
NOTE: You can't use a "url()" token to reference the file "tsx.tsx" because it was loaded with the "tsx" loader, which doesn't provide a URL to embed in the resulting CSS.
entry.css: ERROR: Cannot use "json.json" as a URL
NOTE: You can't use a "url()" token to reference the file "json.json" because it was loaded with the "json" loader, which doesn't provide a URL to embed in the resulting CSS.
entry.css: ERROR: Cannot use "css.css" as a URL
NOTE: You can't use a "url()" token to reference a CSS file, and "css.css" is a CSS file (it was loaded with the "css" loader).
`, */
});
itBundled("css/TextImportURLInCSSText", {
outfile: "/out.css",
files: {
"/entry.css": /* css */ `
a {
background: url(./example.txt);
}
`,
"/example.txt": `This is some text.`,
},
onAfterBundle(api) {
api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ `
/* entry.css */
a {
background: url("data:text/plain;base64,VGhpcyBpcyBzb21lIHRleHQu");
}
`);
},
});
itBundled("css/Png", {
outfile: "/out.css",
// GENERATED
files: {
"/entry.css": /* css */ `
a {
background: url(./example.png);
}
`,
"/example.png": Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
},
onAfterBundle(api) {
api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ `
/* entry.css */
a {
background: url("data:image/png;base64,iVBORw0KGgo=");
}
`);
},
});
// We don't support dataurl rn
// itBundled("css/DataURLImportURLInCSS", {
// outfile: "/out.css",
// // GENERATED
// files: {
// "/entry.css": /* css */ `
// a {
// background: url(./example.png);
// }
// `,
// "/example.png": new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
// },
// loader: {
// ".png": "dataurl",
// },
// onAfterBundle(api) {
// api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ `
// /* entry.css */
// a {
// background: url("data:image/png;base64,iVBORw0KGgo=");
// }
// `);
// },
// });
// We don't support binary loader rn
// itBundled("css/BinaryImportURLInCSS", {
// // GENERATED
// files: {
// "/entry.css": /* css */ `
// a {
// background: url(./example.png);
// }
// `,
// "/example.png": new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
// },
// onAfterBundle(api) {
// api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ `
// /* entry.css */
// a {
// background: url("data:image/png;base64,iVBORw0KGgo=");
// }
// `);
// },
// });
// We don't support base64 loader rn
// itBundled("css/Base64ImportURLInCSS", {
// // GENERATED
// files: {
// "/entry.css": /* css */ `
// a {
// background: url(./example.png);
// }
// `,
// "/example.png": `\x89\x50\x4E\x47\x0D\x0A\x1A\x0A`,
// },
// });
itBundled("css/FileImportURLInCSS", {
files: {
"/entry.css": /* css */ `
@import "./one.css";
@import "./two.css";
`,
"/one.css": `a { background: url(./example.data) }`,
"/two.css": `b { background: url(./example.data) }`,
"/example.data": new Array(128 * 1024 + 1).fill("Z".charCodeAt(0)).join(""),
},
loader: {
".data": "file",
},
outdir: "/out",
async onAfterBundle(api) {
api.expectFile("/out/example-ra0pdz4b.data").toEqual(new Array(128 * 1024 + 1).fill("Z".charCodeAt(0)).join(""));
api.expectFile("/out/entry.css").toEqualIgnoringWhitespace(/* css */ `
/* one.css */
a {
background: url("./example-ra0pdz4b.data");
}
/* two.css */
b {
background: url("./example-ra0pdz4b.data");
}
/* entry.css */
`);
},
});
itBundled("css/IgnoreURLsInAtRulePrelude", {
// GENERATED
files: {
"/entry.css": /* css */ `
/* This should not generate a path resolution error */
@supports (background: url(ignored.png)) {
a { color: red }
}
`,
},
});
itBundled("css/PackageURLsInCSS", {
files: {
"/entry.css": /* css */ `
@import "./test.css";
a { background: url(a/1.png); }
b { background: url(b/2.png); }
c { background: url(c/3.png); }
`,
"/test.css": `.css { color: red }`,
"/a/1.png": `a-1`,
"/node_modules/b/2.png": `b-2-node_modules`,
"/c/3.png": `c-3`,
"/node_modules/c/3.png": `c-3-node_modules`,
},
outfile: "/out.css",
onAfterBundle(api) {
api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ `
/* test.css */
.css {
color: red;
}
/* entry.css */
a {
background: url("data:image/png;base64,YS0x");
}
b {
background: url("data:image/png;base64,Yi0yLW5vZGVfbW9kdWxlcw==");
}
c {
background: url("data:image/png;base64,Yy0z");
}
`);
},
});
itBundled("css/CSSAtImportExtensionOrderCollision", {
files: {
// This should avoid picking ".js" because it's explicitly configured as non-CSS
"/entry.css": `@import "./test";`,
"/test.js": `console.log('js')`,
"/test.css": `.css { color: red }`,
},
outfile: "/out.css",
// extensionOrder: [".js", ".css"],
onAfterBundle(api) {
api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ `
/* test.css */
.css {
color: red;
}
/* entry.css */
`);
},
});
/* We don't support `extensionOrder`/`--resolve-extensions` rn
itBundled("css/CSSAtImportExtensionOrderCollisionUnsupported", {
// GENERATED
files: {
"/entry.css": `@import "./test";`,
"/test.js": `console.log('js')`,
"/test.sass": `// some code`,
},
outfile: "/out.css",
extensionOrder: [".js", ".sass"],
bundleErrors: {
"/entry.css": ['ERROR: No loader is configured for ".sass" files: test.sass'],
},
});
*/
// itBundled("css/CSSAtImportConditionsNoBundle", {
// files: {
// "/entry.css": `@import "./print.css" print;`,
// },
// });
itBundled("css/CSSAtImportConditionsBundleExternal", {
files: {
"/entry.css": /* css */ `@import "https://example.com/print.css" print;`,
},
outfile: "/out.css",
onAfterBundle(api) {
api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ `
@import "https://example.com/print.css" print;
/* entry.css */
`);
},
});
itBundled("css/CSSAtImportConditionsBundleExternalConditionWithURL", {
files: {
"/entry.css": /* css */ `@import "https://example.com/foo.css" supports(background: url("foo.png"));`,
},
});
itBundled("css/CSSAtImportConditionsBundleLOL", {
outfile: "/out.css",
files: {
"/entry.css": /* css */ `
@import url(http://example.com/foo.css);
@import url(http://example.com/foo.css) layer;
@import url(http://example.com/foo.css) layer(layer-name);
@import url(http://example.com/foo.css) layer(layer-name) supports(display: flex);
@import url(http://example.com/foo.css) layer(layer-name) supports(display: flex) (min-width: 768px) and
(max-width: 1024px);
@import url(http://example.com/foo.css) layer(layer-name) (min-width: 768px) and (max-width: 1024px);
@import url(http://example.com/foo.css) supports(display: flex);
@import url(http://example.com/foo.css) supports(display: flex) (min-width: 768px) and (max-width: 1024px);
@import url(http://example.com/foo.css) (min-width: 768px) and (max-width: 1024px);
@import url(./foo.css);
@import url(./foo.css) layer;
@import url(./foo.css) layer(layer-name);
@import url(./foo.css) layer(layer-name) supports(display: flex);
@import url(./foo.css) layer(layer-name) supports(display: flex) (min-width: 768px) and (max-width: 1024px);
@import url(./foo.css) layer(layer-name) (min-width: 768px) and (max-width: 1024px);
@import url(./foo.css) supports(display: flex);
@import url(./foo.css) supports(display: flex) (min-width: 768px) and (max-width: 1024px);
@import url(./foo.css) (min-width: 768px) and (max-width: 1024px);
@import url(./empty-1.css) layer(empty-1);
@import url(./empty-2.css) supports(empty: 2);
@import url(./empty-3.css) (empty: 3);
@import "./nested-layer.css" layer(outer);
@import "./nested-layer.css" supports(outer: true);
@import "./nested-layer.css" (outer: true);
@import "./nested-supports.css" layer(outer);
@import "./nested-supports.css" supports(outer: true);
@import "./nested-supports.css" (outer: true);
@import "./nested-media.css" layer(outer);
@import "./nested-media.css" supports(outer: true);
@import "./nested-media.css" (outer: true);
`,
"/foo.css": /* css */ `body { color: red }`,
"/empty-1.css": ``,
"/empty-2.css": ``,
"/empty-3.css": ``,
"/nested-layer.css": /* css */ `@import "./foo.css" layer(inner);`,
"/nested-supports.css": /* css */ `@import "./foo.css" supports(inner: true);`,
"/nested-media.css": /* css */ `@import "./foo.css" (inner: true);`,
},
onAfterBundle(api) {
// api.expectFile("/out.css").toMatchSnapshot();
api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ `@import "http://example.com/foo.css";
@import "http://example.com/foo.css" layer;
@import "http://example.com/foo.css" layer(layer-name);
@import "http://example.com/foo.css" layer(layer-name) supports(display: flex);
@import "http://example.com/foo.css" layer(layer-name) (min-width: 768px) and (max-width: 1024px);
@import "http://example.com/foo.css" supports(display: flex);
@import "http://example.com/foo.css" (min-width: 768px) and (max-width: 1024px);
/* foo.css */
body {
color: red;
}
/* foo.css */
@layer {
body {
color: red;
}
}
/* foo.css */
@layer layer-name {
body {
color: red;
}
}
/* foo.css */
@supports (display: flex) {
@layer layer-name {
body {
color: red;
}
}
}
/* foo.css */
@media (min-width: 768px) and (max-width: 1024px) {
@layer layer-name {
body {
color: red;
}
}
}
/* foo.css */
@supports (display: flex) {
body {
color: red;
}
}
/* foo.css */
@media (min-width: 768px) and (max-width: 1024px) {
body {
color: red;
}
}
/* empty-1.css */
@layer empty-1;
/* empty-2.css */
/* empty-3.css */
/* foo.css */
@layer outer {
@layer inner {
body {
color: red;
}
}
}
/* nested-layer.css */
@layer outer;
/* foo.css */
@supports (outer: true) {
@layer inner {
body {
color: red;
}
}
}
/* nested-layer.css */
/* foo.css */
@media (outer: true) {
@layer inner {
body {
color: red;
}
}
}
/* nested-layer.css */
/* foo.css */
@layer outer {
@supports (inner: true) {
body {
color: red;
}
}
}
/* nested-supports.css */
@layer outer;
/* foo.css */
@supports (outer: true) {
@supports (inner: true) {
body {
color: red;
}
}
}
/* nested-supports.css */
/* foo.css */
@media (outer: true) {
@supports (inner: true) {
body {
color: red;
}
}
}
/* nested-supports.css */
/* foo.css */
@layer outer {
@media (inner: true) {
body {
color: red;
}
}
}
/* nested-media.css */
@layer outer;
/* foo.css */
@supports (outer: true) {
@media (inner: true) {
body {
color: red;
}
}
}
/* nested-media.css */
/* foo.css */
@media (outer: true) {
@media (inner: true) {
body {
color: red;
}
}
}
/* nested-media.css */
/* entry.css */
`);
},
});
// This tests that bun correctly clones the import records for all import
// condition tokens. If they aren't cloned correctly, then something will
// likely crash with an out-of-bounds error.
itBundled("css/CSSAtImportConditionsWithImportRecordsBundle", {
files: {
"/entry.css": /* css */ `
@import url(./foo.css) supports(background: url(./a.png));
@import url(./foo.css) supports(background: url(./b.png)) list-of-media-queries;
@import url(./foo.css) layer(layer-name) supports(background: url(./a.png));
@import url(./foo.css) layer(layer-name) supports(background: url(./b.png)) list-of-media-queries;
`,
"/foo.css": /* css */ `body { color: red }`,
"/a.png": `A`,
"/b.png": `B`,
},
outfile: "/out.css",
onAfterBundle(api) {
api.expectFile("/out.css").toEqualIgnoringWhitespace(/* css */ `
/* foo.css */
@supports (background: url(./a.png)) {
body {
color: red;
}
}
/* foo.css */
@media list-of-media-queries {
@supports (background: url(./b.png)) {
body {
color: red;
}
}
}
/* foo.css */
@supports (background: url(./a.png)) {
@layer layer-name {
body {
color: red;
}
}
}
/* foo.css */
@media list-of-media-queries {
@supports (background: url(./b.png)) {
@layer layer-name {
body {
color: red;
}
}
}
}
/* entry.css */
`);
},
});
const files = [
"/001/default/style.css",
"/001/relative-url/style.css",
"/at-charset/001/style.css",
"/at-keyframes/001/style.css",
"/at-layer/001/style.css",
"/at-layer/002/style.css",
"/at-layer/003/style.css",
"/at-layer/004/style.css",
"/at-layer/005/style.css",
"/at-layer/006/style.css",
"/at-layer/007/style.css",
"/at-layer/008/style.css",
"/at-media/001/default/style.css",
"/at-media/002/style.css",
"/at-media/003/style.css",
"/at-media/004/style.css",
"/at-media/005/style.css",
"/at-media/006/style.css",
"/at-media/007/style.css",
"/at-media/008/style.css",
"/at-supports/001/style.css",
"/at-supports/002/style.css",
"/at-supports/003/style.css",
"/at-supports/004/style.css",
"/at-supports/005/style.css",
"/cycles/001/style.css",
"/cycles/002/style.css",
"/cycles/003/style.css",
"/cycles/004/style.css",
"/cycles/005/style.css",
"/cycles/006/style.css",
"/cycles/007/style.css",
"/cycles/008/style.css",
"/data-urls/002/style.css",
"/data-urls/003/style.css",
"/duplicates/001/style.css",
"/duplicates/002/style.css",
"/empty/001/style.css",
"/relative-paths/001/style.css",
"/relative-paths/002/style.css",
"/subresource/001/style.css",
"/subresource/002/style.css",
"/subresource/004/style.css",
"/subresource/005/style.css",
"/subresource/007/style.css",
"/url-format/001/default/style.css",
"/url-format/001/relative-url/style.css",
"/url-format/002/default/style.css",
"/url-format/002/relative-url/style.css",
"/url-format/003/default/style.css",
"/url-format/003/relative-url/style.css",
"/url-fragments/001/style.css",
"/url-fragments/002/style.css",
];
// From: https://github.com/romainmenke/css-import-tests. These test cases just
// serve to document any changes in bun's behavior. Any changes in behavior
// should be tested to ensure they don't cause any regressions. The easiest way
// to test the changes is to bundle https://github.com/evanw/css-import-tests
// and visually inspect a browser's rendering of the resulting CSS file.
itBundled("css/CSSAtImportConditionsFromExternalRepo", {
files: {
"/001/default/a.css": `.box { background-color: green; }`,
"/001/default/style.css": `@import url("a.css");`,
"/001/relative-url/a.css": `.box { background-color: green; }`,
"/001/relative-url/style.css": `@import url("./a.css");`,
"/at-charset/001/a.css": `@charset "utf-8"; .box { background-color: red; }`,
"/at-charset/001/b.css": `@charset "utf-8"; .box { background-color: green; }`,
"/at-charset/001/style.css": `@charset "utf-8"; @import url("a.css"); @import url("b.css");`,
"/at-keyframes/001/a.css": `
.box { animation: BOX; animation-duration: 0s; animation-fill-mode: both; }
@keyframes BOX { 0%, 100% { background-color: green; } }
`,
"/at-keyframes/001/b.css": `
.box { animation: BOX; animation-duration: 0s; animation-fill-mode: both; }
@keyframes BOX { 0%, 100% { background-color: red; } }
`,
"/at-keyframes/001/style.css": `@import url("a.css") screen; @import url("b.css") print;`,
"/at-layer/001/a.css": `.box { background-color: red; }`,
"/at-layer/001/b.css": `.box { background-color: green; }`,
"/at-layer/001/style.css": `
@import url("a.css") layer(a);
@import url("b.css") layer(b);
@import url("a.css") layer(a);
`,
"/at-layer/002/a.css": `.box { background-color: green; }`,
"/at-layer/002/b.css": `.box { background-color: red; }`,
"/at-layer/002/style.css": `
@import url("a.css") layer(a) print;
@import url("b.css") layer(b);
@import url("a.css") layer(a);
`,
"/at-layer/003/a.css": `@layer a { .box { background-color: red; } }`,
"/at-layer/003/b.css": `@layer b { .box { background-color: green; } }`,
"/at-layer/003/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`,
"/at-layer/004/a.css": `@layer { .box { background-color: green; } }`,
"/at-layer/004/b.css": `@layer { .box { background-color: red; } }`,
"/at-layer/004/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`,
"/at-layer/005/a.css": `@import url("b.css") layer(b) (width: 1px);`,
"/at-layer/005/b.css": `.box { background-color: red; }`,
"/at-layer/005/style.css": `
@import url("a.css") layer(a) (min-width: 1px);
@layer a.c { .box { background-color: red; } }
@layer a.b { .box { background-color: green; } }
`,
"/at-layer/006/a.css": `@import url("b.css") layer(b) (min-width: 1px);`,
"/at-layer/006/b.css": `.box { background-color: red; }`,
"/at-layer/006/style.css": `
@import url("a.css") layer(a) (min-width: 1px);
@layer a.c { .box { background-color: green; } }
@layer a.b { .box { background-color: red; } }
`,
"/at-layer/007/style.css": `
@layer foo {}
@layer bar {}
@layer bar { .box { background-color: green; } }
@layer foo { .box { background-color: red; } }
`,
"/at-layer/008/a.css": `@import "b.css" layer; .box { background-color: green; }`,
"/at-layer/008/b.css": `.box { background-color: red; }`,
"/at-layer/008/style.css": `@import url("a.css") layer;`,
"/at-media/001/default/a.css": `.box { background-color: green; }`,
"/at-media/001/default/style.css": `@import url("a.css") screen;`,
"/at-media/002/a.css": `.box { background-color: green; }`,
"/at-media/002/b.css": `.box { background-color: red; }`,
"/at-media/002/style.css": `@import url("a.css") screen; @import url("b.css") print;`,
"/at-media/003/a.css": `@import url("b.css") (min-width: 1px);`,
"/at-media/003/b.css": `.box { background-color: green; }`,
"/at-media/003/style.css": `@import url("a.css") screen;`,
"/at-media/004/a.css": `@import url("b.css") print;`,
"/at-media/004/b.css": `.box { background-color: red; }`,
"/at-media/004/c.css": `.box { background-color: green; }`,
"/at-media/004/style.css": `@import url("c.css"); @import url("a.css") print;`,
"/at-media/005/a.css": `@import url("b.css") (max-width: 1px);`,
"/at-media/005/b.css": `.box { background-color: red; }`,
"/at-media/005/c.css": `.box { background-color: green; }`,
"/at-media/005/style.css": `@import url("c.css"); @import url("a.css") (max-width: 1px);`,
"/at-media/006/a.css": `@import url("b.css") (min-width: 1px);`,
"/at-media/006/b.css": `.box { background-color: green; }`,
"/at-media/006/style.css": `@import url("a.css") (min-height: 1px);`,
"/at-media/007/a.css": `@import url("b.css") screen;`,
"/at-media/007/b.css": `.box { background-color: green; }`,
"/at-media/007/style.css": `@import url("a.css") all;`,
"/at-media/008/a.css": `@import url("green.css") layer(alpha) print;`,
"/at-media/008/b.css": `@import url("red.css") layer(beta) print;`,
"/at-media/008/green.css": `.box { background-color: green; }`,
"/at-media/008/red.css": `.box { background-color: red; }`,
"/at-media/008/style.css": `
@import url("a.css") layer(alpha) all;
@import url("b.css") layer(beta) all;
@layer beta { .box { background-color: green; } }
@layer alpha { .box { background-color: red; } }
`,
"/at-supports/001/a.css": `.box { background-color: green; }`,
"/at-supports/001/style.css": `@import url("a.css") supports(display: block);`,
"/at-supports/002/a.css": `@import url("b.css") supports(width: 10px);`,
"/at-supports/002/b.css": `.box { background-color: green; }`,
"/at-supports/002/style.css": `@import url("a.css") supports(display: block);`,
"/at-supports/003/a.css": `@import url("b.css") supports(width: 10px);`,
"/at-supports/003/b.css": `.box { background-color: green; }`,
"/at-supports/003/style.css": `@import url("a.css") supports((display: block) or (display: inline));`,
"/at-supports/004/a.css": `@import url("b.css") layer(b) supports(width: 10px);`,
"/at-supports/004/b.css": `.box { background-color: green; }`,
"/at-supports/004/style.css": `@import url("a.css") layer(a) supports(display: block);`,
"/at-supports/005/a.css": `@import url("green.css") layer(alpha) supports(foo: bar);`,
"/at-supports/005/b.css": `@import url("red.css") layer(beta) supports(foo: bar);`,
"/at-supports/005/green.css": `.box { background-color: green; }`,
"/at-supports/005/red.css": `.box { background-color: red; }`,
"/at-supports/005/style.css": `
@import url("a.css") layer(alpha) supports(display: block);
@import url("b.css") layer(beta) supports(display: block);
@layer beta { .box { background-color: green; } }
@layer alpha { .box { background-color: red; } }
`,
"/cycles/001/style.css": `@import url("style.css"); .box { background-color: green; }`,
"/cycles/002/a.css": `@import url("red.css"); @import url("b.css");`,
"/cycles/002/b.css": `@import url("green.css"); @import url("a.css");`,
"/cycles/002/green.css": `.box { background-color: green; }`,
"/cycles/002/red.css": `.box { background-color: red; }`,
"/cycles/002/style.css": `@import url("a.css");`,
"/cycles/003/a.css": `@import url("b.css"); .box { background-color: green; }`,
"/cycles/003/b.css": `@import url("a.css"); .box { background-color: red; }`,
"/cycles/003/style.css": `@import url("a.css");`,
"/cycles/004/a.css": `@import url("b.css"); .box { background-color: red; }`,
"/cycles/004/b.css": `@import url("a.css"); .box { background-color: green; }`,
"/cycles/004/style.css": `@import url("a.css"); @import url("b.css");`,
"/cycles/005/a.css": `@import url("b.css"); .box { background-color: green; }`,
"/cycles/005/b.css": `@import url("a.css"); .box { background-color: red; }`,
"/cycles/005/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`,
"/cycles/006/a.css": `@import url("red.css"); @import url("b.css");`,
"/cycles/006/b.css": `@import url("green.css"); @import url("a.css");`,
"/cycles/006/c.css": `@import url("a.css");`,
"/cycles/006/green.css": `.box { background-color: green; }`,
"/cycles/006/red.css": `.box { background-color: red; }`,
"/cycles/006/style.css": `@import url("b.css"); @import url("c.css");`,
"/cycles/007/a.css": `@import url("red.css"); @import url("b.css") screen;`,
"/cycles/007/b.css": `@import url("green.css"); @import url("a.css") all;`,
"/cycles/007/c.css": `@import url("a.css") not print;`,
"/cycles/007/green.css": `.box { background-color: green; }`,
"/cycles/007/red.css": `.box { background-color: red; }`,
"/cycles/007/style.css": `@import url("b.css"); @import url("c.css");`,
"/cycles/008/a.css": `@import url("red.css") layer; @import url("b.css");`,
"/cycles/008/b.css": `@import url("green.css") layer; @import url("a.css");`,
"/cycles/008/c.css": `@import url("a.css") layer;`,
"/cycles/008/green.css": `.box { background-color: green; }`,
"/cycles/008/red.css": `.box { background-color: red; }`,
"/cycles/008/style.css": `@import url("b.css"); @import url("c.css");`,
"/data-urls/002/style.css": `@import url('data:text/css;plain,.box%20%7B%0A%09background-color%3A%20green%3B%0A%7D%0A');`,
"/data-urls/003/style.css": `@import url('data:text/css,.box%20%7B%0A%09background-color%3A%20green%3B%0A%7D%0A');`,
"/duplicates/001/a.css": `.box { background-color: green; }`,
"/duplicates/001/b.css": `.box { background-color: red; }`,
"/duplicates/001/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`,
"/duplicates/002/a.css": `.box { background-color: green; }`,
"/duplicates/002/b.css": `.box { background-color: red; }`,
"/duplicates/002/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css"); @import url("b.css"); @import url("a.css");`,
"/empty/001/empty.css": ``,
"/empty/001/style.css": `@import url("./empty.css"); .box { background-color: green; }`,
"/relative-paths/001/a/a.css": `@import url("../b/b.css")`,
"/relative-paths/001/b/b.css": `.box { background-color: green; }`,
"/relative-paths/001/style.css": `@import url("./a/a.css");`,
"/relative-paths/002/a/a.css": `@import url("./../b/b.css")`,
"/relative-paths/002/b/b.css": `.box { background-color: green; }`,
"/relative-paths/002/style.css": `@import url("./a/a.css");`,
"/subresource/001/something/images/green.png": `...`,
"/subresource/001/something/styles/green.css": `.box { background-image: url("../images/green.png"); }`,
"/subresource/001/style.css": `@import url("./something/styles/green.css");`,
"/subresource/002/green.png": `...`,
"/subresource/002/style.css": `@import url("./styles/green.css");`,
"/subresource/002/styles/green.css": `.box { background-image: url("../green.png"); }`,
"/subresource/004/style.css": `@import url("./styles/green.css");`,
"/subresource/004/styles/green.css": `.box { background-image: url("green.png"); }`,
"/subresource/004/styles/green.png": `...`,
"/subresource/005/style.css": `@import url("./styles/green.css");`,
"/subresource/005/styles/green.css": `.box { background-image: url("./green.png"); }`,
"/subresource/005/styles/green.png": `...`,
"/subresource/007/green.png": `...`,
"/subresource/007/style.css": `.box { background-image: url("./green.png"); }`,
"/url-format/001/default/a.css": `.box { background-color: green; }`,
"/url-format/001/default/style.css": `@import url(a.css);`,
"/url-format/001/relative-url/a.css": `.box { background-color: green; }`,
"/url-format/001/relative-url/style.css": `@import url(./a.css);`,
"/url-format/002/default/a.css": `.box { background-color: green; }`,
"/url-format/002/default/style.css": `@import "a.css";`,
"/url-format/002/relative-url/a.css": `.box { background-color: green; }`,
"/url-format/002/relative-url/style.css": `@import "./a.css";`,
"/url-format/003/default/a.css": `.box { background-color: green; }`,
"/url-format/003/default/style.css": `@import url("a.css"`,
"/url-format/003/relative-url/a.css": `.box { background-color: green; }`,
"/url-format/003/relative-url/style.css": `@import url("./a.css"`,
"/url-fragments/001/a.css": `.box { background-color: green; }`,
"/url-fragments/001/style.css": `@import url("./a.css#foo");`,
"/url-fragments/002/a.css": `.box { background-color: green; }`,
"/url-fragments/002/b.css": `.box { background-color: red; }`,
"/url-fragments/002/style.css": `@import url("./a.css#1"); @import url("./b.css#2"); @import url("./a.css#3");`,
},
entryPoints: files,
outputPaths: files,
outdir: "/out",
onAfterBundle(api) {
for (const file of files) {
console.log("Checking snapshot:", file);
api.expectFile(join(file)).toMatchSnapshot(file);
}
},
});
itBundled("css/CSSAtImportConditionsAtLayerBundle", {
files: {
"/case1.css": /* css */ `
@import url(case1-foo.css) layer(first.one);
@import url(case1-foo.css) layer(last.one);
@import url(case1-foo.css) layer(first.one);
`,
"/case1-foo.css": `body { color: red }`,
"/case2.css": /* css */ `
@import url(case2-foo.css);
@import url(case2-bar.css);
@import url(case2-foo.css);
`,
"/case2-foo.css": `@layer first.one { body { color: red } }`,
"/case2-bar.css": `@layer last.one { body { color: green } }`,
"/case3.css": /* css */ `
@import url(case3-foo.css);
@import url(case3-bar.css);
@import url(case3-foo.css);
`,
"/case3-foo.css": `@layer { body { color: red } }`,
"/case3-bar.css": `@layer only.one { body { color: green } }`,
"/case4.css": /* css */ `
@import url(case4-foo.css) layer(first);
@import url(case4-foo.css) layer(last);
@import url(case4-foo.css) layer(first);
`,
"/case4-foo.css": `@layer one { @layer two, three.four; body { color: red } }`,
"/case5.css": /* css */ `
@import url(case5-foo.css) layer;
@import url(case5-foo.css) layer(middle);
@import url(case5-foo.css) layer;
`,
"/case5-foo.css": `@layer one { @layer two, three.four; body { color: red } }`,
// Note: There was a bug that only showed up in this case. We need at least this many cases.
"/case6.css": /* css */ `
@import url(case6-foo.css) layer(first);
@import url(case6-foo.css) layer(last);
@import url(case6-foo.css) layer(first);
`,
"/case6-foo.css": `@layer { @layer two, three.four; body { color: red } }`,
},
entryPoints: ["/case1.css", "/case2.css", "/case3.css", "/case4.css", "/case5.css", "/case6.css"],
outdir: "/out",
onAfterBundle(api) {
const snapshotFiles = ["case1.css", "case2.css", "case3.css", "case4.css", "case5.css", "case6.css"];
for (const file of snapshotFiles) {
console.log("Checking snapshot:", file);
api.expectFile(join("/out", file)).toMatchSnapshot(file);
}
},
});
itBundled("css/CSSAtImportConditionsAtLayerBundleAlternatingLayerInFile", {
files: {
"/a.css": `@layer first { body { color: red } }`,
"/b.css": `@layer last { body { color: green } }`,
"/case1.css": /* css */ `
@import url(a.css);
@import url(a.css);
`,
"/case2.css": /* css */ `
@import url(a.css);
@import url(b.css);
@import url(a.css);
`,
"/case3.css": /* css */ `
@import url(a.css);
@import url(b.css);
@import url(a.css);
@import url(b.css);
`,
"/case4.css": /* css */ `
@import url(a.css);
@import url(b.css);
@import url(a.css);
@import url(b.css);
@import url(a.css);
`,
"/case5.css": /* css */ `
@import url(a.css);
@import url(b.css);
@import url(a.css);
@import url(b.css);
@import url(a.css);
@import url(b.css);
`,
"/case6.css": /* css */ `
@import url(a.css);
@import url(b.css);
@import url(a.css);
@import url(b.css);
@import url(a.css);
@import url(b.css);
@import url(a.css);
`,
},
entryPoints: ["/case1.css", "/case2.css", "/case3.css", "/case4.css", "/case5.css", "/case6.css"],
outdir: "/out",
onAfterBundle(api) {
const snapshotFiles = ["case1.css", "case2.css", "case3.css", "case4.css", "case5.css", "case6.css"];
for (const file of snapshotFiles) {
console.log("Checking snapshot:", file);
api.expectFile(join("/out", file)).toMatchSnapshot(file);
}
},
});
itBundled("css/CSSAtImportConditionsChainExternal", {
files: {
"/entry.css": /* css */ `
@import "a.css" layer(a) not print;
`,
"/a.css": /* css */ `
@import "http://example.com/external1.css";
@import "b.css" layer(b) not tv;
@import "http://example.com/external2.css" layer(a2);
`,
"/b.css": /* css */ `
@import "http://example.com/external3.css";
@import "http://example.com/external4.css" layer(b2);
`,
},
outfile: "/out.css",
});
// This test mainly just makes sure that this scenario doesn't crash
itBundled("css/CSSAndJavaScriptCodeSplittingESBuildIssue1064", {
files: {
"/a.js": /* js */ `
import shared from './shared.js'
console.log(shared() + 1)
`,
"/b.js": /* js */ `
import shared from './shared.js'
console.log(shared() + 2)
`,
"/c.css": /* css */ `
@import "./shared.css";
body { color: red }
`,
"/d.css": /* css */ `
@import "./shared.css";
body { color: blue }
`,
"/shared.js": `export default function() { return 3 }`,
"/shared.css": `body { background: black }`,
},
entryPoints: ["/a.js", "/b.js", "/c.css", "/d.css"],
format: "esm",
splitting: true,
onAfterBundle(api) {
const files = ["/a.js", "/b.js", "/c.css", "/d.css"];
for (const file of files) {
api.expectFile(file).toMatchSnapshot(file);
}
},
});
itBundled("css/CSSExternalQueryAndHashNoMatchESBuildIssue1822", {
files: {
"/entry.css": /* css */ `
a { background: url(foo/bar.png?baz) }
b { background: url(foo/bar.png#baz) }
`,
},
outfile: "/out.css",
bundleErrors: {
"/entry.css": [
`Could not resolve: "foo/bar.png?baz". Maybe you need to "bun install"?`,
`Could not resolve: "foo/bar.png#baz". Maybe you need to "bun install"?`,
],
},
});
itBundled("css/CSSNestingOldBrowser", {
// GENERATED
files: {
"/nested-@layer.css": `a { @layer base { color: red; } }`,
"/nested-@media.css": `a { @media screen { color: red; } }`,
"/nested-ampersand-twice.css": `a { &, & { color: red; } }`,
"/nested-ampersand-first.css": `a { &, b { color: red; } }`,
"/nested-attribute.css": `a { [href] { color: red; } }`,
"/nested-colon.css": `a { :hover { color: red; } }`,
"/nested-dot.css": `a { .cls { color: red; } }`,
"/nested-greaterthan.css": `a { > b { color: red; } }`,
"/nested-hash.css": `a { #id { color: red; } }`,
"/nested-plus.css": `a { + b { color: red; } }`,
"/nested-tilde.css": `a { ~ b { color: red; } }`,
"/toplevel-ampersand-twice.css": `&, & { color: red; }`,
"/toplevel-ampersand-first.css": `&, a { color: red; }`,
"/toplevel-ampersand-second.css": `a, & { color: red; }`,
"/toplevel-attribute.css": `[href] { color: red; }`,
"/toplevel-colon.css": `:hover { color: red; }`,
"/toplevel-dot.css": `.cls { color: red; }`,
"/toplevel-greaterthan.css": `> b { color: red; }`,
"/toplevel-hash.css": `#id { color: red; }`,
"/toplevel-plus.css": `+ b { color: red; }`,
"/toplevel-tilde.css": `~ b { color: red; }`,
},
entryPoints: [
"/nested-@layer.css",
"/nested-@media.css",
"/nested-ampersand-twice.css",
"/nested-ampersand-first.css",
"/nested-attribute.css",
"/nested-colon.css",
"/nested-dot.css",
"/nested-greaterthan.css",
"/nested-hash.css",
"/nested-plus.css",
"/nested-tilde.css",
"/toplevel-ampersand-twice.css",
"/toplevel-ampersand-first.css",
"/toplevel-ampersand-second.css",
"/toplevel-attribute.css",
"/toplevel-colon.css",
"/toplevel-dot.css",
"/toplevel-greaterthan.css",
"/toplevel-hash.css",
"/toplevel-plus.css",
"/toplevel-tilde.css",
],
unsupportedCSSFeatures: ["Nesting"],
/* TODO FIX expectedScanLog: `nested-@layer.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
nested-@media.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
nested-ampersand-first.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
nested-ampersand-twice.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
nested-attribute.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
nested-colon.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
nested-dot.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
nested-greaterthan.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
nested-hash.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
nested-plus.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
nested-tilde.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
toplevel-ampersand-first.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
toplevel-ampersand-second.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
toplevel-ampersand-twice.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
toplevel-ampersand-twice.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
toplevel-greaterthan.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
toplevel-plus.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
toplevel-tilde.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
`, */
});
// TODO: Bun's bundler doesn't support multiple entry points generating CSS outputs
// with identical content hashes to the same output path. This test exposes that
// limitation. Skip until the bundler can deduplicate or handle this case.
itBundled.skip("css/MetafileCSSBundleTwoToOne", {
files: {
"/foo/entry.js": /* js */ `
import '../common.css'
console.log('foo')
`,
"/bar/entry.js": /* js */ `
import '../common.css'
console.log('bar')
`,
"/common.css": `body { color: red }`,
},
metafile: true,
entryPoints: ["/foo/entry.js", "/bar/entry.js"],
entryNaming: "[ext]/[hash]",
outdir: "/",
});
itBundled("css/DeduplicateRules", {
// GENERATED
files: {
"/yes0.css": `a { color: red; color: green; color: red }`,
"/yes1.css": `a { color: red } a { color: green } a { color: red }`,
"/yes2.css": `@media screen { a { color: red } } @media screen { a { color: red } }`,
"/no0.css": `@media screen { a { color: red } } @media screen { & a { color: red } }`,
"/no1.css": `@media screen { a { color: red } } @media screen { a[x] { color: red } }`,
"/no2.css": `@media screen { a { color: red } } @media screen { a.x { color: red } }`,
"/no3.css": `@media screen { a { color: red } } @media screen { a#x { color: red } }`,
"/no4.css": `@media screen { a { color: red } } @media screen { a:x { color: red } }`,
"/no5.css": `@media screen { a:x { color: red } } @media screen { a:x(y) { color: red } }`,
"/no6.css": `@media screen { a b { color: red } } @media screen { a + b { color: red } }`,
"/across-files.css": `@import 'across-files-0.css'; @import 'across-files-1.css'; @import 'across-files-2.css';`,
"/across-files-0.css": `a { color: red; color: red }`,
"/across-files-1.css": `a { color: green }`,
"/across-files-2.css": `a { color: red }`,
"/across-files-url.css": `@import 'across-files-url-0.css'; @import 'across-files-url-1.css'; @import 'across-files-url-2.css';`,
"/across-files-url-0.css": `@import 'http://example.com/some.css'; @font-face { src: url(http://example.com/some.font); }`,
"/across-files-url-1.css": `@font-face { src: url(http://example.com/some.other.font); }`,
"/across-files-url-2.css": `@font-face { src: url(http://example.com/some.font); }`,
},
entryPoints: [
"/yes0.css",
"/yes1.css",
"/yes2.css",
"/no0.css",
"/no1.css",
"/no2.css",
"/no3.css",
"/no4.css",
"/no5.css",
"/no6.css",
"/across-files.css",
"/across-files-url.css",
],
});
});