Skip to content

Commit

Permalink
fix #2753: add an empty loader
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Dec 16, 2022
1 parent 10505f9 commit aee4010
Show file tree
Hide file tree
Showing 12 changed files with 278 additions and 23 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,10 @@

## Unreleased

* Add the `empty` loader ([#1541](https://github.com/evanw/esbuild/issues/1541), [#2753](https://github.com/evanw/esbuild/issues/2753))

The new `empty` loader tells esbuild to pretend that a file is empty. So for example `--loader:.css=empty` effectively skips all imports of `.css` files in JavaScript so that they aren't included in the bundle, since `import "./some-empty-file"` in JavaScript doesn't bundle anything. You can also use the `empty` loader to remove asset references in CSS files. For example `--loader:.png=empty` causes esbuild to replace asset references such as `url(image.png)` with `url()` so that they are no longer included in the resulting style sheet.

* Fix `</script>` and `</style>` escaping for non-default targets ([#2748](https://github.com/evanw/esbuild/issues/2748))

The change in version 0.16.0 to give control over `</script>` escaping via `--supported:inline-script=false` or `--supported:inline-script=true` accidentally broke automatic escaping of `</script>` when an explicit `target` setting is specified. This release restores the correct automatic escaping of `</script>` (which should not depend on what `target` is set to).
Expand Down
4 changes: 2 additions & 2 deletions cmd/esbuild/main.go
Expand Up @@ -38,8 +38,8 @@ var helpText = func(colors logger.Colors) string {
bundling, otherwise default is iife when platform
is browser and cjs when platform is node)
--loader:X=L Use loader L to load file extension X, where L is
one of: base64 | binary | copy | css | dataurl | file |
js | json | jsx | text | ts | tsx
one of: base64 | binary | copy | css | dataurl |
empty | file | js | json | jsx | text | ts | tsx
--minify Minify the output (sets all --minify-* flags)
--outdir=... The output directory (for multiple entry points)
--outfile=... The output file (for one entry point)
Expand Down
3 changes: 3 additions & 0 deletions internal/ast/ast.go
Expand Up @@ -119,6 +119,9 @@ const (

// If true, do not generate "external": true in the metafile
ShouldNotBeExternalInMetafile

// CSS "@import" of an empty file should be removed
WasLoadedWithEmptyLoader
)

func (flags ImportRecordFlags) Has(flag ImportRecordFlags) bool {
Expand Down
12 changes: 8 additions & 4 deletions internal/bundler/bundler.go
Expand Up @@ -160,6 +160,10 @@ func parseFile(args parseArgs) {
loader = loaderFromFileExtension(args.options.ExtensionToLoader, base+ext)
}

if loader == config.LoaderEmpty {
source.Contents = ""
}

result := parseResult{
file: scannerFile{
inputFile: graph.InputFile{
Expand All @@ -182,7 +186,7 @@ func parseFile(args parseArgs) {
}()

switch loader {
case config.LoaderJS:
case config.LoaderJS, config.LoaderEmpty:
ast, ok := args.caches.JSCache.Parse(args.log, source, js_parser.OptionsFromConfig(&args.options))
if len(ast.Parts) <= 1 { // Ignore the implicitly-generated namespace export part
result.file.inputFile.SideEffects.Kind = graph.NoSideEffects_EmptyAST
Expand Down Expand Up @@ -974,7 +978,7 @@ func runOnLoadPlugins(

// Force disabled modules to be empty
if source.KeyPath.IsDisabled() {
return loaderPluginResult{loader: config.LoaderJS}, true
return loaderPluginResult{loader: config.LoaderEmpty}, true
}

// Read normal modules from disk
Expand Down Expand Up @@ -1885,7 +1889,7 @@ func (s *scanner) processScannedFiles(entryPointMeta []graph.EntryPoint) []scann
switch record.Kind {
case ast.ImportAt, ast.ImportAtConditional:
// Using a JavaScript file with CSS "@import" is not allowed
if _, ok := otherFile.inputFile.Repr.(*graph.JSRepr); ok {
if _, ok := otherFile.inputFile.Repr.(*graph.JSRepr); ok && otherFile.inputFile.Loader != config.LoaderEmpty {
s.log.AddError(&tracker, record.Range,
fmt.Sprintf("Cannot import %q into a CSS file", otherFile.inputFile.Source.PrettyPath))
} else if record.Kind == ast.ImportAtConditional {
Expand All @@ -1901,7 +1905,7 @@ func (s *scanner) processScannedFiles(entryPointMeta []graph.EntryPoint) []scann
fmt.Sprintf("Cannot use %q as a URL", otherFile.inputFile.Source.PrettyPath))

case *graph.JSRepr:
if otherRepr.AST.URLForCSS == "" {
if otherRepr.AST.URLForCSS == "" && otherFile.inputFile.Loader != config.LoaderEmpty {
s.log.AddError(&tracker, record.Range,
fmt.Sprintf("Cannot use %q as a URL", otherFile.inputFile.Source.PrettyPath))
}
Expand Down
53 changes: 53 additions & 0 deletions internal/bundler/bundler_loader_test.go
Expand Up @@ -1284,3 +1284,56 @@ NOTE: You need to either reconfigure esbuild to ensure that the loader for this
`,
})
}

func TestEmptyLoaderJS(t *testing.T) {
loader_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import './a.empty'
import * as ns from './b.empty'
import def from './c.empty'
import { named } from './d.empty'
console.log(ns, def, named)
`,
"/a.empty": `throw 'FAIL'`,
"/b.empty": `throw 'FAIL'`,
"/c.empty": `throw 'FAIL'`,
"/d.empty": `throw 'FAIL'`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
SourceMap: config.SourceMapExternalWithoutComment,
NeedsMetafile: true,
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".empty": config.LoaderEmpty,
},
},
expectedCompileLog: `entry.js: WARNING: Import "named" will always be undefined because the file "d.empty" has no exports
`,
})
}

func TestEmptyLoaderCSS(t *testing.T) {
loader_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@import 'a.empty';
a { background: url(b.empty) }
`,
"/a.empty": `body { color: fail }`,
"/b.empty": `fail`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
SourceMap: config.SourceMapExternalWithoutComment,
NeedsMetafile: true,
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderCSS,
".empty": config.LoaderEmpty,
},
},
})
}
15 changes: 7 additions & 8 deletions internal/bundler/linker.go
Expand Up @@ -1267,7 +1267,11 @@ func (c *linkerContext) scanImportsAndExports() {
record.Path.Text = otherRepr.AST.URLForCSS
record.Path.Namespace = ""
record.SourceIndex = ast.Index32{}
record.Flags |= ast.ShouldNotBeExternalInMetafile
if otherFile.InputFile.Loader == config.LoaderEmpty {
record.Flags |= ast.WasLoadedWithEmptyLoader
} else {
record.Flags |= ast.ShouldNotBeExternalInMetafile
}

// Copy the additional files to the output directory
additionalFiles = append(additionalFiles, otherFile.InputFile.AdditionalFiles...)
Expand Down Expand Up @@ -2758,13 +2762,8 @@ func (c *linkerContext) advanceImportTracker(tracker importTracker) (importTrack
return importTracker{}, importExternal, nil
}

// Is this a disabled file?
otherSourceIndex := record.SourceIndex.GetIndex()
if c.graph.Files[otherSourceIndex].InputFile.Source.KeyPath.IsDisabled() {
return importTracker{sourceIndex: otherSourceIndex, importRef: js_ast.InvalidRef}, importDisabled, nil
}

// Is this a named import of a file without any exports?
otherSourceIndex := record.SourceIndex.GetIndex()
otherRepr := c.graph.Files[otherSourceIndex].InputFile.Repr.(*graph.JSRepr)
if !namedImport.AliasIsStar && !otherRepr.AST.HasLazyExport &&
// CommonJS exports
Expand Down Expand Up @@ -3124,7 +3123,7 @@ func (c *linkerContext) findImportedFilesInCSSOrder(entryPoints []uint32) (exter
if record := &repr.AST.ImportRecords[atImport.ImportRecordIndex]; record.SourceIndex.IsValid() {
// Follow internal dependencies
visit(record.SourceIndex.GetIndex())
} else {
} else if (record.Flags & ast.WasLoadedWithEmptyLoader) == 0 {
// Record external dependencies
external := externals[record.Path]

Expand Down
163 changes: 163 additions & 0 deletions internal/bundler/snapshots/snapshots_loader.txt
Expand Up @@ -10,6 +10,169 @@ var require_test = __commonJS({
// entry.js
console.log(require_test());

================================================================================
TestEmptyLoaderCSS
---------- entry.css.map ----------
{
"version": 3,
"sources": ["entry.css"],
"sourcesContent": ["\n\t\t\t\t@import 'a.empty';\n\t\t\t\ta { background: url(b.empty) }\n\t\t\t"],
"mappings": ";AAEI;AAAI;AAAA;",
"names": []
}

---------- entry.css ----------
/* entry.css */
a {
background: url();
}
---------- metafile.json ----------
{
"inputs": {
"a.empty": {
"bytes": 0,
"imports": []
},
"b.empty": {
"bytes": 0,
"imports": []
},
"entry.css": {
"bytes": 62,
"imports": [
{
"path": "a.empty",
"kind": "import-rule"
},
{
"path": "b.empty",
"kind": "url-token"
}
]
}
},
"outputs": {
"entry.css.map": {
"imports": [],
"exports": [],
"inputs": {},
"bytes": 198
},
"entry.css": {
"imports": [
{
"path": "",
"kind": "url-token",
"external": true
}
],
"entryPoint": "entry.css",
"inputs": {
"entry.css": {
"bytesInOutput": 27
}
},
"bytes": 43
}
}
}

================================================================================
TestEmptyLoaderJS
---------- entry.js.map ----------
{
"version": 3,
"sources": ["entry.js"],
"sourcesContent": ["\n\t\t\t\timport './a.empty'\n\t\t\t\timport * as ns from './b.empty'\n\t\t\t\timport def from './c.empty'\n\t\t\t\timport { named } from './d.empty'\n\t\t\t\tconsole.log(ns, def, named)\n\t\t\t"],
"mappings": ";;;;;;;;;;;;;AAEI,SAAoB;AACpB,eAAgB;AAEhB,QAAQ,IAAI,IAAI,SAAAA,SAAK,MAAK;",
"names": ["def"]
}

---------- entry.js ----------
// b.empty
var require_b = __commonJS({
"b.empty"() {
}
});

// c.empty
var require_c = __commonJS({
"c.empty"() {
}
});

// entry.js
var ns = __toESM(require_b());
var import_c = __toESM(require_c());
console.log(ns, import_c.default, void 0);
---------- metafile.json ----------
{
"inputs": {
"a.empty": {
"bytes": 0,
"imports": []
},
"b.empty": {
"bytes": 0,
"imports": []
},
"c.empty": {
"bytes": 0,
"imports": []
},
"d.empty": {
"bytes": 0,
"imports": []
},
"entry.js": {
"bytes": 165,
"imports": [
{
"path": "a.empty",
"kind": "import-statement"
},
{
"path": "b.empty",
"kind": "import-statement"
},
{
"path": "c.empty",
"kind": "import-statement"
},
{
"path": "d.empty",
"kind": "import-statement"
}
]
}
},
"outputs": {
"entry.js.map": {
"imports": [],
"exports": [],
"inputs": {},
"bytes": 377
},
"entry.js": {
"imports": [],
"exports": [],
"entryPoint": "entry.js",
"inputs": {
"b.empty": {
"bytesInOutput": 53
},
"c.empty": {
"bytesInOutput": 53
},
"entry.js": {
"bytesInOutput": 111
}
},
"bytes": 253
}
}
}

================================================================================
TestJSXAutomaticNoNameCollision
---------- /out.js ----------
Expand Down
4 changes: 3 additions & 1 deletion internal/cli_helpers/cli_helpers.go
Expand Up @@ -35,6 +35,8 @@ func ParseLoader(text string) (api.Loader, *ErrorWithNote) {
return api.LoaderDataURL, nil
case "default":
return api.LoaderDefault, nil
case "empty":
return api.LoaderEmpty, nil
case "file":
return api.LoaderFile, nil
case "js":
Expand All @@ -52,7 +54,7 @@ func ParseLoader(text string) (api.Loader, *ErrorWithNote) {
default:
return api.LoaderNone, MakeErrorWithNote(
fmt.Sprintf("Invalid loader value: %q", text),
"Valid values are \"base64\", \"binary\", \"copy\", \"css\", \"dataurl\", \"file\", \"js\", \"json\", \"jsx\", \"text\", \"ts\", or \"tsx\".",
"Valid values are \"base64\", \"binary\", \"copy\", \"css\", \"dataurl\", \"empty\", \"file\", \"js\", \"json\", \"jsx\", \"text\", \"ts\", or \"tsx\".",
)
}
}
2 changes: 2 additions & 0 deletions internal/config/config.go
Expand Up @@ -96,6 +96,7 @@ const (
LoaderCSS
LoaderDataURL
LoaderDefault
LoaderEmpty
LoaderFile
LoaderJS
LoaderJSON
Expand All @@ -114,6 +115,7 @@ var LoaderToString = []string{
"css",
"dataurl",
"default",
"empty",
"file",
"js",
"json",
Expand Down
1 change: 1 addition & 0 deletions pkg/api/api.go
Expand Up @@ -138,6 +138,7 @@ const (
LoaderCSS
LoaderDataURL
LoaderDefault
LoaderEmpty
LoaderFile
LoaderJS
LoaderJSON
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/api_impl.go
Expand Up @@ -234,6 +234,8 @@ func validateLoader(value Loader) config.Loader {
return config.LoaderDataURL
case LoaderDefault:
return config.LoaderDefault
case LoaderEmpty:
return config.LoaderEmpty
case LoaderFile:
return config.LoaderFile
case LoaderJS:
Expand Down

0 comments on commit aee4010

Please sign in to comment.