Files

2654 lines
82 KiB
Go

package bundler_tests
import (
"testing"
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/config"
)
var css_suite = suite{
name: "css",
}
func TestCSSEntryPoint(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
body {
background: white;
color: black }
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
},
})
}
func TestCSSAtImportMissing(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@import "./missing.css";
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
},
expectedScanLog: `entry.css: ERROR: Could not resolve "./missing.css"
`,
})
}
func TestCSSAtImportExternal(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@import "./internal.css";
@import "./external1.css";
@import "./external2.css";
@import "./charset1.css";
@import "./charset2.css";
@import "./external5.css" screen;
`,
"/internal.css": `
@import "./external5.css" print;
.before { color: red }
`,
"/charset1.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": `
@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 }
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExternalSettings: config.ExternalSettings{
PostResolve: config.ExternalMatchers{Exact: map[string]bool{
"/external1.css": true,
"/external2.css": true,
"/external3.css": true,
"/external4.css": true,
"/external5.css": true,
}},
},
},
})
}
func TestCSSAtImport(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@import "./a.css";
@import "./b.css";
.entry { color: red }
`,
"/a.css": `
@import "./shared.css";
.a { color: green }
`,
"/b.css": `
@import "./shared.css";
.b { color: blue }
`,
"/shared.css": `
.shared { color: black }
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
},
})
}
func TestCSSFromJSMissingImport(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import {missing} from "./a.css"
console.log(missing)
`,
"/a.css": `
.a { color: red }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
expectedCompileLog: `entry.js: ERROR: No matching export in "a.css" for import "missing"
`,
})
}
func TestCSSFromJSMissingStarImport(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import * as ns from "./a.css"
console.log(ns.missing)
`,
"/a.css": `
.a { color: red }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
expectedCompileLog: `entry.js: WARNING: Import "missing" will always be undefined because there is no matching export in "a.css"
`,
})
}
func TestImportGlobalCSSFromJS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import "./a.js"
import "./b.js"
`,
"/a.js": `
import * as stylesA from "./a.css"
console.log('a', stylesA.a, stylesA.default.a)
`,
"/a.css": `
.a { color: red }
`,
"/b.js": `
import * as stylesB from "./b.css"
console.log('b', stylesB.b, stylesB.default.b)
`,
"/b.css": `
.b { color: blue }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
expectedCompileLog: `a.js: WARNING: Import "a" will always be undefined because there is no matching export in "a.css"
b.js: WARNING: Import "b" will always be undefined because there is no matching export in "b.css"
`,
})
}
func TestImportLocalCSSFromJS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import "./a.js"
import "./b.js"
`,
"/a.js": `
import * as stylesA from "./dir1/style.css"
console.log('file 1', stylesA.button, stylesA.default.a)
`,
"/dir1/style.css": `
.a { color: red }
.button { display: none }
`,
"/b.js": `
import * as stylesB from "./dir2/style.css"
console.log('file 2', stylesB.button, stylesB.default.b)
`,
"/dir2/style.css": `
.b { color: blue }
.button { display: none }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
},
})
}
func TestImportLocalCSSFromJSMinifyIdentifiers(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import "./a.js"
import "./b.js"
`,
"/a.js": `
import * as stylesA from "./dir1/style.css"
console.log('file 1', stylesA.button, stylesA.default.a)
`,
"/dir1/style.css": `
.a { color: red }
.button { display: none }
`,
"/b.js": `
import * as stylesB from "./dir2/style.css"
console.log('file 2', stylesB.button, stylesB.default.b)
`,
"/dir2/style.css": `
.b { color: blue }
.button { display: none }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
MinifyIdentifiers: true,
},
})
}
func TestImportLocalCSSFromJSMinifyIdentifiersAvoidGlobalNames(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import "./global.css"
import "./local.module.css"
`,
"/global.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": `
.rename-this { color: blue }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderCSS,
".module.css": config.LoaderLocalCSS,
},
MinifyIdentifiers: true,
},
})
}
// See: https://github.com/evanw/esbuild/issues/3295
func TestImportLocalCSSFromJSMinifyIdentifiersMultipleEntryPoints(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/a.js": `
import { foo, bar } from "./a.module.css";
console.log(foo, bar);
`,
"/a.module.css": `
.foo { color: #001; }
.bar { color: #002; }
`,
"/b.js": `
import { foo, bar } from "./b.module.css";
console.log(foo, bar);
`,
"/b.module.css": `
.foo { color: #003; }
.bar { color: #004; }
`,
},
entryPaths: []string{"/a.js", "/b.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
MinifyIdentifiers: true,
},
})
}
func TestImportCSSFromJSLocalVsGlobal(t *testing.T) {
css := `
.top_level { color: #000 }
:global(.GLOBAL) { color: #001 }
:local(.local) { color: #002 }
div:global(.GLOBAL) { color: #003 }
div:local(.local) { color: #004 }
.top_level:global(div) { color: #005 }
.top_level:local(div) { color: #006 }
:global(div.GLOBAL) { color: #007 }
:local(div.local) { color: #008 }
div:global(span.GLOBAL) { color: #009 }
div:local(span.local) { color: #00A }
div:global(#GLOBAL_A.GLOBAL_B.GLOBAL_C):local(.local_a.local_b#local_c) { color: #00B }
div:global(#GLOBAL_A .GLOBAL_B .GLOBAL_C):local(.local_a .local_b #local_c) { color: #00C }
.nested {
:global(&.GLOBAL) { color: #00D }
:local(&.local) { color: #00E }
&:global(.GLOBAL) { color: #00F }
&:local(.local) { color: #010 }
}
:global(.GLOBAL_A .GLOBAL_B) { color: #011 }
:local(.local_a .local_b) { color: #012 }
div:global(.GLOBAL_A .GLOBAL_B):hover { color: #013 }
div:local(.local_a .local_b):hover { color: #014 }
div :global(.GLOBAL_A .GLOBAL_B) span { color: #015 }
div :local(.local_a .local_b) span { color: #016 }
div > :global(.GLOBAL_A ~ .GLOBAL_B) + span { color: #017 }
div > :local(.local_a ~ .local_b) + span { color: #018 }
div:global(+ .GLOBAL_A):hover { color: #019 }
div:local(+ .local_a):hover { color: #01A }
:global.GLOBAL:local.local { color: #01B }
:global .GLOBAL :local .local { color: #01C }
:global {
.GLOBAL {
before: outer;
:local {
before: inner;
.local {
color: #01D;
}
after: inner;
}
after: outer;
}
}
`
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import normalStyles from "./normal.css"
import globalStyles from "./LOCAL.global-css"
import localStyles from "./LOCAL.local-css"
console.log('should be empty:', normalStyles)
console.log('fewer local names:', globalStyles)
console.log('more local names:', localStyles)
`,
"/normal.css": css,
"/LOCAL.global-css": css,
"/LOCAL.local-css": css,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderCSS,
".global-css": config.LoaderGlobalCSS,
".local-css": config.LoaderLocalCSS,
},
},
})
}
func TestImportCSSFromJSLowerBareLocalAndGlobal(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.css"
console.log(styles)
`,
"/styles.css": `
.before { color: #000 }
:local { .button { color: #000 } }
.after { color: #000 }
.before { color: #001 }
:global { .button { color: #001 } }
.after { color: #001 }
div { :local { .button { color: #002 } } }
div { :global { .button { color: #003 } } }
:local(:global) { color: #004 }
:global(:local) { color: #005 }
:local(:global) { .button { color: #006 } }
:global(:local) { .button { color: #007 } }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
UnsupportedCSSFeatures: compat.Nesting,
},
})
}
func TestImportCSSFromJSLocalAtKeyframes(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.css"
console.log(styles)
`,
"/styles.css": `
@keyframes local_name { to { color: red } }
div :global { animation-name: none }
div :local { animation-name: none }
div :global { animation-name: global_name }
div :local { animation-name: local_name }
div :global { animation-name: global_name1, none, global_name2, Inherit, INITIAL, revert, revert-layer, unset }
div :local { animation-name: local_name1, none, local_name2, Inherit, INITIAL, revert, revert-layer, unset }
div :global { animation: 2s infinite global_name }
div :local { animation: 2s infinite local_name }
/* Someone wanted to be able to name their animations "none" */
@keyframes "none" { to { color: red } }
div :global { animation-name: "none" }
div :local { animation-name: "none" }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
UnsupportedCSSFeatures: compat.Nesting,
},
})
}
func TestImportCSSFromJSLocalAtCounterStyle(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import list_style_type from "./list_style_type.css"
import list_style from "./list_style.css"
console.log(list_style_type, list_style)
`,
"/list_style_type.css": `
@counter-style local { symbols: A B C }
div :global { list-style-type: GLOBAL }
div :local { list-style-type: local }
/* Must not accept invalid type values */
div :local { list-style-type: none }
div :local { list-style-type: INITIAL }
div :local { list-style-type: decimal }
div :local { list-style-type: disc }
div :local { list-style-type: SQUARE }
div :local { list-style-type: circle }
div :local { list-style-type: disclosure-OPEN }
div :local { list-style-type: DISCLOSURE-closed }
div :local { list-style-type: LAO }
div :local { list-style-type: "\1F44D" }
`,
"/list_style.css": `
@counter-style local { symbols: A B C }
div :global { list-style: GLOBAL }
div :local { list-style: local }
/* The first one is the type */
div :local { list-style: local none }
div :local { list-style: local url(http://) }
div :local { list-style: local linear-gradient(red, green) }
div :local { list-style: local inside }
div :local { list-style: local outside }
/* The second one is the type */
div :local { list-style: none local }
div :local { list-style: url(http://) local }
div :local { list-style: linear-gradient(red, green) local }
div :local { list-style: local inside }
div :local { list-style: local outside }
div :local { list-style: inside inside }
div :local { list-style: inside outside }
div :local { list-style: outside inside }
div :local { list-style: outside outside }
/* The type is set to "none" here */
div :local { list-style: url(http://) none invalid }
div :local { list-style: linear-gradient(red, green) none invalid }
/* Must not accept invalid type values */
div :local { list-style: INITIAL }
div :local { list-style: decimal }
div :local { list-style: disc }
div :local { list-style: SQUARE }
div :local { list-style: circle }
div :local { list-style: disclosure-OPEN }
div :local { list-style: DISCLOSURE-closed }
div :local { list-style: LAO }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
UnsupportedCSSFeatures: compat.Nesting,
},
})
}
func TestImportCSSFromJSLocalAtContainer(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.css"
console.log(styles)
`,
"/styles.css": `
@container not (max-width: 100px) { div { color: red } }
@container local (max-width: 100px) { div { color: red } }
@container local not (max-width: 100px) { div { color: red } }
@container local (max-width: 100px) or (min-height: 100px) { div { color: red } }
@container local (max-width: 100px) and (min-height: 100px) { div { color: red } }
@container general_enclosed(max-width: 100px) { div { color: red } }
@container local general_enclosed(max-width: 100px) { div { color: red } }
div :global { container-name: NONE initial }
div :local { container-name: none INITIAL }
div :global { container-name: GLOBAL1 GLOBAL2 }
div :local { container-name: local1 local2 }
div :global { container: none }
div :local { container: NONE }
div :global { container: NONE / size }
div :local { container: none / size }
div :global { container: GLOBAL1 GLOBAL2 }
div :local { container: local1 local2 }
div :global { container: GLOBAL1 GLOBAL2 / size }
div :local { container: local1 local2 / size }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
UnsupportedCSSFeatures: compat.Nesting,
},
})
}
func TestImportCSSFromJSNthIndexLocal(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.css"
console.log(styles)
`,
"/styles.css": `
:nth-child(2n of .local) { color: #000 }
:nth-child(2n of :local(#local), :global(.GLOBAL)) { color: #001 }
:nth-child(2n of .local1 :global .GLOBAL1, .GLOBAL2 :local .local2) { color: #002 }
.local1, :nth-child(2n of :global .GLOBAL), .local2 { color: #003 }
:nth-last-child(2n of .local) { color: #000 }
:nth-last-child(2n of :local(#local), :global(.GLOBAL)) { color: #001 }
:nth-last-child(2n of .local1 :global .GLOBAL1, .GLOBAL2 :local .local2) { color: #002 }
.local1, :nth-last-child(2n of :global .GLOBAL), .local2 { color: #003 }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
UnsupportedCSSFeatures: compat.Nesting,
},
})
}
func TestImportCSSFromJSComposes(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.module.css"
console.log(styles)
`,
"/global.css": `
.GLOBAL1 {
color: black;
}
`,
"/styles.module.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": `
.local0 {
composes: base1 base2 from "base.module.css";
color: blue;
}
`,
"/other2.module.css": `
.local0 {
composes: base1 base3 from "base.module.css";
background: purple;
}
`,
"/base.module.css": `
.base1 {
cursor: pointer;
}
.base2 {
display: inline;
}
.base3 {
float: left;
}
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderCSS,
".module.css": config.LoaderLocalCSS,
},
},
})
}
func TestImportCSSFromJSComposesFromMissingImport(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/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;
}
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".module.css": config.LoaderLocalCSS,
".css": config.LoaderCSS,
},
},
expectedCompileLog: `styles.module.css: ERROR: Cannot use global name "y" with "composes"
file.module.css: NOTE: The global name "y" is defined here:
NOTE: Use the ":local" selector to change "y" into a local name.
styles.module.css: ERROR: The name "z" never appears in "file.module.css"
styles.module.css: ERROR: Cannot use global name "x" with "composes"
file.css: NOTE: The global name "x" is defined here:
NOTE: Use the "local-css" loader for "file.css" to enable local names.
`,
})
}
func TestImportCSSFromJSComposesFromNotCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.css"
console.log(styles)
`,
"/styles.css": `
.foo {
composes: bar from "file.txt";
}
`,
"/file.txt": `
.bar {
color: red;
}
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
".txt": config.LoaderText,
},
},
expectedScanLog: `styles.css: ERROR: Cannot use "composes" with "file.txt"
NOTE: You can only use "composes" with CSS files and "file.txt" is not a CSS file (it was loaded with the "text" loader).
`,
})
}
func TestImportCSSFromJSComposesCircular(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.css"
console.log(styles)
`,
"/styles.css": `
.foo {
composes: bar;
}
.bar {
composes: foo;
}
.baz {
composes: baz;
}
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
},
})
}
func TestImportCSSFromJSComposesFromCircular(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.css"
console.log(styles)
`,
"/styles.css": `
.foo {
composes: bar from "other.css";
}
.bar {
composes: bar from "styles.css";
}
`,
"/other.css": `
.bar {
composes: foo from "styles.css";
}
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
},
})
}
func TestImportCSSFromJSComposesFromUndefined(t *testing.T) {
note := "NOTE: The specification of \"composes\" does not define an order when class declarations from separate files are composed together. " +
"The value of the \"zoom\" property for \"foo\" may change unpredictably as the code is edited. " +
"Make sure that all definitions of \"zoom\" for \"foo\" are in a single file."
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.css"
console.log(styles)
`,
"/styles.css": `
@import "well-defined.css";
@import "undefined/case1.css";
@import "undefined/case2.css";
@import "undefined/case3.css";
@import "undefined/case4.css";
@import "undefined/case5.css";
`,
"/well-defined.css": `
.z1 { composes: z2; zoom: 1; }
.z2 { zoom: 2; }
.z4 { zoom: 4; }
.z3 { composes: z4; zoom: 3; }
.z5 { composes: foo bar from "file-1.css"; }
`,
"/undefined/case1.css": `
.foo {
composes: foo from "../file-1.css";
zoom: 2;
}
`,
"/undefined/case2.css": `
.foo {
composes: foo from "../file-1.css";
composes: foo from "../file-2.css";
}
`,
"/undefined/case3.css": `
.foo { composes: nested1 nested2; }
.nested1 { zoom: 3; }
.nested2 { composes: foo from "../file-2.css"; }
`,
"/undefined/case4.css": `
.foo { composes: nested1 nested2; }
.nested1 { composes: foo from "../file-1.css"; }
.nested2 { zoom: 3; }
`,
"/undefined/case5.css": `
.foo { composes: nested1 nested2; }
.nested1 { composes: foo from "../file-1.css"; }
.nested2 { composes: foo from "../file-2.css"; }
`,
"/file-1.css": `
.foo { zoom: 1; }
.bar { zoom: 2; }
`,
"/file-2.css": `
.foo { zoom: 2; }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
},
expectedCompileLog: `undefined/case1.css: WARNING: The value of "zoom" in the "foo" class is undefined
file-1.css: NOTE: The first definition of "zoom" is here:
undefined/case1.css: NOTE: The second definition of "zoom" is here:
` + note + `
undefined/case2.css: WARNING: The value of "zoom" in the "foo" class is undefined
file-1.css: NOTE: The first definition of "zoom" is here:
file-2.css: NOTE: The second definition of "zoom" is here:
` + note + `
undefined/case3.css: WARNING: The value of "zoom" in the "foo" class is undefined
undefined/case3.css: NOTE: The first definition of "zoom" is here:
file-2.css: NOTE: The second definition of "zoom" is here:
` + note + `
undefined/case4.css: WARNING: The value of "zoom" in the "foo" class is undefined
file-1.css: NOTE: The first definition of "zoom" is here:
undefined/case4.css: NOTE: The second definition of "zoom" is here:
` + note + `
undefined/case5.css: WARNING: The value of "zoom" in the "foo" class is undefined
file-1.css: NOTE: The first definition of "zoom" is here:
file-2.css: NOTE: The second definition of "zoom" is here:
` + note + `
`,
})
}
func TestImportCSSFromJSWriteToStdout(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import "./entry.css"
`,
"/entry.css": `
.entry { color: red }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
WriteToStdout: true,
},
expectedScanLog: `entry.js: ERROR: Cannot import "entry.css" into a JavaScript file without an output path configured
`,
})
}
func TestImportJSFromCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
export default 123
`,
"/entry.css": `
@import "./entry.js";
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
expectedScanLog: `entry.css: ERROR: Cannot import "entry.js" into a CSS file
NOTE: An "@import" rule can only be used to import another CSS file and "entry.js" is not a CSS file (it was loaded with the "js" loader).
`,
})
}
func TestImportJSONFromCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.json": `
{}
`,
"/entry.css": `
@import "./entry.json";
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
expectedScanLog: `entry.css: ERROR: Cannot import "entry.json" into a CSS file
NOTE: An "@import" rule can only be used to import another CSS file and "entry.json" is not a CSS file (it was loaded with the "json" loader).
`,
})
}
func TestMissingImportURLInCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/src/entry.css": `
a { background: url(./one.png); }
b { background: url("./two.png"); }
`,
},
entryPaths: []string{"/src/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
expectedScanLog: `src/entry.css: ERROR: Could not resolve "./one.png"
src/entry.css: ERROR: Could not resolve "./two.png"
`,
})
}
func TestExternalImportURLInCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/src/entry.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) }
`,
},
entryPaths: []string{"/src/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExternalSettings: config.ExternalSettings{
PostResolve: config.ExternalMatchers{Exact: map[string]bool{
"/src/external.png": true,
}},
},
},
})
}
func TestInvalidImportURLInCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.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 }`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
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).
`,
})
}
func TestTextImportURLInCSSText(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
a {
background: url(./example.txt);
}
`,
"/example.txt": `This is some text.`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
})
}
func TestDataURLImportURLInCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
a {
background: url(./example.png);
}
`,
"/example.png": "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A",
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderCSS,
".png": config.LoaderDataURL,
},
},
})
}
func TestBinaryImportURLInCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
a {
background: url(./example.png);
}
`,
"/example.png": "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A",
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderCSS,
".png": config.LoaderBinary,
},
},
})
}
func TestBase64ImportURLInCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
a {
background: url(./example.png);
}
`,
"/example.png": "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A",
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderCSS,
".png": config.LoaderBase64,
},
},
})
}
func TestFileImportURLInCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@import "./one.css";
@import "./two.css";
`,
"/one.css": `
a { background: url(./example.data) }
`,
"/two.css": `
b { background: url(./example.data) }
`,
"/example.data": "This is some data.",
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderCSS,
".data": config.LoaderFile,
},
},
})
}
func TestIgnoreURLsInAtRulePrelude(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
/* This should not generate a path resolution error */
@supports (background: url(ignored.png)) {
a { color: red }
}
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
})
}
func TestPackageURLsInCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.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`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderCSS,
".png": config.LoaderBase64,
},
},
})
}
func TestCSSAtImportExtensionOrderCollision(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
// 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 }`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
ExtensionOrder: []string{".js", ".css"},
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderCSS,
},
},
})
}
func TestCSSAtImportExtensionOrderCollisionUnsupported(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
// This still shouldn't pick ".js" even though ".sass" isn't ".css"
"/entry.css": `@import "./test";`,
"/test.js": `console.log('js')`,
"/test.sass": `// some code`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
ExtensionOrder: []string{".js", ".sass"},
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderCSS,
},
},
expectedScanLog: `entry.css: ERROR: No loader is configured for ".sass" files: test.sass
`,
})
}
func TestCSSAtImportConditionsNoBundle(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `@import "./print.css" print;`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputFile: "/out.css",
},
})
}
func TestCSSAtImportConditionsBundleExternal(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `@import "https://example.com/print.css" print;`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
},
})
}
func TestCSSAtImportConditionsBundleExternalConditionWithURL(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@import "https://example.com/foo.css" (foo: url("foo.png")) and (bar: url("bar.png"));
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
},
})
}
func TestCSSAtImportConditionsBundle(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.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(supports-condition);
@import url(http://example.com/foo.css) layer(layer-name) supports(supports-condition) list-of-media-queries;
@import url(http://example.com/foo.css) layer(layer-name) list-of-media-queries;
@import url(http://example.com/foo.css) supports(supports-condition);
@import url(http://example.com/foo.css) supports(supports-condition) list-of-media-queries;
@import url(http://example.com/foo.css) list-of-media-queries;
@import url(foo.css);
@import url(foo.css) layer;
@import url(foo.css) layer(layer-name);
@import url(foo.css) layer(layer-name) supports(supports-condition);
@import url(foo.css) layer(layer-name) supports(supports-condition) list-of-media-queries;
@import url(foo.css) layer(layer-name) list-of-media-queries;
@import url(foo.css) supports(supports-condition);
@import url(foo.css) supports(supports-condition) list-of-media-queries;
@import url(foo.css) list-of-media-queries;
@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": `body { color: red }`,
"/empty-1.css": ``,
"/empty-2.css": ``,
"/empty-3.css": ``,
"/nested-layer.css": `@import "foo.css" layer(inner);`,
"/nested-supports.css": `@import "foo.css" supports(inner: true);`,
"/nested-media.css": `@import "foo.css" (inner: true);`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
},
})
}
func TestCSSAtImportConditionsWithImportRecordsBundle(t *testing.T) {
// This tests that esbuild 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.
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.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": `body { color: red }`,
"/a.png": `A`,
"/b.png": `B`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderCSS,
".png": config.LoaderBase64,
},
},
})
}
// From: https://github.com/romainmenke/css-import-tests. These test cases just
// serve to document any changes in esbuild'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.
func TestCSSAtImportConditionsFromExternalRepo(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/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);
`,
// Note: This case is currently bundled incorrectly. Normal CSS takes
// effect at the position of the last "@import". However, "@layer" CSS
// takes effect at the position of the first "@import". This discrepancy
// in behavior is not currently handled.
"/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");`,
},
entryPaths: []string{
"/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",
},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderCSS,
".png": config.LoaderBase64,
},
},
expectedScanLog: `relative-paths/001/a/a.css: WARNING: Expected ";" but found end of file
relative-paths/002/a/a.css: WARNING: Expected ";" but found end of file
url-format/003/default/style.css: WARNING: Expected ")" to go with "("
url-format/003/default/style.css: NOTE: The unbalanced "(" is here:
url-format/003/relative-url/style.css: WARNING: Expected ")" to go with "("
url-format/003/relative-url/style.css: NOTE: The unbalanced "(" is here:
`,
})
}
func TestCSSAtImportConditionsAtLayerBundle(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/case1.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": `
@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": `
@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": `
@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": `
@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 } }`,
"/case6.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 } }`,
},
entryPaths: []string{
"/case1.css",
"/case2.css",
"/case3.css",
"/case4.css",
"/case5.css",
"/case6.css",
},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
})
}
func TestCSSAtImportConditionsAtLayerBundleAlternatingLayerInFile(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/a.css": `@layer first { body { color: red } }`,
"/b.css": `@layer last { body { color: green } }`,
"/case1.css": `
@import url(a.css);
@import url(a.css);
`,
"/case2.css": `
@import url(a.css);
@import url(b.css);
@import url(a.css);
`,
"/case3.css": `
@import url(a.css);
@import url(b.css);
@import url(a.css);
@import url(b.css);
`,
"/case4.css": `
@import url(a.css);
@import url(b.css);
@import url(a.css);
@import url(b.css);
@import url(a.css);
`,
"/case5.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);
`,
// Note: There was a bug that only showed up in this case. We need at least this many cases.
"/case6.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);
`,
},
entryPaths: []string{
"/case1.css",
"/case2.css",
"/case3.css",
"/case4.css",
"/case5.css",
"/case6.css",
},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
})
}
func TestCSSAtImportConditionsAtLayerBundleAlternatingLayerOnImport(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/a.css": `body { color: red }`,
"/b.css": `body { color: green }`,
"/case1.css": `
@import url(a.css) layer(first);
@import url(a.css) layer(first);
`,
"/case2.css": `
@import url(a.css) layer(first);
@import url(b.css) layer(last);
@import url(a.css) layer(first);
`,
"/case3.css": `
@import url(a.css) layer(first);
@import url(b.css) layer(last);
@import url(a.css) layer(first);
@import url(b.css) layer(last);
`,
"/case4.css": `
@import url(a.css) layer(first);
@import url(b.css) layer(last);
@import url(a.css) layer(first);
@import url(b.css) layer(last);
@import url(a.css) layer(first);
`,
"/case5.css": `
@import url(a.css) layer(first);
@import url(b.css) layer(last);
@import url(a.css) layer(first);
@import url(b.css) layer(last);
@import url(a.css) layer(first);
@import url(b.css) layer(last);
`,
// Note: There was a bug that only showed up in this case. We need at least this many cases.
"/case6.css": `
@import url(a.css) layer(first);
@import url(b.css) layer(last);
@import url(a.css) layer(first);
@import url(b.css) layer(last);
@import url(a.css) layer(first);
@import url(b.css) layer(last);
@import url(a.css) layer(first);
`,
},
entryPaths: []string{
"/case1.css",
"/case2.css",
"/case3.css",
"/case4.css",
"/case5.css",
"/case6.css",
},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
})
}
func TestCSSAtImportConditionsChainExternal(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@import "a.css" layer(a) not print;
`,
"/a.css": `
@import "http://example.com/external1.css";
@import "b.css" layer(b) not tv;
@import "http://example.com/external2.css" layer(a2);
`,
"/b.css": `
@import "http://example.com/external3.css";
@import "http://example.com/external4.css" layer(b2);
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
},
})
}
// This test mainly just makes sure that this scenario doesn't crash
func TestCSSAndJavaScriptCodeSplittingIssue1064(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/a.js": `
import shared from './shared.js'
console.log(shared() + 1)
`,
"/b.js": `
import shared from './shared.js'
console.log(shared() + 2)
`,
"/c.css": `
@import "./shared.css";
body { color: red }
`,
"/d.css": `
@import "./shared.css";
body { color: blue }
`,
"/shared.js": `
export default function() { return 3 }
`,
"/shared.css": `
body { background: black }
`,
},
entryPaths: []string{
"/a.js",
"/b.js",
"/c.css",
"/d.css",
},
options: config.Options{
Mode: config.ModeBundle,
OutputFormat: config.FormatESModule,
CodeSplitting: true,
AbsOutputDir: "/out",
},
})
}
func TestCSSExternalQueryAndHashNoMatchIssue1822(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
a { background: url(foo/bar.png?baz) }
b { background: url(foo/bar.png#baz) }
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
ExternalSettings: config.ExternalSettings{
PreResolve: config.ExternalMatchers{Patterns: []config.WildcardPattern{
{Suffix: ".png"},
}},
},
},
expectedScanLog: `entry.css: ERROR: Could not resolve "foo/bar.png?baz"
NOTE: You can mark the path "foo/bar.png?baz" as external to exclude it from the bundle, which will remove this error and leave the unresolved path in the bundle.
entry.css: ERROR: Could not resolve "foo/bar.png#baz"
NOTE: You can mark the path "foo/bar.png#baz" as external to exclude it from the bundle, which will remove this error and leave the unresolved path in the bundle.
`,
})
}
func TestCSSExternalQueryAndHashMatchIssue1822(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
a { background: url(foo/bar.png?baz) }
b { background: url(foo/bar.png#baz) }
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
ExternalSettings: config.ExternalSettings{
PreResolve: config.ExternalMatchers{Patterns: []config.WildcardPattern{
{Suffix: ".png?baz"},
{Suffix: ".png#baz"},
}},
},
},
})
}
func TestCSSNestingOldBrowser(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
// These are now the only two cases that warn about ":is" not being supported
"/two-type-selectors.css": `a { .c b& { color: red; } }`,
"/two-parent-selectors.css": `a b { .c & { color: red; } }`,
// Make sure this only generates one warning (even though it generates ":is" three times)
"/only-one-warning.css": `.a, .b .c, .d { & > & { color: red; } }`,
"/[email protected]": `a { @layer base { color: red; } }`,
"/[email protected]": `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; }`,
"/media-ampersand-twice.css": `@media screen { &, & { color: red; } }`,
"/media-ampersand-first.css": `@media screen { &, a { color: red; } }`,
"/media-ampersand-second.css": `@media screen { a, & { color: red; } }`,
"/media-attribute.css": `@media screen { [href] { color: red; } }`,
"/media-colon.css": `@media screen { :hover { color: red; } }`,
"/media-dot.css": `@media screen { .cls { color: red; } }`,
"/media-greaterthan.css": `@media screen { > b { color: red; } }`,
"/media-hash.css": `@media screen { #id { color: red; } }`,
"/media-plus.css": `@media screen { + b { color: red; } }`,
"/media-tilde.css": `@media screen { ~ b { color: red; } }`,
// See: https://github.com/evanw/esbuild/issues/3197
"/page-no-warning.css": `@page { @top-left { background: red } }`,
},
entryPaths: []string{
"/two-type-selectors.css",
"/two-parent-selectors.css",
"/only-one-warning.css",
"/[email protected]",
"/[email protected]",
"/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",
"/media-ampersand-twice.css",
"/media-ampersand-first.css",
"/media-ampersand-second.css",
"/media-attribute.css",
"/media-colon.css",
"/media-dot.css",
"/media-greaterthan.css",
"/media-hash.css",
"/media-plus.css",
"/media-tilde.css",
"/page-no-warning.css",
},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
UnsupportedCSSFeatures: compat.Nesting | compat.IsPseudoClass,
OriginalTargetEnv: "chrome10",
},
expectedScanLog: `only-one-warning.css: WARNING: Transforming this CSS nesting syntax is not supported in the configured target environment (chrome10)
NOTE: The nesting transform for this case must generate an ":is(...)" but the configured target environment does not support the ":is" pseudo-class.
two-parent-selectors.css: WARNING: Transforming this CSS nesting syntax is not supported in the configured target environment (chrome10)
NOTE: The nesting transform for this case must generate an ":is(...)" but the configured target environment does not support the ":is" pseudo-class.
two-type-selectors.css: WARNING: Transforming this CSS nesting syntax is not supported in the configured target environment (chrome10)
NOTE: The nesting transform for this case must generate an ":is(...)" but the configured target environment does not support the ":is" pseudo-class.
`,
})
}
// The mapping of JS entry point to associated CSS bundle isn't necessarily 1:1.
// Here is a case where it isn't. Two JS entry points share the same associated
// CSS bundle. This must be reflected in the metafile by only having the JS
// entry points point to the associated CSS bundle but not the other way around
// (since there isn't one JS entry point to point to). This test mainly exists
// to document this edge case.
func TestMetafileCSSBundleTwoToOne(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/foo/entry.js": `
import '../common.css'
console.log('foo')
`,
"/bar/entry.js": `
import '../common.css'
console.log('bar')
`,
"/common.css": `
body { color: red }
`,
},
entryPaths: []string{
"/foo/entry.js",
"/bar/entry.js",
},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
EntryPathTemplate: []config.PathTemplate{
// "[ext]/[hash]"
{Data: "./", Placeholder: config.ExtPlaceholder},
{Data: "/", Placeholder: config.HashPlaceholder},
},
NeedsMetafile: true,
},
})
}
func TestDeduplicateRules(t *testing.T) {
// These are done as bundler tests instead of parser tests because rule
// deduplication now happens during linking (so that it has effects across files)
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/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 } }",
"/yes3.css": "@media screen { a { color: red } } @media screen { & a { color: red } }",
"/no0.css": "@media screen { a { color: red } } @media screen { b 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); }",
},
entryPaths: []string{
"/yes0.css",
"/yes1.css",
"/yes2.css",
"/yes3.css",
"/no0.css",
"/no1.css",
"/no2.css",
"/no3.css",
"/no4.css",
"/no5.css",
"/no6.css",
"/across-files.css",
"/across-files-url.css",
},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
MinifySyntax: true,
},
})
}
func TestDeduplicateRulesGlobalVsLocalNames(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@import "a.css";
@import "b.css";
`,
"/a.css": `
a { color: red } /* SHOULD BE REMOVED */
b { color: green }
:global(.foo) { color: red } /* SHOULD BE REMOVED */
:global(.bar) { color: green }
:local(.foo) { color: red }
:local(.bar) { color: green }
div :global { animation-name: anim_global } /* SHOULD BE REMOVED */
div :local { animation-name: anim_local }
`,
"/b.css": `
a { color: red }
b { color: blue }
:global(.foo) { color: red }
:global(.bar) { color: blue }
:local(.foo) { color: red }
:local(.bar) { color: blue }
div :global { animation-name: anim_global }
div :local { animation-name: anim_local }
`,
},
entryPaths: []string{"entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
MinifySyntax: true,
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderLocalCSS,
},
},
})
}
// This test makes sure JS files that import local CSS names using the
// wrong name (e.g. a typo) get a warning so that the problem is noticed.
func TestUndefinedImportWarningCSS(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import * as empty_js from './empty.js'
import * as empty_esm_js from './empty.esm.js'
import * as empty_json from './empty.json'
import * as empty_css from './empty.css'
import * as empty_global_css from './empty.global-css'
import * as empty_local_css from './empty.local-css'
import * as pkg_empty_js from 'pkg/empty.js'
import * as pkg_empty_esm_js from 'pkg/empty.esm.js'
import * as pkg_empty_json from 'pkg/empty.json'
import * as pkg_empty_css from 'pkg/empty.css'
import * as pkg_empty_global_css from 'pkg/empty.global-css'
import * as pkg_empty_local_css from 'pkg/empty.local-css'
import 'pkg'
console.log(
empty_js.foo,
empty_esm_js.foo,
empty_json.foo,
empty_css.foo,
empty_global_css.foo,
empty_local_css.foo,
)
console.log(
pkg_empty_js.foo,
pkg_empty_esm_js.foo,
pkg_empty_json.foo,
pkg_empty_css.foo,
pkg_empty_global_css.foo,
pkg_empty_local_css.foo,
)
`,
"/empty.js": ``,
"/empty.esm.js": `export {}`,
"/empty.json": `{}`,
"/empty.css": ``,
"/empty.global-css": ``,
"/empty.local-css": ``,
"/node_modules/pkg/empty.js": ``,
"/node_modules/pkg/empty.esm.js": `export {}`,
"/node_modules/pkg/empty.json": `{}`,
"/node_modules/pkg/empty.css": ``,
"/node_modules/pkg/empty.global-css": ``,
"/node_modules/pkg/empty.local-css": ``,
// Files inside of "node_modules" should not generate a warning
"/node_modules/pkg/index.js": `
import * as empty_js from './empty.js'
import * as empty_esm_js from './empty.esm.js'
import * as empty_json from './empty.json'
import * as empty_css from './empty.css'
import * as empty_global_css from './empty.global-css'
import * as empty_local_css from './empty.local-css'
console.log(
empty_js.foo,
empty_esm_js.foo,
empty_json.foo,
empty_css.foo,
empty_global_css.foo,
empty_local_css.foo,
)
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".json": config.LoaderJSON,
".css": config.LoaderCSS,
".global-css": config.LoaderGlobalCSS,
".local-css": config.LoaderLocalCSS,
},
},
expectedCompileLog: `entry.js: WARNING: Import "foo" will always be undefined because the file "empty.js" has no exports
entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "empty.esm.js"
entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "empty.json"
entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "empty.css"
entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "empty.global-css"
entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "empty.local-css"
entry.js: WARNING: Import "foo" will always be undefined because the file "node_modules/pkg/empty.js" has no exports
entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "node_modules/pkg/empty.esm.js"
entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "node_modules/pkg/empty.json"
entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "node_modules/pkg/empty.css"
entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "node_modules/pkg/empty.global-css"
entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "node_modules/pkg/empty.local-css"
`,
})
}
func TestCSSMalformedAtImport(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@import "./url-token-eof.css";
@import "./url-token-whitespace-eof.css";
@import "./function-token-eof.css";
@import "./function-token-whitespace-eof.css";
`,
"/url-token-eof.css": `@import url(https://example.com/url-token-eof.css`,
"/url-token-whitespace-eof.css": `
@import url(https://example.com/url-token-whitespace-eof.css
`,
"/function-token-eof.css": `@import url("https://example.com/function-token-eof.css"`,
"/function-token-whitespace-eof.css": `
@import url("https://example.com/function-token-whitespace-eof.css"
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
expectedScanLog: `function-token-eof.css: WARNING: Expected ")" to go with "("
function-token-eof.css: NOTE: The unbalanced "(" is here:
function-token-whitespace-eof.css: WARNING: Expected ")" to go with "("
function-token-whitespace-eof.css: NOTE: The unbalanced "(" is here:
url-token-eof.css: WARNING: Expected ")" to end URL token
url-token-eof.css: NOTE: The unbalanced "(" is here:
url-token-eof.css: WARNING: Expected ";" but found end of file
url-token-whitespace-eof.css: WARNING: Expected ")" to end URL token
url-token-whitespace-eof.css: NOTE: The unbalanced "(" is here:
url-token-whitespace-eof.css: WARNING: Expected ";" but found end of file
`,
})
}
func TestCSSAtLayerBeforeImportNoBundle(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@layer layer1, layer2.layer3;
@import "a.css";
@import "b.css";
@layer layer6.layer7, layer8;
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputDir: "/out",
},
})
}
func TestCSSAtLayerBeforeImportBundle(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@layer layer1, layer2.layer3;
@import "a.css";
@import "b.css";
@layer layer6.layer7, layer8;
`,
"/a.css": `
@layer layer4 {
a { color: red }
}
`,
"/b.css": `
@layer layer5 {
b { color: red }
}
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
})
}
func TestCSSAtLayerMergingWithImportConditions(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@import "a.css" supports(color: first);
@import "a.css" supports(color: second);
@import "b.css" supports(color: second);
@import "a.css" supports(color: first);
@import "b.css" supports(color: first);
@import "a.css" supports(color: second);
@import "b.css" supports(color: second);
@import "b.css" supports(color: first);
`,
"/a.css": `
@layer a;
@import "http://example.com/a.css";
`,
"/b.css": `
@layer b;
@import "http://example.com/b.css";
`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
})
}
func TestCSSCaseInsensitivity(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
/* "@IMPORT" should be recognized as an import */
/* "LAYER(...)" should wrap with "@layer" */
/* "SUPPORTS(...)" should wrap with "@supports" */
@IMPORT Url("nested.css") LAYER(layer-name) SUPPORTS(supports-condition) list-of-media-queries;
`,
"/nested.css": `
/* "from" should be recognized and optimized to "0%" */
@KeyFrames Foo {
froM { OPAcity: 0 }
tO { opaCITY: 1 }
}
body {
/* "#FF0000" should be optimized to "red" because "BACKGROUND-color" should be recognized */
BACKGROUND-color: #FF0000;
/* This should be optimized to 50px */
width: CaLc(20Px + 30pX);
/* This URL token should be recognized and bundled */
background-IMAGE: Url(image.png);
}
`,
"/image.png": `...`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
MinifySyntax: true,
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderCSS,
".png": config.LoaderCopy,
},
},
})
}
func TestCSSAssetPathsWithSpacesBundle(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
a {
background: url(foo.copy);
background: url(foo.file);
}
/*! The URLs for "foo 2" files must have quotes in the final CSS */
b {
background: url('foo 2.copy');
background: url('foo 2.file');
}
`,
"/foo.file": `...`,
"/foo.copy": `...`,
"/foo 2.file": `...`,
"/foo 2.copy": `...`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
MinifySyntax: true,
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderCSS,
".file": config.LoaderFile,
".copy": config.LoaderCopy,
},
},
})
}