From 95c3c63a09f1ac02670d3c44db93a7b41b855048 Mon Sep 17 00:00:00 2001 From: Chris Casola Date: Tue, 25 May 2021 17:29:20 -0400 Subject: [PATCH 1/5] Allow OnResolve plugins to mark modules as side effect free Adding a SideEffectFree property to OnResolve that plugins can set to indicate that the resolved module is side effect free. Fixes #1009 Co-authored-by: Adam Gaynor --- cmd/esbuild/service.go | 3 + internal/bundler/bundler.go | 14 +++- internal/bundler/bundler_dce_test.go | 77 ++++++++++++++++++++ internal/bundler/snapshots/snapshots_dce.txt | 32 ++++++++ internal/config/config.go | 7 +- lib/shared/common.ts | 2 + lib/shared/stdio_protocol.ts | 1 + lib/shared/types.ts | 1 + pkg/api/api.go | 9 ++- pkg/api/api_impl.go | 1 + scripts/plugin-tests.js | 55 ++++++++++++++ 11 files changed, 192 insertions(+), 10 deletions(-) diff --git a/cmd/esbuild/service.go b/cmd/esbuild/service.go index cef1ec97296..524e2f68a77 100644 --- a/cmd/esbuild/service.go +++ b/cmd/esbuild/service.go @@ -715,6 +715,9 @@ func (service *serviceType) convertPlugins(key int, jsPlugins interface{}) ([]ap if value, ok := response["external"]; ok { result.External = value.(bool) } + if value, ok := response["sideEffectFree"]; ok { + result.SideEffectFree = value.(bool) + } if value, ok := response["pluginData"]; ok { result.PluginData = value.(int) } diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index ee8b337d8ba..e0c394d79bb 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -734,10 +734,18 @@ func runOnResolvePlugins( return nil, true, resolver.DebugMeta{} } + var sideEffectsData *resolver.SideEffectsData + if result.SideEffectFree { + sideEffectsData = &resolver.SideEffectsData{ + IsSideEffectsArrayInJSON: false, + } + } + return &resolver.ResolveResult{ - PathPair: resolver.PathPair{Primary: result.Path}, - IsExternal: result.External, - PluginData: result.PluginData, + PathPair: resolver.PathPair{Primary: result.Path}, + IsExternal: result.External, + PluginData: result.PluginData, + PrimarySideEffectsData: sideEffectsData, }, false, resolver.DebugMeta{} } } diff --git a/internal/bundler/bundler_dce_test.go b/internal/bundler/bundler_dce_test.go index 7066822420a..ff4c03b25f3 100644 --- a/internal/bundler/bundler_dce_test.go +++ b/internal/bundler/bundler_dce_test.go @@ -1,9 +1,11 @@ package bundler import ( + "regexp" "testing" "github.com/evanw/esbuild/internal/config" + "github.com/evanw/esbuild/internal/logger" ) var dce_suite = suite{ @@ -1655,3 +1657,78 @@ func TestTreeShakingInESMWrapper(t *testing.T) { }, }) } + +func TestPackageJsonSideEffectsFalsePluginResolver(t *testing.T) { + pk2Index := ` + export {default as Cmp1} from './cmp1.vue'; + export {default as Cmp2} from './cmp2'; + ` + + testPackageJsonSideEffectsFalsePluginResolver(t, pk2Index) +} + +func TestPackageJsonSideEffectsFalseNoPlugins(t *testing.T) { + pk2Index := ` + export {default as Cmp1} from './cmp1'; + export {default as Cmp2} from './cmp2'; + ` + + testPackageJsonSideEffectsFalsePluginResolver(t, pk2Index) +} + +func testPackageJsonSideEffectsFalsePluginResolver(t *testing.T, pkg2Index string) { + t.Helper() + + mockFiles := map[string]string{ + "/Users/user/project/src/entry.js": ` + import {Cmp2} from "demo-pkg2" + console.log(Cmp2); + `, + "/Users/user/project/node_modules/demo-pkg2/cmp1.js": ` + import {__decorate} from './helper'; + let Something = {} + __decorate(Something); + export default Something; + `, + "/Users/user/project/node_modules/demo-pkg2/cmp2.js": ` + import {__decorate} from './helper'; + class Something2 {} + __decorate(Something2); + export default Something2; + `, + "/Users/user/project/node_modules/demo-pkg2/helper.js": ` + export function __decorate(s) { + } + `, + "/Users/user/project/node_modules/demo-pkg2/package.json": ` + { + "sideEffects": false + } + `, + "/Users/user/project/node_modules/demo-pkg2/index.js": pkg2Index, + } + + dce_suite.expectBundled(t, bundled{ + files: mockFiles, + entryPaths: []string{"/Users/user/project/src/entry.js"}, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputFile: "/out.js", + Plugins: []config.Plugin{ + { + OnResolve: []config.OnResolve{ + { + Filter: regexp.MustCompile("\\.vue$"), + Callback: func(ora config.OnResolveArgs) config.OnResolveResult { + return config.OnResolveResult{ + Path: logger.Path{Text: "/Users/user/project/node_modules/demo-pkg2/cmp1.js"}, + SideEffectFree: true, + } + }, + }, + }, + }, + }, + }, + }) +} diff --git a/internal/bundler/snapshots/snapshots_dce.txt b/internal/bundler/snapshots/snapshots_dce.txt index 8c844e2522a..e33a71d9bfb 100644 --- a/internal/bundler/snapshots/snapshots_dce.txt +++ b/internal/bundler/snapshots/snapshots_dce.txt @@ -414,6 +414,22 @@ console.log("hello"); // Users/user/project/src/entry.js console.log(demo_pkg_exports); +================================================================================ +TestPackageJsonSideEffectsFalseNoPlugins +---------- /out.js ---------- +// Users/user/project/node_modules/demo-pkg2/helper.js +function __decorate(s) { +} + +// Users/user/project/node_modules/demo-pkg2/cmp2.js +var Something2 = class { +}; +__decorate(Something2); +var cmp2_default = Something2; + +// Users/user/project/src/entry.js +console.log(cmp2_default); + ================================================================================ TestPackageJsonSideEffectsFalseNoWarningInNodeModulesIssue999 ---------- /out.js ---------- @@ -462,6 +478,22 @@ var init_a = __esm({ // Users/user/project/src/entry.js Promise.resolve().then(() => (init_a(), a_exports)).then((x) => assert(x.foo === "foo")); +================================================================================ +TestPackageJsonSideEffectsFalsePluginResolver +---------- /out.js ---------- +// Users/user/project/node_modules/demo-pkg2/helper.js +function __decorate(s) { +} + +// Users/user/project/node_modules/demo-pkg2/cmp2.js +var Something2 = class { +}; +__decorate(Something2); +var cmp2_default = Something2; + +// Users/user/project/src/entry.js +console.log(cmp2_default); + ================================================================================ TestPackageJsonSideEffectsFalseRemoveBareImportCommonJS ---------- /out.js ---------- diff --git a/internal/config/config.go b/internal/config/config.go index ccdadfb50c9..b2dd6c50124 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -490,9 +490,10 @@ type OnResolveArgs struct { type OnResolveResult struct { PluginName string - Path logger.Path - External bool - PluginData interface{} + Path logger.Path + External bool + SideEffectFree bool + PluginData interface{} Msgs []logger.Msg ThrownError error diff --git a/lib/shared/common.ts b/lib/shared/common.ts index d1e41756a8e..dda8cdf1e93 100644 --- a/lib/shared/common.ts +++ b/lib/shared/common.ts @@ -780,6 +780,7 @@ export function createChannel(streamIn: StreamIn): StreamOut { let path = getFlag(result, keys, 'path', mustBeString); let namespace = getFlag(result, keys, 'namespace', mustBeString); let external = getFlag(result, keys, 'external', mustBeBoolean); + let sideEffectFree = getFlag(result, keys, 'sideEffectFree', mustBeBoolean); let pluginData = getFlag(result, keys, 'pluginData', canBeAnything); let errors = getFlag(result, keys, 'errors', mustBeArray); let warnings = getFlag(result, keys, 'warnings', mustBeArray); @@ -792,6 +793,7 @@ export function createChannel(streamIn: StreamIn): StreamOut { if (path != null) response.path = path; if (namespace != null) response.namespace = namespace; if (external != null) response.external = external; + if (sideEffectFree != null) response.sideEffectFree = sideEffectFree; if (pluginData != null) response.pluginData = stash.store(pluginData); if (errors != null) response.errors = sanitizeMessages(errors, 'errors', stash, name); if (warnings != null) response.warnings = sanitizeMessages(warnings, 'warnings', stash, name); diff --git a/lib/shared/stdio_protocol.ts b/lib/shared/stdio_protocol.ts index d01402c8b17..6d6fd1940ea 100644 --- a/lib/shared/stdio_protocol.ts +++ b/lib/shared/stdio_protocol.ts @@ -157,6 +157,7 @@ export interface OnResolveResponse { path?: string; external?: boolean; + sideEffectFree?: boolean; namespace?: string; pluginData?: number; diff --git a/lib/shared/types.ts b/lib/shared/types.ts index bd0a2c29965..b2abe288e41 100644 --- a/lib/shared/types.ts +++ b/lib/shared/types.ts @@ -241,6 +241,7 @@ export interface OnResolveResult { path?: string; external?: boolean; + sideEffectFree?: boolean; namespace?: string; pluginData?: any; diff --git a/pkg/api/api.go b/pkg/api/api.go index e1faa85d5bd..a839ec952f7 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -464,10 +464,11 @@ type OnResolveResult struct { Errors []Message Warnings []Message - Path string - External bool - Namespace string - PluginData interface{} + Path string + External bool + SideEffectFree bool + Namespace string + PluginData interface{} WatchFiles []string WatchDirs []string diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index 59aaf3aff5c..43103cddd59 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -1460,6 +1460,7 @@ func (impl *pluginImpl) OnResolve(options OnResolveOptions, callback func(OnReso result.Path = logger.Path{Text: response.Path, Namespace: response.Namespace} result.External = response.External + result.SideEffectFree = response.SideEffectFree result.PluginData = response.PluginData // Convert log messages diff --git a/scripts/plugin-tests.js b/scripts/plugin-tests.js index 5bf797cff3a..56c93637702 100644 --- a/scripts/plugin-tests.js +++ b/scripts/plugin-tests.js @@ -709,6 +709,61 @@ let pluginTests = { assert.strictEqual(result.default, 123) }, + async resolveWithSideEffectFree({ esbuild, testDir }) { + const input = path.join(testDir, 'in.js') + const cmp1 = path.join(testDir, 'cmp1.js') + const cmp2 = path.join(testDir, 'cmp2.js') + const cmpIndex = path.join(testDir, 'cmpIndex.js') + const helper = path.join(testDir, 'helper.js') + + await writeFileAsync(input, ` + import {Cmp2} from "./cmpIndex" + console.log(Cmp2); + `) + await writeFileAsync(cmp1, ` + import {__decorate} from './helper'; + let Something = {} + __decorate(Something); + export default Something; + `) + await writeFileAsync(cmp2, ` + import {__decorate} from './helper'; + let Something2 = {} + __decorate(Something2); + export default Something2; + `) + await writeFileAsync(cmpIndex, ` + export {default as Cmp1} from './cmp1.vue'; + export {default as Cmp2} from './cmp2'; + `) + await writeFileAsync(helper, ` + export function __decorate(s) { + } + `) + + const result = await esbuild.build({ + entryPoints: [input], + bundle: true, + write: false, + format: 'cjs', + plugins: [{ + name: 'name', + setup(build) { + build.onResolve({ filter: /\.vue$/ }, async (args) => { + return { + path: path.join(args.resolveDir, args.path.replace('.vue', '.js')), + sideEffectFree: true, + }; + }); + }, + }], + }) + + const output = result.outputFiles[0].text; + + assert.doesNotMatch(output, /cmp1.js/); + }, + async noResolveDirInFileModule({ esbuild, testDir }) { const input = path.join(testDir, 'in.js') const output = path.join(testDir, 'out.js') From f25612dc8c49c37dd96b87041da70bc2e0e23e99 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Mon, 7 Jun 2021 23:59:14 -0700 Subject: [PATCH 2/5] change the API so the JS API matches Webpack --- cmd/esbuild/service.go | 8 ++++++-- internal/bundler/bundler.go | 25 +++++++++++++++---------- internal/bundler/bundler_dce_test.go | 4 ++-- internal/config/config.go | 8 ++++---- internal/resolver/resolver.go | 3 +++ lib/shared/common.ts | 4 ++-- lib/shared/stdio_protocol.ts | 2 +- lib/shared/types.ts | 2 +- pkg/api/api.go | 17 ++++++++++++----- pkg/api/api_impl.go | 2 +- scripts/plugin-tests.js | 2 +- 11 files changed, 48 insertions(+), 29 deletions(-) diff --git a/cmd/esbuild/service.go b/cmd/esbuild/service.go index 524e2f68a77..1550606f9c9 100644 --- a/cmd/esbuild/service.go +++ b/cmd/esbuild/service.go @@ -715,8 +715,12 @@ func (service *serviceType) convertPlugins(key int, jsPlugins interface{}) ([]ap if value, ok := response["external"]; ok { result.External = value.(bool) } - if value, ok := response["sideEffectFree"]; ok { - result.SideEffectFree = value.(bool) + if value, ok := response["sideEffects"]; ok { + if value.(bool) { + result.SideEffects = api.SideEffectsTrue + } else { + result.SideEffects = api.SideEffectsFalse + } } if value, ok := response["pluginData"]; ok { result.PluginData = value.(int) diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index e0c394d79bb..29153eee3f4 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -735,9 +735,9 @@ func runOnResolvePlugins( } var sideEffectsData *resolver.SideEffectsData - if result.SideEffectFree { + if result.IsSideEffectFree { sideEffectsData = &resolver.SideEffectsData{ - IsSideEffectsArrayInJSON: false, + PluginName: pluginName, } } @@ -1704,19 +1704,24 @@ func (s *scanner) processScannedFiles() []scannerFile { // effect. otherModule.SideEffects.Kind != graph.NoSideEffects_PureData_FromPlugin { var notes []logger.MsgData + var by string if data := otherModule.SideEffects.Data; data != nil { - var text string - if data.IsSideEffectsArrayInJSON { - text = "It was excluded from the \"sideEffects\" array in the enclosing \"package.json\" file" + if data.PluginName != "" { + by = fmt.Sprintf(" by plugin %q", data.PluginName) } else { - text = "\"sideEffects\" is false in the enclosing \"package.json\" file" + var text string + if data.IsSideEffectsArrayInJSON { + text = "It was excluded from the \"sideEffects\" array in the enclosing \"package.json\" file" + } else { + text = "\"sideEffects\" is false in the enclosing \"package.json\" file" + } + tracker := logger.MakeLineColumnTracker(data.Source) + notes = append(notes, logger.RangeData(&tracker, data.Range, text)) } - tracker := logger.MakeLineColumnTracker(data.Source) - notes = append(notes, logger.RangeData(&tracker, data.Range, text)) } s.log.AddRangeWarningWithNotes(&tracker, record.Range, - fmt.Sprintf("Ignoring this import because %q was marked as having no side effects", - otherModule.Source.PrettyPath), notes) + fmt.Sprintf("Ignoring this import because %q was marked as having no side effects%s", + otherModule.Source.PrettyPath, by), notes) } } } diff --git a/internal/bundler/bundler_dce_test.go b/internal/bundler/bundler_dce_test.go index ff4c03b25f3..252dc27e1f7 100644 --- a/internal/bundler/bundler_dce_test.go +++ b/internal/bundler/bundler_dce_test.go @@ -1721,8 +1721,8 @@ func testPackageJsonSideEffectsFalsePluginResolver(t *testing.T, pkg2Index strin Filter: regexp.MustCompile("\\.vue$"), Callback: func(ora config.OnResolveArgs) config.OnResolveResult { return config.OnResolveResult{ - Path: logger.Path{Text: "/Users/user/project/node_modules/demo-pkg2/cmp1.js"}, - SideEffectFree: true, + Path: logger.Path{Text: "/Users/user/project/node_modules/demo-pkg2/cmp1.js"}, + IsSideEffectFree: true, } }, }, diff --git a/internal/config/config.go b/internal/config/config.go index b2dd6c50124..6630029ebdc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -490,10 +490,10 @@ type OnResolveArgs struct { type OnResolveResult struct { PluginName string - Path logger.Path - External bool - SideEffectFree bool - PluginData interface{} + Path logger.Path + External bool + IsSideEffectFree bool + PluginData interface{} Msgs []logger.Msg ThrownError error diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index da33e52ffaa..0baf5902e3e 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -83,6 +83,9 @@ type SideEffectsData struct { Source *logger.Source Range logger.Range + // If non-empty, this false value came from a plugin + PluginName string + // If true, "sideEffects" was an array. If false, "sideEffects" was false. IsSideEffectsArrayInJSON bool } diff --git a/lib/shared/common.ts b/lib/shared/common.ts index dda8cdf1e93..a4a206ba6b8 100644 --- a/lib/shared/common.ts +++ b/lib/shared/common.ts @@ -780,7 +780,7 @@ export function createChannel(streamIn: StreamIn): StreamOut { let path = getFlag(result, keys, 'path', mustBeString); let namespace = getFlag(result, keys, 'namespace', mustBeString); let external = getFlag(result, keys, 'external', mustBeBoolean); - let sideEffectFree = getFlag(result, keys, 'sideEffectFree', mustBeBoolean); + let sideEffects = getFlag(result, keys, 'sideEffects', mustBeBoolean); let pluginData = getFlag(result, keys, 'pluginData', canBeAnything); let errors = getFlag(result, keys, 'errors', mustBeArray); let warnings = getFlag(result, keys, 'warnings', mustBeArray); @@ -793,7 +793,7 @@ export function createChannel(streamIn: StreamIn): StreamOut { if (path != null) response.path = path; if (namespace != null) response.namespace = namespace; if (external != null) response.external = external; - if (sideEffectFree != null) response.sideEffectFree = sideEffectFree; + if (sideEffects != null) response.sideEffects = sideEffects; if (pluginData != null) response.pluginData = stash.store(pluginData); if (errors != null) response.errors = sanitizeMessages(errors, 'errors', stash, name); if (warnings != null) response.warnings = sanitizeMessages(warnings, 'warnings', stash, name); diff --git a/lib/shared/stdio_protocol.ts b/lib/shared/stdio_protocol.ts index 6d6fd1940ea..cc1d8884c3f 100644 --- a/lib/shared/stdio_protocol.ts +++ b/lib/shared/stdio_protocol.ts @@ -157,7 +157,7 @@ export interface OnResolveResponse { path?: string; external?: boolean; - sideEffectFree?: boolean; + sideEffects?: boolean; namespace?: string; pluginData?: number; diff --git a/lib/shared/types.ts b/lib/shared/types.ts index b2abe288e41..470f15cd6c5 100644 --- a/lib/shared/types.ts +++ b/lib/shared/types.ts @@ -241,7 +241,7 @@ export interface OnResolveResult { path?: string; external?: boolean; - sideEffectFree?: boolean; + sideEffects?: boolean; namespace?: string; pluginData?: any; diff --git a/pkg/api/api.go b/pkg/api/api.go index a839ec952f7..1cfed46642b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -426,6 +426,13 @@ func Serve(serveOptions ServeOptions, buildOptions BuildOptions) (ServeResult, e //////////////////////////////////////////////////////////////////////////////// // Plugin API +type SideEffects uint8 + +const ( + SideEffectsTrue SideEffects = iota + SideEffectsFalse +) + type Plugin struct { Name string Setup func(PluginBuild) @@ -464,11 +471,11 @@ type OnResolveResult struct { Errors []Message Warnings []Message - Path string - External bool - SideEffectFree bool - Namespace string - PluginData interface{} + Path string + External bool + SideEffects SideEffects + Namespace string + PluginData interface{} WatchFiles []string WatchDirs []string diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index 43103cddd59..efb4977b8b6 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -1460,7 +1460,7 @@ func (impl *pluginImpl) OnResolve(options OnResolveOptions, callback func(OnReso result.Path = logger.Path{Text: response.Path, Namespace: response.Namespace} result.External = response.External - result.SideEffectFree = response.SideEffectFree + result.IsSideEffectFree = response.SideEffects == SideEffectsFalse result.PluginData = response.PluginData // Convert log messages diff --git a/scripts/plugin-tests.js b/scripts/plugin-tests.js index 56c93637702..06402cd0101 100644 --- a/scripts/plugin-tests.js +++ b/scripts/plugin-tests.js @@ -752,7 +752,7 @@ let pluginTests = { build.onResolve({ filter: /\.vue$/ }, async (args) => { return { path: path.join(args.resolveDir, args.path.replace('.vue', '.js')), - sideEffectFree: true, + sideEffects: false, }; }); }, From 088b08988466b852105fa08fab09c882a23e7c3b Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Tue, 8 Jun 2021 00:02:21 -0700 Subject: [PATCH 3/5] remove redundant go plugin test --- internal/bundler/bundler_dce_test.go | 77 -------------------- internal/bundler/snapshots/snapshots_dce.txt | 32 -------- 2 files changed, 109 deletions(-) diff --git a/internal/bundler/bundler_dce_test.go b/internal/bundler/bundler_dce_test.go index 252dc27e1f7..7066822420a 100644 --- a/internal/bundler/bundler_dce_test.go +++ b/internal/bundler/bundler_dce_test.go @@ -1,11 +1,9 @@ package bundler import ( - "regexp" "testing" "github.com/evanw/esbuild/internal/config" - "github.com/evanw/esbuild/internal/logger" ) var dce_suite = suite{ @@ -1657,78 +1655,3 @@ func TestTreeShakingInESMWrapper(t *testing.T) { }, }) } - -func TestPackageJsonSideEffectsFalsePluginResolver(t *testing.T) { - pk2Index := ` - export {default as Cmp1} from './cmp1.vue'; - export {default as Cmp2} from './cmp2'; - ` - - testPackageJsonSideEffectsFalsePluginResolver(t, pk2Index) -} - -func TestPackageJsonSideEffectsFalseNoPlugins(t *testing.T) { - pk2Index := ` - export {default as Cmp1} from './cmp1'; - export {default as Cmp2} from './cmp2'; - ` - - testPackageJsonSideEffectsFalsePluginResolver(t, pk2Index) -} - -func testPackageJsonSideEffectsFalsePluginResolver(t *testing.T, pkg2Index string) { - t.Helper() - - mockFiles := map[string]string{ - "/Users/user/project/src/entry.js": ` - import {Cmp2} from "demo-pkg2" - console.log(Cmp2); - `, - "/Users/user/project/node_modules/demo-pkg2/cmp1.js": ` - import {__decorate} from './helper'; - let Something = {} - __decorate(Something); - export default Something; - `, - "/Users/user/project/node_modules/demo-pkg2/cmp2.js": ` - import {__decorate} from './helper'; - class Something2 {} - __decorate(Something2); - export default Something2; - `, - "/Users/user/project/node_modules/demo-pkg2/helper.js": ` - export function __decorate(s) { - } - `, - "/Users/user/project/node_modules/demo-pkg2/package.json": ` - { - "sideEffects": false - } - `, - "/Users/user/project/node_modules/demo-pkg2/index.js": pkg2Index, - } - - dce_suite.expectBundled(t, bundled{ - files: mockFiles, - entryPaths: []string{"/Users/user/project/src/entry.js"}, - options: config.Options{ - Mode: config.ModeBundle, - AbsOutputFile: "/out.js", - Plugins: []config.Plugin{ - { - OnResolve: []config.OnResolve{ - { - Filter: regexp.MustCompile("\\.vue$"), - Callback: func(ora config.OnResolveArgs) config.OnResolveResult { - return config.OnResolveResult{ - Path: logger.Path{Text: "/Users/user/project/node_modules/demo-pkg2/cmp1.js"}, - IsSideEffectFree: true, - } - }, - }, - }, - }, - }, - }, - }) -} diff --git a/internal/bundler/snapshots/snapshots_dce.txt b/internal/bundler/snapshots/snapshots_dce.txt index e33a71d9bfb..8c844e2522a 100644 --- a/internal/bundler/snapshots/snapshots_dce.txt +++ b/internal/bundler/snapshots/snapshots_dce.txt @@ -414,22 +414,6 @@ console.log("hello"); // Users/user/project/src/entry.js console.log(demo_pkg_exports); -================================================================================ -TestPackageJsonSideEffectsFalseNoPlugins ----------- /out.js ---------- -// Users/user/project/node_modules/demo-pkg2/helper.js -function __decorate(s) { -} - -// Users/user/project/node_modules/demo-pkg2/cmp2.js -var Something2 = class { -}; -__decorate(Something2); -var cmp2_default = Something2; - -// Users/user/project/src/entry.js -console.log(cmp2_default); - ================================================================================ TestPackageJsonSideEffectsFalseNoWarningInNodeModulesIssue999 ---------- /out.js ---------- @@ -478,22 +462,6 @@ var init_a = __esm({ // Users/user/project/src/entry.js Promise.resolve().then(() => (init_a(), a_exports)).then((x) => assert(x.foo === "foo")); -================================================================================ -TestPackageJsonSideEffectsFalsePluginResolver ----------- /out.js ---------- -// Users/user/project/node_modules/demo-pkg2/helper.js -function __decorate(s) { -} - -// Users/user/project/node_modules/demo-pkg2/cmp2.js -var Something2 = class { -}; -__decorate(Something2); -var cmp2_default = Something2; - -// Users/user/project/src/entry.js -console.log(cmp2_default); - ================================================================================ TestPackageJsonSideEffectsFalseRemoveBareImportCommonJS ---------- /out.js ---------- From f92e88bdcd51b5c6854ecf133133bc276830605c Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Tue, 8 Jun 2021 00:23:14 -0700 Subject: [PATCH 4/5] more comprehensive js test --- scripts/plugin-tests.js | 77 +++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/scripts/plugin-tests.js b/scripts/plugin-tests.js index 06402cd0101..a3e0de719c2 100644 --- a/scripts/plugin-tests.js +++ b/scripts/plugin-tests.js @@ -709,36 +709,29 @@ let pluginTests = { assert.strictEqual(result.default, 123) }, - async resolveWithSideEffectFree({ esbuild, testDir }) { + async resolveWithSideEffectsFalse({ esbuild, testDir }) { const input = path.join(testDir, 'in.js') - const cmp1 = path.join(testDir, 'cmp1.js') - const cmp2 = path.join(testDir, 'cmp2.js') - const cmpIndex = path.join(testDir, 'cmpIndex.js') - const helper = path.join(testDir, 'helper.js') await writeFileAsync(input, ` - import {Cmp2} from "./cmpIndex" - console.log(Cmp2); + import './re-export-unused' + import {a, b, c} from './re-export-used' + import './import-unused' + use([a, b, c]) `) - await writeFileAsync(cmp1, ` - import {__decorate} from './helper'; - let Something = {} - __decorate(Something); - export default Something; + await writeFileAsync(path.join(testDir, 're-export-unused.js'), ` + export {default as a} from 'plugin:unused-false' + export {default as b} from 'plugin:unused-true' + export {default as c} from 'plugin:unused-none' `) - await writeFileAsync(cmp2, ` - import {__decorate} from './helper'; - let Something2 = {} - __decorate(Something2); - export default Something2; + await writeFileAsync(path.join(testDir, 're-export-used.js'), ` + export {default as a} from 'plugin:used-false' + export {default as b} from 'plugin:used-true' + export {default as c} from 'plugin:used-none' `) - await writeFileAsync(cmpIndex, ` - export {default as Cmp1} from './cmp1.vue'; - export {default as Cmp2} from './cmp2'; - `) - await writeFileAsync(helper, ` - export function __decorate(s) { - } + await writeFileAsync(path.join(testDir, 'import-unused.js'), ` + import 'plugin:ignored-false' + import 'plugin:ignored-true' + import 'plugin:ignored-none' `) const result = await esbuild.build({ @@ -746,22 +739,48 @@ let pluginTests = { bundle: true, write: false, format: 'cjs', + logLevel: 'error', plugins: [{ name: 'name', setup(build) { - build.onResolve({ filter: /\.vue$/ }, async (args) => { + build.onResolve({ filter: /^plugin:/ }, args => { return { - path: path.join(args.resolveDir, args.path.replace('.vue', '.js')), - sideEffects: false, + path: args.path, + namespace: 'ns', + sideEffects: + args.path.endsWith('-true') ? true : + args.path.endsWith('-false') ? false : + undefined, }; }); + build.onLoad({ filter: /^plugin:/ }, args => { + return { contents: `export default use(${JSON.stringify(args.path)})` }; + }); }, }], }) - const output = result.outputFiles[0].text; + // Validate that the unused "sideEffects: false" files were omitted + const used = []; + new Function('use', result.outputFiles[0].text)(x => used.push(x)); + assert.deepStrictEqual(used, [ + 'plugin:unused-true', + 'plugin:unused-none', + + 'plugin:used-false', + 'plugin:used-true', + 'plugin:used-none', + + 'plugin:ignored-true', + 'plugin:ignored-none', - assert.doesNotMatch(output, /cmp1.js/); + [3, 4, 5], + ]) + + // Check that the warning for "sideEffect: false" imports mentions the plugin + assert.strictEqual(result.warnings.length, 1) + assert.strictEqual(result.warnings[0].text, + 'Ignoring this import because "ns:plugin:ignored-false" was marked as having no side effects by plugin "name"') }, async noResolveDirInFileModule({ esbuild, testDir }) { From b26e12a426fbcefce4f7996938f57e8f37a47b1f Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Tue, 8 Jun 2021 00:40:15 -0700 Subject: [PATCH 5/5] release notes --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b75133f78d6..4ae9a595590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,36 @@ This fix was contributed by [@lbwa](https://github.com/lbwa). +* Plugins can now specify `sideEffects: false` ([#1009](https://github.com/evanw/esbuild/issues/1009)) + + The default path resolution behavior in esbuild determines if a given file can be considered side-effect free (in the [Webpack-specific sense](https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free)) by reading the contents of the nearest enclosing `package.json` file and looking for `"sideEffects": false`. However, up until now this was impossible to achieve in an esbuild plugin because there was no way of returning this metadata back to esbuild. + + With this release, esbuild plugins can now return `sideEffects: false` to mark a file as having no side effects. Here's an example: + + ```js + esbuild.build({ + entryPoints: ['app.js'], + bundle: true, + plugins: [{ + name: 'env-plugin', + setup(build) { + build.onResolve({ filter: /^env$/ }, args => ({ + path: args.path, + namespace: 'some-ns', + sideEffects: false, + })) + build.onLoad({ filter: /.*/, namespace: 'some-ns' }, () => ({ + contents: `export default self.env || (self.env = getEnv())`, + })) + }, + }], + }) + ``` + + This plugin creates a virtual module that can be generated by importing the string `env`. However, since the plugin returns `sideEffects: false`, the generated virtual module will not be included in the bundle if all of the imported values from the module `env` end up being unused. + + This feature was contributed by [@chriscasola](https://github.com/chriscasola). + ## 0.12.6 * Improve template literal lowering transformation conformance ([#1327](https://github.com/evanw/esbuild/issues/1327))