Skip to content

Commit e04a690

Browse files
committed
fix #3605: print the original JSX AST unmodified
1 parent f571399 commit e04a690

File tree

6 files changed

+125
-186
lines changed

6 files changed

+125
-186
lines changed

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,30 @@
22

33
## Unreleased
44

5+
* The "preserve" JSX mode now preserves JSX text verbatim ([#3605](https://github.com/evanw/esbuild/issues/3605))
6+
7+
The [JSX specification](https://facebook.github.io/jsx/) deliberately doesn't specify how JSX text is supposed to be interpreted and there is no canonical way to interpret JSX text. Two most popular interpretations are Babel and TypeScript. Yes [they are different](https://twitter.com/jarredsumner/status/1456118847937781764) (esbuild [deliberately follows TypeScript](https://twitter.com/evanwallace/status/1456122279453208576) by the way). Previously esbuild normalized text to the TypeScript interpretation when the "preserve" JSX mode is active. However, "preserve" should arguably reproduce the original JSX text verbatim so that whatever JSX transform runs after esbuild is free to interpret it however it wants. So with this release, esbuild will now pass JSX text through unmodified:
8+
9+
```jsx
10+
// Original code
11+
let el =
12+
<a href={'/'} title='&apos;&quot;'> some text
13+
{foo}
14+
more text </a>
15+
16+
// Old output (with --loader=jsx --jsx=preserve)
17+
let el = <a href="/" title={`'"`}>
18+
{" some text"}
19+
{foo}
20+
{"more text "}
21+
</a>;
22+
23+
// New output (with --loader=jsx --jsx=preserve)
24+
let el = <a href={"/"} title='&apos;&quot;'> some text
25+
{foo}
26+
more text </a>;
27+
```
28+
529
* Allow JSX elements as JSX attribute values
630

731
JSX has an obscure feature where you can use JSX elements in attribute position without surrounding them with `{...}`. It looks like this:

internal/bundler_tests/snapshots/snapshots_default.txt

Lines changed: 18 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -711,48 +711,24 @@ console.log(
711711
x
712712
}</>,
713713
// Comments on absent AST nodes
714-
<div>
715-
{"before"}
716-
{}
717-
{"after"}
718-
</div>,
719-
<div>
720-
{"before"}
721-
{
722-
/* comment 1 */
723-
/* comment 2 */
724-
}
725-
{"after"}
726-
</div>,
727-
<div>
728-
{"before"}
729-
{
730-
// comment 1
731-
// comment 2
732-
}
733-
{"after"}
734-
</div>,
735-
<>
736-
{"before"}
737-
{}
738-
{"after"}
739-
</>,
740-
<>
741-
{"before"}
742-
{
743-
/* comment 1 */
744-
/* comment 2 */
745-
}
746-
{"after"}
747-
</>,
748-
<>
749-
{"before"}
750-
{
751-
// comment 1
752-
// comment 2
753-
}
754-
{"after"}
755-
</>
714+
<div>before{}after</div>,
715+
<div>before{
716+
/* comment 1 */
717+
/* comment 2 */
718+
}after</div>,
719+
<div>before{
720+
// comment 1
721+
// comment 2
722+
}after</div>,
723+
<>before{}after</>,
724+
<>before{
725+
/* comment 1 */
726+
/* comment 2 */
727+
}after</>,
728+
<>before{
729+
// comment 1
730+
// comment 2
731+
}after</>
756732
);
757733

758734
================================================================================

internal/js_ast/js_ast.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ func (*EImportIdentifier) isExpr() {}
449449
func (*EPrivateIdentifier) isExpr() {}
450450
func (*ENameOfSymbol) isExpr() {}
451451
func (*EJSXElement) isExpr() {}
452+
func (*EJSXText) isExpr() {}
452453
func (*EMissing) isExpr() {}
453454
func (*ENumber) isExpr() {}
454455
func (*EBigInt) isExpr() {}
@@ -770,6 +771,16 @@ type EJSXElement struct {
770771
IsTagSingleLine bool
771772
}
772773

774+
// The JSX specification doesn't say how JSX text is supposed to be interpreted
775+
// so our "preserve" JSX transform should reproduce the original source code
776+
// verbatim. One reason why this matters is because there is no canonical way
777+
// to interpret JSX text (Babel and TypeScript differ in what newlines mean).
778+
// Another reason is that some people want to do custom things such as this:
779+
// https://github.com/evanw/esbuild/issues/3605
780+
type EJSXText struct {
781+
Raw string
782+
}
783+
773784
type ENumber struct{ Value float64 }
774785

775786
type EBigInt struct{ Value string }

internal/js_parser/js_parser.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5043,12 +5043,17 @@ func (p *parser) parseJSXElement(loc logger.Loc) js_ast.Expr {
50435043
if p.lexer.PreviousBackslashQuoteInJSX.Loc.Start > stringLoc.Start {
50445044
previousStringWithBackslashLoc = stringLoc
50455045
}
5046-
value = js_ast.Expr{Loc: stringLoc, Data: &js_ast.EString{Value: p.lexer.StringLiteral()}}
5046+
if p.options.jsx.Preserve {
5047+
value = js_ast.Expr{Loc: stringLoc, Data: &js_ast.EJSXText{Raw: p.lexer.Raw()}}
5048+
} else {
5049+
value = js_ast.Expr{Loc: stringLoc, Data: &js_ast.EString{Value: p.lexer.StringLiteral()}}
5050+
}
50475051
p.lexer.NextInsideJSXElement()
50485052
} else if p.lexer.Token == js_lexer.TLessThan {
50495053
// This may be removed in the future: https://github.com/facebook/jsx/issues/53
50505054
loc := p.lexer.Loc()
50515055
p.lexer.NextInsideJSXElement()
5056+
flags |= js_ast.PropertyWasShorthand
50525057
value = p.parseJSXElement(loc)
50535058

50545059
// The call to parseJSXElement() above doesn't consume the last
@@ -5199,7 +5204,11 @@ func (p *parser) parseJSXElement(loc logger.Loc) js_ast.Expr {
51995204
for {
52005205
switch p.lexer.Token {
52015206
case js_lexer.TStringLiteral:
5202-
nullableChildren = append(nullableChildren, js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.EString{Value: p.lexer.StringLiteral()}})
5207+
if p.options.jsx.Preserve {
5208+
nullableChildren = append(nullableChildren, js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.EJSXText{Raw: p.lexer.Raw()}})
5209+
} else {
5210+
nullableChildren = append(nullableChildren, js_ast.Expr{Loc: p.lexer.Loc(), Data: &js_ast.EString{Value: p.lexer.StringLiteral()}})
5211+
}
52035212
p.lexer.NextJSXElementChild()
52045213

52055214
case js_lexer.TOpenBrace:
@@ -5209,7 +5218,7 @@ func (p *parser) parseJSXElement(loc logger.Loc) js_ast.Expr {
52095218
// The expression is optional, and may be absent
52105219
if p.lexer.Token == js_lexer.TCloseBrace {
52115220
// Save comments even for absent expressions
5212-
nullableChildren = append(nullableChildren, js_ast.Expr{Loc: p.saveExprCommentsHere()})
5221+
nullableChildren = append(nullableChildren, js_ast.Expr{Loc: p.saveExprCommentsHere(), Data: nil})
52135222
} else {
52145223
if p.lexer.Token == js_lexer.TDotDotDot {
52155224
// TypeScript preserves "..." before JSX child expressions here.
@@ -12689,7 +12698,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1268912698
// it doesn't affect these mitigations by ensuring that the mitigations are not
1269012699
// applied in those cases (e.g. by adding an additional conditional check).
1269112700
switch e := expr.Data.(type) {
12692-
case *js_ast.ENull, *js_ast.ESuper, *js_ast.EBoolean, *js_ast.EBigInt, *js_ast.EUndefined:
12701+
case *js_ast.ENull, *js_ast.ESuper, *js_ast.EBoolean, *js_ast.EBigInt, *js_ast.EUndefined, *js_ast.EJSXText:
1269312702

1269412703
case *js_ast.ENameOfSymbol:
1269512704
e.Ref = p.symbolForMangledProp(p.loadNameFromRef(e.Ref))
@@ -13097,7 +13106,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1309713106
propName := helpers.UTF16ToString(str.Value)
1309813107
switch propName {
1309913108
case "key":
13100-
if property.Flags.Has(js_ast.PropertyWasShorthand) {
13109+
if boolean, ok := property.ValueOrNil.Data.(*js_ast.EBoolean); ok && boolean.Value && property.Flags.Has(js_ast.PropertyWasShorthand) {
1310113110
r := js_lexer.RangeOfIdentifier(p.source, property.Loc)
1310213111
msg := logger.Msg{
1310313112
Kind: logger.Error,

internal/js_printer/js_printer.go

Lines changed: 18 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -246,80 +246,6 @@ func (p *printer) printUnquotedUTF16(text []uint16, quote rune, flags printQuote
246246
p.js = js
247247
}
248248

249-
// Use JS strings for JSX attributes that need escape characters. Technically
250-
// the JSX specification doesn't say anything about using XML character escape
251-
// sequences, so JSX implementations may not be able to consume them. See
252-
// https://facebook.github.io/jsx/ for the specification.
253-
func (p *printer) canPrintTextAsJSXAttribute(text []uint16) (quote string, ok bool) {
254-
single := true
255-
double := true
256-
257-
for _, c := range text {
258-
// Use JS strings for control characters
259-
if c < firstASCII {
260-
return "", false
261-
}
262-
263-
// Use JS strings if we need to escape non-ASCII characters
264-
if p.options.ASCIIOnly && c > lastASCII {
265-
return "", false
266-
}
267-
268-
switch c {
269-
case '&':
270-
// Use JS strings if the text would need to be escaped with "&amp;"
271-
return "", false
272-
273-
case '"':
274-
double = false
275-
if !single {
276-
break
277-
}
278-
279-
case '\'':
280-
single = false
281-
if !double {
282-
break
283-
}
284-
}
285-
}
286-
287-
// Prefer duble quotes to single quotes
288-
if double {
289-
return "\"", true
290-
}
291-
if single {
292-
return "'", true
293-
}
294-
return "", false
295-
}
296-
297-
// Use JS strings for text inside JSX elements that need escape characters.
298-
// Technically the JSX specification doesn't say anything about using XML
299-
// character escape sequences, so JSX implementations may not be able to
300-
// consume them. See https://facebook.github.io/jsx/ for the specification.
301-
func (p *printer) canPrintTextAsJSXChild(text []uint16) bool {
302-
for _, c := range text {
303-
// Use JS strings for control characters
304-
if c < firstASCII {
305-
return false
306-
}
307-
308-
// Use JS strings if we need to escape non-ASCII characters
309-
if p.options.ASCIIOnly && c > lastASCII {
310-
return false
311-
}
312-
313-
switch c {
314-
case '&', '<', '>', '{', '}':
315-
// Use JS strings if the text would need to be escaped
316-
return false
317-
}
318-
}
319-
320-
return true
321-
}
322-
323249
// JSX tag syntax doesn't support character escapes so non-ASCII identifiers
324250
// must be printed as UTF-8 even when the charset is set to ASCII.
325251
func (p *printer) printJSXTag(tagOrNil js_ast.Expr) {
@@ -2143,26 +2069,28 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFla
21432069

21442070
isMultiLine := p.willPrintExprCommentsAtLoc(property.ValueOrNil.Loc)
21452071

2146-
// Don't use shorthand syntax if it would discard comments
2147-
if !isMultiLine {
2148-
// Special-case string values
2149-
if str, ok := property.ValueOrNil.Data.(*js_ast.EString); ok {
2150-
if quote, ok := p.canPrintTextAsJSXAttribute(str.Value); ok {
2151-
p.print("=")
2152-
p.addSourceMapping(property.ValueOrNil.Loc)
2153-
p.print(quote)
2154-
p.print(helpers.UTF16ToString(str.Value))
2155-
p.print(quote)
2156-
continue
2157-
}
2072+
if property.Flags.Has(js_ast.PropertyWasShorthand) {
2073+
// Implicit "true" value
2074+
if boolean, ok := property.ValueOrNil.Data.(*js_ast.EBoolean); ok && boolean.Value {
2075+
continue
21582076
}
21592077

2160-
// Implicit "true" value
2161-
if boolean, ok := property.ValueOrNil.Data.(*js_ast.EBoolean); ok && boolean.Value && property.Flags.Has(js_ast.PropertyWasShorthand) {
2078+
// JSX element as JSX attribute value
2079+
if _, ok := property.ValueOrNil.Data.(*js_ast.EJSXElement); ok {
2080+
p.print("=")
2081+
p.printExpr(property.ValueOrNil, js_ast.LLowest, 0)
21622082
continue
21632083
}
21642084
}
21652085

2086+
// Special-case raw text
2087+
if text, ok := property.ValueOrNil.Data.(*js_ast.EJSXText); ok {
2088+
p.print("=")
2089+
p.addSourceMapping(property.ValueOrNil.Loc)
2090+
p.print(text.Raw)
2091+
continue
2092+
}
2093+
21662094
// Generic JS value
21672095
p.print("={")
21682096
if isMultiLine {
@@ -2197,30 +2125,13 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFla
21972125
}
21982126
p.print(">")
21992127

2200-
isSingleLine := true
2201-
if !p.options.MinifyWhitespace {
2202-
isSingleLine = len(e.NullableChildren) < 2
2203-
if len(e.NullableChildren) == 1 {
2204-
if _, ok := e.NullableChildren[0].Data.(*js_ast.EJSXElement); !ok {
2205-
isSingleLine = true
2206-
}
2207-
}
2208-
}
2209-
if !isSingleLine {
2210-
p.options.Indent++
2211-
}
2212-
22132128
// Print the children
22142129
for _, childOrNil := range e.NullableChildren {
2215-
if !isSingleLine {
2216-
p.printNewline()
2217-
p.printIndent()
2218-
}
22192130
if _, ok := childOrNil.Data.(*js_ast.EJSXElement); ok {
22202131
p.printExpr(childOrNil, js_ast.LLowest, 0)
2221-
} else if str, ok := childOrNil.Data.(*js_ast.EString); ok && isSingleLine && p.canPrintTextAsJSXChild(str.Value) {
2132+
} else if text, ok := childOrNil.Data.(*js_ast.EJSXText); ok {
22222133
p.addSourceMapping(childOrNil.Loc)
2223-
p.print(helpers.UTF16ToString(str.Value))
2134+
p.print(text.Raw)
22242135
} else if childOrNil.Data != nil {
22252136
isMultiLine := p.willPrintExprCommentsAtLoc(childOrNil.Loc)
22262137
p.print("{")
@@ -2251,11 +2162,6 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFla
22512162
}
22522163

22532164
// Print the closing tag
2254-
if !isSingleLine {
2255-
p.options.Indent--
2256-
p.printNewline()
2257-
p.printIndent()
2258-
}
22592165
p.addSourceMapping(e.CloseLoc)
22602166
p.print("</")
22612167
p.printJSXTag(e.TagOrNil)

0 commit comments

Comments
 (0)