Skip to content

Commit db1b8ca

Browse files
committed
fix #3792: import attributes and the copy loader
1 parent de572d0 commit db1b8ca

File tree

4 files changed

+118
-30
lines changed

4 files changed

+118
-30
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
## Unreleased
44

5+
* Allow unknown import attributes to be used with the `copy` loader ([#3792](https://github.com/evanw/esbuild/issues/3792))
6+
7+
Import attributes (the `with` keyword on `import` statements) are allowed to alter how that path is loaded. For example, esbuild cannot assume that it knows how to load `./bagel.js` as type `bagel`:
8+
9+
```js
10+
// This is an error with "--bundle" without also using "--external:./bagel.js"
11+
import tasty from "./bagel.js" with { type: "bagel" }
12+
```
13+
14+
Because of that, bundling this code with esbuild is an error unless the file `./bagel.js` is external to the bundle (such as with `--bundle --external:./bagel.js`).
15+
16+
However, there is an additional case where it's ok for esbuild to allow this: if the file is loaded using the `copy` loader. That's because the `copy` loader behaves similarly to `--external` in that the file is left external to the bundle. The difference is that the `copy` loader copies the file into the output folder and rewrites the import path while `--external` doesn't. That means the following will now work with the `copy` loader (such as with `--bundle --loader:.bagel=copy`):
17+
18+
```js
19+
// This is no longer an error with "--bundle" and "--loader:.bagel=copy"
20+
import tasty from "./tasty.bagel" with { type: "bagel" }
21+
```
22+
523
* Fix internal error with `--supported:object-accessors=false` ([#3794](https://github.com/evanw/esbuild/issues/3794))
624
725
This release fixes a regression in 0.21.0 where some code that was added to esbuild's internal runtime library of helper functions for JavaScript decorators fails to parse when you configure esbuild with `--supported:object-accessors=false`. The reason is that esbuild introduced code that does `{ get [name]() {} }` which uses both the `object-extensions` feature for the `[name]` and the `object-accessors` feature for the `get`, but esbuild was incorrectly only checking for `object-extensions` and not for `object-accessors`. Additional tests have been added to avoid this type of issue in the future. A workaround for this issue in earlier releases is to also add `--supported:object-extensions=false`.

internal/bundler/bundler.go

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ func parseFile(args parseArgs) {
149149
&source,
150150
args.importSource,
151151
args.importPathRange,
152-
args.importWith,
153152
args.pluginData,
154153
args.options.WatchMode,
155154
)
@@ -175,6 +174,44 @@ func parseFile(args parseArgs) {
175174
loader = loaderFromFileExtension(args.options.ExtensionToLoader, base+ext)
176175
}
177176

177+
// Reject unsupported import attributes when the loader isn't "copy" (since
178+
// "copy" is kind of like "external"). But only do this if this file was not
179+
// loaded by a plugin. Plugins are allowed to assign whatever semantics they
180+
// want to import attributes.
181+
if loader != config.LoaderCopy && pluginName == "" {
182+
for _, attr := range source.KeyPath.ImportAttributes.DecodeIntoArray() {
183+
var errorText string
184+
var errorRange js_lexer.KeyOrValue
185+
186+
// We only currently handle "type: json"
187+
if attr.Key != "type" {
188+
errorText = fmt.Sprintf("Importing with the %q attribute is not supported", attr.Key)
189+
errorRange = js_lexer.KeyRange
190+
} else if attr.Value == "json" {
191+
loader = config.LoaderWithTypeJSON
192+
continue
193+
} else {
194+
errorText = fmt.Sprintf("Importing with a type attribute of %q is not supported", attr.Value)
195+
errorRange = js_lexer.ValueRange
196+
}
197+
198+
// Everything else is an error
199+
r := args.importPathRange
200+
if args.importWith != nil {
201+
r = js_lexer.RangeOfImportAssertOrWith(*args.importSource, *ast.FindAssertOrWithEntry(args.importWith.Entries, attr.Key), errorRange)
202+
}
203+
tracker := logger.MakeLineColumnTracker(args.importSource)
204+
args.log.AddError(&tracker, r, errorText)
205+
if args.inject != nil {
206+
args.inject <- config.InjectedFile{
207+
Source: source,
208+
}
209+
}
210+
args.results <- parseResult{}
211+
return
212+
}
213+
}
214+
178215
if loader == config.LoaderEmpty {
179216
source.Contents = ""
180217
}
@@ -991,7 +1028,6 @@ func runOnLoadPlugins(
9911028
source *logger.Source,
9921029
importSource *logger.Source,
9931030
importPathRange logger.Range,
994-
importWith *ast.ImportAssertOrWith,
9951031
pluginData interface{},
9961032
isWatchMode bool,
9971033
) (loaderPluginResult, bool) {
@@ -1058,30 +1094,6 @@ func runOnLoadPlugins(
10581094
}
10591095
}
10601096

1061-
// Reject unsupported import attributes
1062-
loader := config.LoaderDefault
1063-
for _, attr := range source.KeyPath.ImportAttributes.DecodeIntoArray() {
1064-
if attr.Key == "type" {
1065-
if attr.Value == "json" {
1066-
loader = config.LoaderWithTypeJSON
1067-
} else {
1068-
r := importPathRange
1069-
if importWith != nil {
1070-
r = js_lexer.RangeOfImportAssertOrWith(*importSource, *ast.FindAssertOrWithEntry(importWith.Entries, attr.Key), js_lexer.ValueRange)
1071-
}
1072-
log.AddError(&tracker, r, fmt.Sprintf("Importing with a type attribute of %q is not supported", attr.Value))
1073-
return loaderPluginResult{}, false
1074-
}
1075-
} else {
1076-
r := importPathRange
1077-
if importWith != nil {
1078-
r = js_lexer.RangeOfImportAssertOrWith(*importSource, *ast.FindAssertOrWithEntry(importWith.Entries, attr.Key), js_lexer.KeyRange)
1079-
}
1080-
log.AddError(&tracker, r, fmt.Sprintf("Importing with the %q attribute is not supported", attr.Key))
1081-
return loaderPluginResult{}, false
1082-
}
1083-
}
1084-
10851097
// Force disabled modules to be empty
10861098
if source.KeyPath.IsDisabled() {
10871099
return loaderPluginResult{loader: config.LoaderEmpty}, true
@@ -1092,7 +1104,7 @@ func runOnLoadPlugins(
10921104
if contents, err, originalError := fsCache.ReadFile(fs, source.KeyPath.Text); err == nil {
10931105
source.Contents = contents
10941106
return loaderPluginResult{
1095-
loader: loader,
1107+
loader: config.LoaderDefault,
10961108
absResolveDir: fs.Dir(source.KeyPath.Text),
10971109
}, true
10981110
} else {
@@ -1121,9 +1133,6 @@ func runOnLoadPlugins(
11211133
return loaderPluginResult{loader: config.LoaderNone}, true
11221134
} else {
11231135
source.Contents = contents
1124-
if loader != config.LoaderDefault {
1125-
return loaderPluginResult{loader: loader}, true
1126-
}
11271136
if mimeType := parsed.DecodeMIMEType(); mimeType != resolver.MIMETypeUnsupported {
11281137
switch mimeType {
11291138
case resolver.MIMETypeTextCSS:

internal/bundler_tests/bundler_loader_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,55 @@ func TestLoaderBundleWithImportAttributes(t *testing.T) {
16141614
})
16151615
}
16161616

1617+
func TestLoaderBundleWithUnknownImportAttributesAndJSLoader(t *testing.T) {
1618+
loader_suite.expectBundled(t, bundled{
1619+
files: map[string]string{
1620+
"/entry.js": `
1621+
import foo from "./foo.js" with { type: 'js' }
1622+
import bar from "./bar.js" with { js: 'true' }
1623+
import foo2 from "data:text/javascript,foo" with { type: 'js' }
1624+
import bar2 from "data:text/javascript,bar" with { js: 'true' }
1625+
console.log(foo, bar, foo2, bar2)
1626+
`,
1627+
"/foo.js": `...`,
1628+
"/bar.js": `,,,`,
1629+
},
1630+
entryPaths: []string{"/entry.js"},
1631+
options: config.Options{
1632+
Mode: config.ModeBundle,
1633+
AbsOutputFile: "/out.js",
1634+
},
1635+
expectedScanLog: `entry.js: ERROR: Importing with a type attribute of "js" is not supported
1636+
entry.js: ERROR: Importing with the "js" attribute is not supported
1637+
entry.js: ERROR: Importing with a type attribute of "js" is not supported
1638+
entry.js: ERROR: Importing with the "js" attribute is not supported
1639+
`,
1640+
})
1641+
}
1642+
1643+
func TestLoaderBundleWithUnknownImportAttributesAndCopyLoader(t *testing.T) {
1644+
loader_suite.expectBundled(t, bundled{
1645+
files: map[string]string{
1646+
"/entry.js": `
1647+
import foo from "./foo.thing" with { type: 'whatever' }
1648+
import bar from "./bar.thing" with { whatever: 'true' }
1649+
console.log(foo, bar)
1650+
`,
1651+
"/foo.thing": `...`,
1652+
"/bar.thing": `,,,`,
1653+
},
1654+
entryPaths: []string{"/entry.js"},
1655+
options: config.Options{
1656+
Mode: config.ModeBundle,
1657+
ExtensionToLoader: map[string]config.Loader{
1658+
".js": config.LoaderJS,
1659+
".thing": config.LoaderCopy,
1660+
},
1661+
AbsOutputFile: "/out.js",
1662+
},
1663+
})
1664+
}
1665+
16171666
func TestLoaderBundleWithTypeJSONOnlyDefaultExport(t *testing.T) {
16181667
loader_suite.expectBundled(t, bundled{
16191668
files: map[string]string{

internal/bundler_tests/snapshots/snapshots_loader.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,18 @@ var data_default2 = { works: true };
268268
// entry.js
269269
console.log(data_default === data_default, data_default !== data_default2);
270270

271+
================================================================================
272+
TestLoaderBundleWithUnknownImportAttributesAndCopyLoader
273+
---------- /foo-AKINYSFH.thing ----------
274+
...
275+
---------- /bar-AXZXSLHF.thing ----------
276+
,,,
277+
---------- /out.js ----------
278+
// entry.js
279+
import foo from "./foo-AKINYSFH.thing" with { type: "whatever" };
280+
import bar from "./bar-AXZXSLHF.thing" with { whatever: "true" };
281+
console.log(foo, bar);
282+
271283
================================================================================
272284
TestLoaderCopyEntryPointAdvanced
273285
---------- /out/xyz-DYPYXS7B.copy ----------

0 commit comments

Comments
 (0)