Skip to content

Commit 953dae9

Browse files
committed
fix #3797: import attributes and glob-style import
1 parent 98cb2ed commit 953dae9

File tree

5 files changed

+110
-10
lines changed

5 files changed

+110
-10
lines changed

CHANGELOG.md

+23
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@
2020
import tasty from "./tasty.bagel" with { type: "bagel" }
2121
```
2222
23+
* Support import attributes with glob-style imports ([#3797](https://github.com/evanw/esbuild/issues/3797))
24+
25+
This release adds support for import attributes (the `with` option) to glob-style imports (dynamic imports with certain string literal patterns as paths). These imports previously didn't support import attributes due to an oversight. So code like this will now work correctly:
26+
27+
```ts
28+
async function loadLocale(locale: string): Locale {
29+
const data = await import(`./locales/${locale}.data`, { with: { type: 'json' } })
30+
return unpackLocale(locale, data)
31+
}
32+
```
33+
34+
Previously this didn't work even though esbuild normally supports forcing the JSON loader using an import attribute. Attempting to do this used to result in the following error:
35+
36+
```
37+
✘ [ERROR] No loader is configured for ".data" files: locales/en-US.data
38+
39+
example.ts:2:28:
40+
2 │ const data = await import(`./locales/${locale}.data`, { with: { type: 'json' } })
41+
╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~
42+
```
43+
44+
In addition, this change means plugins can now access the contents of `with` for glob-style imports.
45+
2346
* Support `${configDir}` in `tsconfig.json` files ([#3782](https://github.com/evanw/esbuild/issues/3782))
2447
2548
This adds support for a new feature from the upcoming TypeScript 5.5 release. The character sequence `${configDir}` is now respected at the start of `baseUrl` and `paths` values, which are used by esbuild during bundling to correctly map import paths to file system paths. This feature lets base `tsconfig.json` files specified via `extends` refer to the directory of the top-level `tsconfig.json` file. Here is an example:

internal/bundler/bundler.go

+17-10
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,16 @@ func parseFile(args parseArgs) {
435435
continue
436436
}
437437

438+
// Encode the import attributes
439+
var attrs logger.ImportAttributes
440+
if record.AssertOrWith != nil && record.AssertOrWith.Keyword == ast.WithKeyword {
441+
data := make(map[string]string, len(record.AssertOrWith.Entries))
442+
for _, entry := range record.AssertOrWith.Entries {
443+
data[helpers.UTF16ToString(entry.Key)] = helpers.UTF16ToString(entry.Value)
444+
}
445+
attrs = logger.EncodeImportAttributes(data)
446+
}
447+
438448
// Special-case glob pattern imports
439449
if record.GlobPattern != nil {
440450
prettyPath := helpers.GlobPatternToString(record.GlobPattern.Parts)
@@ -451,6 +461,13 @@ func parseFile(args parseArgs) {
451461
if result.globResolveResults == nil {
452462
result.globResolveResults = make(map[uint32]globResolveResult)
453463
}
464+
for key, result := range results {
465+
result.PathPair.Primary.ImportAttributes = attrs
466+
if result.PathPair.HasSecondary() {
467+
result.PathPair.Secondary.ImportAttributes = attrs
468+
}
469+
results[key] = result
470+
}
454471
result.globResolveResults[uint32(importRecordIndex)] = globResolveResult{
455472
resolveResults: results,
456473
absPath: args.fs.Join(absResolveDir, "(glob)"),
@@ -469,16 +486,6 @@ func parseFile(args parseArgs) {
469486
continue
470487
}
471488

472-
// Encode the import attributes
473-
var attrs logger.ImportAttributes
474-
if record.AssertOrWith != nil && record.AssertOrWith.Keyword == ast.WithKeyword {
475-
data := make(map[string]string, len(record.AssertOrWith.Entries))
476-
for _, entry := range record.AssertOrWith.Entries {
477-
data[helpers.UTF16ToString(entry.Key)] = helpers.UTF16ToString(entry.Value)
478-
}
479-
attrs = logger.EncodeImportAttributes(data)
480-
}
481-
482489
// Cache the path in case it's imported multiple times in this file
483490
cacheKey := cacheKey{
484491
kind: record.Kind,

internal/bundler_tests/bundler_loader_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,21 @@ func TestWithTypeJSONOverrideLoader(t *testing.T) {
13031303
})
13041304
}
13051305

1306+
func TestWithTypeJSONOverrideLoaderGlob(t *testing.T) {
1307+
loader_suite.expectBundled(t, bundled{
1308+
files: map[string]string{
1309+
"/entry.js": `
1310+
import("./foo" + bar, { with: { type: 'json' } }).then(console.log)
1311+
`,
1312+
"/foo.js": `{ "this is json not js": true }`,
1313+
},
1314+
entryPaths: []string{"/entry.js"},
1315+
options: config.Options{
1316+
Mode: config.ModeBundle,
1317+
},
1318+
})
1319+
}
1320+
13061321
func TestWithBadType(t *testing.T) {
13071322
loader_suite.expectBundled(t, bundled{
13081323
files: map[string]string{

internal/bundler_tests/snapshots/snapshots_loader.txt

+23
Original file line numberDiff line numberDiff line change
@@ -1035,3 +1035,26 @@ var foo_default = { "this is json not js": true };
10351035

10361036
// entry.js
10371037
console.log(foo_default);
1038+
1039+
================================================================================
1040+
TestWithTypeJSONOverrideLoaderGlob
1041+
---------- entry.js ----------
1042+
// foo.js
1043+
var foo_exports = {};
1044+
__export(foo_exports, {
1045+
default: () => foo_default
1046+
});
1047+
var foo_default;
1048+
var init_foo = __esm({
1049+
"foo.js"() {
1050+
foo_default = { "this is json not js": true };
1051+
}
1052+
});
1053+
1054+
// import("./foo*") in entry.js
1055+
var globImport_foo = __glob({
1056+
"./foo.js": () => Promise.resolve().then(() => (init_foo(), foo_exports))
1057+
});
1058+
1059+
// entry.js
1060+
globImport_foo("./foo" + bar).then(console.log);

scripts/plugin-tests.js

+32
Original file line numberDiff line numberDiff line change
@@ -2562,6 +2562,38 @@ console.log(foo_default, foo_default2);
25622562
`)
25632563
},
25642564

