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 }