Skip to content

Commit

Permalink
fix #3797: import attributes and glob-style import
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jun 9, 2024
1 parent 98cb2ed commit 953dae9
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 10 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@
import tasty from "./tasty.bagel" with { type: "bagel" }
```
* Support import attributes with glob-style imports ([#3797](https://github.com/evanw/esbuild/issues/3797))
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:

```ts
async function loadLocale(locale: string): Locale {
const data = await import(`./locales/${locale}.data`, { with: { type: 'json' } })
return unpackLocale(locale, data)
}
```

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:
```
✘ [ERROR] No loader is configured for ".data" files: locales/en-US.data
example.ts:2:28:
2 │ const data = await import(`./locales/${locale}.data`, { with: { type: 'json' } })
╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~
```
In addition, this change means plugins can now access the contents of `with` for glob-style imports.
* Support `${configDir}` in `tsconfig.json` files ([#3782](https://github.com/evanw/esbuild/issues/3782))
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:
Expand Down
27 changes: 17 additions & 10 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,16 @@ func parseFile(args parseArgs) {
continue
}

// Encode the import attributes
var attrs logger.ImportAttributes
if record.AssertOrWith != nil && record.AssertOrWith.Keyword == ast.WithKeyword {
data := make(map[string]string, len(record.AssertOrWith.Entries))
for _, entry := range record.AssertOrWith.Entries {
data[helpers.UTF16ToString(entry.Key)] = helpers.UTF16ToString(entry.Value)
}
attrs = logger.EncodeImportAttributes(data)
}

// Special-case glob pattern imports
if record.GlobPattern != nil {
prettyPath := helpers.GlobPatternToString(record.GlobPattern.Parts)
Expand All @@ -451,6 +461,13 @@ func parseFile(args parseArgs) {
if result.globResolveResults == nil {
result.globResolveResults = make(map[uint32]globResolveResult)
}
for key, result := range results {
result.PathPair.Primary.ImportAttributes = attrs
if result.PathPair.HasSecondary() {
result.PathPair.Secondary.ImportAttributes = attrs
}
results[key] = result
}
result.globResolveResults[uint32(importRecordIndex)] = globResolveResult{
resolveResults: results,
absPath: args.fs.Join(absResolveDir, "(glob)"),
Expand All @@ -469,16 +486,6 @@ func parseFile(args parseArgs) {
continue
}

// Encode the import attributes
var attrs logger.ImportAttributes
if record.AssertOrWith != nil && record.AssertOrWith.Keyword == ast.WithKeyword {
data := make(map[string]string, len(record.AssertOrWith.Entries))
for _, entry := range record.AssertOrWith.Entries {
data[helpers.UTF16ToString(entry.Key)] = helpers.UTF16ToString(entry.Value)
}
attrs = logger.EncodeImportAttributes(data)
}

// Cache the path in case it's imported multiple times in this file
cacheKey := cacheKey{
kind: record.Kind,
Expand Down
15 changes: 15 additions & 0 deletions internal/bundler_tests/bundler_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,21 @@ func TestWithTypeJSONOverrideLoader(t *testing.T) {
})
}

func TestWithTypeJSONOverrideLoaderGlob(t *testing.T) {
loader_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import("./foo" + bar, { with: { type: 'json' } }).then(console.log)
`,
"/foo.js": `{ "this is json not js": true }`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
},
})
}

func TestWithBadType(t *testing.T) {
loader_suite.expectBundled(t, bundled{
files: map[string]string{
Expand Down
23 changes: 23 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_loader.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1035,3 +1035,26 @@ var foo_default = { "this is json not js": true };

// entry.js
console.log(foo_default);

================================================================================
TestWithTypeJSONOverrideLoaderGlob
---------- entry.js ----------
// foo.js
var foo_exports = {};
__export(foo_exports, {
default: () => foo_default
});
var foo_default;
var init_foo = __esm({
"foo.js"() {
foo_default = { "this is json not js": true };
}
});

// import("./foo*") in entry.js
var globImport_foo = __glob({
"./foo.js": () => Promise.resolve().then(() => (init_foo(), foo_exports))
});

// entry.js
globImport_foo("./foo" + bar).then(console.log);
32 changes: 32 additions & 0 deletions scripts/plugin-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2562,6 +2562,38 @@ console.log(foo_default, foo_default2);
`)
},

async importAttributesOnLoadGlob({ esbuild, testDir }) {
const entry = path.join(testDir, 'entry.js')
const foo = path.join(testDir, 'foo.js')
await writeFileAsync(entry, `
Promise.all([
import('./foo' + js, { with: { type: 'cheese' } }),
import('./foo' + js, { with: { pizza: 'true' } }),
]).then(resolve)
`)
await writeFileAsync(foo, `export default 123`)
const result = await esbuild.build({
entryPoints: [entry],
bundle: true,
format: 'esm',
charset: 'utf8',
write: false,
plugins: [{
name: 'name',
setup(build) {
build.onLoad({ filter: /.*/ }, args => {
if (args.with.type === 'cheese') return { contents: `export default "🧀"` }
if (args.with.pizza === 'true') return { contents: `export default "🍕"` }
})
},
}],
})
const callback = new Function('js', 'resolve', result.outputFiles[0].text)
const [cheese, pizza] = await new Promise(resolve => callback('.js', resolve))
assert.strictEqual(cheese.default, '🧀')
assert.strictEqual(pizza.default, '🍕')
},

async importAttributesResolve({ esbuild }) {
const onResolve = []
const resolve = []
Expand Down

0 comments on commit 953dae9

Please sign in to comment.