diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cbd9e29bf..7eb5cfe757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,20 @@ The above code will be considered valid starting with this release. This change to esbuild follows a [similar change to TypeScript](https://github.com/microsoft/TypeScript/pull/60225) which will allow this syntax starting with TypeScript 5.7. +* Allow quoted property names in `--define` and `--pure` ([#4008](https://github.com/evanw/esbuild/issues/4008)) + + The `define` and `pure` API options now accept identifier expressions containing quoted property names. Previously all identifiers in the identifier expression had to be bare identifiers. This change now makes `--define` and `--pure` consistent with `--global-name`, which already supported quoted property names. For example, the following is now possible: + + ```js + // The following code now transforms to "return true;\n" + console.log(esbuild.transformSync( + `return process.env['SOME-TEST-VAR']`, + { define: { 'process.env["SOME-TEST-VAR"]': 'true' } }, + )) + ``` + + Note that if you're passing values like this on the command line using esbuild's `--define` flag, then you'll need to know how to escape quote characters for your shell. You may find esbuild's JavaScript API more ergonomic and portable than writing shell code. + * Minify empty `try`/`catch`/`finally` blocks ([#4003](https://github.com/evanw/esbuild/issues/4003)) With this release, esbuild will now attempt to minify empty `try` blocks: diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index 69757d2cc3..d294d366b9 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -386,11 +386,11 @@ func validateSupported(log logger.Log, supported map[string]bool) ( return } -func validateGlobalName(log logger.Log, text string) []string { +func validateGlobalName(log logger.Log, text string, path string) []string { if text != "" { source := logger.Source{ - KeyPath: logger.Path{Text: "(global path)"}, - PrettyPath: "(global name)", + KeyPath: logger.Path{Text: path}, + PrettyPath: path, Contents: text, } @@ -560,20 +560,11 @@ func validateDefines( for _, key := range sortedKeys { value := defines[key] - keyParts := strings.Split(key, ".") - mapKey := mapKeyForDefine(keyParts) - - // The key must be a dot-separated identifier list - for _, part := range keyParts { - if !js_ast.IsIdentifier(part) { - if part == key { - log.AddError(nil, logger.Range{}, fmt.Sprintf("The define key %q must be a valid identifier", key)) - } else { - log.AddError(nil, logger.Range{}, fmt.Sprintf("The define key %q contains invalid identifier %q", key, part)) - } - continue - } + keyParts := validateGlobalName(log, key, "(define name)") + if keyParts == nil { + continue } + mapKey := mapKeyForDefine(keyParts) // Parse the value defineExpr, injectExpr := js_parser.ParseDefineExprOrJSON(value) @@ -678,16 +669,11 @@ func validateDefines( } for _, key := range pureFns { - keyParts := strings.Split(key, ".") - mapKey := mapKeyForDefine(keyParts) - - // The key must be a dot-separated identifier list - for _, part := range keyParts { - if !js_ast.IsIdentifier(part) { - log.AddError(nil, logger.Range{}, fmt.Sprintf("Invalid pure function: %q", key)) - continue - } + keyParts := validateGlobalName(log, key, "(pure name)") + if keyParts == nil { + continue } + mapKey := mapKeyForDefine(keyParts) // Merge with any previously-specified defines define := rawDefines[mapKey] @@ -1285,7 +1271,7 @@ func validateBuildOptions( ASCIIOnly: validateASCIIOnly(buildOpts.Charset), IgnoreDCEAnnotations: buildOpts.IgnoreAnnotations, TreeShaking: validateTreeShaking(buildOpts.TreeShaking, buildOpts.Bundle, buildOpts.Format), - GlobalName: validateGlobalName(log, buildOpts.GlobalName), + GlobalName: validateGlobalName(log, buildOpts.GlobalName, "(global name)"), CodeSplitting: buildOpts.Splitting, OutputFormat: validateFormat(buildOpts.Format), AbsOutputFile: validatePath(log, realFS, buildOpts.Outfile, "outfile path"), @@ -1729,7 +1715,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult SourceRoot: transformOpts.SourceRoot, ExcludeSourcesContent: transformOpts.SourcesContent == SourcesContentExclude, OutputFormat: validateFormat(transformOpts.Format), - GlobalName: validateGlobalName(log, transformOpts.GlobalName), + GlobalName: validateGlobalName(log, transformOpts.GlobalName, "(global name)"), MinifySyntax: transformOpts.MinifySyntax, MinifyWhitespace: transformOpts.MinifyWhitespace, MinifyIdentifiers: transformOpts.MinifyIdentifiers, diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 534375a181..d9d6fea7a0 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -6088,7 +6088,7 @@ class Foo { assert.strictEqual(code, `if (x) ;\n`) }, - async define({ esbuild }) { + async defineProcessEnvNodeEnv({ esbuild }) { const define = { 'process.env.NODE_ENV': '"something"' } const { code: code1 } = await esbuild.transform(`console.log(process.env.NODE_ENV)`, { define }) @@ -6165,6 +6165,32 @@ class Foo { `) }, + async defineQuotedPropertyNameTransform({ esbuild }) { + const { code: code1 } = await esbuild.transform(`return x.y['z']`, { define: { 'x.y["z"]': 'true' } }) + assert.strictEqual(code1, `return true;\n`) + + const { code: code2 } = await esbuild.transform(`foo(x['y'].z, x.y['z'], x['y']['z'])`, { define: { 'x.y.z': 'true' } }) + assert.strictEqual(code2, `foo(true, true, true);\n`) + + const { code: code3 } = await esbuild.transform(`foo(x['y'].z, x.y['z'], x['y']['z'])`, { define: { 'x["y"].z': 'true' } }) + assert.strictEqual(code3, `foo(true, true, true);\n`) + + const { code: code4 } = await esbuild.transform(`foo(x['y'].z, x.y['z'], x['y']['z'])`, { define: { 'x.y["z"]': 'true' } }) + assert.strictEqual(code4, `foo(true, true, true);\n`) + + const { code: code5 } = await esbuild.transform(`foo(x['y'].z, x.y['z'], x['y']['z'])`, { define: { 'x["y"][\'z\']': 'true' } }) + assert.strictEqual(code5, `foo(true, true, true);\n`) + }, + + async defineQuotedPropertyNameBuild({ esbuild }) { + const { outputFiles } = await esbuild.build({ + stdin: { contents: `return process.env['SOME-TEST-VAR']` }, + define: { 'process.env["SOME-TEST-VAR"]': 'true' }, + write: false, + }) + assert.strictEqual(outputFiles[0].text, `return true;\n`) + }, + async json({ esbuild }) { const { code } = await esbuild.transform(`{ "x": "y" }`, { loader: 'json' }) assert.strictEqual(code, `module.exports = { x: "y" };\n`)