diff --git a/CHANGELOG.md b/CHANGELOG.md index 81354646958..884273be1cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,10 @@ Note that using this feature means esbuild will potentially do a lot of file system I/O to find all possible files that might match the pattern. This is by design, and is not a bug. If this is a concern, I recommend either avoiding the `/**/` pattern (e.g. by not putting a `/` before a wildcard) or using this feature only in directory subtrees which do not have many files that don't match the pattern (e.g. making a subdirectory for your JSON files and explicitly including that subdirectory in the pattern). +* Use the `local-css` loader for `.module.css` files by default ([#20](https://github.com/evanw/esbuild/issues/20)) + + With this release the `css` loader is still used for `.css` files except that `.module.css` files now use the `local-css` loader. This is a common convention in the web development community. If you need `.module.css` files to use the `css` loader instead, then you can override this behavior with `--loader:.module.css=css`. + ## 0.18.20 * Support advanced CSS `@import` rules ([#953](https://github.com/evanw/esbuild/issues/953), [#3137](https://github.com/evanw/esbuild/issues/3137)) diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 604c255369e..47e5b713334 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -2552,18 +2552,19 @@ func (s *scanner) validateTLA(sourceIndex uint32) tlaCheck { func DefaultExtensionToLoaderMap() map[string]config.Loader { return map[string]config.Loader{ - "": config.LoaderJS, // This represents files without an extension - ".js": config.LoaderJS, - ".mjs": config.LoaderJS, - ".cjs": config.LoaderJS, - ".jsx": config.LoaderJSX, - ".ts": config.LoaderTS, - ".cts": config.LoaderTSNoAmbiguousLessThan, - ".mts": config.LoaderTSNoAmbiguousLessThan, - ".tsx": config.LoaderTSX, - ".css": config.LoaderCSS, - ".json": config.LoaderJSON, - ".txt": config.LoaderText, + "": config.LoaderJS, // This represents files without an extension + ".js": config.LoaderJS, + ".mjs": config.LoaderJS, + ".cjs": config.LoaderJS, + ".jsx": config.LoaderJSX, + ".ts": config.LoaderTS, + ".cts": config.LoaderTSNoAmbiguousLessThan, + ".mts": config.LoaderTSNoAmbiguousLessThan, + ".tsx": config.LoaderTSX, + ".css": config.LoaderCSS, + ".module.css": config.LoaderLocalCSS, + ".json": config.LoaderJSON, + ".txt": config.LoaderText, } } diff --git a/internal/bundler_tests/snapshots/snapshots_css.txt b/internal/bundler_tests/snapshots/snapshots_css.txt index e9dd440d6f9..55b02d1534f 100644 --- a/internal/bundler_tests/snapshots/snapshots_css.txt +++ b/internal/bundler_tests/snapshots/snapshots_css.txt @@ -1810,17 +1810,17 @@ TestIgnoreURLsInAtRulePrelude TestImportCSSFromJSComposes ---------- /out/entry.js ---------- // styles.module.css -var styles_module_default = { - local0: "GLOBAL1 GLOBAL2 styles_module_local4 styles_module_local3 styles_module_local1 GLOBAL3 styles_module_local2 GLOBAL4 styles_module_local0", - local1: "styles_module_local4 styles_module_local3 styles_module_local1", - local2: "styles_module_local2", - local3: "styles_module_local4 styles_module_local3", - local4: "styles_module_local4", - fromOtherFile: "base_module_base1 base_module_base2 other1_module_local0 base_module_base3 other2_module_local0 styles_module_fromOtherFile" +var styles_default = { + local0: "GLOBAL1 GLOBAL2 styles_local4 styles_local3 styles_local1 GLOBAL3 styles_local2 GLOBAL4 styles_local0", + local1: "styles_local4 styles_local3 styles_local1", + local2: "styles_local2", + local3: "styles_local4 styles_local3", + local4: "styles_local4", + fromOtherFile: "base_base1 base_base2 other1_local0 base_base3 other2_local0 styles_fromOtherFile" }; // entry.js -console.log(styles_module_default); +console.log(styles_default); ---------- /out/entry.css ---------- /* global.css */ @@ -1829,44 +1829,44 @@ console.log(styles_module_default); } /* other1.module.css */ -.other1_module_local0 { +.other1_local0 { color: blue; } /* base.module.css */ -.base_module_base1 { +.base_base1 { cursor: pointer; } -.base_module_base2 { +.base_base2 { display: inline; } -.base_module_base3 { +.base_base3 { float: left; } /* other2.module.css */ -.other2_module_local0 { +.other2_local0 { background: purple; } /* styles.module.css */ -.styles_module_local0 { +.styles_local0 { } -.styles_module_local0 { +.styles_local0 { background: green; } -.styles_module_local0 { +.styles_local0 { } -.styles_module_local3 { +.styles_local3 { border: 1px solid black; } -.styles_module_local4 { +.styles_local4 { opacity: 0.5; } -.styles_module_local1 { +.styles_local1 { color: red; } -.styles_module_fromOtherFile { +.styles_fromOtherFile { } ================================================================================ diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 7a682c0d647..54f9efd30b0 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -323,7 +323,18 @@ func PlatformIndependentPathDirBaseExt(path string) (dir string, base string, ex // Strip off the extension if dot := strings.LastIndexByte(base, '.'); dot >= 0 { - base, ext = base[:dot], base[dot:] + ext = base[dot:] + + // We default to the "local-css" loader for ".module.css" files. Make sure + // the string names generated by this don't all have "_module_" in them. + if ext == ".css" { + if dot2 := strings.LastIndexByte(base[:dot], '.'); dot2 >= 0 && base[dot2:] == ".module.css" { + dot = dot2 + ext = base[dot:] + } + } + + base = base[:dot] } return } diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index 8130136e49e..9c86ea3285d 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -7845,7 +7845,7 @@ tests.push( }), ) -// Test CSS-related warning ranges +// Tests for CSS modules tests.push( test(['in.js', '--outfile=node.js', '--bundle', '--loader:.css=local-css'], { 'in.js': ` @@ -7871,6 +7871,42 @@ tests.push( `, }), + test(['in.js', '--outfile=node.js', '--bundle'], { + 'in.js': ` + import * as foo_styles from "./foo.css" + import * as bar_styles from "./bar" + const { foo } = foo_styles + const { bar } = bar_styles + if (foo !== void 0) throw 'fail: foo=' + foo + if (bar !== void 0) throw 'fail: bar=' + bar + `, + 'foo.css': `.foo { color: red }`, + 'bar.css': `.bar { color: green }`, + }), + test(['in.js', '--outfile=node.js', '--bundle'], { + 'in.js': ` + import * as foo_styles from "./foo.module.css" + import * as bar_styles from "./bar.module" + const { foo } = foo_styles + const { bar } = bar_styles + if (foo !== 'foo_foo') throw 'fail: foo=' + foo + if (bar !== 'bar_bar') throw 'fail: bar=' + bar + `, + 'foo.module.css': `.foo { color: red }`, + 'bar.module.css': `.bar { color: green }`, + }), + test(['in.js', '--outfile=node.js', '--bundle', '--loader:.module.css=css'], { + 'in.js': ` + import * as foo_styles from "./foo.module.css" + import * as bar_styles from "./bar.module" + const { foo } = foo_styles + const { bar } = bar_styles + if (foo !== void 0) throw 'fail: foo=' + foo + if (bar !== void 0) throw 'fail: bar=' + bar + `, + 'foo.module.css': `.foo { color: red }`, + 'bar.module.css': `.bar { color: green }`, + }), ) // Test writing to stdout