2565+
async importAttributesOnLoadGlob({ esbuild, testDir }) {
2566+
const entry = path.join(testDir, 'entry.js')
2567+
const foo = path.join(testDir, 'foo.js')
2568+
await writeFileAsync(entry, `
2569+
Promise.all([
2570+
import('./foo' + js, { with: { type: 'cheese' } }),
2571+
import('./foo' + js, { with: { pizza: 'true' } }),
2572+
]).then(resolve)
2573+
`)
2574+
await writeFileAsync(foo, `export default 123`)
2575+
const result = await esbuild.build({
2576+
entryPoints: [entry],
2577+
bundle: true,
2578+
format: 'esm',
2579+
charset: 'utf8',
2580+
write: false,
2581+
plugins: [{
2582+
name: 'name',
2583+
setup(build) {
2584+
build.onLoad({ filter: /.*/ }, args => {
2585+
if (args.with.type === 'cheese') return { contents: `export default "🧀"` }
2586+
if (args.with.pizza === 'true') return { contents: `export default "🍕"` }
2587+
})
2588+
},
2589+
}],
2590+
})
2591+
const callback = new Function('js', 'resolve', result.outputFiles[0].text)
2592+
const [cheese, pizza] = await new Promise(resolve => callback('.js', resolve))
2593+
assert.strictEqual(cheese.default, '🧀')
2594+
assert.strictEqual(pizza.default, '🍕')
2595+
},
2596+
25652597
async importAttributesResolve({ esbuild }) {
25662598
const onResolve = []
25672599
const resolve = []

0 commit comments

Comments
 (0)