github.com/evanw/esbuild@v0.21.4/internal/bundler_tests/bundler_css_test.go (about)

     1  package bundler_tests
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/evanw/esbuild/internal/compat"
     7  	"github.com/evanw/esbuild/internal/config"
     8  )
     9  
    10  var css_suite = suite{
    11  	name: "css",
    12  }
    13  
    14  func TestCSSEntryPoint(t *testing.T) {
    15  	css_suite.expectBundled(t, bundled{
    16  		files: map[string]string{
    17  			"/entry.css": `
    18  				body {
    19  					background: white;
    20  					color: black }
    21  			`,
    22  		},
    23  		entryPaths: []string{"/entry.css"},
    24  		options: config.Options{
    25  			Mode:          config.ModeBundle,
    26  			AbsOutputFile: "/out.css",
    27  		},
    28  	})
    29  }
    30  
    31  func TestCSSAtImportMissing(t *testing.T) {
    32  	css_suite.expectBundled(t, bundled{
    33  		files: map[string]string{
    34  			"/entry.css": `
    35  				@import "./missing.css";
    36  			`,
    37  		},
    38  		entryPaths: []string{"/entry.css"},
    39  		options: config.Options{
    40  			Mode:          config.ModeBundle,
    41  			AbsOutputFile: "/out.css",
    42  		},
    43  		expectedScanLog: `entry.css: ERROR: Could not resolve "./missing.css"
    44  `,
    45  	})
    46  }
    47  
    48  func TestCSSAtImportExternal(t *testing.T) {
    49  	css_suite.expectBundled(t, bundled{
    50  		files: map[string]string{
    51  			"/entry.css": `
    52  				@import "./internal.css";
    53  				@import "./external1.css";
    54  				@import "./external2.css";
    55  				@import "./charset1.css";
    56  				@import "./charset2.css";
    57  				@import "./external5.css" screen;
    58  			`,
    59  			"/internal.css": `
    60  				@import "./external5.css" print;
    61  				.before { color: red }
    62  			`,
    63  			"/charset1.css": `
    64  				@charset "UTF-8";
    65  				@import "./external3.css";
    66  				@import "./external4.css";
    67  				@import "./external5.css";
    68  				@import "https://www.example.com/style1.css";
    69  				@import "https://www.example.com/style2.css";
    70  				@import "https://www.example.com/style3.css" print;
    71  				.middle { color: green }
    72  			`,
    73  			"/charset2.css": `
    74  				@charset "UTF-8";
    75  				@import "./external3.css";
    76  				@import "./external5.css" screen;
    77  				@import "https://www.example.com/style1.css";
    78  				@import "https://www.example.com/style3.css";
    79  				.after { color: blue }
    80  			`,
    81  		},
    82  		entryPaths: []string{"/entry.css"},
    83  		options: config.Options{
    84  			Mode:         config.ModeBundle,
    85  			AbsOutputDir: "/out",
    86  			ExternalSettings: config.ExternalSettings{
    87  				PostResolve: config.ExternalMatchers{Exact: map[string]bool{
    88  					"/external1.css": true,
    89  					"/external2.css": true,
    90  					"/external3.css": true,
    91  					"/external4.css": true,
    92  					"/external5.css": true,
    93  				}},
    94  			},
    95  		},
    96  	})
    97  }
    98  
    99  func TestCSSAtImport(t *testing.T) {
   100  	css_suite.expectBundled(t, bundled{
   101  		files: map[string]string{
   102  			"/entry.css": `
   103  				@import "./a.css";
   104  				@import "./b.css";
   105  				.entry { color: red }
   106  			`,
   107  			"/a.css": `
   108  				@import "./shared.css";
   109  				.a { color: green }
   110  			`,
   111  			"/b.css": `
   112  				@import "./shared.css";
   113  				.b { color: blue }
   114  			`,
   115  			"/shared.css": `
   116  				.shared { color: black }
   117  			`,
   118  		},
   119  		entryPaths: []string{"/entry.css"},
   120  		options: config.Options{
   121  			Mode:          config.ModeBundle,
   122  			AbsOutputFile: "/out.css",
   123  		},
   124  	})
   125  }
   126  
   127  func TestCSSFromJSMissingImport(t *testing.T) {
   128  	css_suite.expectBundled(t, bundled{
   129  		files: map[string]string{
   130  			"/entry.js": `
   131  				import {missing} from "./a.css"
   132  				console.log(missing)
   133  			`,
   134  			"/a.css": `
   135  				.a { color: red }
   136  			`,
   137  		},
   138  		entryPaths: []string{"/entry.js"},
   139  		options: config.Options{
   140  			Mode:         config.ModeBundle,
   141  			AbsOutputDir: "/out",
   142  		},
   143  		expectedCompileLog: `entry.js: ERROR: No matching export in "a.css" for import "missing"
   144  `,
   145  	})
   146  }
   147  
   148  func TestCSSFromJSMissingStarImport(t *testing.T) {
   149  	css_suite.expectBundled(t, bundled{
   150  		files: map[string]string{
   151  			"/entry.js": `
   152  				import * as ns from "./a.css"
   153  				console.log(ns.missing)
   154  			`,
   155  			"/a.css": `
   156  				.a { color: red }
   157  			`,
   158  		},
   159  		entryPaths: []string{"/entry.js"},
   160  		options: config.Options{
   161  			Mode:         config.ModeBundle,
   162  			AbsOutputDir: "/out",
   163  		},
   164  		expectedCompileLog: `entry.js: WARNING: Import "missing" will always be undefined because there is no matching export in "a.css"
   165  `,
   166  	})
   167  }
   168  
   169  func TestImportGlobalCSSFromJS(t *testing.T) {
   170  	css_suite.expectBundled(t, bundled{
   171  		files: map[string]string{
   172  			"/entry.js": `
   173  				import "./a.js"
   174  				import "./b.js"
   175  			`,
   176  			"/a.js": `
   177  				import * as stylesA from "./a.css"
   178  				console.log('a', stylesA.a, stylesA.default.a)
   179  			`,
   180  			"/a.css": `
   181  				.a { color: red }
   182  			`,
   183  			"/b.js": `
   184  				import * as stylesB from "./b.css"
   185  				console.log('b', stylesB.b, stylesB.default.b)
   186  			`,
   187  			"/b.css": `
   188  				.b { color: blue }
   189  			`,
   190  		},
   191  		entryPaths: []string{"/entry.js"},
   192  		options: config.Options{
   193  			Mode:         config.ModeBundle,
   194  			AbsOutputDir: "/out",
   195  		},
   196  		expectedCompileLog: `a.js: WARNING: Import "a" will always be undefined because there is no matching export in "a.css"
   197  b.js: WARNING: Import "b" will always be undefined because there is no matching export in "b.css"
   198  `,
   199  	})
   200  }
   201  
   202  func TestImportLocalCSSFromJS(t *testing.T) {
   203  	css_suite.expectBundled(t, bundled{
   204  		files: map[string]string{
   205  			"/entry.js": `
   206  				import "./a.js"
   207  				import "./b.js"
   208  			`,
   209  			"/a.js": `
   210  				import * as stylesA from "./dir1/style.css"
   211  				console.log('file 1', stylesA.button, stylesA.default.a)
   212  			`,
   213  			"/dir1/style.css": `
   214  				.a { color: red }
   215  				.button { display: none }
   216  			`,
   217  			"/b.js": `
   218  				import * as stylesB from "./dir2/style.css"
   219  				console.log('file 2', stylesB.button, stylesB.default.b)
   220  			`,
   221  			"/dir2/style.css": `
   222  				.b { color: blue }
   223  				.button { display: none }
   224  			`,
   225  		},
   226  		entryPaths: []string{"/entry.js"},
   227  		options: config.Options{
   228  			Mode:         config.ModeBundle,
   229  			AbsOutputDir: "/out",
   230  			ExtensionToLoader: map[string]config.Loader{
   231  				".js":  config.LoaderJS,
   232  				".css": config.LoaderLocalCSS,
   233  			},
   234  		},
   235  	})
   236  }
   237  
   238  func TestImportLocalCSSFromJSMinifyIdentifiers(t *testing.T) {
   239  	css_suite.expectBundled(t, bundled{
   240  		files: map[string]string{
   241  			"/entry.js": `
   242  				import "./a.js"
   243  				import "./b.js"
   244  			`,
   245  			"/a.js": `
   246  				import * as stylesA from "./dir1/style.css"
   247  				console.log('file 1', stylesA.button, stylesA.default.a)
   248  			`,
   249  			"/dir1/style.css": `
   250  				.a { color: red }
   251  				.button { display: none }
   252  			`,
   253  			"/b.js": `
   254  				import * as stylesB from "./dir2/style.css"
   255  				console.log('file 2', stylesB.button, stylesB.default.b)
   256  			`,
   257  			"/dir2/style.css": `
   258  				.b { color: blue }
   259  				.button { display: none }
   260  			`,
   261  		},
   262  		entryPaths: []string{"/entry.js"},
   263  		options: config.Options{
   264  			Mode:         config.ModeBundle,
   265  			AbsOutputDir: "/out",
   266  			ExtensionToLoader: map[string]config.Loader{
   267  				".js":  config.LoaderJS,
   268  				".css": config.LoaderLocalCSS,
   269  			},
   270  			MinifyIdentifiers: true,
   271  		},
   272  	})
   273  }
   274  
   275  func TestImportLocalCSSFromJSMinifyIdentifiersAvoidGlobalNames(t *testing.T) {
   276  	css_suite.expectBundled(t, bundled{
   277  		files: map[string]string{
   278  			"/entry.js": `
   279  				import "./global.css"
   280  				import "./local.module.css"
   281  			`,
   282  			"/global.css": `
   283  				: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),
   284  				: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),
   285  				._ { color: red }
   286  			`,
   287  			"/local.module.css": `
   288  				.rename-this { color: blue }
   289  			`,
   290  		},
   291  		entryPaths: []string{"/entry.js"},
   292  		options: config.Options{
   293  			Mode:         config.ModeBundle,
   294  			AbsOutputDir: "/out",
   295  			ExtensionToLoader: map[string]config.Loader{
   296  				".js":         config.LoaderJS,
   297  				".css":        config.LoaderCSS,
   298  				".module.css": config.LoaderLocalCSS,
   299  			},
   300  			MinifyIdentifiers: true,
   301  		},
   302  	})
   303  }
   304  
   305  // See: https://github.com/evanw/esbuild/issues/3295
   306  func TestImportLocalCSSFromJSMinifyIdentifiersMultipleEntryPoints(t *testing.T) {
   307  	css_suite.expectBundled(t, bundled{
   308  		files: map[string]string{
   309  			"/a.js": `
   310  				import { foo, bar } from "./a.module.css";
   311  				console.log(foo, bar);
   312  			`,
   313  			"/a.module.css": `
   314  				.foo { color: #001; }
   315  				.bar { color: #002; }
   316  			`,
   317  			"/b.js": `
   318  				import { foo, bar } from "./b.module.css";
   319  				console.log(foo, bar);
   320  			`,
   321  			"/b.module.css": `
   322  				.foo { color: #003; }
   323  				.bar { color: #004; }
   324  			`,
   325  		},
   326  		entryPaths: []string{"/a.js", "/b.js"},
   327  		options: config.Options{
   328  			Mode:              config.ModeBundle,
   329  			AbsOutputDir:      "/out",
   330  			MinifyIdentifiers: true,
   331  		},
   332  	})
   333  }
   334  
   335  func TestImportCSSFromJSLocalVsGlobal(t *testing.T) {
   336  	css := `
   337  		.top_level { color: #000 }
   338  
   339  		:global(.GLOBAL) { color: #001 }
   340  		:local(.local) { color: #002 }
   341  
   342  		div:global(.GLOBAL) { color: #003 }
   343  		div:local(.local) { color: #004 }
   344  
   345  		.top_level:global(div) { color: #005 }
   346  		.top_level:local(div) { color: #006 }
   347  
   348  		:global(div.GLOBAL) { color: #007 }
   349  		:local(div.local) { color: #008 }
   350  
   351  		div:global(span.GLOBAL) { color: #009 }
   352  		div:local(span.local) { color: #00A }
   353  
   354  		div:global(#GLOBAL_A.GLOBAL_B.GLOBAL_C):local(.local_a.local_b#local_c) { color: #00B }
   355  		div:global(#GLOBAL_A .GLOBAL_B .GLOBAL_C):local(.local_a .local_b #local_c) { color: #00C }
   356  
   357  		.nested {
   358  			:global(&.GLOBAL) { color: #00D }
   359  			:local(&.local) { color: #00E }
   360  
   361  			&:global(.GLOBAL) { color: #00F }
   362  			&:local(.local) { color: #010 }
   363  		}
   364  
   365  		:global(.GLOBAL_A .GLOBAL_B) { color: #011 }
   366  		:local(.local_a .local_b) { color: #012 }
   367  
   368  		div:global(.GLOBAL_A .GLOBAL_B):hover { color: #013 }
   369  		div:local(.local_a .local_b):hover { color: #014 }
   370  
   371  		div :global(.GLOBAL_A .GLOBAL_B) span { color: #015 }
   372  		div :local(.local_a .local_b) span { color: #016 }
   373  
   374  		div > :global(.GLOBAL_A ~ .GLOBAL_B) + span { color: #017 }
   375  		div > :local(.local_a ~ .local_b) + span { color: #018 }
   376  
   377  		div:global(+ .GLOBAL_A):hover { color: #019 }
   378  		div:local(+ .local_a):hover { color: #01A }
   379  
   380  		:global.GLOBAL:local.local { color: #01B }
   381  		:global .GLOBAL :local .local { color: #01C }
   382  
   383  		:global {
   384  			.GLOBAL {
   385  				before: outer;
   386  				:local {
   387  					before: inner;
   388  					.local {
   389  						color: #01D;
   390  					}
   391  					after: inner;
   392  				}
   393  				after: outer;
   394  			}
   395  		}
   396  	`
   397  
   398  	css_suite.expectBundled(t, bundled{
   399  		files: map[string]string{
   400  			"/entry.js": `
   401  				import normalStyles from "./normal.css"
   402  				import globalStyles from "./LOCAL.global-css"
   403  				import localStyles from "./LOCAL.local-css"
   404  
   405  				console.log('should be empty:', normalStyles)
   406  				console.log('fewer local names:', globalStyles)
   407  				console.log('more local names:', localStyles)
   408  			`,
   409  			"/normal.css":       css,
   410  			"/LOCAL.global-css": css,
   411  			"/LOCAL.local-css":  css,
   412  		},
   413  		entryPaths: []string{"/entry.js"},
   414  		options: config.Options{
   415  			Mode:         config.ModeBundle,
   416  			AbsOutputDir: "/out",
   417  			ExtensionToLoader: map[string]config.Loader{
   418  				".js":         config.LoaderJS,
   419  				".css":        config.LoaderCSS,
   420  				".global-css": config.LoaderGlobalCSS,
   421  				".local-css":  config.LoaderLocalCSS,
   422  			},
   423  		},
   424  	})
   425  }
   426  
   427  func TestImportCSSFromJSLowerBareLocalAndGlobal(t *testing.T) {
   428  	css_suite.expectBundled(t, bundled{
   429  		files: map[string]string{
   430  			"/entry.js": `
   431  				import styles from "./styles.css"
   432  				console.log(styles)
   433  			`,
   434  			"/styles.css": `
   435  				.before { color: #000 }
   436  				:local { .button { color: #000 } }
   437  				.after { color: #000 }
   438  
   439  				.before { color: #001 }
   440  				:global { .button { color: #001 } }
   441  				.after { color: #001 }
   442  
   443  				div { :local { .button { color: #002 } } }
   444  				div { :global { .button { color: #003 } } }
   445  
   446  				:local(:global) { color: #004 }
   447  				:global(:local) { color: #005 }
   448  
   449  				:local(:global) { .button { color: #006 } }
   450  				:global(:local) { .button { color: #007 } }
   451  			`,
   452  		},
   453  		entryPaths: []string{"/entry.js"},
   454  		options: config.Options{
   455  			Mode:         config.ModeBundle,
   456  			AbsOutputDir: "/out",
   457  			ExtensionToLoader: map[string]config.Loader{
   458  				".js":  config.LoaderJS,
   459  				".css": config.LoaderLocalCSS,
   460  			},
   461  			UnsupportedCSSFeatures: compat.Nesting,
   462  		},
   463  	})
   464  }
   465  
   466  func TestImportCSSFromJSLocalAtKeyframes(t *testing.T) {
   467  	css_suite.expectBundled(t, bundled{
   468  		files: map[string]string{
   469  			"/entry.js": `
   470  				import styles from "./styles.css"
   471  				console.log(styles)
   472  			`,
   473  			"/styles.css": `
   474  				@keyframes local_name { to { color: red } }
   475  
   476  				div :global { animation-name: none }
   477  				div :local { animation-name: none }
   478  
   479  				div :global { animation-name: global_name }
   480  				div :local { animation-name: local_name }
   481  
   482  				div :global { animation-name: global_name1, none, global_name2, Inherit, INITIAL, revert, revert-layer, unset }
   483  				div :local { animation-name: local_name1, none, local_name2, Inherit, INITIAL, revert, revert-layer, unset }
   484  
   485  				div :global { animation: 2s infinite global_name }
   486  				div :local { animation: 2s infinite local_name }
   487  
   488  				/* Someone wanted to be able to name their animations "none" */
   489  				@keyframes "none" { to { color: red } }
   490  				div :global { animation-name: "none" }
   491  				div :local { animation-name: "none" }
   492  			`,
   493  		},
   494  		entryPaths: []string{"/entry.js"},
   495  		options: config.Options{
   496  			Mode:         config.ModeBundle,
   497  			AbsOutputDir: "/out",
   498  			ExtensionToLoader: map[string]config.Loader{
   499  				".js":  config.LoaderJS,
   500  				".css": config.LoaderLocalCSS,
   501  			},
   502  			UnsupportedCSSFeatures: compat.Nesting,
   503  		},
   504  	})
   505  }
   506  
   507  func TestImportCSSFromJSLocalAtCounterStyle(t *testing.T) {
   508  	css_suite.expectBundled(t, bundled{
   509  		files: map[string]string{
   510  			"/entry.js": `
   511  				import list_style_type from "./list_style_type.css"
   512  				import list_style from "./list_style.css"
   513  				console.log(list_style_type, list_style)
   514  			`,
   515  			"/list_style_type.css": `
   516  				@counter-style local { symbols: A B C }
   517  
   518  				div :global { list-style-type: GLOBAL }
   519  				div :local { list-style-type: local }
   520  
   521  				/* Must not accept invalid type values */
   522  				div :local { list-style-type: none }
   523  				div :local { list-style-type: INITIAL }
   524  				div :local { list-style-type: decimal }
   525  				div :local { list-style-type: disc }
   526  				div :local { list-style-type: SQUARE }
   527  				div :local { list-style-type: circle }
   528  				div :local { list-style-type: disclosure-OPEN }
   529  				div :local { list-style-type: DISCLOSURE-closed }
   530  				div :local { list-style-type: LAO }
   531  				div :local { list-style-type: "\1F44D" }
   532  			`,
   533  
   534  			"/list_style.css": `
   535  				@counter-style local { symbols: A B C }
   536  
   537  				div :global { list-style: GLOBAL }
   538  				div :local { list-style: local }
   539  
   540  				/* The first one is the type */
   541  				div :local { list-style: local none }
   542  				div :local { list-style: local url(http://) }
   543  				div :local { list-style: local linear-gradient(red, green) }
   544  				div :local { list-style: local inside }
   545  				div :local { list-style: local outside }
   546  
   547  				/* The second one is the type */
   548  				div :local { list-style: none local }
   549  				div :local { list-style: url(http://) local }
   550  				div :local { list-style: linear-gradient(red, green) local }
   551  				div :local { list-style: local inside }
   552  				div :local { list-style: local outside }
   553  				div :local { list-style: inside inside }
   554  				div :local { list-style: inside outside }
   555  				div :local { list-style: outside inside }
   556  				div :local { list-style: outside outside }
   557  
   558  				/* The type is set to "none" here */
   559  				div :local { list-style: url(http://) none invalid }
   560  				div :local { list-style: linear-gradient(red, green) none invalid }
   561  
   562  				/* Must not accept invalid type values */
   563  				div :local { list-style: INITIAL }
   564  				div :local { list-style: decimal }
   565  				div :local { list-style: disc }
   566  				div :local { list-style: SQUARE }
   567  				div :local { list-style: circle }
   568  				div :local { list-style: disclosure-OPEN }
   569  				div :local { list-style: DISCLOSURE-closed }
   570  				div :local { list-style: LAO }
   571  			`,
   572  		},
   573  		entryPaths: []string{"/entry.js"},
   574  		options: config.Options{
   575  			Mode:         config.ModeBundle,
   576  			AbsOutputDir: "/out",
   577  			ExtensionToLoader: map[string]config.Loader{
   578  				".js":  config.LoaderJS,
   579  				".css": config.LoaderLocalCSS,
   580  			},
   581  			UnsupportedCSSFeatures: compat.Nesting,
   582  		},
   583  	})
   584  }
   585  
   586  func TestImportCSSFromJSLocalAtContainer(t *testing.T) {
   587  	css_suite.expectBundled(t, bundled{
   588  		files: map[string]string{
   589  			"/entry.js": `
   590  				import styles from "./styles.css"
   591  				console.log(styles)
   592  			`,
   593  			"/styles.css": `
   594  				@container not (max-width: 100px) { div { color: red } }
   595  				@container local (max-width: 100px) { div { color: red } }
   596  				@container local not (max-width: 100px) { div { color: red } }
   597  				@container local (max-width: 100px) or (min-height: 100px) { div { color: red } }
   598  				@container local (max-width: 100px) and (min-height: 100px) { div { color: red } }
   599  				@container general_enclosed(max-width: 100px) { div { color: red } }
   600  				@container local general_enclosed(max-width: 100px) { div { color: red } }
   601  
   602  				div :global { container-name: NONE initial }
   603  				div :local { container-name: none INITIAL }
   604  				div :global { container-name: GLOBAL1 GLOBAL2 }
   605  				div :local { container-name: local1 local2 }
   606  
   607  				div :global { container: none }
   608  				div :local { container: NONE }
   609  				div :global { container: NONE / size }
   610  				div :local { container: none / size }
   611  
   612  				div :global { container: GLOBAL1 GLOBAL2 }
   613  				div :local { container: local1 local2 }
   614  				div :global { container: GLOBAL1 GLOBAL2 / size }
   615  				div :local { container: local1 local2 / size }
   616  			`,
   617  		},
   618  		entryPaths: []string{"/entry.js"},
   619  		options: config.Options{
   620  			Mode:         config.ModeBundle,
   621  			AbsOutputDir: "/out",
   622  			ExtensionToLoader: map[string]config.Loader{
   623  				".js":  config.LoaderJS,
   624  				".css": config.LoaderLocalCSS,
   625  			},
   626  			UnsupportedCSSFeatures: compat.Nesting,
   627  		},
   628  	})
   629  }
   630  
   631  func TestImportCSSFromJSNthIndexLocal(t *testing.T) {
   632  	css_suite.expectBundled(t, bundled{
   633  		files: map[string]string{
   634  			"/entry.js": `
   635  				import styles from "./styles.css"
   636  				console.log(styles)
   637  			`,
   638  			"/styles.css": `
   639  				:nth-child(2n of .local) { color: #000 }
   640  				:nth-child(2n of :local(#local), :global(.GLOBAL)) { color: #001 }
   641  				:nth-child(2n of .local1 :global .GLOBAL1, .GLOBAL2 :local .local2) { color: #002 }
   642  				.local1, :nth-child(2n of :global .GLOBAL), .local2 { color: #003 }
   643  
   644  				:nth-last-child(2n of .local) { color: #000 }
   645  				:nth-last-child(2n of :local(#local), :global(.GLOBAL)) { color: #001 }
   646  				:nth-last-child(2n of .local1 :global .GLOBAL1, .GLOBAL2 :local .local2) { color: #002 }
   647  				.local1, :nth-last-child(2n of :global .GLOBAL), .local2 { color: #003 }
   648  			`,
   649  		},
   650  		entryPaths: []string{"/entry.js"},
   651  		options: config.Options{
   652  			Mode:         config.ModeBundle,
   653  			AbsOutputDir: "/out",
   654  			ExtensionToLoader: map[string]config.Loader{
   655  				".js":  config.LoaderJS,
   656  				".css": config.LoaderLocalCSS,
   657  			},
   658  			UnsupportedCSSFeatures: compat.Nesting,
   659  		},
   660  	})
   661  }
   662  
   663  func TestImportCSSFromJSComposes(t *testing.T) {
   664  	css_suite.expectBundled(t, bundled{
   665  		files: map[string]string{
   666  			"/entry.js": `
   667  				import styles from "./styles.module.css"
   668  				console.log(styles)
   669  			`,
   670  			"/global.css": `
   671  				.GLOBAL1 {
   672  					color: black;
   673  				}
   674  			`,
   675  			"/styles.module.css": `
   676  				@import "global.css";
   677  				.local0 {
   678  					composes: local1;
   679  					:global {
   680  						composes: GLOBAL1 GLOBAL2;
   681  					}
   682  				}
   683  				.local0 {
   684  					composes: GLOBAL2 GLOBAL3 from global;
   685  					composes: local1 local2;
   686  					background: green;
   687  				}
   688  				.local0 :global {
   689  					composes: GLOBAL4;
   690  				}
   691  				.local3 {
   692  					border: 1px solid black;
   693  					composes: local4;
   694  				}
   695  				.local4 {
   696  					opacity: 0.5;
   697  				}
   698  				.local1 {
   699  					color: red;
   700  					composes: local3;
   701  				}
   702  				.fromOtherFile {
   703  					composes: local0 from "other1.module.css";
   704  					composes: local0 from "other2.module.css";
   705  				}
   706  			`,
   707  			"/other1.module.css": `
   708  				.local0 {
   709  					composes: base1 base2 from "base.module.css";
   710  					color: blue;
   711  				}
   712  			`,
   713  			"/other2.module.css": `
   714  				.local0 {
   715  					composes: base1 base3 from "base.module.css";
   716  					background: purple;
   717  				}
   718  			`,
   719  			"/base.module.css": `
   720  				.base1 {
   721  					cursor: pointer;
   722  				}
   723  				.base2 {
   724  					display: inline;
   725  				}
   726  				.base3 {
   727  					float: left;
   728  				}
   729  			`,
   730  		},
   731  		entryPaths: []string{"/entry.js"},
   732  		options: config.Options{
   733  			Mode:         config.ModeBundle,
   734  			AbsOutputDir: "/out",
   735  			ExtensionToLoader: map[string]config.Loader{
   736  				".js":         config.LoaderJS,
   737  				".css":        config.LoaderCSS,
   738  				".module.css": config.LoaderLocalCSS,
   739  			},
   740  		},
   741  	})
   742  }
   743  
   744  func TestImportCSSFromJSComposesFromMissingImport(t *testing.T) {
   745  	css_suite.expectBundled(t, bundled{
   746  		files: map[string]string{
   747  			"/entry.js": `
   748  				import styles from "./styles.module.css"
   749  				console.log(styles)
   750  			`,
   751  			"/styles.module.css": `
   752  				.foo {
   753  					composes: x from "file.module.css";
   754  					composes: y from "file.module.css";
   755  					composes: z from "file.module.css";
   756  					composes: x from "file.css";
   757  				}
   758  			`,
   759  			"/file.module.css": `
   760  				.x {
   761  					color: red;
   762  				}
   763  				:global(.y) {
   764  					color: blue;
   765  				}
   766  			`,
   767  			"/file.css": `
   768  				.x {
   769  					color: red;
   770  				}
   771  			`,
   772  		},
   773  		entryPaths: []string{"/entry.js"},
   774  		options: config.Options{
   775  			Mode:         config.ModeBundle,
   776  			AbsOutputDir: "/out",
   777  			ExtensionToLoader: map[string]config.Loader{
   778  				".js":         config.LoaderJS,
   779  				".module.css": config.LoaderLocalCSS,
   780  				".css":        config.LoaderCSS,
   781  			},
   782  		},
   783  		expectedCompileLog: `styles.module.css: ERROR: Cannot use global name "y" with "composes"
   784  file.module.css: NOTE: The global name "y" is defined here:
   785  NOTE: Use the ":local" selector to change "y" into a local name.
   786  styles.module.css: ERROR: The name "z" never appears in "file.module.css"
   787  styles.module.css: ERROR: Cannot use global name "x" with "composes"
   788  file.css: NOTE: The global name "x" is defined here:
   789  NOTE: Use the "local-css" loader for "file.css" to enable local names.
   790  `,
   791  	})
   792  }
   793  
   794  func TestImportCSSFromJSComposesFromNotCSS(t *testing.T) {
   795  	css_suite.expectBundled(t, bundled{
   796  		files: map[string]string{
   797  			"/entry.js": `
   798  				import styles from "./styles.css"
   799  				console.log(styles)
   800  			`,
   801  			"/styles.css": `
   802  				.foo {
   803  					composes: bar from "file.txt";
   804  				}
   805  			`,
   806  			"/file.txt": `
   807  				.bar {
   808  					color: red;
   809  				}
   810  			`,
   811  		},
   812  		entryPaths: []string{"/entry.js"},
   813  		options: config.Options{
   814  			Mode:         config.ModeBundle,
   815  			AbsOutputDir: "/out",
   816  			ExtensionToLoader: map[string]config.Loader{
   817  				".js":  config.LoaderJS,
   818  				".css": config.LoaderLocalCSS,
   819  				".txt": config.LoaderText,
   820  			},
   821  		},
   822  		expectedScanLog: `styles.css: ERROR: Cannot use "composes" with "file.txt"
   823  NOTE: You can only use "composes" with CSS files and "file.txt" is not a CSS file (it was loaded with the "text" loader).
   824  `,
   825  	})
   826  }
   827  
   828  func TestImportCSSFromJSComposesCircular(t *testing.T) {
   829  	css_suite.expectBundled(t, bundled{
   830  		files: map[string]string{
   831  			"/entry.js": `
   832  				import styles from "./styles.css"
   833  				console.log(styles)
   834  			`,
   835  			"/styles.css": `
   836  				.foo {
   837  					composes: bar;
   838  				}
   839  				.bar {
   840  					composes: foo;
   841  				}
   842  				.baz {
   843  					composes: baz;
   844  				}
   845  			`,
   846  		},
   847  		entryPaths: []string{"/entry.js"},
   848  		options: config.Options{
   849  			Mode:         config.ModeBundle,
   850  			AbsOutputDir: "/out",
   851  			ExtensionToLoader: map[string]config.Loader{
   852  				".js":  config.LoaderJS,
   853  				".css": config.LoaderLocalCSS,
   854  			},
   855  		},
   856  	})
   857  }
   858  
   859  func TestImportCSSFromJSComposesFromCircular(t *testing.T) {
   860  	css_suite.expectBundled(t, bundled{
   861  		files: map[string]string{
   862  			"/entry.js": `
   863  				import styles from "./styles.css"
   864  				console.log(styles)
   865  			`,
   866  			"/styles.css": `
   867  				.foo {
   868  					composes: bar from "other.css";
   869  				}
   870  				.bar {
   871  					composes: bar from "styles.css";
   872  				}
   873  			`,
   874  			"/other.css": `
   875  				.bar {
   876  					composes: foo from "styles.css";
   877  				}
   878  			`,
   879  		},
   880  		entryPaths: []string{"/entry.js"},
   881  		options: config.Options{
   882  			Mode:         config.ModeBundle,
   883  			AbsOutputDir: "/out",
   884  			ExtensionToLoader: map[string]config.Loader{
   885  				".js":  config.LoaderJS,
   886  				".css": config.LoaderLocalCSS,
   887  			},
   888  		},
   889  	})
   890  }
   891  
   892  func TestImportCSSFromJSComposesFromUndefined(t *testing.T) {
   893  	note := "NOTE: The specification of \"composes\" does not define an order when class declarations from separate files are composed together. " +
   894  		"The value of the \"zoom\" property for \"foo\" may change unpredictably as the code is edited. " +
   895  		"Make sure that all definitions of \"zoom\" for \"foo\" are in a single file."
   896  	css_suite.expectBundled(t, bundled{
   897  		files: map[string]string{
   898  			"/entry.js": `
   899  				import styles from "./styles.css"
   900  				console.log(styles)
   901  			`,
   902  			"/styles.css": `
   903  				@import "well-defined.css";
   904  				@import "undefined/case1.css";
   905  				@import "undefined/case2.css";
   906  				@import "undefined/case3.css";
   907  				@import "undefined/case4.css";
   908  				@import "undefined/case5.css";
   909  			`,
   910  			"/well-defined.css": `
   911  				.z1 { composes: z2; zoom: 1; }
   912  				.z2 { zoom: 2; }
   913  
   914  				.z4 { zoom: 4; }
   915  				.z3 { composes: z4; zoom: 3; }
   916  
   917  				.z5 { composes: foo bar from "file-1.css"; }
   918  			`,
   919  			"/undefined/case1.css": `
   920  				.foo {
   921  					composes: foo from "../file-1.css";
   922  					zoom: 2;
   923  				}
   924  			`,
   925  			"/undefined/case2.css": `
   926  				.foo {
   927  					composes: foo from "../file-1.css";
   928  					composes: foo from "../file-2.css";
   929  				}
   930  			`,
   931  			"/undefined/case3.css": `
   932  				.foo { composes: nested1 nested2; }
   933  				.nested1 { zoom: 3; }
   934  				.nested2 { composes: foo from "../file-2.css"; }
   935  			`,
   936  			"/undefined/case4.css": `
   937  				.foo { composes: nested1 nested2; }
   938  				.nested1 { composes: foo from "../file-1.css"; }
   939  				.nested2 { zoom: 3; }
   940  			`,
   941  			"/undefined/case5.css": `
   942  				.foo { composes: nested1 nested2; }
   943  				.nested1 { composes: foo from "../file-1.css"; }
   944  				.nested2 { composes: foo from "../file-2.css"; }
   945  			`,
   946  			"/file-1.css": `
   947  				.foo { zoom: 1; }
   948  				.bar { zoom: 2; }
   949  			`,
   950  			"/file-2.css": `
   951  				.foo { zoom: 2; }
   952  			`,
   953  		},
   954  		entryPaths: []string{"/entry.js"},
   955  		options: config.Options{
   956  			Mode:         config.ModeBundle,
   957  			AbsOutputDir: "/out",
   958  			ExtensionToLoader: map[string]config.Loader{
   959  				".js":  config.LoaderJS,
   960  				".css": config.LoaderLocalCSS,
   961  			},
   962  		},
   963  		expectedCompileLog: `undefined/case1.css: WARNING: The value of "zoom" in the "foo" class is undefined
   964  file-1.css: NOTE: The first definition of "zoom" is here:
   965  undefined/case1.css: NOTE: The second definition of "zoom" is here:
   966  ` + note + `
   967  undefined/case2.css: WARNING: The value of "zoom" in the "foo" class is undefined
   968  file-1.css: NOTE: The first definition of "zoom" is here:
   969  file-2.css: NOTE: The second definition of "zoom" is here:
   970  ` + note + `
   971  undefined/case3.css: WARNING: The value of "zoom" in the "foo" class is undefined
   972  undefined/case3.css: NOTE: The first definition of "zoom" is here:
   973  file-2.css: NOTE: The second definition of "zoom" is here:
   974  ` + note + `
   975  undefined/case4.css: WARNING: The value of "zoom" in the "foo" class is undefined
   976  file-1.css: NOTE: The first definition of "zoom" is here:
   977  undefined/case4.css: NOTE: The second definition of "zoom" is here:
   978  ` + note + `
   979  undefined/case5.css: WARNING: The value of "zoom" in the "foo" class is undefined
   980  file-1.css: NOTE: The first definition of "zoom" is here:
   981  file-2.css: NOTE: The second definition of "zoom" is here:
   982  ` + note + `
   983  `,
   984  	})
   985  }
   986  
   987  func TestImportCSSFromJSWriteToStdout(t *testing.T) {
   988  	css_suite.expectBundled(t, bundled{
   989  		files: map[string]string{
   990  			"/entry.js": `
   991  				import "./entry.css"
   992  			`,
   993  			"/entry.css": `
   994  				.entry { color: red }
   995  			`,
   996  		},
   997  		entryPaths: []string{"/entry.js"},
   998  		options: config.Options{
   999  			Mode:          config.ModeBundle,
  1000  			WriteToStdout: true,
  1001  		},
  1002  		expectedScanLog: `entry.js: ERROR: Cannot import "entry.css" into a JavaScript file without an output path configured
  1003  `,
  1004  	})
  1005  }
  1006  
  1007  func TestImportJSFromCSS(t *testing.T) {
  1008  	css_suite.expectBundled(t, bundled{
  1009  		files: map[string]string{
  1010  			"/entry.js": `
  1011  				export default 123
  1012  			`,
  1013  			"/entry.css": `
  1014  				@import "./entry.js";
  1015  			`,
  1016  		},
  1017  		entryPaths: []string{"/entry.css"},
  1018  		options: config.Options{
  1019  			Mode:         config.ModeBundle,
  1020  			AbsOutputDir: "/out",
  1021  		},
  1022  		expectedScanLog: `entry.css: ERROR: Cannot import "entry.js" into a CSS file
  1023  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).
  1024  `,
  1025  	})
  1026  }
  1027  
  1028  func TestImportJSONFromCSS(t *testing.T) {
  1029  	css_suite.expectBundled(t, bundled{
  1030  		files: map[string]string{
  1031  			"/entry.json": `
  1032  				{}
  1033  			`,
  1034  			"/entry.css": `
  1035  				@import "./entry.json";
  1036  			`,
  1037  		},
  1038  		entryPaths: []string{"/entry.css"},
  1039  		options: config.Options{
  1040  			Mode:         config.ModeBundle,
  1041  			AbsOutputDir: "/out",
  1042  		},
  1043  		expectedScanLog: `entry.css: ERROR: Cannot import "entry.json" into a CSS file
  1044  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).
  1045  `,
  1046  	})
  1047  }
  1048  
  1049  func TestMissingImportURLInCSS(t *testing.T) {
  1050  	css_suite.expectBundled(t, bundled{
  1051  		files: map[string]string{
  1052  			"/src/entry.css": `
  1053  				a { background: url(./one.png); }
  1054  				b { background: url("./two.png"); }
  1055  			`,
  1056  		},
  1057  		entryPaths: []string{"/src/entry.css"},
  1058  		options: config.Options{
  1059  			Mode:         config.ModeBundle,
  1060  			AbsOutputDir: "/out",
  1061  		},
  1062  		expectedScanLog: `src/entry.css: ERROR: Could not resolve "./one.png"
  1063  src/entry.css: ERROR: Could not resolve "./two.png"
  1064  `,
  1065  	})
  1066  }
  1067  
  1068  func TestExternalImportURLInCSS(t *testing.T) {
  1069  	css_suite.expectBundled(t, bundled{
  1070  		files: map[string]string{
  1071  			"/src/entry.css": `
  1072  				div:after {
  1073  					content: 'If this is recognized, the path should become "../src/external.png"';
  1074  					background: url(./external.png);
  1075  				}
  1076  
  1077  				/* These URLs should be external automatically */
  1078  				a { background: url(http://example.com/images/image.png) }
  1079  				b { background: url(https://example.com/images/image.png) }
  1080  				c { background: url(//example.com/images/image.png) }
  1081  				d { background: url(data:image/png;base64,iVBORw0KGgo=) }
  1082  				path { fill: url(#filter) }
  1083  			`,
  1084  		},
  1085  		entryPaths: []string{"/src/entry.css"},
  1086  		options: config.Options{
  1087  			Mode:         config.ModeBundle,
  1088  			AbsOutputDir: "/out",
  1089  			ExternalSettings: config.ExternalSettings{
  1090  				PostResolve: config.ExternalMatchers{Exact: map[string]bool{
  1091  					"/src/external.png": true,
  1092  				}},
  1093  			},
  1094  		},
  1095  	})
  1096  }
  1097  
  1098  func TestInvalidImportURLInCSS(t *testing.T) {
  1099  	css_suite.expectBundled(t, bundled{
  1100  		files: map[string]string{
  1101  			"/entry.css": `
  1102  				a {
  1103  					background: url(./js.js);
  1104  					background: url("./jsx.jsx");
  1105  					background: url(./ts.ts);
  1106  					background: url('./tsx.tsx');
  1107  					background: url(./json.json);
  1108  					background: url(./css.css);
  1109  				}
  1110  			`,
  1111  			"/js.js":     `export default 123`,
  1112  			"/jsx.jsx":   `export default 123`,
  1113  			"/ts.ts":     `export default 123`,
  1114  			"/tsx.tsx":   `export default 123`,
  1115  			"/json.json": `{ "test": true }`,
  1116  			"/css.css":   `a { color: red }`,
  1117  		},
  1118  		entryPaths: []string{"/entry.css"},
  1119  		options: config.Options{
  1120  			Mode:         config.ModeBundle,
  1121  			AbsOutputDir: "/out",
  1122  		},
  1123  		expectedScanLog: `entry.css: ERROR: Cannot use "js.js" as a URL
  1124  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.
  1125  entry.css: ERROR: Cannot use "jsx.jsx" as a URL
  1126  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.
  1127  entry.css: ERROR: Cannot use "ts.ts" as a URL
  1128  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.
  1129  entry.css: ERROR: Cannot use "tsx.tsx" as a URL
  1130  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.
  1131  entry.css: ERROR: Cannot use "json.json" as a URL
  1132  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.
  1133  entry.css: ERROR: Cannot use "css.css" as a URL
  1134  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).
  1135  `,
  1136  	})
  1137  }
  1138  
  1139  func TestTextImportURLInCSSText(t *testing.T) {
  1140  	css_suite.expectBundled(t, bundled{
  1141  		files: map[string]string{
  1142  			"/entry.css": `
  1143  				a {
  1144  					background: url(./example.txt);
  1145  				}
  1146  			`,
  1147  			"/example.txt": `This is some text.`,
  1148  		},
  1149  		entryPaths: []string{"/entry.css"},
  1150  		options: config.Options{
  1151  			Mode:         config.ModeBundle,
  1152  			AbsOutputDir: "/out",
  1153  		},
  1154  	})
  1155  }
  1156  
  1157  func TestDataURLImportURLInCSS(t *testing.T) {
  1158  	css_suite.expectBundled(t, bundled{
  1159  		files: map[string]string{
  1160  			"/entry.css": `
  1161  				a {
  1162  					background: url(./example.png);
  1163  				}
  1164  			`,
  1165  			"/example.png": "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A",
  1166  		},
  1167  		entryPaths: []string{"/entry.css"},
  1168  		options: config.Options{
  1169  			Mode:         config.ModeBundle,
  1170  			AbsOutputDir: "/out",
  1171  			ExtensionToLoader: map[string]config.Loader{
  1172  				".css": config.LoaderCSS,
  1173  				".png": config.LoaderDataURL,
  1174  			},
  1175  		},
  1176  	})
  1177  }
  1178  
  1179  func TestBinaryImportURLInCSS(t *testing.T) {
  1180  	css_suite.expectBundled(t, bundled{
  1181  		files: map[string]string{
  1182  			"/entry.css": `
  1183  				a {
  1184  					background: url(./example.png);
  1185  				}
  1186  			`,
  1187  			"/example.png": "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A",
  1188  		},
  1189  		entryPaths: []string{"/entry.css"},
  1190  		options: config.Options{
  1191  			Mode:         config.ModeBundle,
  1192  			AbsOutputDir: "/out",
  1193  			ExtensionToLoader: map[string]config.Loader{
  1194  				".css": config.LoaderCSS,
  1195  				".png": config.LoaderBinary,
  1196  			},
  1197  		},
  1198  	})
  1199  }
  1200  
  1201  func TestBase64ImportURLInCSS(t *testing.T) {
  1202  	css_suite.expectBundled(t, bundled{
  1203  		files: map[string]string{
  1204  			"/entry.css": `
  1205  				a {
  1206  					background: url(./example.png);
  1207  				}
  1208  			`,
  1209  			"/example.png": "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A",
  1210  		},
  1211  		entryPaths: []string{"/entry.css"},
  1212  		options: config.Options{
  1213  			Mode:         config.ModeBundle,
  1214  			AbsOutputDir: "/out",
  1215  			ExtensionToLoader: map[string]config.Loader{
  1216  				".css": config.LoaderCSS,
  1217  				".png": config.LoaderBase64,
  1218  			},
  1219  		},
  1220  	})
  1221  }
  1222  
  1223  func TestFileImportURLInCSS(t *testing.T) {
  1224  	css_suite.expectBundled(t, bundled{
  1225  		files: map[string]string{
  1226  			"/entry.css": `
  1227  				@import "./one.css";
  1228  				@import "./two.css";
  1229  			`,
  1230  			"/one.css": `
  1231  				a { background: url(./example.data) }
  1232  			`,
  1233  			"/two.css": `
  1234  				b { background: url(./example.data) }
  1235  			`,
  1236  			"/example.data": "This is some data.",
  1237  		},
  1238  		entryPaths: []string{"/entry.css"},
  1239  		options: config.Options{
  1240  			Mode:         config.ModeBundle,
  1241  			AbsOutputDir: "/out",
  1242  			ExtensionToLoader: map[string]config.Loader{
  1243  				".css":  config.LoaderCSS,
  1244  				".data": config.LoaderFile,
  1245  			},
  1246  		},
  1247  	})
  1248  }
  1249  
  1250  func TestIgnoreURLsInAtRulePrelude(t *testing.T) {
  1251  	css_suite.expectBundled(t, bundled{
  1252  		files: map[string]string{
  1253  			"/entry.css": `
  1254  				/* This should not generate a path resolution error */
  1255  				@supports (background: url(ignored.png)) {
  1256  					a { color: red }
  1257  				}
  1258  			`,
  1259  		},
  1260  		entryPaths: []string{"/entry.css"},
  1261  		options: config.Options{
  1262  			Mode:         config.ModeBundle,
  1263  			AbsOutputDir: "/out",
  1264  		},
  1265  	})
  1266  }
  1267  
  1268  func TestPackageURLsInCSS(t *testing.T) {
  1269  	css_suite.expectBundled(t, bundled{
  1270  		files: map[string]string{
  1271  			"/entry.css": `
  1272  			  @import "test.css";
  1273  
  1274  				a { background: url(a/1.png); }
  1275  				b { background: url(b/2.png); }
  1276  				c { background: url(c/3.png); }
  1277  			`,
  1278  			"/test.css":             `.css { color: red }`,
  1279  			"/a/1.png":              `a-1`,
  1280  			"/node_modules/b/2.png": `b-2-node_modules`,
  1281  			"/c/3.png":              `c-3`,
  1282  			"/node_modules/c/3.png": `c-3-node_modules`,
  1283  		},
  1284  		entryPaths: []string{"/entry.css"},
  1285  		options: config.Options{
  1286  			Mode:         config.ModeBundle,
  1287  			AbsOutputDir: "/out",
  1288  			ExtensionToLoader: map[string]config.Loader{
  1289  				".css": config.LoaderCSS,
  1290  				".png": config.LoaderBase64,
  1291  			},
  1292  		},
  1293  	})
  1294  }
  1295  
  1296  func TestCSSAtImportExtensionOrderCollision(t *testing.T) {
  1297  	css_suite.expectBundled(t, bundled{
  1298  		files: map[string]string{
  1299  			// This should avoid picking ".js" because it's explicitly configured as non-CSS
  1300  			"/entry.css": `@import "./test";`,
  1301  			"/test.js":   `console.log('js')`,
  1302  			"/test.css":  `.css { color: red }`,
  1303  		},
  1304  		entryPaths: []string{"/entry.css"},
  1305  		options: config.Options{
  1306  			Mode:           config.ModeBundle,
  1307  			AbsOutputFile:  "/out.css",
  1308  			ExtensionOrder: []string{".js", ".css"},
  1309  			ExtensionToLoader: map[string]config.Loader{
  1310  				".js":  config.LoaderJS,
  1311  				".css": config.LoaderCSS,
  1312  			},
  1313  		},
  1314  	})
  1315  }
  1316  
  1317  func TestCSSAtImportExtensionOrderCollisionUnsupported(t *testing.T) {
  1318  	css_suite.expectBundled(t, bundled{
  1319  		files: map[string]string{
  1320  			// This still shouldn't pick ".js" even though ".sass" isn't ".css"
  1321  			"/entry.css": `@import "./test";`,
  1322  			"/test.js":   `console.log('js')`,
  1323  			"/test.sass": `// some code`,
  1324  		},
  1325  		entryPaths: []string{"/entry.css"},
  1326  		options: config.Options{
  1327  			Mode:           config.ModeBundle,
  1328  			AbsOutputFile:  "/out.css",
  1329  			ExtensionOrder: []string{".js", ".sass"},
  1330  			ExtensionToLoader: map[string]config.Loader{
  1331  				".js":  config.LoaderJS,
  1332  				".css": config.LoaderCSS,
  1333  			},
  1334  		},
  1335  		expectedScanLog: `entry.css: ERROR: No loader is configured for ".sass" files: test.sass
  1336  `,
  1337  	})
  1338  }
  1339  
  1340  func TestCSSAtImportConditionsNoBundle(t *testing.T) {
  1341  	css_suite.expectBundled(t, bundled{
  1342  		files: map[string]string{
  1343  			"/entry.css": `@import "./print.css" print;`,
  1344  		},
  1345  		entryPaths: []string{"/entry.css"},
  1346  		options: config.Options{
  1347  			Mode:          config.ModePassThrough,
  1348  			AbsOutputFile: "/out.css",
  1349  		},
  1350  	})
  1351  }
  1352  
  1353  func TestCSSAtImportConditionsBundleExternal(t *testing.T) {
  1354  	css_suite.expectBundled(t, bundled{
  1355  		files: map[string]string{
  1356  			"/entry.css": `@import "https://example.com/print.css" print;`,
  1357  		},
  1358  		entryPaths: []string{"/entry.css"},
  1359  		options: config.Options{
  1360  			Mode:          config.ModeBundle,
  1361  			AbsOutputFile: "/out.css",
  1362  		},
  1363  	})
  1364  }
  1365  
  1366  func TestCSSAtImportConditionsBundleExternalConditionWithURL(t *testing.T) {
  1367  	css_suite.expectBundled(t, bundled{
  1368  		files: map[string]string{
  1369  			"/entry.css": `
  1370  				@import "https://example.com/foo.css" (foo: url("foo.png")) and (bar: url("bar.png"));
  1371  			`,
  1372  		},
  1373  		entryPaths: []string{"/entry.css"},
  1374  		options: config.Options{
  1375  			Mode:          config.ModeBundle,
  1376  			AbsOutputFile: "/out.css",
  1377  		},
  1378  	})
  1379  }
  1380  
  1381  func TestCSSAtImportConditionsBundle(t *testing.T) {
  1382  	css_suite.expectBundled(t, bundled{
  1383  		files: map[string]string{
  1384  			"/entry.css": `
  1385  				@import url(http://example.com/foo.css);
  1386  				@import url(http://example.com/foo.css) layer;
  1387  				@import url(http://example.com/foo.css) layer(layer-name);
  1388  				@import url(http://example.com/foo.css) layer(layer-name) supports(supports-condition);
  1389  				@import url(http://example.com/foo.css) layer(layer-name) supports(supports-condition) list-of-media-queries;
  1390  				@import url(http://example.com/foo.css) layer(layer-name) list-of-media-queries;
  1391  				@import url(http://example.com/foo.css) supports(supports-condition);
  1392  				@import url(http://example.com/foo.css) supports(supports-condition) list-of-media-queries;
  1393  				@import url(http://example.com/foo.css) list-of-media-queries;
  1394  
  1395  				@import url(foo.css);
  1396  				@import url(foo.css) layer;
  1397  				@import url(foo.css) layer(layer-name);
  1398  				@import url(foo.css) layer(layer-name) supports(supports-condition);
  1399  				@import url(foo.css) layer(layer-name) supports(supports-condition) list-of-media-queries;
  1400  				@import url(foo.css) layer(layer-name) list-of-media-queries;
  1401  				@import url(foo.css) supports(supports-condition);
  1402  				@import url(foo.css) supports(supports-condition) list-of-media-queries;
  1403  				@import url(foo.css) list-of-media-queries;
  1404  
  1405  				@import url(empty-1.css) layer(empty-1);
  1406  				@import url(empty-2.css) supports(empty: 2);
  1407  				@import url(empty-3.css) (empty: 3);
  1408  
  1409  				@import "nested-layer.css" layer(outer);
  1410  				@import "nested-layer.css" supports(outer: true);
  1411  				@import "nested-layer.css" (outer: true);
  1412  				@import "nested-supports.css" layer(outer);
  1413  				@import "nested-supports.css" supports(outer: true);
  1414  				@import "nested-supports.css" (outer: true);
  1415  				@import "nested-media.css" layer(outer);
  1416  				@import "nested-media.css" supports(outer: true);
  1417  				@import "nested-media.css" (outer: true);
  1418  			`,
  1419  
  1420  			"/foo.css": `body { color: red }`,
  1421  
  1422  			"/empty-1.css": ``,
  1423  			"/empty-2.css": ``,
  1424  			"/empty-3.css": ``,
  1425  
  1426  			"/nested-layer.css":    `@import "foo.css" layer(inner);`,
  1427  			"/nested-supports.css": `@import "foo.css" supports(inner: true);`,
  1428  			"/nested-media.css":    `@import "foo.css" (inner: true);`,
  1429  		},
  1430  		entryPaths: []string{"/entry.css"},
  1431  		options: config.Options{
  1432  			Mode:          config.ModeBundle,
  1433  			AbsOutputFile: "/out.css",
  1434  		},
  1435  	})
  1436  }
  1437  
  1438  func TestCSSAtImportConditionsWithImportRecordsBundle(t *testing.T) {
  1439  	// This tests that esbuild correctly clones the import records for all import
  1440  	// condition tokens. If they aren't cloned correctly, then something will
  1441  	// likely crash with an out-of-bounds error.
  1442  	css_suite.expectBundled(t, bundled{
  1443  		files: map[string]string{
  1444  			"/entry.css": `
  1445  				@import url(foo.css) supports(background: url(a.png));
  1446  				@import url(foo.css) supports(background: url(b.png)) list-of-media-queries;
  1447  				@import url(foo.css) layer(layer-name) supports(background: url(a.png));
  1448  				@import url(foo.css) layer(layer-name) supports(background: url(b.png)) list-of-media-queries;
  1449  			`,
  1450  			"/foo.css": `body { color: red }`,
  1451  			"/a.png":   `A`,
  1452  			"/b.png":   `B`,
  1453  		},
  1454  		entryPaths: []string{"/entry.css"},
  1455  		options: config.Options{
  1456  			Mode:          config.ModeBundle,
  1457  			AbsOutputFile: "/out.css",
  1458  			ExtensionToLoader: map[string]config.Loader{
  1459  				".css": config.LoaderCSS,
  1460  				".png": config.LoaderBase64,
  1461  			},
  1462  		},
  1463  	})
  1464  }
  1465  
  1466  // From: https://github.com/romainmenke/css-import-tests. These test cases just
  1467  // serve to document any changes in esbuild's behavior. Any changes in behavior
  1468  // should be tested to ensure they don't cause any regressions. The easiest way
  1469  // to test the changes is to bundle https://github.com/evanw/css-import-tests
  1470  // and visually inspect a browser's rendering of the resulting CSS file.
  1471  func TestCSSAtImportConditionsFromExternalRepo(t *testing.T) {
  1472  	css_suite.expectBundled(t, bundled{
  1473  		files: map[string]string{
  1474  			"/001/default/a.css":     `.box { background-color: green; }`,
  1475  			"/001/default/style.css": `@import url("a.css");`,
  1476  
  1477  			"/001/relative-url/a.css":     `.box { background-color: green; }`,
  1478  			"/001/relative-url/style.css": `@import url("./a.css");`,
  1479  
  1480  			"/at-charset/001/a.css":     `@charset "utf-8"; .box { background-color: red; }`,
  1481  			"/at-charset/001/b.css":     `@charset "utf-8"; .box { background-color: green; }`,
  1482  			"/at-charset/001/style.css": `@charset "utf-8"; @import url("a.css"); @import url("b.css");`,
  1483  
  1484  			"/at-keyframes/001/a.css": `
  1485  				.box { animation: BOX; animation-duration: 0s; animation-fill-mode: both; }
  1486  				@keyframes BOX { 0%, 100% { background-color: green; } }
  1487  			`,
  1488  			"/at-keyframes/001/b.css": `
  1489  				.box { animation: BOX; animation-duration: 0s; animation-fill-mode: both; }
  1490  				@keyframes BOX { 0%, 100% { background-color: red; } }
  1491  			`,
  1492  			"/at-keyframes/001/style.css": `@import url("a.css") screen; @import url("b.css") print;`,
  1493  
  1494  			"/at-layer/001/a.css": `.box { background-color: red; }`,
  1495  			"/at-layer/001/b.css": `.box { background-color: green; }`,
  1496  			"/at-layer/001/style.css": `
  1497  				@import url("a.css") layer(a);
  1498  				@import url("b.css") layer(b);
  1499  				@import url("a.css") layer(a);
  1500  			`,
  1501  
  1502  			"/at-layer/002/a.css": `.box { background-color: green; }`,
  1503  			"/at-layer/002/b.css": `.box { background-color: red; }`,
  1504  			"/at-layer/002/style.css": `
  1505  				@import url("a.css") layer(a) print;
  1506  				@import url("b.css") layer(b);
  1507  				@import url("a.css") layer(a);
  1508  			`,
  1509  
  1510  			// Note: This case is currently bundled incorrectly. Normal CSS takes
  1511  			// effect at the position of the last "@import". However, "@layer" CSS
  1512  			// takes effect at the position of the first "@import". This discrepancy
  1513  			// in behavior is not currently handled.
  1514  			"/at-layer/003/a.css":     `@layer a { .box { background-color: red; } }`,
  1515  			"/at-layer/003/b.css":     `@layer b { .box { background-color: green; } }`,
  1516  			"/at-layer/003/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`,
  1517  
  1518  			"/at-layer/004/a.css":     `@layer { .box { background-color: green; } }`,
  1519  			"/at-layer/004/b.css":     `@layer { .box { background-color: red; } }`,
  1520  			"/at-layer/004/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`,
  1521  
  1522  			"/at-layer/005/a.css": `@import url("b.css") layer(b) (width: 1px);`,
  1523  			"/at-layer/005/b.css": `.box { background-color: red; }`,
  1524  			"/at-layer/005/style.css": `
  1525  				@import url("a.css") layer(a) (min-width: 1px);
  1526  				@layer a.c { .box { background-color: red; } }
  1527  				@layer a.b { .box { background-color: green; } }
  1528  			`,
  1529  
  1530  			"/at-layer/006/a.css": `@import url("b.css") layer(b) (min-width: 1px);`,
  1531  			"/at-layer/006/b.css": `.box { background-color: red; }`,
  1532  			"/at-layer/006/style.css": `
  1533  				@import url("a.css") layer(a) (min-width: 1px);
  1534  				@layer a.c { .box { background-color: green; } }
  1535  				@layer a.b { .box { background-color: red; } }
  1536  			`,
  1537  
  1538  			"/at-layer/007/style.css": `
  1539  				@layer foo {}
  1540  				@layer bar {}
  1541  				@layer bar { .box { background-color: green; } }
  1542  				@layer foo { .box { background-color: red; } }
  1543  			`,
  1544  
  1545  			"/at-layer/008/a.css":     `@import "b.css" layer; .box { background-color: green; }`,
  1546  			"/at-layer/008/b.css":     `.box { background-color: red; }`,
  1547  			"/at-layer/008/style.css": `@import url("a.css") layer;`,
  1548  
  1549  			"/at-media/001/default/a.css":     `.box { background-color: green; }`,
  1550  			"/at-media/001/default/style.css": `@import url("a.css") screen;`,
  1551  
  1552  			"/at-media/002/a.css":     `.box { background-color: green; }`,
  1553  			"/at-media/002/b.css":     `.box { background-color: red; }`,
  1554  			"/at-media/002/style.css": `@import url("a.css") screen; @import url("b.css") print;`,
  1555  
  1556  			"/at-media/003/a.css":     `@import url("b.css") (min-width: 1px);`,
  1557  			"/at-media/003/b.css":     `.box { background-color: green; }`,
  1558  			"/at-media/003/style.css": `@import url("a.css") screen;`,
  1559  
  1560  			"/at-media/004/a.css":     `@import url("b.css") print;`,
  1561  			"/at-media/004/b.css":     `.box { background-color: red; }`,
  1562  			"/at-media/004/c.css":     `.box { background-color: green; }`,
  1563  			"/at-media/004/style.css": `@import url("c.css"); @import url("a.css") print;`,
  1564  
  1565  			"/at-media/005/a.css":     `@import url("b.css") (max-width: 1px);`,
  1566  			"/at-media/005/b.css":     `.box { background-color: red; }`,
  1567  			"/at-media/005/c.css":     `.box { background-color: green; }`,
  1568  			"/at-media/005/style.css": `@import url("c.css"); @import url("a.css") (max-width: 1px);`,
  1569  
  1570  			"/at-media/006/a.css":     `@import url("b.css") (min-width: 1px);`,
  1571  			"/at-media/006/b.css":     `.box { background-color: green; }`,
  1572  			"/at-media/006/style.css": `@import url("a.css") (min-height: 1px);`,
  1573  
  1574  			"/at-media/007/a.css":     `@import url("b.css") screen;`,
  1575  			"/at-media/007/b.css":     `.box { background-color: green; }`,
  1576  			"/at-media/007/style.css": `@import url("a.css") all;`,
  1577  
  1578  			"/at-media/008/a.css":     `@import url("green.css") layer(alpha) print;`,
  1579  			"/at-media/008/b.css":     `@import url("red.css") layer(beta) print;`,
  1580  			"/at-media/008/green.css": `.box { background-color: green; }`,
  1581  			"/at-media/008/red.css":   `.box { background-color: red; }`,
  1582  			"/at-media/008/style.css": `
  1583  				@import url("a.css") layer(alpha) all;
  1584  				@import url("b.css") layer(beta) all;
  1585  				@layer beta { .box { background-color: green; } }
  1586  				@layer alpha { .box { background-color: red; } }
  1587  			`,
  1588  
  1589  			"/at-supports/001/a.css":     `.box { background-color: green; }`,
  1590  			"/at-supports/001/style.css": `@import url("a.css") supports(display: block);`,
  1591  
  1592  			"/at-supports/002/a.css":     `@import url("b.css") supports(width: 10px);`,
  1593  			"/at-supports/002/b.css":     `.box { background-color: green; }`,
  1594  			"/at-supports/002/style.css": `@import url("a.css") supports(display: block);`,
  1595  
  1596  			"/at-supports/003/a.css":     `@import url("b.css") supports(width: 10px);`,
  1597  			"/at-supports/003/b.css":     `.box { background-color: green; }`,
  1598  			"/at-supports/003/style.css": `@import url("a.css") supports((display: block) or (display: inline));`,
  1599  
  1600  			"/at-supports/004/a.css":     `@import url("b.css") layer(b) supports(width: 10px);`,
  1601  			"/at-supports/004/b.css":     `.box { background-color: green; }`,
  1602  			"/at-supports/004/style.css": `@import url("a.css") layer(a) supports(display: block);`,
  1603  
  1604  			"/at-supports/005/a.css":     `@import url("green.css") layer(alpha) supports(foo: bar);`,
  1605  			"/at-supports/005/b.css":     `@import url("red.css") layer(beta) supports(foo: bar);`,
  1606  			"/at-supports/005/green.css": `.box { background-color: green; }`,
  1607  			"/at-supports/005/red.css":   `.box { background-color: red; }`,
  1608  			"/at-supports/005/style.css": `
  1609  				@import url("a.css") layer(alpha) supports(display: block);
  1610  				@import url("b.css") layer(beta) supports(display: block);
  1611  				@layer beta { .box { background-color: green; } }
  1612  				@layer alpha { .box { background-color: red; } }
  1613  			`,
  1614  
  1615  			"/cycles/001/style.css": `@import url("style.css"); .box { background-color: green; }`,
  1616  
  1617  			"/cycles/002/a.css":     `@import url("red.css"); @import url("b.css");`,
  1618  			"/cycles/002/b.css":     `@import url("green.css"); @import url("a.css");`,
  1619  			"/cycles/002/green.css": `.box { background-color: green; }`,
  1620  			"/cycles/002/red.css":   `.box { background-color: red; }`,
  1621  			"/cycles/002/style.css": `@import url("a.css");`,
  1622  
  1623  			"/cycles/003/a.css":     `@import url("b.css"); .box { background-color: green; }`,
  1624  			"/cycles/003/b.css":     `@import url("a.css"); .box { background-color: red; }`,
  1625  			"/cycles/003/style.css": `@import url("a.css");`,
  1626  
  1627  			"/cycles/004/a.css":     `@import url("b.css"); .box { background-color: red; }`,
  1628  			"/cycles/004/b.css":     `@import url("a.css"); .box { background-color: green; }`,
  1629  			"/cycles/004/style.css": `@import url("a.css"); @import url("b.css");`,
  1630  
  1631  			"/cycles/005/a.css":     `@import url("b.css"); .box { background-color: green; }`,
  1632  			"/cycles/005/b.css":     `@import url("a.css"); .box { background-color: red; }`,
  1633  			"/cycles/005/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`,
  1634  
  1635  			"/cycles/006/a.css":     `@import url("red.css"); @import url("b.css");`,
  1636  			"/cycles/006/b.css":     `@import url("green.css"); @import url("a.css");`,
  1637  			"/cycles/006/c.css":     `@import url("a.css");`,
  1638  			"/cycles/006/green.css": `.box { background-color: green; }`,
  1639  			"/cycles/006/red.css":   `.box { background-color: red; }`,
  1640  			"/cycles/006/style.css": `@import url("b.css"); @import url("c.css");`,
  1641  
  1642  			"/cycles/007/a.css":     `@import url("red.css"); @import url("b.css") screen;`,
  1643  			"/cycles/007/b.css":     `@import url("green.css"); @import url("a.css") all;`,
  1644  			"/cycles/007/c.css":     `@import url("a.css") not print;`,
  1645  			"/cycles/007/green.css": `.box { background-color: green; }`,
  1646  			"/cycles/007/red.css":   `.box { background-color: red; }`,
  1647  			"/cycles/007/style.css": `@import url("b.css"); @import url("c.css");`,
  1648  
  1649  			"/cycles/008/a.css":     `@import url("red.css") layer; @import url("b.css");`,
  1650  			"/cycles/008/b.css":     `@import url("green.css") layer; @import url("a.css");`,
  1651  			"/cycles/008/c.css":     `@import url("a.css") layer;`,
  1652  			"/cycles/008/green.css": `.box { background-color: green; }`,
  1653  			"/cycles/008/red.css":   `.box { background-color: red; }`,
  1654  			"/cycles/008/style.css": `@import url("b.css"); @import url("c.css");`,
  1655  
  1656  			"/data-urls/002/style.css": `@import url('data:text/css;plain,.box%20%7B%0A%09background-color%3A%20green%3B%0A%7D%0A');`,
  1657  
  1658  			"/data-urls/003/style.css": `@import url('data:text/css,.box%20%7B%0A%09background-color%3A%20green%3B%0A%7D%0A');`,
  1659  
  1660  			"/duplicates/001/a.css":     `.box { background-color: green; }`,
  1661  			"/duplicates/001/b.css":     `.box { background-color: red; }`,
  1662  			"/duplicates/001/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css");`,
  1663  
  1664  			"/duplicates/002/a.css":     `.box { background-color: green; }`,
  1665  			"/duplicates/002/b.css":     `.box { background-color: red; }`,
  1666  			"/duplicates/002/style.css": `@import url("a.css"); @import url("b.css"); @import url("a.css"); @import url("b.css"); @import url("a.css");`,
  1667  
  1668  			"/empty/001/empty.css": ``,
  1669  			"/empty/001/style.css": `@import url("./empty.css"); .box { background-color: green; }`,
  1670  
  1671  			"/relative-paths/001/a/a.css":   `@import url("../b/b.css")`,
  1672  			"/relative-paths/001/b/b.css":   `.box { background-color: green; }`,
  1673  			"/relative-paths/001/style.css": `@import url("./a/a.css");`,
  1674  
  1675  			"/relative-paths/002/a/a.css":   `@import url("./../b/b.css")`,
  1676  			"/relative-paths/002/b/b.css":   `.box { background-color: green; }`,
  1677  			"/relative-paths/002/style.css": `@import url("./a/a.css");`,
  1678  
  1679  			"/subresource/001/something/images/green.png": `...`,
  1680  			"/subresource/001/something/styles/green.css": `.box { background-image: url("../images/green.png"); }`,
  1681  			"/subresource/001/style.css":                  `@import url("./something/styles/green.css");`,
  1682  
  1683  			"/subresource/002/green.png":        `...`,
  1684  			"/subresource/002/style.css":        `@import url("./styles/green.css");`,
  1685  			"/subresource/002/styles/green.css": `.box { background-image: url("../green.png"); }`,
  1686  
  1687  			"/subresource/004/style.css":        `@import url("./styles/green.css");`,
  1688  			"/subresource/004/styles/green.css": `.box { background-image: url("green.png"); }`,
  1689  			"/subresource/004/styles/green.png": `...`,
  1690  
  1691  			"/subresource/005/style.css":        `@import url("./styles/green.css");`,
  1692  			"/subresource/005/styles/green.css": `.box { background-image: url("./green.png"); }`,
  1693  			"/subresource/005/styles/green.png": `...`,
  1694  
  1695  			"/subresource/007/green.png": `...`,
  1696  			"/subresource/007/style.css": `.box { background-image: url("./green.png"); }`,
  1697  
  1698  			"/url-format/001/default/a.css":     `.box { background-color: green; }`,
  1699  			"/url-format/001/default/style.css": `@import url(a.css);`,
  1700  
  1701  			"/url-format/001/relative-url/a.css":     `.box { background-color: green; }`,
  1702  			"/url-format/001/relative-url/style.css": `@import url(./a.css);`,
  1703  
  1704  			"/url-format/002/default/a.css":     `.box { background-color: green; }`,
  1705  			"/url-format/002/default/style.css": `@import "a.css";`,
  1706  
  1707  			"/url-format/002/relative-url/a.css":     `.box { background-color: green; }`,
  1708  			"/url-format/002/relative-url/style.css": `@import "./a.css";`,
  1709  
  1710  			"/url-format/003/default/a.css":     `.box { background-color: green; }`,
  1711  			"/url-format/003/default/style.css": `@import url("a.css"`,
  1712  
  1713  			"/url-format/003/relative-url/a.css":     `.box { background-color: green; }`,
  1714  			"/url-format/003/relative-url/style.css": `@import url("./a.css"`,
  1715  
  1716  			"/url-fragments/001/a.css":     `.box { background-color: green; }`,
  1717  			"/url-fragments/001/style.css": `@import url("./a.css#foo");`,
  1718  
  1719  			"/url-fragments/002/a.css":     `.box { background-color: green; }`,
  1720  			"/url-fragments/002/b.css":     `.box { background-color: red; }`,
  1721  			"/url-fragments/002/style.css": `@import url("./a.css#1"); @import url("./b.css#2"); @import url("./a.css#3");`,
  1722  		},
  1723  		entryPaths: []string{
  1724  			"/001/default/style.css",
  1725  			"/001/relative-url/style.css",
  1726  
  1727  			"/at-charset/001/style.css",
  1728  
  1729  			"/at-keyframes/001/style.css",
  1730  
  1731  			"/at-layer/001/style.css",
  1732  			"/at-layer/002/style.css",
  1733  			"/at-layer/003/style.css",
  1734  			"/at-layer/004/style.css",
  1735  			"/at-layer/005/style.css",
  1736  			"/at-layer/006/style.css",
  1737  			"/at-layer/007/style.css",
  1738  			"/at-layer/008/style.css",
  1739  
  1740  			"/at-media/001/default/style.css",
  1741  			"/at-media/002/style.css",
  1742  			"/at-media/003/style.css",
  1743  			"/at-media/004/style.css",
  1744  			"/at-media/005/style.css",
  1745  			"/at-media/006/style.css",
  1746  			"/at-media/007/style.css",
  1747  			"/at-media/008/style.css",
  1748  
  1749  			"/at-supports/001/style.css",
  1750  			"/at-supports/002/style.css",
  1751  			"/at-supports/003/style.css",
  1752  			"/at-supports/004/style.css",
  1753  			"/at-supports/005/style.css",
  1754  
  1755  			"/cycles/001/style.css",
  1756  			"/cycles/002/style.css",
  1757  			"/cycles/003/style.css",
  1758  			"/cycles/004/style.css",
  1759  			"/cycles/005/style.css",
  1760  			"/cycles/006/style.css",
  1761  			"/cycles/007/style.css",
  1762  			"/cycles/008/style.css",
  1763  
  1764  			"/data-urls/002/style.css",
  1765  			"/data-urls/003/style.css",
  1766  
  1767  			"/duplicates/001/style.css",
  1768  			"/duplicates/002/style.css",
  1769  
  1770  			"/empty/001/style.css",
  1771  
  1772  			"/relative-paths/001/style.css",
  1773  			"/relative-paths/002/style.css",
  1774  
  1775  			"/subresource/001/style.css",
  1776  			"/subresource/002/style.css",
  1777  			"/subresource/004/style.css",
  1778  			"/subresource/005/style.css",
  1779  			"/subresource/007/style.css",
  1780  
  1781  			"/url-format/001/default/style.css",
  1782  			"/url-format/001/relative-url/style.css",
  1783  			"/url-format/002/default/style.css",
  1784  			"/url-format/002/relative-url/style.css",
  1785  			"/url-format/003/default/style.css",
  1786  			"/url-format/003/relative-url/style.css",
  1787  			"/url-fragments/001/style.css",
  1788  			"/url-fragments/002/style.css",
  1789  		},
  1790  		options: config.Options{
  1791  			Mode:         config.ModeBundle,
  1792  			AbsOutputDir: "/out",
  1793  			ExtensionToLoader: map[string]config.Loader{
  1794  				".css": config.LoaderCSS,
  1795  				".png": config.LoaderBase64,
  1796  			},
  1797  		},
  1798  		expectedScanLog: `relative-paths/001/a/a.css: WARNING: Expected ";" but found end of file
  1799  relative-paths/002/a/a.css: WARNING: Expected ";" but found end of file
  1800  url-format/003/default/style.css: WARNING: Expected ")" to go with "("
  1801  url-format/003/default/style.css: NOTE: The unbalanced "(" is here:
  1802  url-format/003/relative-url/style.css: WARNING: Expected ")" to go with "("
  1803  url-format/003/relative-url/style.css: NOTE: The unbalanced "(" is here:
  1804  `,
  1805  	})
  1806  }
  1807  
  1808  func TestCSSAtImportConditionsAtLayerBundle(t *testing.T) {
  1809  	css_suite.expectBundled(t, bundled{
  1810  		files: map[string]string{
  1811  			"/case1.css": `
  1812  				@import url(case1-foo.css) layer(first.one);
  1813  				@import url(case1-foo.css) layer(last.one);
  1814  				@import url(case1-foo.css) layer(first.one);
  1815  			`,
  1816  			"/case1-foo.css": `body { color: red }`,
  1817  
  1818  			"/case2.css": `
  1819  				@import url(case2-foo.css);
  1820  				@import url(case2-bar.css);
  1821  				@import url(case2-foo.css);
  1822  			`,
  1823  			"/case2-foo.css": `@layer first.one { body { color: red } }`,
  1824  			"/case2-bar.css": `@layer last.one { body { color: green } }`,
  1825  
  1826  			"/case3.css": `
  1827  				@import url(case3-foo.css);
  1828  				@import url(case3-bar.css);
  1829  				@import url(case3-foo.css);
  1830  			`,
  1831  			"/case3-foo.css": `@layer { body { color: red } }`,
  1832  			"/case3-bar.css": `@layer only.one { body { color: green } }`,
  1833  
  1834  			"/case4.css": `
  1835  				@import url(case4-foo.css) layer(first);
  1836  				@import url(case4-foo.css) layer(last);
  1837  				@import url(case4-foo.css) layer(first);
  1838  			`,
  1839  			"/case4-foo.css": `@layer one { @layer two, three.four; body { color: red } }`,
  1840  
  1841  			"/case5.css": `
  1842  				@import url(case5-foo.css) layer;
  1843  				@import url(case5-foo.css) layer(middle);
  1844  				@import url(case5-foo.css) layer;
  1845  			`,
  1846  			"/case5-foo.css": `@layer one { @layer two, three.four; body { color: red } }`,
  1847  
  1848  			"/case6.css": `
  1849  				@import url(case6-foo.css) layer(first);
  1850  				@import url(case6-foo.css) layer(last);
  1851  				@import url(case6-foo.css) layer(first);
  1852  			`,
  1853  			"/case6-foo.css": `@layer { @layer two, three.four; body { color: red } }`,
  1854  		},
  1855  		entryPaths: []string{
  1856  			"/case1.css",
  1857  			"/case2.css",
  1858  			"/case3.css",
  1859  			"/case4.css",
  1860  			"/case5.css",
  1861  			"/case6.css",
  1862  		},
  1863  		options: config.Options{
  1864  			Mode:         config.ModeBundle,
  1865  			AbsOutputDir: "/out",
  1866  		},
  1867  	})
  1868  }
  1869  
  1870  func TestCSSAtImportConditionsAtLayerBundleAlternatingLayerInFile(t *testing.T) {
  1871  	css_suite.expectBundled(t, bundled{
  1872  		files: map[string]string{
  1873  			"/a.css": `@layer first { body { color: red } }`,
  1874  			"/b.css": `@layer last { body { color: green } }`,
  1875  
  1876  			"/case1.css": `
  1877  				@import url(a.css);
  1878  				@import url(a.css);
  1879  			`,
  1880  
  1881  			"/case2.css": `
  1882  				@import url(a.css);
  1883  				@import url(b.css);
  1884  				@import url(a.css);
  1885  			`,
  1886  
  1887  			"/case3.css": `
  1888  				@import url(a.css);
  1889  				@import url(b.css);
  1890  				@import url(a.css);
  1891  				@import url(b.css);
  1892  			`,
  1893  
  1894  			"/case4.css": `
  1895  				@import url(a.css);
  1896  				@import url(b.css);
  1897  				@import url(a.css);
  1898  				@import url(b.css);
  1899  				@import url(a.css);
  1900  			`,
  1901  
  1902  			"/case5.css": `
  1903  				@import url(a.css);
  1904  				@import url(b.css);
  1905  				@import url(a.css);
  1906  				@import url(b.css);
  1907  				@import url(a.css);
  1908  				@import url(b.css);
  1909  			`,
  1910  
  1911  			// Note: There was a bug that only showed up in this case. We need at least this many cases.
  1912  			"/case6.css": `
  1913  				@import url(a.css);
  1914  				@import url(b.css);
  1915  				@import url(a.css);
  1916  				@import url(b.css);
  1917  				@import url(a.css);
  1918  				@import url(b.css);
  1919  				@import url(a.css);
  1920  			`,
  1921  		},
  1922  		entryPaths: []string{
  1923  			"/case1.css",
  1924  			"/case2.css",
  1925  			"/case3.css",
  1926  			"/case4.css",
  1927  			"/case5.css",
  1928  			"/case6.css",
  1929  		},
  1930  		options: config.Options{
  1931  			Mode:         config.ModeBundle,
  1932  			AbsOutputDir: "/out",
  1933  		},
  1934  	})
  1935  }
  1936  
  1937  func TestCSSAtImportConditionsAtLayerBundleAlternatingLayerOnImport(t *testing.T) {
  1938  	css_suite.expectBundled(t, bundled{
  1939  		files: map[string]string{
  1940  			"/a.css": `body { color: red }`,
  1941  			"/b.css": `body { color: green }`,
  1942  
  1943  			"/case1.css": `
  1944  				@import url(a.css) layer(first);
  1945  				@import url(a.css) layer(first);
  1946  			`,
  1947  
  1948  			"/case2.css": `
  1949  				@import url(a.css) layer(first);
  1950  				@import url(b.css) layer(last);
  1951  				@import url(a.css) layer(first);
  1952  			`,
  1953  
  1954  			"/case3.css": `
  1955  				@import url(a.css) layer(first);
  1956  				@import url(b.css) layer(last);
  1957  				@import url(a.css) layer(first);
  1958  				@import url(b.css) layer(last);
  1959  			`,
  1960  
  1961  			"/case4.css": `
  1962  				@import url(a.css) layer(first);
  1963  				@import url(b.css) layer(last);
  1964  				@import url(a.css) layer(first);
  1965  				@import url(b.css) layer(last);
  1966  				@import url(a.css) layer(first);
  1967  			`,
  1968  
  1969  			"/case5.css": `
  1970  				@import url(a.css) layer(first);
  1971  				@import url(b.css) layer(last);
  1972  				@import url(a.css) layer(first);
  1973  				@import url(b.css) layer(last);
  1974  				@import url(a.css) layer(first);
  1975  				@import url(b.css) layer(last);
  1976  			`,
  1977  
  1978  			// Note: There was a bug that only showed up in this case. We need at least this many cases.
  1979  			"/case6.css": `
  1980  				@import url(a.css) layer(first);
  1981  				@import url(b.css) layer(last);
  1982  				@import url(a.css) layer(first);
  1983  				@import url(b.css) layer(last);
  1984  				@import url(a.css) layer(first);
  1985  				@import url(b.css) layer(last);
  1986  				@import url(a.css) layer(first);
  1987  			`,
  1988  		},
  1989  		entryPaths: []string{
  1990  			"/case1.css",
  1991  			"/case2.css",
  1992  			"/case3.css",
  1993  			"/case4.css",
  1994  			"/case5.css",
  1995  			"/case6.css",
  1996  		},
  1997  		options: config.Options{
  1998  			Mode:         config.ModeBundle,
  1999  			AbsOutputDir: "/out",
  2000  		},
  2001  	})
  2002  }
  2003  
  2004  func TestCSSAtImportConditionsChainExternal(t *testing.T) {
  2005  	css_suite.expectBundled(t, bundled{
  2006  		files: map[string]string{
  2007  			"/entry.css": `
  2008  				@import "a.css" layer(a) not print;
  2009  			`,
  2010  			"/a.css": `
  2011  				@import "http://example.com/external1.css";
  2012  				@import "b.css" layer(b) not tv;
  2013  				@import "http://example.com/external2.css" layer(a2);
  2014  			`,
  2015  			"/b.css": `
  2016  				@import "http://example.com/external3.css";
  2017  				@import "http://example.com/external4.css" layer(b2);
  2018  			`,
  2019  		},
  2020  		entryPaths: []string{"/entry.css"},
  2021  		options: config.Options{
  2022  			Mode:          config.ModeBundle,
  2023  			AbsOutputFile: "/out.css",
  2024  		},
  2025  	})
  2026  }
  2027  
  2028  // This test mainly just makes sure that this scenario doesn't crash
  2029  func TestCSSAndJavaScriptCodeSplittingIssue1064(t *testing.T) {
  2030  	css_suite.expectBundled(t, bundled{
  2031  		files: map[string]string{
  2032  			"/a.js": `
  2033  				import shared from './shared.js'
  2034  				console.log(shared() + 1)
  2035  			`,
  2036  			"/b.js": `
  2037  				import shared from './shared.js'
  2038  				console.log(shared() + 2)
  2039  			`,
  2040  			"/c.css": `
  2041  				@import "./shared.css";
  2042  				body { color: red }
  2043  			`,
  2044  			"/d.css": `
  2045  				@import "./shared.css";
  2046  				body { color: blue }
  2047  			`,
  2048  			"/shared.js": `
  2049  				export default function() { return 3 }
  2050  			`,
  2051  			"/shared.css": `
  2052  				body { background: black }
  2053  			`,
  2054  		},
  2055  		entryPaths: []string{
  2056  			"/a.js",
  2057  			"/b.js",
  2058  			"/c.css",
  2059  			"/d.css",
  2060  		},
  2061  		options: config.Options{
  2062  			Mode:          config.ModeBundle,
  2063  			OutputFormat:  config.FormatESModule,
  2064  			CodeSplitting: true,
  2065  			AbsOutputDir:  "/out",
  2066  		},
  2067  	})
  2068  }
  2069  
  2070  func TestCSSExternalQueryAndHashNoMatchIssue1822(t *testing.T) {
  2071  	css_suite.expectBundled(t, bundled{
  2072  		files: map[string]string{
  2073  			"/entry.css": `
  2074  				a { background: url(foo/bar.png?baz) }
  2075  				b { background: url(foo/bar.png#baz) }
  2076  			`,
  2077  		},
  2078  		entryPaths: []string{"/entry.css"},
  2079  		options: config.Options{
  2080  			Mode:          config.ModeBundle,
  2081  			AbsOutputFile: "/out.css",
  2082  			ExternalSettings: config.ExternalSettings{
  2083  				PreResolve: config.ExternalMatchers{Patterns: []config.WildcardPattern{
  2084  					{Suffix: ".png"},
  2085  				}},
  2086  			},
  2087  		},
  2088  		expectedScanLog: `entry.css: ERROR: Could not resolve "foo/bar.png?baz"
  2089  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.
  2090  entry.css: ERROR: Could not resolve "foo/bar.png#baz"
  2091  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.
  2092  `,
  2093  	})
  2094  }
  2095  
  2096  func TestCSSExternalQueryAndHashMatchIssue1822(t *testing.T) {
  2097  	css_suite.expectBundled(t, bundled{
  2098  		files: map[string]string{
  2099  			"/entry.css": `
  2100  				a { background: url(foo/bar.png?baz) }
  2101  				b { background: url(foo/bar.png#baz) }
  2102  			`,
  2103  		},
  2104  		entryPaths: []string{"/entry.css"},
  2105  		options: config.Options{
  2106  			Mode:          config.ModeBundle,
  2107  			AbsOutputFile: "/out.css",
  2108  			ExternalSettings: config.ExternalSettings{
  2109  				PreResolve: config.ExternalMatchers{Patterns: []config.WildcardPattern{
  2110  					{Suffix: ".png?baz"},
  2111  					{Suffix: ".png#baz"},
  2112  				}},
  2113  			},
  2114  		},
  2115  	})
  2116  }
  2117  
  2118  func TestCSSNestingOldBrowser(t *testing.T) {
  2119  	css_suite.expectBundled(t, bundled{
  2120  		files: map[string]string{
  2121  			// These are now the only two cases that warn about ":is" not being supported
  2122  			"/two-type-selectors.css":   `a { .c b& { color: red; } }`,
  2123  			"/two-parent-selectors.css": `a b { .c & { color: red; } }`,
  2124  
  2125  			// Make sure this only generates one warning (even though it generates ":is" three times)
  2126  			"/only-one-warning.css": `.a, .b .c, .d { & > & { color: red; } }`,
  2127  
  2128  			"/nested-@layer.css":          `a { @layer base { color: red; } }`,
  2129  			"/nested-@media.css":          `a { @media screen { color: red; } }`,
  2130  			"/nested-ampersand-twice.css": `a { &, & { color: red; } }`,
  2131  			"/nested-ampersand-first.css": `a { &, b { color: red; } }`,
  2132  			"/nested-attribute.css":       `a { [href] { color: red; } }`,
  2133  			"/nested-colon.css":           `a { :hover { color: red; } }`,
  2134  			"/nested-dot.css":             `a { .cls { color: red; } }`,
  2135  			"/nested-greaterthan.css":     `a { > b { color: red; } }`,
  2136  			"/nested-hash.css":            `a { #id { color: red; } }`,
  2137  			"/nested-plus.css":            `a { + b { color: red; } }`,
  2138  			"/nested-tilde.css":           `a { ~ b { color: red; } }`,
  2139  
  2140  			"/toplevel-ampersand-twice.css":  `&, & { color: red; }`,
  2141  			"/toplevel-ampersand-first.css":  `&, a { color: red; }`,
  2142  			"/toplevel-ampersand-second.css": `a, & { color: red; }`,
  2143  			"/toplevel-attribute.css":        `[href] { color: red; }`,
  2144  			"/toplevel-colon.css":            `:hover { color: red; }`,
  2145  			"/toplevel-dot.css":              `.cls { color: red; }`,
  2146  			"/toplevel-greaterthan.css":      `> b { color: red; }`,
  2147  			"/toplevel-hash.css":             `#id { color: red; }`,
  2148  			"/toplevel-plus.css":             `+ b { color: red; }`,
  2149  			"/toplevel-tilde.css":            `~ b { color: red; }`,
  2150  
  2151  			"/media-ampersand-twice.css":  `@media screen { &, & { color: red; } }`,
  2152  			"/media-ampersand-first.css":  `@media screen { &, a { color: red; } }`,
  2153  			"/media-ampersand-second.css": `@media screen { a, & { color: red; } }`,
  2154  			"/media-attribute.css":        `@media screen { [href] { color: red; } }`,
  2155  			"/media-colon.css":            `@media screen { :hover { color: red; } }`,
  2156  			"/media-dot.css":              `@media screen { .cls { color: red; } }`,
  2157  			"/media-greaterthan.css":      `@media screen { > b { color: red; } }`,
  2158  			"/media-hash.css":             `@media screen { #id { color: red; } }`,
  2159  			"/media-plus.css":             `@media screen { + b { color: red; } }`,
  2160  			"/media-tilde.css":            `@media screen { ~ b { color: red; } }`,
  2161  
  2162  			// See: https://github.com/evanw/esbuild/issues/3197
  2163  			"/page-no-warning.css": `@page { @top-left { background: red } }`,
  2164  		},
  2165  		entryPaths: []string{
  2166  			"/two-type-selectors.css",
  2167  			"/two-parent-selectors.css",
  2168  
  2169  			"/only-one-warning.css",
  2170  
  2171  			"/nested-@layer.css",
  2172  			"/nested-@media.css",
  2173  			"/nested-ampersand-twice.css",
  2174  			"/nested-ampersand-first.css",
  2175  			"/nested-attribute.css",
  2176  			"/nested-colon.css",
  2177  			"/nested-dot.css",
  2178  			"/nested-greaterthan.css",
  2179  			"/nested-hash.css",
  2180  			"/nested-plus.css",
  2181  			"/nested-tilde.css",
  2182  
  2183  			"/toplevel-ampersand-twice.css",
  2184  			"/toplevel-ampersand-first.css",
  2185  			"/toplevel-ampersand-second.css",
  2186  			"/toplevel-attribute.css",
  2187  			"/toplevel-colon.css",
  2188  			"/toplevel-dot.css",
  2189  			"/toplevel-greaterthan.css",
  2190  			"/toplevel-hash.css",
  2191  			"/toplevel-plus.css",
  2192  			"/toplevel-tilde.css",
  2193  
  2194  			"/media-ampersand-twice.css",
  2195  			"/media-ampersand-first.css",
  2196  			"/media-ampersand-second.css",
  2197  			"/media-attribute.css",
  2198  			"/media-colon.css",
  2199  			"/media-dot.css",
  2200  			"/media-greaterthan.css",
  2201  			"/media-hash.css",
  2202  			"/media-plus.css",
  2203  			"/media-tilde.css",
  2204  
  2205  			"/page-no-warning.css",
  2206  		},
  2207  		options: config.Options{
  2208  			Mode:                   config.ModeBundle,
  2209  			AbsOutputDir:           "/out",
  2210  			UnsupportedCSSFeatures: compat.Nesting | compat.IsPseudoClass,
  2211  			OriginalTargetEnv:      "chrome10",
  2212  		},
  2213  		expectedScanLog: `only-one-warning.css: WARNING: Transforming this CSS nesting syntax is not supported in the configured target environment (chrome10)
  2214  NOTE: The nesting transform for this case must generate an ":is(...)" but the configured target environment does not support the ":is" pseudo-class.
  2215  two-parent-selectors.css: WARNING: Transforming this CSS nesting syntax is not supported in the configured target environment (chrome10)
  2216  NOTE: The nesting transform for this case must generate an ":is(...)" but the configured target environment does not support the ":is" pseudo-class.
  2217  two-type-selectors.css: WARNING: Transforming this CSS nesting syntax is not supported in the configured target environment (chrome10)
  2218  NOTE: The nesting transform for this case must generate an ":is(...)" but the configured target environment does not support the ":is" pseudo-class.
  2219  `,
  2220  	})
  2221  }
  2222  
  2223  // The mapping of JS entry point to associated CSS bundle isn't necessarily 1:1.
  2224  // Here is a case where it isn't. Two JS entry points share the same associated
  2225  // CSS bundle. This must be reflected in the metafile by only having the JS
  2226  // entry points point to the associated CSS bundle but not the other way around
  2227  // (since there isn't one JS entry point to point to). This test mainly exists
  2228  // to document this edge case.
  2229  func TestMetafileCSSBundleTwoToOne(t *testing.T) {
  2230  	css_suite.expectBundled(t, bundled{
  2231  		files: map[string]string{
  2232  			"/foo/entry.js": `
  2233  				import '../common.css'
  2234  				console.log('foo')
  2235  			`,
  2236  			"/bar/entry.js": `
  2237  				import '../common.css'
  2238  				console.log('bar')
  2239  			`,
  2240  			"/common.css": `
  2241  				body { color: red }
  2242  			`,
  2243  		},
  2244  		entryPaths: []string{
  2245  			"/foo/entry.js",
  2246  			"/bar/entry.js",
  2247  		},
  2248  		options: config.Options{
  2249  			Mode:         config.ModeBundle,
  2250  			AbsOutputDir: "/out",
  2251  			EntryPathTemplate: []config.PathTemplate{
  2252  				// "[ext]/[hash]"
  2253  				{Data: "./", Placeholder: config.ExtPlaceholder},
  2254  				{Data: "/", Placeholder: config.HashPlaceholder},
  2255  			},
  2256  			NeedsMetafile: true,
  2257  		},
  2258  	})
  2259  }
  2260  
  2261  func TestDeduplicateRules(t *testing.T) {
  2262  	// These are done as bundler tests instead of parser tests because rule
  2263  	// deduplication now happens during linking (so that it has effects across files)
  2264  	css_suite.expectBundled(t, bundled{
  2265  		files: map[string]string{
  2266  			"/yes0.css": "a { color: red; color: green; color: red }",
  2267  			"/yes1.css": "a { color: red } a { color: green } a { color: red }",
  2268  			"/yes2.css": "@media screen { a { color: red } } @media screen { a { color: red } }",
  2269  			"/yes3.css": "@media screen { a { color: red } } @media screen { & a { color: red } }",
  2270  
  2271  			"/no0.css": "@media screen { a { color: red } } @media screen { b a& { color: red } }",
  2272  			"/no1.css": "@media screen { a { color: red } } @media screen { a[x] { color: red } }",
  2273  			"/no2.css": "@media screen { a { color: red } } @media screen { a.x { color: red } }",
  2274  			"/no3.css": "@media screen { a { color: red } } @media screen { a#x { color: red } }",
  2275  			"/no4.css": "@media screen { a { color: red } } @media screen { a:x { color: red } }",
  2276  			"/no5.css": "@media screen { a:x { color: red } } @media screen { a:x(y) { color: red } }",
  2277  			"/no6.css": "@media screen { a b { color: red } } @media screen { a + b { color: red } }",
  2278  
  2279  			"/across-files.css":   "@import 'across-files-0.css'; @import 'across-files-1.css'; @import 'across-files-2.css';",
  2280  			"/across-files-0.css": "a { color: red; color: red }",
  2281  			"/across-files-1.css": "a { color: green }",
  2282  			"/across-files-2.css": "a { color: red }",
  2283  
  2284  			"/across-files-url.css":   "@import 'across-files-url-0.css'; @import 'across-files-url-1.css'; @import 'across-files-url-2.css';",
  2285  			"/across-files-url-0.css": "@import 'http://example.com/some.css'; @font-face { src: url(http://example.com/some.font); }",
  2286  			"/across-files-url-1.css": "@font-face { src: url(http://example.com/some.other.font); }",
  2287  			"/across-files-url-2.css": "@font-face { src: url(http://example.com/some.font); }",
  2288  		},
  2289  		entryPaths: []string{
  2290  			"/yes0.css",
  2291  			"/yes1.css",
  2292  			"/yes2.css",
  2293  			"/yes3.css",
  2294  
  2295  			"/no0.css",
  2296  			"/no1.css",
  2297  			"/no2.css",
  2298  			"/no3.css",
  2299  			"/no4.css",
  2300  			"/no5.css",
  2301  			"/no6.css",
  2302  
  2303  			"/across-files.css",
  2304  			"/across-files-url.css",
  2305  		},
  2306  		options: config.Options{
  2307  			Mode:         config.ModeBundle,
  2308  			AbsOutputDir: "/out",
  2309  			MinifySyntax: true,
  2310  		},
  2311  	})
  2312  }
  2313  
  2314  func TestDeduplicateRulesGlobalVsLocalNames(t *testing.T) {
  2315  	css_suite.expectBundled(t, bundled{
  2316  		files: map[string]string{
  2317  			"/entry.css": `
  2318  				@import "a.css";
  2319  				@import "b.css";
  2320  			`,
  2321  			"/a.css": `
  2322  				a { color: red } /* SHOULD BE REMOVED */
  2323  				b { color: green }
  2324  
  2325  				:global(.foo) { color: red } /* SHOULD BE REMOVED */
  2326  				:global(.bar) { color: green }
  2327  
  2328  				:local(.foo) { color: red }
  2329  				:local(.bar) { color: green }
  2330  
  2331  				div :global { animation-name: anim_global } /* SHOULD BE REMOVED */
  2332  				div :local { animation-name: anim_local }
  2333  			`,
  2334  			"/b.css": `
  2335  				a { color: red }
  2336  				b { color: blue }
  2337  
  2338  				:global(.foo) { color: red }
  2339  				:global(.bar) { color: blue }
  2340  
  2341  				:local(.foo) { color: red }
  2342  				:local(.bar) { color: blue }
  2343  
  2344  				div :global { animation-name: anim_global }
  2345  				div :local { animation-name: anim_local }
  2346  			`,
  2347  		},
  2348  		entryPaths: []string{"entry.css"},
  2349  		options: config.Options{
  2350  			Mode:         config.ModeBundle,
  2351  			AbsOutputDir: "/out",
  2352  			MinifySyntax: true,
  2353  			ExtensionToLoader: map[string]config.Loader{
  2354  				".css": config.LoaderLocalCSS,
  2355  			},
  2356  		},
  2357  	})
  2358  }
  2359  
  2360  // This test makes sure JS files that import local CSS names using the
  2361  // wrong name (e.g. a typo) get a warning so that the problem is noticed.
  2362  func TestUndefinedImportWarningCSS(t *testing.T) {
  2363  	css_suite.expectBundled(t, bundled{
  2364  		files: map[string]string{
  2365  			"/entry.js": `
  2366  				import * as empty_js from './empty.js'
  2367  				import * as empty_esm_js from './empty.esm.js'
  2368  				import * as empty_json from './empty.json'
  2369  				import * as empty_css from './empty.css'
  2370  				import * as empty_global_css from './empty.global-css'
  2371  				import * as empty_local_css from './empty.local-css'
  2372  
  2373  				import * as pkg_empty_js from 'pkg/empty.js'
  2374  				import * as pkg_empty_esm_js from 'pkg/empty.esm.js'
  2375  				import * as pkg_empty_json from 'pkg/empty.json'
  2376  				import * as pkg_empty_css from 'pkg/empty.css'
  2377  				import * as pkg_empty_global_css from 'pkg/empty.global-css'
  2378  				import * as pkg_empty_local_css from 'pkg/empty.local-css'
  2379  
  2380  				import 'pkg'
  2381  
  2382  				console.log(
  2383  					empty_js.foo,
  2384  					empty_esm_js.foo,
  2385  					empty_json.foo,
  2386  					empty_css.foo,
  2387  					empty_global_css.foo,
  2388  					empty_local_css.foo,
  2389  				)
  2390  
  2391  				console.log(
  2392  					pkg_empty_js.foo,
  2393  					pkg_empty_esm_js.foo,
  2394  					pkg_empty_json.foo,
  2395  					pkg_empty_css.foo,
  2396  					pkg_empty_global_css.foo,
  2397  					pkg_empty_local_css.foo,
  2398  				)
  2399  			`,
  2400  
  2401  			"/empty.js":         ``,
  2402  			"/empty.esm.js":     `export {}`,
  2403  			"/empty.json":       `{}`,
  2404  			"/empty.css":        ``,
  2405  			"/empty.global-css": ``,
  2406  			"/empty.local-css":  ``,
  2407  
  2408  			"/node_modules/pkg/empty.js":         ``,
  2409  			"/node_modules/pkg/empty.esm.js":     `export {}`,
  2410  			"/node_modules/pkg/empty.json":       `{}`,
  2411  			"/node_modules/pkg/empty.css":        ``,
  2412  			"/node_modules/pkg/empty.global-css": ``,
  2413  			"/node_modules/pkg/empty.local-css":  ``,
  2414  
  2415  			// Files inside of "node_modules" should not generate a warning
  2416  			"/node_modules/pkg/index.js": `
  2417  				import * as empty_js from './empty.js'
  2418  				import * as empty_esm_js from './empty.esm.js'
  2419  				import * as empty_json from './empty.json'
  2420  				import * as empty_css from './empty.css'
  2421  				import * as empty_global_css from './empty.global-css'
  2422  				import * as empty_local_css from './empty.local-css'
  2423  
  2424  				console.log(
  2425  					empty_js.foo,
  2426  					empty_esm_js.foo,
  2427  					empty_json.foo,
  2428  					empty_css.foo,
  2429  					empty_global_css.foo,
  2430  					empty_local_css.foo,
  2431  				)
  2432  			`,
  2433  		},
  2434  		entryPaths: []string{"/entry.js"},
  2435  		options: config.Options{
  2436  			Mode:         config.ModeBundle,
  2437  			AbsOutputDir: "/out",
  2438  			ExtensionToLoader: map[string]config.Loader{
  2439  				".js":         config.LoaderJS,
  2440  				".json":       config.LoaderJSON,
  2441  				".css":        config.LoaderCSS,
  2442  				".global-css": config.LoaderGlobalCSS,
  2443  				".local-css":  config.LoaderLocalCSS,
  2444  			},
  2445  		},
  2446  		expectedCompileLog: `entry.js: WARNING: Import "foo" will always be undefined because the file "empty.js" has no exports
  2447  entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "empty.esm.js"
  2448  entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "empty.json"
  2449  entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "empty.css"
  2450  entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "empty.global-css"
  2451  entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "empty.local-css"
  2452  entry.js: WARNING: Import "foo" will always be undefined because the file "node_modules/pkg/empty.js" has no exports
  2453  entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "node_modules/pkg/empty.esm.js"
  2454  entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "node_modules/pkg/empty.json"
  2455  entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "node_modules/pkg/empty.css"
  2456  entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "node_modules/pkg/empty.global-css"
  2457  entry.js: WARNING: Import "foo" will always be undefined because there is no matching export in "node_modules/pkg/empty.local-css"
  2458  `,
  2459  	})
  2460  }
  2461  
  2462  func TestCSSMalformedAtImport(t *testing.T) {
  2463  	css_suite.expectBundled(t, bundled{
  2464  		files: map[string]string{
  2465  			"/entry.css": `
  2466  				@import "./url-token-eof.css";
  2467  				@import "./url-token-whitespace-eof.css";
  2468  				@import "./function-token-eof.css";
  2469  				@import "./function-token-whitespace-eof.css";
  2470  			`,
  2471  			"/url-token-eof.css": `@import url(https://example.com/url-token-eof.css`,
  2472  			"/url-token-whitespace-eof.css": `
  2473  				@import url(https://example.com/url-token-whitespace-eof.css
  2474  			`,
  2475  			"/function-token-eof.css": `@import url("https://example.com/function-token-eof.css"`,
  2476  			"/function-token-whitespace-eof.css": `
  2477  				@import url("https://example.com/function-token-whitespace-eof.css"
  2478  			`,
  2479  		},
  2480  		entryPaths: []string{"/entry.css"},
  2481  		options: config.Options{
  2482  			Mode:         config.ModeBundle,
  2483  			AbsOutputDir: "/out",
  2484  		},
  2485  		expectedScanLog: `function-token-eof.css: WARNING: Expected ")" to go with "("
  2486  function-token-eof.css: NOTE: The unbalanced "(" is here:
  2487  function-token-whitespace-eof.css: WARNING: Expected ")" to go with "("
  2488  function-token-whitespace-eof.css: NOTE: The unbalanced "(" is here:
  2489  url-token-eof.css: WARNING: Expected ")" to end URL token
  2490  url-token-eof.css: NOTE: The unbalanced "(" is here:
  2491  url-token-eof.css: WARNING: Expected ";" but found end of file
  2492  url-token-whitespace-eof.css: WARNING: Expected ")" to end URL token
  2493  url-token-whitespace-eof.css: NOTE: The unbalanced "(" is here:
  2494  url-token-whitespace-eof.css: WARNING: Expected ";" but found end of file
  2495  `,
  2496  	})
  2497  }
  2498  
  2499  func TestCSSAtLayerBeforeImportNoBundle(t *testing.T) {
  2500  	css_suite.expectBundled(t, bundled{
  2501  		files: map[string]string{
  2502  			"/entry.css": `
  2503  				@layer layer1, layer2.layer3;
  2504  				@import "a.css";
  2505  				@import "b.css";
  2506  				@layer layer6.layer7, layer8;
  2507  			`,
  2508  		},
  2509  		entryPaths: []string{"/entry.css"},
  2510  		options: config.Options{
  2511  			Mode:         config.ModePassThrough,
  2512  			AbsOutputDir: "/out",
  2513  		},
  2514  	})
  2515  }
  2516  
  2517  func TestCSSAtLayerBeforeImportBundle(t *testing.T) {
  2518  	css_suite.expectBundled(t, bundled{
  2519  		files: map[string]string{
  2520  			"/entry.css": `
  2521  				@layer layer1, layer2.layer3;
  2522  				@import "a.css";
  2523  				@import "b.css";
  2524  				@layer layer6.layer7, layer8;
  2525  			`,
  2526  			"/a.css": `
  2527  				@layer layer4 {
  2528  					a { color: red }
  2529  				}
  2530  			`,
  2531  			"/b.css": `
  2532  				@layer layer5 {
  2533  					b { color: red }
  2534  				}
  2535  			`,
  2536  		},
  2537  		entryPaths: []string{"/entry.css"},
  2538  		options: config.Options{
  2539  			Mode:         config.ModeBundle,
  2540  			AbsOutputDir: "/out",
  2541  		},
  2542  	})
  2543  }
  2544  
  2545  func TestCSSAtLayerMergingWithImportConditions(t *testing.T) {
  2546  	css_suite.expectBundled(t, bundled{
  2547  		files: map[string]string{
  2548  			"/entry.css": `
  2549  				@import "a.css" supports(color: first);
  2550  
  2551  				@import "a.css" supports(color: second);
  2552  				@import "b.css" supports(color: second);
  2553  
  2554  				@import "a.css" supports(color: first);
  2555  				@import "b.css" supports(color: first);
  2556  
  2557  				@import "a.css" supports(color: second);
  2558  				@import "b.css" supports(color: second);
  2559  
  2560  				@import "b.css" supports(color: first);
  2561  			`,
  2562  			"/a.css": `
  2563  				@layer a;
  2564  				@import "http://example.com/a.css";
  2565  			`,
  2566  			"/b.css": `
  2567  				@layer b;
  2568  				@import "http://example.com/b.css";
  2569  			`,
  2570  		},
  2571  		entryPaths: []string{"/entry.css"},
  2572  		options: config.Options{
  2573  			Mode:         config.ModeBundle,
  2574  			AbsOutputDir: "/out",
  2575  		},
  2576  	})
  2577  }
  2578  
  2579  func TestCSSCaseInsensitivity(t *testing.T) {
  2580  	css_suite.expectBundled(t, bundled{
  2581  		files: map[string]string{
  2582  			"/entry.css": `
  2583  				/* "@IMPORT" should be recognized as an import */
  2584  				/* "LAYER(...)" should wrap with "@layer" */
  2585  				/* "SUPPORTS(...)" should wrap with "@supports" */
  2586  				@IMPORT Url("nested.css") LAYER(layer-name) SUPPORTS(supports-condition) list-of-media-queries;
  2587  			`,
  2588  			"/nested.css": `
  2589  				/* "from" should be recognized and optimized to "0%" */
  2590  				@KeyFrames Foo {
  2591  					froM { OPAcity: 0 }
  2592  					tO { opaCITY: 1 }
  2593  				}
  2594  
  2595  				body {
  2596  					/* "#FF0000" should be optimized to "red" because "BACKGROUND-color" should be recognized */
  2597  					BACKGROUND-color: #FF0000;
  2598  
  2599  					/* This should be optimized to 50px */
  2600  					width: CaLc(20Px + 30pX);
  2601  
  2602  					/* This URL token should be recognized and bundled */
  2603  					background-IMAGE: Url(image.png);
  2604  				}
  2605  			`,
  2606  			"/image.png": `...`,
  2607  		},
  2608  		entryPaths: []string{"/entry.css"},
  2609  		options: config.Options{
  2610  			Mode:          config.ModeBundle,
  2611  			AbsOutputFile: "/out.css",
  2612  			MinifySyntax:  true,
  2613  			ExtensionToLoader: map[string]config.Loader{
  2614  				".css": config.LoaderCSS,
  2615  				".png": config.LoaderCopy,
  2616  			},
  2617  		},
  2618  	})
  2619  }
  2620  
  2621  func TestCSSAssetPathsWithSpacesBundle(t *testing.T) {
  2622  	css_suite.expectBundled(t, bundled{
  2623  		files: map[string]string{
  2624  			"/entry.css": `
  2625  				a {
  2626  					background: url(foo.copy);
  2627  					background: url(foo.file);
  2628  				}
  2629  
  2630  				/*! The URLs for "foo 2" files must have quotes in the final CSS */
  2631  				b {
  2632  					background: url('foo 2.copy');
  2633  					background: url('foo 2.file');
  2634  				}
  2635  			`,
  2636  			"/foo.file":   `...`,
  2637  			"/foo.copy":   `...`,
  2638  			"/foo 2.file": `...`,
  2639  			"/foo 2.copy": `...`,
  2640  		},
  2641  		entryPaths: []string{"/entry.css"},
  2642  		options: config.Options{
  2643  			Mode:          config.ModeBundle,
  2644  			AbsOutputFile: "/out.css",
  2645  			MinifySyntax:  true,
  2646  			ExtensionToLoader: map[string]config.Loader{
  2647  				".css":  config.LoaderCSS,
  2648  				".file": config.LoaderFile,
  2649  				".copy": config.LoaderCopy,
  2650  			},
  2651  		},
  2652  	})
  2653  }