diff --git a/CHANGELOG.md b/CHANGELOG.md index f74c20627af..ea331be5d1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,20 @@ In addition, adding `assert { type: 'json' }` to an import statement now means esbuild will generate an error if the loader for the file is anything other than `json`, which is required by the import assertion specification. +* Provide a way to disable automatic escaping of `` ([#2649](https://github.com/evanw/esbuild/issues/2649)) + + If you inject esbuild's output into a script tag in an HTML file, code containing the literal characters `` will cause the tag to be ended early which will break the code: + + ```html + "); + + ``` + + To avoid this, esbuild automatically escapes these strings in generated JavaScript files (e.g. `""` becomes `"<\/script>"` instead). This also applies to `` in generated CSS files. Previously this always happened and there wasn't a way to turn this off. + + With this release, esbuild will now only do this if the `platform` setting is set to `browser` (the default value). Setting `platform` to `node` or `neutral` will disable this behavior. This behavior can also now be disabled with `--supported:inline-script=false` (for JS) and `--supported:inline-style=false` (for CSS). + * Throw an early error if decoded UTF-8 text isn't a `Uint8Array` ([#2532](https://github.com/evanw/esbuild/issues/2532)) If you run esbuild's JavaScript API in a broken JavaScript environment where `new TextEncoder().encode("") instanceof Uint8Array` is false, then esbuild's API will fail with a confusing serialization error message that makes it seem like esbuild has a bug even though the real problem is that the JavaScript environment itself is broken. This can happen when using the test framework called [Jest](https://jestjs.io/). With this release, esbuild's API will now throw earlier when it detects that the environment is unable to encode UTF-8 text correctly with an error message that makes it more clear that this is not a problem with esbuild. diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 420a3917484..1951313b45a 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -2216,6 +2216,17 @@ func applyOptionDefaults(options *config.Options) { compat.ClassField|compat.ClassPrivateAccessor|compat.ClassPrivateBrandCheck|compat.ClassPrivateField| compat.ClassPrivateMethod|compat.ClassPrivateStaticAccessor|compat.ClassPrivateStaticField| compat.ClassPrivateStaticMethod|compat.ClassStaticBlocks|compat.ClassStaticField) + + // If we're not building for the browser, automatically disable support for + // inline and tags if there aren't currently any overrides + if options.Platform != config.PlatformBrowser { + if !options.UnsupportedJSFeatureOverridesMask.Has(compat.InlineScript) { + options.UnsupportedJSFeatures |= compat.InlineScript + } + if !options.UnsupportedCSSFeatureOverridesMask.Has(compat.InlineStyle) { + options.UnsupportedCSSFeatures |= compat.InlineStyle + } + } } func fixInvalidUnsupportedJSFeatureOverrides(options *config.Options, implies compat.JSFeature, implied compat.JSFeature) { diff --git a/internal/bundler/linker.go b/internal/bundler/linker.go index c4fdbaa6590..a13dd82f8d9 100644 --- a/internal/bundler/linker.go +++ b/internal/bundler/linker.go @@ -5322,13 +5322,14 @@ func (c *linkerContext) generateChunkCSS(chunks []chunkInfo, chunkIndex int, chu } cssOptions := css_printer.Options{ - MinifyWhitespace: c.options.MinifyWhitespace, - ASCIIOnly: c.options.ASCIIOnly, - LegalComments: c.options.LegalComments, - SourceMap: c.options.SourceMap, - AddSourceMappings: addSourceMappings, - InputSourceMap: inputSourceMap, - LineOffsetTables: lineOffsetTables, + MinifyWhitespace: c.options.MinifyWhitespace, + ASCIIOnly: c.options.ASCIIOnly, + LegalComments: c.options.LegalComments, + SourceMap: c.options.SourceMap, + UnsupportedFeatures: c.options.UnsupportedCSSFeatures, + AddSourceMappings: addSourceMappings, + InputSourceMap: inputSourceMap, + LineOffsetTables: lineOffsetTables, } compileResult.PrintResult = css_printer.Print(asts[i], cssOptions) compileResult.sourceIndex = sourceIndex diff --git a/internal/compat/css_table.go b/internal/compat/css_table.go index 81932fdc11e..4d5cc13e619 100644 --- a/internal/compat/css_table.go +++ b/internal/compat/css_table.go @@ -4,7 +4,7 @@ type CSSFeature uint8 const ( HexRGBA CSSFeature = 1 << iota - + InlineStyle RebeccaPurple // This feature includes all of the following: @@ -20,20 +20,13 @@ const ( var StringToCSSFeature = map[string]CSSFeature{ "hex-rgba": HexRGBA, + "inline-style": InlineStyle, "rebecca-purple": RebeccaPurple, "modern-rgb-hsl": Modern_RGB_HSL, "inset-property": InsetProperty, "nesting": Nesting, } -var CSSFeatureToString = map[CSSFeature]string{ - HexRGBA: "hex-rgba", - RebeccaPurple: "rebecca-purple", - Modern_RGB_HSL: "modern-rgb-hsl", - InsetProperty: "inset-property", - Nesting: "nesting", -} - func (features CSSFeature) Has(feature CSSFeature) bool { return (features & feature) != 0 } diff --git a/internal/compat/js_table.go b/internal/compat/js_table.go index 4615a9ab695..8a313e3f139 100644 --- a/internal/compat/js_table.go +++ b/internal/compat/js_table.go @@ -81,6 +81,7 @@ const ( Hashbang ImportAssertions ImportMeta + InlineScript LogicalAssignment NestedRestBinding NewTarget @@ -135,6 +136,7 @@ var StringToJSFeature = map[string]JSFeature{ "hashbang": Hashbang, "import-assertions": ImportAssertions, "import-meta": ImportMeta, + "inline-script": InlineScript, "logical-assignment": LogicalAssignment, "nested-rest-binding": NestedRestBinding, "new-target": NewTarget, @@ -159,60 +161,6 @@ var StringToJSFeature = map[string]JSFeature{ "unicode-escapes": UnicodeEscapes, } -var JSFeatureToString = map[JSFeature]string{ - ArbitraryModuleNamespaceNames: "arbitrary-module-namespace-names", - ArraySpread: "array-spread", - Arrow: "arrow", - AsyncAwait: "async-await", - AsyncGenerator: "async-generator", - Bigint: "bigint", - Class: "class", - ClassField: "class-field", - ClassPrivateAccessor: "class-private-accessor", - ClassPrivateBrandCheck: "class-private-brand-check", - ClassPrivateField: "class-private-field", - ClassPrivateMethod: "class-private-method", - ClassPrivateStaticAccessor: "class-private-static-accessor", - ClassPrivateStaticField: "class-private-static-field", - ClassPrivateStaticMethod: "class-private-static-method", - ClassStaticBlocks: "class-static-blocks", - ClassStaticField: "class-static-field", - ConstAndLet: "const-and-let", - DefaultArgument: "default-argument", - Destructuring: "destructuring", - DynamicImport: "dynamic-import", - ExponentOperator: "exponent-operator", - ExportStarAs: "export-star-as", - ForAwait: "for-await", - ForOf: "for-of", - Generator: "generator", - Hashbang: "hashbang", - ImportAssertions: "import-assertions", - ImportMeta: "import-meta", - LogicalAssignment: "logical-assignment", - NestedRestBinding: "nested-rest-binding", - NewTarget: "new-target", - NodeColonPrefixImport: "node-colon-prefix-import", - NodeColonPrefixRequire: "node-colon-prefix-require", - NullishCoalescing: "nullish-coalescing", - ObjectAccessors: "object-accessors", - ObjectExtensions: "object-extensions", - ObjectRestSpread: "object-rest-spread", - OptionalCatchBinding: "optional-catch-binding", - OptionalChain: "optional-chain", - RegexpDotAllFlag: "regexp-dot-all-flag", - RegexpLookbehindAssertions: "regexp-lookbehind-assertions", - RegexpMatchIndices: "regexp-match-indices", - RegexpNamedCaptureGroups: "regexp-named-capture-groups", - RegexpStickyAndUnicodeFlags: "regexp-sticky-and-unicode-flags", - RegexpUnicodePropertyEscapes: "regexp-unicode-property-escapes", - RestArgument: "rest-argument", - TemplateLiteral: "template-literal", - TopLevelAwait: "top-level-await", - TypeofExoticObjectIsObject: "typeof-exotic-object-is-object", - UnicodeEscapes: "unicode-escapes", -} - func (features JSFeature) Has(feature JSFeature) bool { return (features & feature) != 0 } @@ -527,6 +475,7 @@ var jsTable = map[JSFeature]map[Engine][]versionRange{ Opera: {{start: v{51, 0, 0}}}, Safari: {{start: v{11, 1, 0}}}, }, + InlineScript: {}, LogicalAssignment: { Chrome: {{start: v{85, 0, 0}}}, Deno: {{start: v{1, 2, 0}}}, diff --git a/internal/css_printer/css_printer.go b/internal/css_printer/css_printer.go index 088a27686b8..7ac83c0b76f 100644 --- a/internal/css_printer/css_printer.go +++ b/internal/css_printer/css_printer.go @@ -6,6 +6,7 @@ import ( "unicode/utf8" "github.com/evanw/esbuild/internal/ast" + "github.com/evanw/esbuild/internal/compat" "github.com/evanw/esbuild/internal/config" "github.com/evanw/esbuild/internal/css_ast" "github.com/evanw/esbuild/internal/css_lexer" @@ -32,11 +33,12 @@ type Options struct { // us do binary search on to figure out what line a given AST node came from LineOffsetTables []sourcemap.LineOffsetTable - MinifyWhitespace bool - ASCIIOnly bool - SourceMap config.SourceMap - AddSourceMappings bool - LegalComments config.LegalComments + UnsupportedFeatures compat.CSSFeature + MinifyWhitespace bool + ASCIIOnly bool + SourceMap config.SourceMap + AddSourceMappings bool + LegalComments config.LegalComments } type PrintResult struct { @@ -268,7 +270,9 @@ func (p *printer) printRule(rule css_ast.Rule, indent int32, omitTrailingSemicol func (p *printer) printIndentedComment(indent int32, text string) { // Avoid generating a comment containing the character sequence "= 1 && text[i-1] == '<' && i+6 <= len(text) && strings.EqualFold(text[i+1:i+6], "style") { + if !p.options.UnsupportedFeatures.Has(compat.InlineStyle) && i >= 1 && text[i-1] == '<' && i+6 <= len(text) && strings.EqualFold(text[i+1:i+6], "style") { escape = escapeBackslash } diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index e66529100d1..7bbc9b9ae52 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -12654,7 +12654,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO // Lower tagged template literals that include "= 2 && text[i-2] == '<' && i+6 <= len(text) { + if !p.options.UnsupportedFeatures.Has(compat.InlineScript) && i >= 2 && text[i-2] == '<' && i+6 <= len(text) { script := "script" matches := true for j := 0; j < 6; j++ { @@ -2378,7 +2378,7 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFla n := len(buffer) // Avoid forming a single-line comment or " 0 { + if !p.options.UnsupportedFeatures.Has(compat.InlineScript) && n > 0 { if last := buffer[n-1]; last == '/' || (last == '<' && len(e.Value) >= 7 && strings.EqualFold(e.Value[:7], "/script")) { p.print(" ") } @@ -3053,7 +3053,9 @@ func (p *printer) printIf(s *js_ast.SIf) { func (p *printer) printIndentedComment(text string) { // Avoid generating a comment containing the character sequence "" in JS and "" +// in CSS. Otherwise the containing HTML tag will be ended early. +mergeVersions('InlineScript', {}) + // This is a special case. Node added support for it to both v12.20+ and v13.2+ // so the range is inconveniently discontiguous. Sources: // @@ -433,10 +438,6 @@ var StringToJSFeature = map[string]JSFeature{ ${simpleMap(Object.keys(versions).sort().map(x => [`"${jsFeatureString(x)}"`, x]))} } -var JSFeatureToString = map[JSFeature]string{ -${simpleMap(Object.keys(versions).sort().map(x => [x, `"${jsFeatureString(x)}"`]))} -} - func (features JSFeature) Has(feature JSFeature) bool { \treturn (features & feature) != 0 } diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index d205adae7e0..93fb87ee898 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -5212,6 +5212,50 @@ let transformTests = { assert.strictEqual(fromPromiseResolve(code4), `Promise.resolve().then(function(){return __toESM(require(foo))});\n`) }, + async inlineScript({ esbuild }) { + let p + assert.strictEqual((await esbuild.transform(`x = ''`, {})).code, `x = "<\\/script>";\n`) + assert.strictEqual((await esbuild.transform(`x = ' inline'`, { supported: { 'inline-script': true } })).code, `x = "<\\/script> inline";\n`) + assert.strictEqual((await esbuild.transform(`x = ' noinline'`, { supported: { 'inline-script': false } })).code, `x = " noinline";\n`) + + p = { platform: 'browser' } + assert.strictEqual((await esbuild.transform(`x = ' browser'`, { ...p })).code, `x = "<\\/script> browser";\n`) + assert.strictEqual((await esbuild.transform(`x = ' browser inline'`, { ...p, supported: { 'inline-script': true } })).code, `x = "<\\/script> browser inline";\n`) + assert.strictEqual((await esbuild.transform(`x = ' browser noinline'`, { ...p, supported: { 'inline-script': false } })).code, `x = " browser noinline";\n`) + + p = { platform: 'node' } + assert.strictEqual((await esbuild.transform(`x = ' node'`, { ...p })).code, `x = " node";\n`) + assert.strictEqual((await esbuild.transform(`x = ' node inline'`, { ...p, supported: { 'inline-script': true } })).code, `x = "<\\/script> node inline";\n`) + assert.strictEqual((await esbuild.transform(`x = ' node noinline'`, { ...p, supported: { 'inline-script': false } })).code, `x = " node noinline";\n`) + + p = { platform: 'neutral' } + assert.strictEqual((await esbuild.transform(`x = ' neutral'`, { ...p })).code, `x = " neutral";\n`) + assert.strictEqual((await esbuild.transform(`x = ' neutral inline'`, { ...p, supported: { 'inline-script': true } })).code, `x = "<\\/script> neutral inline";\n`) + assert.strictEqual((await esbuild.transform(`x = ' neutral noinline'`, { ...p, supported: { 'inline-script': false } })).code, `x = " neutral noinline";\n`) + }, + + async inlineStyle({ esbuild }) { + let p = { loader: 'css' } + assert.strictEqual((await esbuild.transform(`x { y: '' }`, { ...p })).code, `x {\n y: "<\\/style>";\n}\n`) + assert.strictEqual((await esbuild.transform(`x { y: ' inline' }`, { ...p, supported: { 'inline-style': true } })).code, `x {\n y: "<\\/style> inline";\n}\n`) + assert.strictEqual((await esbuild.transform(`x { y: ' noinline' }`, { ...p, supported: { 'inline-style': false } })).code, `x {\n y: " noinline";\n}\n`) + + p = { loader: 'css', platform: 'browser' } + assert.strictEqual((await esbuild.transform(`x { y: ' browser' }`, { ...p })).code, `x {\n y: "<\\/style> browser";\n}\n`) + assert.strictEqual((await esbuild.transform(`x { y: ' browser inline' }`, { ...p, supported: { 'inline-style': true } })).code, `x {\n y: "<\\/style> browser inline";\n}\n`) + assert.strictEqual((await esbuild.transform(`x { y: ' browser noinline' }`, { ...p, supported: { 'inline-style': false } })).code, `x {\n y: " browser noinline";\n}\n`) + + p = { loader: 'css', platform: 'node' } + assert.strictEqual((await esbuild.transform(`x { y: ' node' }`, { ...p })).code, `x {\n y: " node";\n}\n`) + assert.strictEqual((await esbuild.transform(`x { y: ' node inline' }`, { ...p, supported: { 'inline-style': true } })).code, `x {\n y: "<\\/style> node inline";\n}\n`) + assert.strictEqual((await esbuild.transform(`x { y: ' node noinline' }`, { ...p, supported: { 'inline-style': false } })).code, `x {\n y: " node noinline";\n}\n`) + + p = { loader: 'css', platform: 'neutral' } + assert.strictEqual((await esbuild.transform(`x { y: ' neutral' }`, { ...p })).code, `x {\n y: " neutral";\n}\n`) + assert.strictEqual((await esbuild.transform(`x { y: ' neutral inline' }`, { ...p, supported: { 'inline-style': true } })).code, `x {\n y: "<\\/style> neutral inline";\n}\n`) + assert.strictEqual((await esbuild.transform(`x { y: ' neutral noinline' }`, { ...p, supported: { 'inline-style': false } })).code, `x {\n y: " neutral noinline";\n}\n`) + }, + async typeofEqualsUndefinedTarget({ esbuild }) { assert.strictEqual((await esbuild.transform(`a = typeof b !== 'undefined'`, { minify: true })).code, `a=typeof b<"u";\n`) assert.strictEqual((await esbuild.transform(`a = typeof b !== 'undefined'`, { minify: true, target: 'es2020' })).code, `a=typeof b<"u";\n`)