Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support to preserve specific comments #2632

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/esbuild/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ var helpText = func(colors logger.Colors) string {
--out-extension:.js=.mjs Use a custom output extension instead of ".js"
--outbase=... The base path used to determine entry point output
paths (for multiple entry points)
--preserve-comments=... Experimental: preserve comments matching a regular
expression
--preserve-symlinks Disable symlink resolution for module lookup
--public-path=... Set the base URL for the "file" loader
--pure:N Mark the name N as a pure function for tree shaking
Expand Down
38 changes: 38 additions & 0 deletions internal/bundler/bundler_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3209,6 +3209,25 @@ func TestImportMetaNoBundle(t *testing.T) {
})
}

func TestPreserveComments(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
/* [PositionForAppBegin] */
App({})
/* [PositionForAppEnd] */
/* should be removed */
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputFile: "/out.js",
PreserveComments: regexp.MustCompile(`\[PositionFor`),
},
})
}

func TestLegalCommentsNone(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
Expand Down Expand Up @@ -3450,6 +3469,25 @@ func TestLegalCommentsAvoidSlashTagExternal(t *testing.T) {
})
}

func TestLegalCommentsHaveLowerPriorityThanPreserveComments(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
//! Copyright
//! [PositionForAppStart]
export let x
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputDir: "/out",
LegalComments: config.LegalCommentsExternalWithoutComment,
PreserveComments: regexp.MustCompile(`\[PositionFor`),
},
})
}

