From 947f99fb085024ff711055d776b3982a75383d51 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Fri, 20 Dec 2024 11:38:31 -0500 Subject: [PATCH] fix #4010, fix #4012: `import.meta` regression --- CHANGELOG.md | 6 +++++ internal/js_parser/global_name_parser.go | 17 ++++++++++++-- internal/linker/linker.go | 30 ++++++++++++++++++------ scripts/js-api-tests.js | 30 ++++++++++++++++++++---- 4 files changed, 70 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d99d05c428e..2a111ed4be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +* Fix regression with `--define` and `import.meta` ([#4010](https://github.com/evanw/esbuild/issues/4010), [#4012](https://github.com/evanw/esbuild/issues/4012)) + + The previous change in version 0.24.1 to use a more expression-like parser for `define` values to allow quoted property names introduced a regression that removed the ability to use `--define:import.meta=...`. Even though `import` is normally a keyword that can't be used as an identifier, ES modules special-case the `import.meta` expression to behave like an identifier anyway. This change fixes the regression. + ## 0.24.1 * Allow `es2024` as a target in `tsconfig.json` ([#4004](https://github.com/evanw/esbuild/issues/4004)) diff --git a/internal/js_parser/global_name_parser.go b/internal/js_parser/global_name_parser.go index 0064990b75a..78649ead8aa 100644 --- a/internal/js_parser/global_name_parser.go +++ b/internal/js_parser/global_name_parser.go @@ -19,9 +19,22 @@ func ParseGlobalName(log logger.Log, source logger.Source) (result []string, ok lexer := js_lexer.NewLexerGlobalName(log, source) - // Start off with an identifier + // Start off with an identifier or a keyword that results in an object result = append(result, lexer.Identifier.String) - lexer.Expect(js_lexer.TIdentifier) + switch lexer.Token { + case js_lexer.TThis: + lexer.Next() + + case js_lexer.TImport: + // Handle "import.meta" + lexer.Next() + lexer.Expect(js_lexer.TDot) + result = append(result, lexer.Identifier.String) + lexer.ExpectContextualKeyword("meta") + + default: + lexer.Expect(js_lexer.TIdentifier) + } // Follow with dot or index expressions for lexer.Token != js_lexer.TEndOfFile { diff --git a/internal/linker/linker.go b/internal/linker/linker.go index a3271f204db..b2511cf439e 100644 --- a/internal/linker/linker.go +++ b/internal/linker/linker.go @@ -5973,7 +5973,7 @@ func (c *linkerContext) generateChunkJS(chunkIndex int, chunkWaitGroup *sync.Wai func (c *linkerContext) generateGlobalNamePrefix() string { var text string globalName := c.options.GlobalName - prefix := globalName[0] + prefix, globalName := globalName[0], globalName[1:] space := " " join := ";\n" @@ -5982,9 +5982,18 @@ func (c *linkerContext) generateGlobalNamePrefix() string { join = ";" } + // Assume the "this" and "import.meta" objects always exist + isExistingObject := prefix == "this" + if prefix == "import" && len(globalName) > 0 && globalName[0] == "meta" { + prefix, globalName = "import.meta", globalName[1:] + isExistingObject = true + } + // Use "||=" to make the code more compact when it's supported - if len(globalName) > 1 && !c.options.UnsupportedJSFeatures.Has(compat.LogicalAssignment) { - if js_printer.CanEscapeIdentifier(prefix, c.options.UnsupportedJSFeatures, c.options.ASCIIOnly) { + if len(globalName) > 0 && !c.options.UnsupportedJSFeatures.Has(compat.LogicalAssignment) { + if isExistingObject { + // Keep the prefix as it is + } else if js_printer.CanEscapeIdentifier(prefix, c.options.UnsupportedJSFeatures, c.options.ASCIIOnly) { if c.options.ASCIIOnly { prefix = string(js_printer.QuoteIdentifier(nil, prefix, c.options.UnsupportedJSFeatures)) } @@ -5992,7 +6001,7 @@ func (c *linkerContext) generateGlobalNamePrefix() string { } else { prefix = fmt.Sprintf("this[%s]", helpers.QuoteForJSON(prefix, c.options.ASCIIOnly)) } - for _, name := range globalName[1:] { + for _, name := range globalName { var dotOrIndex string if js_printer.CanEscapeIdentifier(name, c.options.UnsupportedJSFeatures, c.options.ASCIIOnly) { if c.options.ASCIIOnly { @@ -6002,12 +6011,19 @@ func (c *linkerContext) generateGlobalNamePrefix() string { } else { dotOrIndex = fmt.Sprintf("[%s]", helpers.QuoteForJSON(name, c.options.ASCIIOnly)) } - prefix = fmt.Sprintf("(%s%s||=%s{})%s", prefix, space, space, dotOrIndex) + if isExistingObject { + prefix = fmt.Sprintf("%s%s", prefix, dotOrIndex) + isExistingObject = false + } else { + prefix = fmt.Sprintf("(%s%s||=%s{})%s", prefix, space, space, dotOrIndex) + } } return fmt.Sprintf("%s%s%s=%s", text, prefix, space, space) } - if js_printer.CanEscapeIdentifier(prefix, c.options.UnsupportedJSFeatures, c.options.ASCIIOnly) { + if isExistingObject { + text = fmt.Sprintf("%s%s=%s", prefix, space, space) + } else if js_printer.CanEscapeIdentifier(prefix, c.options.UnsupportedJSFeatures, c.options.ASCIIOnly) { if c.options.ASCIIOnly { prefix = string(js_printer.QuoteIdentifier(nil, prefix, c.options.UnsupportedJSFeatures)) } @@ -6017,7 +6033,7 @@ func (c *linkerContext) generateGlobalNamePrefix() string { text = fmt.Sprintf("%s%s=%s", prefix, space, space) } - for _, name := range globalName[1:] { + for _, name := range globalName { oldPrefix := prefix if js_printer.CanEscapeIdentifier(name, c.options.UnsupportedJSFeatures, c.options.ASCIIOnly) { if c.options.ASCIIOnly { diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index d9d6fea7a06..cdd8021612e 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -6009,6 +6009,22 @@ class Foo { π["π 𐀀"]["𐀀"]["𐀀 π"] = `) }, + async iifeGlobalNameThis({ esbuild }) { + const { code } = await esbuild.transform(`export default 123`, { format: 'iife', globalName: 'this.foo.bar' }) + const globals = {} + vm.createContext(globals) + vm.runInContext(code, globals) + assert.strictEqual(globals.foo.bar.default, 123) + assert.strictEqual(code.slice(0, code.indexOf('(() => {\n')), `(this.foo ||= {}).bar = `) + }, + + async iifeGlobalNameImportMeta({ esbuild }) { + const { code } = await esbuild.transform(`export default 123`, { format: 'iife', globalName: 'import.meta.foo.bar' }) + const { default: import_meta } = await import('data:text/javascript,' + code + '\nexport default import.meta') + assert.strictEqual(import_meta.foo.bar.default, 123) + assert.strictEqual(code.slice(0, code.indexOf('(() => {\n')), `(import.meta.foo ||= {}).bar = `) + }, + async jsx({ esbuild }) { const { code } = await esbuild.transform(`console.log(
)`, { loader: 'jsx' }) assert.strictEqual(code, `console.log(/* @__PURE__ */ React.createElement("div", null));\n`) @@ -6123,13 +6139,19 @@ class Foo { }, async defineThis({ esbuild }) { - const { code } = await esbuild.transform(`console.log(a, b); export {}`, { define: { a: 'this', b: 'this.foo' }, format: 'esm' }) - assert.strictEqual(code, `console.log(void 0, (void 0).foo);\n`) + const { code: code1 } = await esbuild.transform(`console.log(a, b); export {}`, { define: { a: 'this', b: 'this.foo' }, format: 'esm' }) + assert.strictEqual(code1, `console.log(void 0, (void 0).foo);\n`) + + const { code: code2 } = await esbuild.transform(`console.log(this, this.x); export {}`, { define: { this: 'a', 'this.x': 'b' }, format: 'esm' }) + assert.strictEqual(code2, `console.log(a, b);\n`) }, async defineImportMetaESM({ esbuild }) { - const { code } = await esbuild.transform(`console.log(a, b); export {}`, { define: { a: 'import.meta', b: 'import.meta.foo' }, format: 'esm' }) - assert.strictEqual(code, `console.log(import.meta, import.meta.foo);\n`) + const { code: code1 } = await esbuild.transform(`console.log(a, b); export {}`, { define: { a: 'import.meta', b: 'import.meta.foo' }, format: 'esm' }) + assert.strictEqual(code1, `console.log(import.meta, import.meta.foo);\n`) + + const { code: code2 } = await esbuild.transform(`console.log(import.meta, import.meta.x); export {}`, { define: { 'import.meta': 'a', 'import.meta.x': 'b' }, format: 'esm' }) + assert.strictEqual(code2, `console.log(a, b);\n`) }, async defineImportMetaIIFE({ esbuild }) {