mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
2654 lines
82 KiB
Go
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,
|
|
},
|
|
},
|
|
})
|
|
}
|