diff --git a/cmd/esbuild/main.go b/cmd/esbuild/main.go index 4a43b5bf6e0..93487b589a8 100644 --- a/cmd/esbuild/main.go +++ b/cmd/esbuild/main.go @@ -72,6 +72,8 @@ var helpText = func(colors logger.Colors) string { --footer:T=... Text to be appended to each output file of type T where T is one of: css | js --global-name=... The name of the global for the IIFE format + --ignore-annotations Enable this to work with packages that have + incorrect tree-shaking annotations --inject:F Import the file F into all input files and automatically replace matching globals with imports --jsx-factory=... What to use for JSX instead of React.createElement @@ -105,8 +107,7 @@ var helpText = func(colors logger.Colors) string { --sourcemap=external Do not link to the source map with a comment --sourcemap=inline Emit the source map with an inline data URL --sources-content=false Omit "sourcesContent" in generated source maps - --tree-shaking=... Set to "ignore-annotations" to work with packages - that have incorrect tree-shaking annotations + --tree-shaking=... Force tree shaking on or off (false | true) --tsconfig=... Use this tsconfig.json file instead of other ones --version Print the current version (` + esbuildVersion + `) and exit diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 4d25160d95d..3726c4aefe7 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -2310,7 +2310,7 @@ func (cache *runtimeCache) parseRuntime(options *config.Options) (source logger. // Always do tree shaking for the runtime because we never want to // include unnecessary runtime code - Mode: config.ModeBundle, + TreeShaking: true, })) if log.HasErrors() { msgs := "Internal error: failed to parse runtime:\n" diff --git a/internal/bundler/bundler_default_test.go b/internal/bundler/bundler_default_test.go index 90f65cef213..3618827a907 100644 --- a/internal/bundler/bundler_default_test.go +++ b/internal/bundler/bundler_default_test.go @@ -3786,6 +3786,7 @@ func TestInjectNoBundle(t *testing.T) { entryPaths: []string{"/entry.js"}, options: config.Options{ Mode: config.ModePassThrough, + TreeShaking: true, AbsOutputFile: "/out.js", Defines: &defines, InjectAbsPaths: []string{ diff --git a/internal/bundler/bundler_test.go b/internal/bundler/bundler_test.go index de2f062f54a..b9fc6d1b219 100644 --- a/internal/bundler/bundler_test.go +++ b/internal/bundler/bundler_test.go @@ -77,6 +77,10 @@ func (s *suite) expectBundled(t *testing.T, args bundled) { if args.options.AbsOutputFile != "" { args.options.AbsOutputDir = path.Dir(args.options.AbsOutputFile) } + if args.options.Mode == config.ModeBundle || (args.options.Mode == config.ModeConvertFormat && args.options.OutputFormat == config.FormatIIFE) { + // Apply this default to all tests since it was not configurable when the tests were written + args.options.TreeShaking = true + } log := logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug) caches := cache.MakeCacheSet() resolver := resolver.NewResolver(fs, log, caches, args.options) diff --git a/internal/bundler/linker.go b/internal/bundler/linker.go index 527d6300eb5..1e57da5bfe2 100644 --- a/internal/bundler/linker.go +++ b/internal/bundler/linker.go @@ -2571,8 +2571,6 @@ func (c *linkerContext) markFileLiveForTreeShaking(sourceIndex uint32) { switch repr := file.InputFile.Repr.(type) { case *graph.JSRepr: - isTreeShakingEnabled := config.IsTreeShakingEnabled(c.options.Mode, c.options.OutputFormat) - // If the JavaScript stub for a CSS file is included, also include the CSS file if repr.CSSSourceIndex.IsValid() { c.markFileLiveForTreeShaking(repr.CSSSourceIndex.GetIndex()) @@ -2609,7 +2607,7 @@ func (c *linkerContext) markFileLiveForTreeShaking(sourceIndex uint32) { // Include all parts in this file with side effects, or just include // everything if tree-shaking is disabled. Note that we still want to // perform tree-shaking on the runtime even if tree-shaking is disabled. - if !canBeRemovedIfUnused || (!part.ForceTreeShaking && !isTreeShakingEnabled && file.IsEntryPoint()) { + if !canBeRemovedIfUnused || (!part.ForceTreeShaking && !c.options.TreeShaking && file.IsEntryPoint()) { c.markPartLiveForTreeShaking(sourceIndex, uint32(partIndex)) } } diff --git a/internal/config/config.go b/internal/config/config.go index 0c2f99f27f4..fca533b0742 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -221,6 +221,7 @@ type Options struct { ASCIIOnly bool KeepNames bool IgnoreDCEAnnotations bool + TreeShaking bool Defines *ProcessedDefines TS TSOptions @@ -384,10 +385,6 @@ func SubstituteTemplate(template []PathTemplate, placeholders PathPlaceholders) return result } -func IsTreeShakingEnabled(mode Mode, outputFormat Format) bool { - return mode == ModeBundle || (mode == ModeConvertFormat && outputFormat == FormatIIFE) -} - func ShouldCallRuntimeRequire(mode Mode, outputFormat Format) bool { return mode == ModeBundle && outputFormat != FormatCommonJS } diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 36f5c4e6636..e1baebc0354 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -336,6 +336,7 @@ type optionsThatSupportStructuralEquality struct { minifyIdentifiers bool omitRuntimeForTests bool ignoreDCEAnnotations bool + treeShaking bool preserveUnusedImportsTS bool useDefineForClassFields config.MaybeBool } @@ -361,6 +362,7 @@ func OptionsFromConfig(options *config.Options) Options { minifyIdentifiers: options.MinifyIdentifiers, omitRuntimeForTests: options.OmitRuntimeForTests, ignoreDCEAnnotations: options.IgnoreDCEAnnotations, + treeShaking: options.TreeShaking, preserveUnusedImportsTS: options.PreserveUnusedImportsTS, useDefineForClassFields: options.UseDefineForClassFields, }, @@ -13904,11 +13906,11 @@ func Parse(log logger.Log, source logger.Source, options Options) (result js_ast // single pass, but it turns out it's pretty much impossible to do this // correctly while handling arrow functions because of the grammar // ambiguities. - if !config.IsTreeShakingEnabled(p.options.mode, p.options.outputFormat) { - // When not bundling, everything comes in a single part + if !p.options.treeShaking { + // When tree shaking is disabled, everything comes in a single part parts = p.appendPart(parts, stmts) } else { - // When bundling, each top-level statement is potentially a separate part + // When tree shaking is enabled, each top-level statement is potentially a separate part for _, stmt := range stmts { switch s := stmt.Data.(type) { case *js_ast.SLocal: diff --git a/lib/shared/common.ts b/lib/shared/common.ts index 9d5925d6422..2054e4c85c2 100644 --- a/lib/shared/common.ts +++ b/lib/shared/common.ts @@ -108,7 +108,8 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe let minifyWhitespace = getFlag(options, keys, 'minifyWhitespace', mustBeBoolean); let minifyIdentifiers = getFlag(options, keys, 'minifyIdentifiers', mustBeBoolean); let charset = getFlag(options, keys, 'charset', mustBeString); - let treeShaking = getFlag(options, keys, 'treeShaking', mustBeStringOrBoolean); + let treeShaking = getFlag(options, keys, 'treeShaking', mustBeBoolean); + let ignoreAnnotations = getFlag(options, keys, 'ignoreAnnotations', mustBeBoolean); let jsx = getFlag(options, keys, 'jsx', mustBeString); let jsxFactory = getFlag(options, keys, 'jsxFactory', mustBeString); let jsxFragment = getFlag(options, keys, 'jsxFragment', mustBeString); @@ -131,7 +132,8 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe if (minifyWhitespace) flags.push('--minify-whitespace'); if (minifyIdentifiers) flags.push('--minify-identifiers'); if (charset) flags.push(`--charset=${charset}`); - if (treeShaking !== void 0 && treeShaking !== true) flags.push(`--tree-shaking=${treeShaking}`); + if (treeShaking !== void 0) flags.push(`--tree-shaking=${treeShaking}`); + if (ignoreAnnotations) flags.push(`--ignore-annotations`); if (jsx) flags.push(`--jsx=${jsx}`); if (jsxFactory) flags.push(`--jsx-factory=${jsxFactory}`); diff --git a/lib/shared/types.ts b/lib/shared/types.ts index 43456e5b043..51395849383 100644 --- a/lib/shared/types.ts +++ b/lib/shared/types.ts @@ -3,7 +3,6 @@ export type Format = 'iife' | 'cjs' | 'esm'; export type Loader = 'js' | 'jsx' | 'ts' | 'tsx' | 'css' | 'json' | 'text' | 'base64' | 'file' | 'dataurl' | 'binary' | 'default'; export type LogLevel = 'verbose' | 'debug' | 'info' | 'warning' | 'error' | 'silent'; export type Charset = 'ascii' | 'utf8'; -export type TreeShaking = true | 'ignore-annotations'; interface CommonOptions { sourcemap?: boolean | 'inline' | 'external' | 'both'; @@ -20,7 +19,8 @@ interface CommonOptions { minifyIdentifiers?: boolean; minifySyntax?: boolean; charset?: Charset; - treeShaking?: TreeShaking; + treeShaking?: boolean; + ignoreAnnotations?: boolean; jsx?: 'transform' | 'preserve'; jsxFactory?: string; diff --git a/pkg/api/api.go b/pkg/api/api.go index 93ced11282e..d4b8a84d77a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -235,7 +235,8 @@ type TreeShaking uint8 const ( TreeShakingDefault TreeShaking = iota - TreeShakingIgnoreAnnotations + TreeShakingFalse + TreeShakingTrue ) //////////////////////////////////////////////////////////////////////////////// @@ -258,6 +259,7 @@ type BuildOptions struct { MinifySyntax bool Charset Charset TreeShaking TreeShaking + IgnoreAnnotations bool LegalComments LegalComments JSXMode JSXMode @@ -366,6 +368,7 @@ type TransformOptions struct { MinifySyntax bool Charset Charset TreeShaking TreeShaking + IgnoreAnnotations bool LegalComments LegalComments JSXMode JSXMode diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index a8d8648fc0d..89629d5ad44 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -204,11 +204,19 @@ func validateASCIIOnly(value Charset) bool { } } -func validateIgnoreDCEAnnotations(value TreeShaking) bool { +func validateTreeShaking(value TreeShaking, bundle bool, format Format) bool { switch value { case TreeShakingDefault: + // If we're in an IIFE then there's no way to concatenate additional code + // to the end of our output so we assume tree shaking is safe. And when + // bundling we assume that tree shaking is safe because if you want to add + // code to the bundle, you should be doing that by including it in the + // bundle instead of concatenating it afterward, so we also assume tree + // shaking is safe then. Otherwise we assume tree shaking is not safe. + return bundle || format == FormatIIFE + case TreeShakingFalse: return false - case TreeShakingIgnoreAnnotations: + case TreeShakingTrue: return true default: panic("Invalid tree shaking") @@ -828,7 +836,8 @@ func rebuildImpl( MinifyIdentifiers: buildOpts.MinifyIdentifiers, AllowOverwrite: buildOpts.AllowOverwrite, ASCIIOnly: validateASCIIOnly(buildOpts.Charset), - IgnoreDCEAnnotations: validateIgnoreDCEAnnotations(buildOpts.TreeShaking), + IgnoreDCEAnnotations: buildOpts.IgnoreAnnotations, + TreeShaking: validateTreeShaking(buildOpts.TreeShaking, buildOpts.Bundle, buildOpts.Format), GlobalName: validateGlobalName(log, buildOpts.GlobalName), CodeSplitting: buildOpts.Splitting, OutputFormat: validateFormat(buildOpts.Format), @@ -1311,7 +1320,8 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult RemoveWhitespace: transformOpts.MinifyWhitespace, MinifyIdentifiers: transformOpts.MinifyIdentifiers, ASCIIOnly: validateASCIIOnly(transformOpts.Charset), - IgnoreDCEAnnotations: validateIgnoreDCEAnnotations(transformOpts.TreeShaking), + IgnoreDCEAnnotations: transformOpts.IgnoreAnnotations, + TreeShaking: validateTreeShaking(transformOpts.TreeShaking, false /* bundle */, transformOpts.Format), AbsOutputFile: transformOpts.Sourcefile + "-out", KeepNames: transformOpts.KeepNames, UseDefineForClassFields: useDefineForClassFieldsTS, diff --git a/pkg/cli/cli_impl.go b/pkg/cli/cli_impl.go index 3418aad5b90..3aa75e85b33 100644 --- a/pkg/cli/cli_impl.go +++ b/pkg/cli/cli_impl.go @@ -144,10 +144,19 @@ func parseOptionsImpl( } name := arg[len("--tree-shaking="):] switch name { - case "ignore-annotations": - *value = api.TreeShakingIgnoreAnnotations + case "false": + *value = api.TreeShakingFalse + case "true": + *value = api.TreeShakingTrue default: - return fmt.Errorf("Invalid tree shaking value: %q (valid: ignore-annotations)", name), nil + return fmt.Errorf("Invalid tree shaking value: %q (valid: false, true)", name), nil + } + + case arg == "--ignore-annotations": + if buildOpts != nil { + buildOpts.IgnoreAnnotations = true + } else { + transformOpts.IgnoreAnnotations = true } case arg == "--keep-names": diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index a3159dd4f47..7fd615036a4 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -1984,7 +1984,7 @@ console.log("success"); }, write: false, bundle: true, - treeShaking: 'ignore-annotations', + ignoreAnnotations: true, }) assert.strictEqual(outputFiles[0].text, `(() => { // @@ -3205,7 +3205,63 @@ let transformTests = { assert.strictEqual(code2, `/* @__PURE__ */ factory(fragment, null, /* @__PURE__ */ factory("div", null));\n`) }, + // Note: tree shaking is disabled when the output format isn't IIFE async treeShakingDefault({ esbuild }) { + const { code } = await esbuild.transform(`var unused = 123`, { + loader: 'jsx', + format: 'esm', + treeShaking: undefined, + }) + assert.strictEqual(code, `var unused = 123;\n`) + }, + + async treeShakingFalse({ esbuild }) { + const { code } = await esbuild.transform(`var unused = 123`, { + loader: 'jsx', + format: 'esm', + treeShaking: false, + }) + assert.strictEqual(code, `var unused = 123;\n`) + }, + + async treeShakingTrue({ esbuild }) { + const { code } = await esbuild.transform(`var unused = 123`, { + loader: 'jsx', + format: 'esm', + treeShaking: true, + }) + assert.strictEqual(code, ``) + }, + + // Note: tree shaking is enabled when the output format is IIFE + async treeShakingDefaultIIFE({ esbuild }) { + const { code } = await esbuild.transform(`var unused = 123`, { + loader: 'jsx', + format: 'iife', + treeShaking: undefined, + }) + assert.strictEqual(code, `(() => {\n})();\n`) + }, + + async treeShakingFalseIIFE({ esbuild }) { + const { code } = await esbuild.transform(`var unused = 123`, { + loader: 'jsx', + format: 'iife', + treeShaking: false, + }) + assert.strictEqual(code, `(() => {\n var unused = 123;\n})();\n`) + }, + + async treeShakingTrueIIFE({ esbuild }) { + const { code } = await esbuild.transform(`var unused = 123`, { + loader: 'jsx', + format: 'iife', + treeShaking: true, + }) + assert.strictEqual(code, `(() => {\n})();\n`) + }, + + async ignoreAnnotationsDefault({ esbuild }) { const { code } = await esbuild.transform(`/* @__PURE__ */ fn();
`, { loader: 'jsx', minifySyntax: true, @@ -3213,20 +3269,20 @@ let transformTests = { assert.strictEqual(code, ``) }, - async treeShakingTrue({ esbuild }) { + async ignoreAnnotationsFalse({ esbuild }) { const { code } = await esbuild.transform(`/* @__PURE__ */ fn();
`, { loader: 'jsx', minifySyntax: true, - treeShaking: true, + ignoreAnnotations: false, }) assert.strictEqual(code, ``) }, - async treeShakingIgnoreAnnotations({ esbuild }) { + async ignoreAnnotationsTrue({ esbuild }) { const { code } = await esbuild.transform(`/* @__PURE__ */ fn();
`, { loader: 'jsx', minifySyntax: true, - treeShaking: 'ignore-annotations', + ignoreAnnotations: true, }) assert.strictEqual(code, `fn(), React.createElement("div", null);\n`) },