diff --git a/internal/js_ast/js_ast.go b/internal/js_ast/js_ast.go index 725eabe9126..628e9aed64b 100644 --- a/internal/js_ast/js_ast.go +++ b/internal/js_ast/js_ast.go @@ -743,7 +743,7 @@ type EImportString struct { // because esbuild is not Webpack. But we do preserve them since doing so is // harmless, easy to maintain, and useful to people. See the Webpack docs for // more info: https://webpack.js.org/api/module-methods/#magic-comments. - LeadingInteriorComments []Comment + WebpackComments []Comment ImportRecordIndex uint32 } @@ -753,7 +753,7 @@ type EImportCall struct { OptionsOrNil Expr // See the comment for this same field on "EImportString" for more information - LeadingInteriorComments []Comment + WebpackComments []Comment } type Stmt struct { diff --git a/internal/js_lexer/js_lexer.go b/internal/js_lexer/js_lexer.go index 85c12cddc17..ebcdb672e14 100644 --- a/internal/js_lexer/js_lexer.go +++ b/internal/js_lexer/js_lexer.go @@ -245,7 +245,8 @@ type MaybeSubstring struct { } type Lexer struct { - CommentsToPreserveBefore []js_ast.Comment + LegalCommentsBeforeToken []js_ast.Comment + WebpackComments *[]js_ast.Comment AllOriginalComments []logger.Range Identifier MaybeSubstring log logger.Log @@ -288,7 +289,6 @@ type Lexer struct { ts config.TSOptions HasNewlineBefore bool HasPureCommentBefore bool - PreserveAllCommentsBefore bool IsLegacyOctalLiteral bool PrevTokenWasAwaitKeyword bool rescanCloseBraceAsTemplateToken bool @@ -1212,7 +1212,7 @@ func (lexer *Lexer) Next() { lexer.HasNewlineBefore = lexer.end == 0 lexer.HasPureCommentBefore = false lexer.PrevTokenWasAwaitKeyword = false - lexer.CommentsToPreserveBefore = nil + lexer.LegalCommentsBeforeToken = nil for { lexer.start = lexer.end @@ -2746,10 +2746,19 @@ func scanForPragmaArg(kind pragmaArg, start int, pragma string, text string) (lo }, true } +func isUpperASCII(c byte) bool { + return c >= 'A' && c <= 'Z' +} + +func isLetterASCII(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + func (lexer *Lexer) scanCommentText() { text := lexer.source.Contents[lexer.start:lexer.end] hasLegalAnnotation := len(text) > 2 && text[2] == '!' isMultiLineComment := text[1] == '*' + isWebpackComment := false // Save the original comment text so we can subtract comments from the // character frequency analysis used by symbol minification @@ -2800,15 +2809,43 @@ func (lexer *Lexer) scanCommentText() { lexer.SourceMappingURL = arg } } + + case 'w': + // Webpack magic comments use this regular expression: /(^|\W)webpack[A-Z]{1,}[A-Za-z]{1,}:/ + if lexer.WebpackComments != nil && !isWebpackComment && strings.HasPrefix(text[i:], "webpack") && !isLetterASCII(text[i-1]) { + j := i + 7 + upperCount := 0 + for isUpperASCII(text[j]) { + upperCount++ + j++ + } + if upperCount > 0 { + letterCount := 0 + for isLetterASCII(text[j]) { + letterCount++ + j++ + } + if letterCount > 0 && text[j] == ':' { + isWebpackComment = true + } + } + } } } - if hasLegalAnnotation || lexer.PreserveAllCommentsBefore { - if isMultiLineComment { - text = helpers.RemoveMultiLineCommentIndent(lexer.source.Contents[:lexer.start], text) - } + if isMultiLineComment && (hasLegalAnnotation || isWebpackComment) { + text = helpers.RemoveMultiLineCommentIndent(lexer.source.Contents[:lexer.start], text) + } + + if hasLegalAnnotation { + lexer.LegalCommentsBeforeToken = append(lexer.LegalCommentsBeforeToken, js_ast.Comment{ + Loc: logger.Loc{Start: int32(lexer.start)}, + Text: text, + }) + } - lexer.CommentsToPreserveBefore = append(lexer.CommentsToPreserveBefore, js_ast.Comment{ + if isWebpackComment { + *lexer.WebpackComments = append(*lexer.WebpackComments, js_ast.Comment{ Loc: logger.Loc{Start: int32(lexer.start)}, Text: text, }) diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 179d5b13a82..9a48489af21 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -3673,10 +3673,10 @@ func (p *parser) parseImportExpr(loc logger.Loc, level js_ast.L) js_ast.Expr { oldAllowIn := p.allowIn p.allowIn = true - p.lexer.PreserveAllCommentsBefore = true + var webpackComments []js_ast.Comment + oldWebpackComments := p.lexer.WebpackComments + p.lexer.WebpackComments = &webpackComments p.lexer.Expect(js_lexer.TOpenParen) - comments := p.lexer.CommentsToPreserveBefore - p.lexer.PreserveAllCommentsBefore = false value := p.parseExpr(js_ast.LComma) var optionsOrNil js_ast.Expr @@ -3696,13 +3696,14 @@ func (p *parser) parseImportExpr(loc logger.Loc, level js_ast.L) js_ast.Expr { } } + p.lexer.WebpackComments = oldWebpackComments p.lexer.Expect(js_lexer.TCloseParen) p.allowIn = oldAllowIn return js_ast.Expr{Loc: loc, Data: &js_ast.EImportCall{ - Expr: value, - OptionsOrNil: optionsOrNil, - LeadingInteriorComments: comments, + Expr: value, + OptionsOrNil: optionsOrNil, + WebpackComments: webpackComments, }} } @@ -7256,7 +7257,7 @@ func (p *parser) parseStmtsUpTo(end js_lexer.T, opts parseStmtOpts) []js_ast.Stm for { // Preserve some statement-level comments - comments := p.lexer.CommentsToPreserveBefore + comments := p.lexer.LegalCommentsBeforeToken if len(comments) > 0 { for _, comment := range comments { stmts = append(stmts, js_ast.Stmt{ @@ -14054,8 +14055,8 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO } p.importRecordsForCurrentPart = append(p.importRecordsForCurrentPart, importRecordIndex) return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EImportString{ - ImportRecordIndex: importRecordIndex, - LeadingInteriorComments: e.LeadingInteriorComments, + ImportRecordIndex: importRecordIndex, + WebpackComments: e.WebpackComments, }} } @@ -14108,9 +14109,9 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO } return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EImportCall{ - Expr: arg, - OptionsOrNil: e.OptionsOrNil, - LeadingInteriorComments: e.LeadingInteriorComments, + Expr: arg, + OptionsOrNil: e.OptionsOrNil, + WebpackComments: e.WebpackComments, }} }), exprOut{} diff --git a/internal/js_printer/js_printer.go b/internal/js_printer/js_printer.go index 5acbad873f7..9119f80a808 100644 --- a/internal/js_printer/js_printer.go +++ b/internal/js_printer/js_printer.go @@ -1090,7 +1090,7 @@ func (p *printer) printQuotedUTF16(data []uint16, allowBacktick bool) { func (p *printer) printRequireOrImportExpr( importRecordIndex uint32, - leadingInteriorComments []js_ast.Comment, + webpackComments []js_ast.Comment, level js_ast.L, flags printExprFlags, ) { @@ -1175,10 +1175,10 @@ func (p *printer) printRequireOrImportExpr( p.print("(") defer p.print(")") } - if len(leadingInteriorComments) > 0 { + if len(webpackComments) > 0 { p.printNewline() p.options.Indent++ - for _, comment := range leadingInteriorComments { + for _, comment := range webpackComments { p.printIndentedComment(comment.Text) } p.printIndent() @@ -1188,7 +1188,7 @@ func (p *printer) printRequireOrImportExpr( if !p.options.UnsupportedFeatures.Has(compat.DynamicImport) { p.printImportCallAssertions(record.Assertions) } - if len(leadingInteriorComments) > 0 { + if len(webpackComments) > 0 { p.printNewline() p.options.Indent-- p.printIndent() @@ -1915,17 +1915,17 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFla } case *js_ast.EImportString: - var leadingInteriorComments []js_ast.Comment + var webpackComments []js_ast.Comment if !p.options.MinifyWhitespace { - leadingInteriorComments = e.LeadingInteriorComments + webpackComments = e.WebpackComments } p.addSourceMapping(expr.Loc) - p.printRequireOrImportExpr(e.ImportRecordIndex, leadingInteriorComments, level, flags) + p.printRequireOrImportExpr(e.ImportRecordIndex, webpackComments, level, flags) case *js_ast.EImportCall: - var leadingInteriorComments []js_ast.Comment + var webpackComments []js_ast.Comment if !p.options.MinifyWhitespace { - leadingInteriorComments = e.LeadingInteriorComments + webpackComments = e.WebpackComments } wrap := level >= js_ast.LNew || (flags&forbidCall) != 0 if wrap { @@ -1934,10 +1934,10 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFla p.printSpaceBeforeIdentifier() p.addSourceMapping(expr.Loc) p.print("import(") - if len(leadingInteriorComments) > 0 { + if len(webpackComments) > 0 { p.printNewline() p.options.Indent++ - for _, comment := range leadingInteriorComments { + for _, comment := range webpackComments { p.printIndentedComment(comment.Text) } p.printIndent() @@ -1947,7 +1947,7 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFla // Just omit import assertions if they aren't supported if e.OptionsOrNil.Data != nil && !p.options.UnsupportedFeatures.Has(compat.ImportAssertions) { p.print(",") - if len(leadingInteriorComments) > 0 { + if len(webpackComments) > 0 { p.printNewline() p.printIndent() } else { @@ -1956,7 +1956,7 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFla p.printExpr(e.OptionsOrNil, js_ast.LComma, 0) } - if len(leadingInteriorComments) > 0 { + if len(webpackComments) > 0 { p.printNewline() p.options.Indent-- p.printIndent() diff --git a/internal/js_printer/js_printer_test.go b/internal/js_printer/js_printer_test.go index c25f148b554..093102320a0 100644 --- a/internal/js_printer/js_printer_test.go +++ b/internal/js_printer/js_printer_test.go @@ -667,13 +667,23 @@ func TestPrivateIdentifiers(t *testing.T) { func TestImport(t *testing.T) { expectPrinted(t, "import('path');", "import(\"path\");\n") // The semicolon must not be a separate statement - // Test preservation of leading interior comments - expectPrinted(t, "import(// comment 1\n // comment 2\n 'path');", "import(\n // comment 1\n // comment 2\n \"path\"\n);\n") - expectPrinted(t, "import(// comment 1\n // comment 2\n 'path', {type: 'module'});", "import(\n // comment 1\n // comment 2\n \"path\",\n { type: \"module\" }\n);\n") - expectPrinted(t, "import(/* comment 1 */ /* comment 2 */ 'path');", "import(\n /* comment 1 */\n /* comment 2 */\n \"path\"\n);\n") - expectPrinted(t, "import(/* comment 1 */ /* comment 2 */ 'path', {type: 'module'});", "import(\n /* comment 1 */\n /* comment 2 */\n \"path\",\n { type: \"module\" }\n);\n") - expectPrinted(t, "import(\n /* multi\n * line\n * comment */ 'path');", "import(\n /* multi\n * line\n * comment */\n \"path\"\n);\n") - expectPrinted(t, "import(/* comment 1 */ 'path' /* comment 2 */);", "import(\n /* comment 1 */\n \"path\"\n);\n") + // Test preservation of Webpack-specific comments + expectPrinted(t, "import(// webpackFoo: 1\n // webpackBar: 2\n 'path');", "import(\n // webpackFoo: 1\n // webpackBar: 2\n \"path\"\n);\n") + expectPrinted(t, "import(// webpackFoo: 1\n // webpackBar: 2\n 'path', {type: 'module'});", "import(\n // webpackFoo: 1\n // webpackBar: 2\n \"path\",\n { type: \"module\" }\n);\n") + expectPrinted(t, "import(/* webpackFoo: 1 */ /* webpackBar: 2 */ 'path');", "import(\n /* webpackFoo: 1 */\n /* webpackBar: 2 */\n \"path\"\n);\n") + expectPrinted(t, "import(/* webpackFoo: 1 */ /* webpackBar: 2 */ 'path', {type: 'module'});", "import(\n /* webpackFoo: 1 */\n /* webpackBar: 2 */\n \"path\",\n { type: \"module\" }\n);\n") + expectPrinted(t, "import(\n /* multi\n * line\n * webpackBar: */ 'path');", "import(\n /* multi\n * line\n * webpackBar: */\n \"path\"\n);\n") + expectPrinted(t, "import(/* webpackFoo: 1 */ 'path' /* webpackBar:2 */);", "import(\n /* webpackFoo: 1 */\n /* webpackBar:2 */\n \"path\"\n);\n") + expectPrinted(t, "import(/* webpackFoo: 1 */ 'path' /* webpackBar:2 */ ,);", "import(\n /* webpackFoo: 1 */\n /* webpackBar:2 */\n \"path\"\n);\n") + expectPrinted(t, "import(/* webpackFoo: 1 */ 'path', /* webpackBar:2 */ );", "import(\n /* webpackFoo: 1 */\n /* webpackBar:2 */\n \"path\"\n);\n") + expectPrinted(t, "import(/* webpackFoo: 1 */ 'path', { type: 'module' } /* webpackBar:2 */ );", "import(\n /* webpackFoo: 1 */\n /* webpackBar:2 */\n \"path\",\n { type: \"module\" }\n);\n") + expectPrinted(t, "import(new URL('path', /* webpackFoo: these can go anywhere */ import.meta.url))", "import(\n /* webpackFoo: these can go anywhere */\n new URL(\"path\", import.meta.url)\n);\n") + + // Other comments should not be preserved + expectPrinted(t, "import(// comment 1\n // comment 2\n 'path');", "import(\"path\");\n") + expectPrinted(t, "import(// comment 1\n // comment 2\n 'path', {type: 'module'});", "import(\"path\", { type: \"module\" });\n") + expectPrinted(t, "import(/* comment 1 */ /* comment 2 */ 'path');", "import(\"path\");\n") + expectPrinted(t, "import(/* comment 1 */ /* comment 2 */ 'path', {type: 'module'});", "import(\"path\", { type: \"module\" });\n") } func TestExportDefault(t *testing.T) {