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')