// The IIFE should not be an arrow function when targeting ES5
func TestIIFE_ES5(t *testing.T) {
default_suite.expectBundled(t, bundled{
Expand Down
16 changes: 16 additions & 0 deletions internal/bundler/snapshots/snapshots_default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1860,6 +1860,15 @@ c {

/* entry.css */

================================================================================
TestLegalCommentsHaveLowerPriorityThanPreserveComments
---------- /out/entry.js.LEGAL.txt ----------
//! Copyright

---------- /out/entry.js ----------
//! [PositionForAppStart]
export let x;

================================================================================
TestLegalCommentsInline
---------- /out/entry.js ----------
Expand Down Expand Up @@ -2782,6 +2791,13 @@ TestOutputExtensionRemappingFile
// entry.js
console.log("test");

================================================================================
TestPreserveComments
---------- /out.js ----------
/* [PositionForAppBegin] */
App({});
/* [PositionForAppEnd] */

================================================================================
TestQuotedProperty
---------- /out/entry.js ----------
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ type Options struct {
NeedsMetafile bool
SourceMap SourceMap
ExcludeSourcesContent bool

PreserveComments *regexp.Regexp
}

type TargetFromAPI uint8
Expand Down
5 changes: 3 additions & 2 deletions internal/js_ast/js_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,9 @@ type LocRef struct {
}

type Comment struct {
Text string
Loc logger.Loc
Text string
Loc logger.Loc
IsLegalComment bool
}

type PropertyKind uint8
Expand Down
29 changes: 12 additions & 17 deletions internal/js_lexer/js_lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ type MaybeSubstring struct {
}

type Lexer struct {
CommentsToPreserveBefore []js_ast.Comment
AllOriginalComments []js_ast.Comment
LeadingComments []js_ast.Comment
AllOriginalCommentTexts []string
Identifier MaybeSubstring
log logger.Log
source logger.Source
Expand Down Expand Up @@ -288,7 +288,6 @@ type Lexer struct {
ts config.TSOptions
HasNewlineBefore bool
HasPureCommentBefore bool
PreserveAllCommentsBefore bool
IsLegacyOctalLiteral bool
PrevTokenWasAwaitKeyword bool
rescanCloseBraceAsTemplateToken bool
Expand Down Expand Up @@ -1212,7 +1211,7 @@ func (lexer *Lexer) Next() {
lexer.HasNewlineBefore = lexer.end == 0
lexer.HasPureCommentBefore = false
lexer.PrevTokenWasAwaitKeyword = false
lexer.CommentsToPreserveBefore = nil
lexer.LeadingComments = nil

for {
lexer.start = lexer.end
Expand Down Expand Up @@ -2753,10 +2752,7 @@ func (lexer *Lexer) scanCommentText() {

// Save the original comment text so we can subtract comments from the
// character frequency analysis used by symbol minification
lexer.AllOriginalComments = append(lexer.AllOriginalComments, js_ast.Comment{
Loc: logger.Loc{Start: int32(lexer.start)},
Text: text,
})
lexer.AllOriginalCommentTexts = append(lexer.AllOriginalCommentTexts, text)

// Omit the trailing "*/" from the checks below
endOfCommentText := len(text)
Expand Down Expand Up @@ -2806,14 +2802,13 @@ func (lexer *Lexer) scanCommentText() {
}
}

if hasLegalAnnotation || lexer.PreserveAllCommentsBefore {
if isMultiLineComment {
text = helpers.RemoveMultiLineCommentIndent(lexer.source.Contents[:lexer.start], text)
}

lexer.CommentsToPreserveBefore = append(lexer.CommentsToPreserveBefore, js_ast.Comment{
Loc: logger.Loc{Start: int32(lexer.start)},
Text: text,
})
if isMultiLineComment {
text = helpers.RemoveMultiLineCommentIndent(lexer.source.Contents[:lexer.start], text)
}

lexer.LeadingComments = append(lexer.LeadingComments, js_ast.Comment{
Loc: logger.Loc{Start: int32(lexer.start)},
Text: text,
IsLegalComment: hasLegalAnnotation,
})
}
44 changes: 29 additions & 15 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ type Options struct {
mangleProps *regexp.Regexp
reserveProps *regexp.Regexp

preserveComments *regexp.Regexp

// This pointer will always be different for each build but the contents
// shouldn't ever behave different semantically. We ignore this field for the
// equality comparison.
Expand Down Expand Up @@ -445,6 +447,8 @@ func OptionsFromConfig(options *config.Options) Options {
mangleProps: options.MangleProps,
reserveProps: options.ReserveProps,

preserveComments: options.PreserveComments,

optionsThatSupportStructuralEquality: optionsThatSupportStructuralEquality{
unsupportedJSFeatures: options.UnsupportedJSFeatures,
unsupportedJSFeatureOverrides: options.UnsupportedJSFeatureOverrides,
Expand Down Expand Up @@ -490,8 +494,8 @@ func (a *Options) Equal(b *Options) bool {
return false
}

// Compare "MangleProps" and "ReserveProps"
if !isSameRegexp(a.mangleProps, b.mangleProps) || !isSameRegexp(a.reserveProps, b.reserveProps) {
// Compare "MangleProps", "ReserveProps" and "PreserveComments"
if !isSameRegexp(a.mangleProps, b.mangleProps) || !isSameRegexp(a.reserveProps, b.reserveProps) || !isSameRegexp(a.preserveComments, b.preserveComments) {
return false
}

Expand Down Expand Up @@ -3673,10 +3677,8 @@ func (p *parser) parseImportExpr(loc logger.Loc, level js_ast.L) js_ast.Expr {
oldAllowIn := p.allowIn
p.allowIn = true

p.lexer.PreserveAllCommentsBefore = true
p.lexer.Expect(js_lexer.TOpenParen)
comments := p.lexer.CommentsToPreserveBefore
p.lexer.PreserveAllCommentsBefore = false
comments := p.lexer.LeadingComments

value := p.parseExpr(js_ast.LComma)
var optionsOrNil js_ast.Expr
Expand Down Expand Up @@ -7249,16 +7251,28 @@ 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.LeadingComments
if len(comments) > 0 {
for _, comment := range comments {
stmts = append(stmts, js_ast.Stmt{
Loc: comment.Loc,
Data: &js_ast.SComment{
Text: comment.Text,
IsLegalComment: true,
},
})
// User-defined rules have higher priority than legal comment rules,
// then those comments can avoid being processed as legal comments.
if p.options.preserveComments != nil && p.options.preserveComments.MatchString(comment.Text) {
stmts = append(stmts, js_ast.Stmt{
Loc: comment.Loc,
Data: &js_ast.SComment{
Text: comment.Text,
IsLegalComment: false,
},
})
} else if comment.IsLegalComment {
stmts = append(stmts, js_ast.Stmt{
Loc: comment.Loc,
Data: &js_ast.SComment{
Text: comment.Text,
IsLegalComment: true,
},
})
}
}
}

Expand Down Expand Up @@ -16372,8 +16386,8 @@ func (p *parser) computeCharacterFrequency() *js_ast.CharFreq {
charFreq.Scan(p.source.Contents, 1)

// Subtract out all comments
for _, comment := range p.lexer.AllOriginalComments {
charFreq.Scan(comment.Text, -1)
for _, text := range p.lexer.AllOriginalCommentTexts {
charFreq.Scan(text, -1)
}

// Subtract out all import paths
Expand Down
3 changes: 3 additions & 0 deletions lib/shared/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
let pure = getFlag(options, keys, 'pure', mustBeArray);
let keepNames = getFlag(options, keys, 'keepNames', mustBeBoolean);
let platform = getFlag(options, keys, 'platform', mustBeString);
let preserveComments = getFlag(options, keys, 'preserveComments', mustBeRegExp);

if (legalComments) flags.push(`--legal-comments=${legalComments}`);
if (sourceRoot !== void 0) flags.push(`--source-root=${sourceRoot}`);
Expand Down Expand Up @@ -203,6 +204,8 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe
}
if (pure) for (let fn of pure) flags.push(`--pure:${fn}`);
if (keepNames) flags.push(`--keep-names`);

if (preserveComments) flags.push(`--preserve-comments=${preserveComments.source}`)
}

function flagsForBuildOptions(
Expand Down
3 changes: 3 additions & 0 deletions lib/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ interface CommonOptions {
logLimit?: number;
/** Documentation: https://esbuild.github.io/api/#log-override */
logOverride?: Record<string, LogLevel>;

/** Experimental: preserve comments matching a regular expression */
preserveComments?: RegExp
}

export interface BuildOptions extends CommonOptions {
Expand Down
4 changes: 4 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ type BuildOptions struct {
Plugins []Plugin // Documentation: https://esbuild.github.io/plugins/

Watch *WatchMode // Documentation: https://esbuild.github.io/api/#watch

PreserveComments string // Experimental: preserve comments matching a regular expression
}

type EntryPoint struct {
Expand Down Expand Up @@ -416,6 +418,8 @@ type TransformOptions struct {

Sourcefile string // Documentation: https://esbuild.github.io/api/#sourcefile
Loader Loader // Documentation: https://esbuild.github.io/api/#loader

PreserveComments string // Experimental: preserve comments matching a regular expression
}

type TransformResult struct {
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/api_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,7 @@ func rebuildImpl(
MinifyIdentifiers: buildOpts.MinifyIdentifiers,
MangleProps: validateRegex(log, "mangle props", buildOpts.MangleProps),
ReserveProps: validateRegex(log, "reserve props", buildOpts.ReserveProps),
PreserveComments: validateRegex(log, "preserve comments", buildOpts.PreserveComments),
MangleQuoted: buildOpts.MangleQuoted == MangleQuotedTrue,
DropDebugger: (buildOpts.Drop & DropDebugger) != 0,
AllowOverwrite: buildOpts.AllowOverwrite,
Expand Down Expand Up @@ -1444,6 +1445,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult
MinifyIdentifiers: transformOpts.MinifyIdentifiers,
MangleProps: validateRegex(log, "mangle props", transformOpts.MangleProps),
ReserveProps: validateRegex(log, "reserve props", transformOpts.ReserveProps),
PreserveComments: validateRegex(log, "preserve comments", transformOpts.PreserveComments),
MangleQuoted: transformOpts.MangleQuoted == MangleQuotedTrue,
DropDebugger: (transformOpts.Drop & DropDebugger) != 0,
ASCIIOnly: validateASCIIOnly(transformOpts.Charset),
Expand Down
8 changes: 8 additions & 0 deletions pkg/cli/cli_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,14 @@ func parseOptionsImpl(
transformOpts.ReserveProps = value
}

case strings.HasPrefix(arg, "--preserve-comments="):
value := arg[len("--preserve-comments="):]
if buildOpts != nil {
buildOpts.PreserveComments = value
} else {
transformOpts.PreserveComments = value
}

case strings.HasPrefix(arg, "--mangle-cache=") && buildOpts != nil && kind == kindInternal:
value := arg[len("--mangle-cache="):]
extras.mangleCache = &value
Expand Down