From b79b73cd8bb98c10ab7eedae154bb5c1e03ced7d Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 23 Apr 2023 21:33:24 +0200 Subject: [PATCH] feat: tree-shake deterministic dynamic imports (#4952) * chore: init * feat: progress! * chore: refactor * feat: it works! * feat: support one more case * chore: update * chore: rename * chore: rename * feat: add more case * chore: improve coverage * chore: revert solo flag * chore: refactor * feat: support more case * chore: clean up * feat: support side-effect only dynamic import * chore: clean up * fix: side-effects * chore: update snapshot * chore: cleanup * chore: apply suggestions * fix: auto includeNamespaceMembers * fix: coverage missing variable * chore: assert for generated code * chore: update * chore: update test --------- Co-authored-by: Lukas Taegert-Atkinson --- src/Module.ts | 39 ++- src/ast/nodes/ImportExpression.ts | 116 +++++++++ src/ast/variables/NamespaceVariable.ts | 8 +- .../_expected/amd/entry.js | 9 +- .../_expected/cjs/entry.js | 9 +- .../_expected/es/entry.js | 9 +- .../_expected/system/entry.js | 9 +- .../_expected/amd/entry.js | 9 +- .../_expected/cjs/entry.js | 9 +- .../_expected/es/entry.js | 9 +- .../_expected/system/entry.js | 9 +- .../_expected/amd/main.js | 4 +- .../_expected/cjs/main.js | 4 +- .../_expected/es/main.js | 4 +- .../_expected/system/main.js | 4 +- .../_expected.js | 9 +- .../dynamic-import-inlining/_expected.js | 9 +- .../_expected.js | 9 +- .../dynamic-import-inlining/_expected.js | 9 +- test/form/samples/no-treeshake/_config.js | 3 +- test/form/samples/no-treeshake/_expected.js | 12 + .../samples/no-treeshake/dynamic-imported.js | 3 + test/form/samples/no-treeshake/main.js | 2 + .../_config.js | 40 +++ .../_expected.js | 239 ++++++++++++++++++ .../main.js | 46 ++++ .../sub1.js | 9 + .../sub2.js | 19 ++ .../sub3.js | 13 + .../sub4.js | 4 + .../foo.js | 2 + .../_config.js | 46 ++++ .../main.js | 26 ++ 33 files changed, 661 insertions(+), 90 deletions(-) create mode 100644 test/form/samples/no-treeshake/dynamic-imported.js create mode 100644 test/form/samples/treeshake-deterministic-dynamic-import/_config.js create mode 100644 test/form/samples/treeshake-deterministic-dynamic-import/_expected.js create mode 100644 test/form/samples/treeshake-deterministic-dynamic-import/main.js create mode 100644 test/form/samples/treeshake-deterministic-dynamic-import/sub1.js create mode 100644 test/form/samples/treeshake-deterministic-dynamic-import/sub2.js create mode 100644 test/form/samples/treeshake-deterministic-dynamic-import/sub3.js create mode 100644 test/form/samples/treeshake-deterministic-dynamic-import/sub4.js create mode 100644 test/function/samples/tree-shake-deterministic-dynamic-import/_config.js create mode 100644 test/function/samples/tree-shake-deterministic-dynamic-import/main.js diff --git a/src/Module.ts b/src/Module.ts index c6b1b12663b..8d539ad2c90 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -716,6 +716,33 @@ export default class Module { this.includeAllExports(false); } + includeExportsByNames(names: readonly string[]): void { + if (!this.isExecuted) { + markModuleAndImpureDependenciesAsExecuted(this); + this.graph.needsTreeshakingPass = true; + } + + let includeNamespaceMembers = false; + + for (const name of names) { + const variable = this.getVariableForExportName(name)[0]; + if (variable) { + variable.deoptimizePath(UNKNOWN_PATH); + if (!variable.included) { + this.includeVariable(variable); + } + } + + if (!this.exports.has(name) && !this.reexportDescriptions.has(name)) { + includeNamespaceMembers = true; + } + } + + if (includeNamespaceMembers) { + this.namespace.setMergedNamespaces(this.includeAndGetAdditionalMergedNamespaces()); + } + } + isIncluded(): boolean | null { // Modules where this.ast is missing have been loaded via this.load and are // not yet fully processed, hence they cannot be included. @@ -1218,9 +1245,19 @@ export default class Module { resolution: string | Module | ExternalModule | undefined; } ).resolution; + if (resolution instanceof Module) { resolution.includedDynamicImporters.push(this); - resolution.includeAllExports(true); + + const importedNames = this.options.treeshake + ? node.getDeterministicImportedNames() + : undefined; + + if (importedNames) { + resolution.includeExportsByNames(importedNames); + } else { + resolution.includeAllExports(true); + } } } diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts index 81604c4de5b..c37976a788f 100644 --- a/src/ast/nodes/ImportExpression.ts +++ b/src/ast/nodes/ImportExpression.ts @@ -3,6 +3,7 @@ import ExternalModule from '../../ExternalModule'; import type Module from '../../Module'; import type { GetInterop, NormalizedOutputOptions } from '../../rollup/types'; import type { PluginDriver } from '../../utils/PluginDriver'; +import { EMPTY_ARRAY } from '../../utils/blank'; import type { GenerateCodeSnippets } from '../../utils/generateCodeSnippets'; import { INTEROP_NAMESPACE_DEFAULT_ONLY_VARIABLE, @@ -12,8 +13,17 @@ import { findFirstOccurrenceOutsideComment, type RenderOptions } from '../../uti import type { InclusionContext } from '../ExecutionContext'; import type ChildScope from '../scopes/ChildScope'; import type NamespaceVariable from '../variables/NamespaceVariable'; +import ArrowFunctionExpression from './ArrowFunctionExpression'; +import AwaitExpression from './AwaitExpression'; +import CallExpression from './CallExpression'; +import ExpressionStatement from './ExpressionStatement'; +import FunctionExpression from './FunctionExpression'; +import Identifier from './Identifier'; +import MemberExpression from './MemberExpression'; import type * as NodeType from './NodeType'; import type ObjectExpression from './ObjectExpression'; +import ObjectPattern from './ObjectPattern'; +import VariableDeclarator from './VariableDeclarator'; import { type ExpressionNode, type GenericEsTreeNode, @@ -45,6 +55,100 @@ export default class ImportExpression extends NodeBase { this.source.bind(); } + /** + * Get imported variables for deterministic usage, valid cases are: + * + * - `const { foo } = await import('bar')`. + * - `(await import('bar')).foo` + * - `import('bar').then(({ foo }) => {})` + * + * Returns empty array if it's side-effect only import. + * Returns undefined if it's not fully deterministic. + */ + getDeterministicImportedNames(): readonly string[] | undefined { + const parent1 = this.parent; + + // Side-effect only: import('bar') + if (parent1 instanceof ExpressionStatement) { + return EMPTY_ARRAY; + } + + if (parent1 instanceof AwaitExpression) { + const parent2 = parent1.parent; + + // Side-effect only: await import('bar') + if (parent2 instanceof ExpressionStatement) { + return EMPTY_ARRAY; + } + + // Case 1: const { foo } = await import('bar') + if (parent2 instanceof VariableDeclarator) { + const declaration = parent2.id; + return declaration instanceof ObjectPattern + ? getDeterministicObjectDestructure(declaration) + : undefined; + } + + // Case 2: (await import('bar')).foo + if (parent2 instanceof MemberExpression) { + const id = parent2.property; + if (!parent2.computed && id instanceof Identifier) { + return [id.name]; + } + } + + return; + } + + // Case 3: import('bar').then(({ foo }) => {}) + if (parent1 instanceof MemberExpression) { + const callExpression = parent1.parent; + const property = parent1.property; + + if (!(callExpression instanceof CallExpression) || !(property instanceof Identifier)) { + return; + } + + const memberName = property.name; + + // side-effect only, when only chaining .catch or .finally + if ( + callExpression.parent instanceof ExpressionStatement && + ['catch', 'finally'].includes(memberName) + ) { + return EMPTY_ARRAY; + } + + if (memberName !== 'then') return; + + // Side-effect only: import('bar').then() + if (callExpression.arguments.length === 0) { + return EMPTY_ARRAY; + } + + const argument = callExpression.arguments[0]; + + if ( + callExpression.arguments.length !== 1 || + !(argument instanceof ArrowFunctionExpression || argument instanceof FunctionExpression) + ) { + return; + } + + // Side-effect only: import('bar').then(() => {}) + if (argument.params.length === 0) { + return EMPTY_ARRAY; + } + + const declaration = argument.params[0]; + if (argument.params.length === 1 && declaration instanceof ObjectPattern) { + return getDeterministicObjectDestructure(declaration); + } + + return; + } + } + hasEffects(): boolean { return true; } @@ -296,3 +400,15 @@ const accessedImportGlobals: Record = { cjs: ['require'], system: ['module'] }; + +function getDeterministicObjectDestructure(objectPattern: ObjectPattern): string[] | undefined { + const variables: string[] = []; + + for (const property of objectPattern.properties) { + if (property.type === 'RestElement' || property.computed || property.key.type !== 'Identifier') + return; + variables.push((property.key as Identifier).name); + } + + return variables; +} diff --git a/src/ast/variables/NamespaceVariable.ts b/src/ast/variables/NamespaceVariable.ts index 36226830cbf..011b3088c5a 100644 --- a/src/ast/variables/NamespaceVariable.ts +++ b/src/ast/variables/NamespaceVariable.ts @@ -133,8 +133,9 @@ export default class NamespaceVariable extends Variable { snippets: { _, cnst, getObject, getPropertyAccess, n, s } } = options; const memberVariables = this.getMemberVariables(); - const members: [key: string | null, value: string][] = Object.entries(memberVariables).map( - ([name, original]) => { + const members: [key: string | null, value: string][] = Object.entries(memberVariables) + .filter(([_, variable]) => variable.included) + .map(([name, original]) => { if (this.referencedEarly || original.isReassigned) { return [ null, @@ -143,8 +144,7 @@ export default class NamespaceVariable extends Variable { } return [name, original.getName(getPropertyAccess)]; - } - ); + }); members.unshift([null, `__proto__:${_}null`]); let output = getObject(members, { lineBreakIndent: { base: '', t } }); diff --git a/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/amd/entry.js b/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/amd/entry.js index a7385ad2c1b..fef3de875f4 100644 --- a/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/amd/entry.js +++ b/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/amd/entry.js @@ -1,13 +1,10 @@ define(['exports'], (function (exports) { 'use strict'; const bar = 2; - Promise.resolve().then(function () { return foo$1; }); + Promise.resolve().then(function () { return foo; }); - const foo = 1; - - var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo + var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); exports.bar = bar; diff --git a/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/cjs/entry.js b/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/cjs/entry.js index e20ce0c5bb8..218beb76d7d 100644 --- a/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/cjs/entry.js +++ b/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/cjs/entry.js @@ -1,13 +1,10 @@ 'use strict'; const bar = 2; -Promise.resolve().then(function () { return foo$1; }); +Promise.resolve().then(function () { return foo; }); -const foo = 1; - -var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo +var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); exports.bar = bar; diff --git a/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/es/entry.js b/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/es/entry.js index ae434046e6c..5a35de939db 100644 --- a/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/es/entry.js +++ b/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/es/entry.js @@ -1,11 +1,8 @@ const bar = 2; -Promise.resolve().then(function () { return foo$1; }); +Promise.resolve().then(function () { return foo; }); -const foo = 1; - -var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo +var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); export { bar }; diff --git a/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/system/entry.js b/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/system/entry.js index f3161f5e77f..2f4d23eacdf 100644 --- a/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/system/entry.js +++ b/test/chunking-form/samples/deprecated/dynamic-import-inlining-object/_expected/system/entry.js @@ -4,13 +4,10 @@ System.register([], (function (exports) { execute: (function () { const bar = exports('bar', 2); - Promise.resolve().then(function () { return foo$1; }); + Promise.resolve().then(function () { return foo; }); - const foo = 1; - - var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo + var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); }) diff --git a/test/chunking-form/samples/dynamic-import-inlining-object/_expected/amd/entry.js b/test/chunking-form/samples/dynamic-import-inlining-object/_expected/amd/entry.js index a7385ad2c1b..fef3de875f4 100644 --- a/test/chunking-form/samples/dynamic-import-inlining-object/_expected/amd/entry.js +++ b/test/chunking-form/samples/dynamic-import-inlining-object/_expected/amd/entry.js @@ -1,13 +1,10 @@ define(['exports'], (function (exports) { 'use strict'; const bar = 2; - Promise.resolve().then(function () { return foo$1; }); + Promise.resolve().then(function () { return foo; }); - const foo = 1; - - var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo + var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); exports.bar = bar; diff --git a/test/chunking-form/samples/dynamic-import-inlining-object/_expected/cjs/entry.js b/test/chunking-form/samples/dynamic-import-inlining-object/_expected/cjs/entry.js index e20ce0c5bb8..218beb76d7d 100644 --- a/test/chunking-form/samples/dynamic-import-inlining-object/_expected/cjs/entry.js +++ b/test/chunking-form/samples/dynamic-import-inlining-object/_expected/cjs/entry.js @@ -1,13 +1,10 @@ 'use strict'; const bar = 2; -Promise.resolve().then(function () { return foo$1; }); +Promise.resolve().then(function () { return foo; }); -const foo = 1; - -var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo +var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); exports.bar = bar; diff --git a/test/chunking-form/samples/dynamic-import-inlining-object/_expected/es/entry.js b/test/chunking-form/samples/dynamic-import-inlining-object/_expected/es/entry.js index ae434046e6c..5a35de939db 100644 --- a/test/chunking-form/samples/dynamic-import-inlining-object/_expected/es/entry.js +++ b/test/chunking-form/samples/dynamic-import-inlining-object/_expected/es/entry.js @@ -1,11 +1,8 @@ const bar = 2; -Promise.resolve().then(function () { return foo$1; }); +Promise.resolve().then(function () { return foo; }); -const foo = 1; - -var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo +var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); export { bar }; diff --git a/test/chunking-form/samples/dynamic-import-inlining-object/_expected/system/entry.js b/test/chunking-form/samples/dynamic-import-inlining-object/_expected/system/entry.js index f3161f5e77f..2f4d23eacdf 100644 --- a/test/chunking-form/samples/dynamic-import-inlining-object/_expected/system/entry.js +++ b/test/chunking-form/samples/dynamic-import-inlining-object/_expected/system/entry.js @@ -4,13 +4,10 @@ System.register([], (function (exports) { execute: (function () { const bar = exports('bar', 2); - Promise.resolve().then(function () { return foo$1; }); + Promise.resolve().then(function () { return foo; }); - const foo = 1; - - var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo + var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); }) diff --git a/test/chunking-form/samples/resolve-dynamic-import/_expected/amd/main.js b/test/chunking-form/samples/resolve-dynamic-import/_expected/amd/main.js index 9b96903bba5..07244980fcc 100644 --- a/test/chunking-form/samples/resolve-dynamic-import/_expected/amd/main.js +++ b/test/chunking-form/samples/resolve-dynamic-import/_expected/amd/main.js @@ -24,12 +24,10 @@ define(['require', './direct-relative-external', 'to-indirect-relative-external' new Promise(function (resolve, reject) { require(['direct-absolute-external'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); new Promise(function (resolve, reject) { require(['to-indirect-absolute-external'], function (m) { resolve(/*#__PURE__*/_interopNamespaceDefault(m)); }, reject); }); - const value = 'existing'; console.log('existing'); var existing = /*#__PURE__*/Object.freeze({ - __proto__: null, - value: value + __proto__: null }); //main diff --git a/test/chunking-form/samples/resolve-dynamic-import/_expected/cjs/main.js b/test/chunking-form/samples/resolve-dynamic-import/_expected/cjs/main.js index 75bb253218c..118daa57a84 100644 --- a/test/chunking-form/samples/resolve-dynamic-import/_expected/cjs/main.js +++ b/test/chunking-form/samples/resolve-dynamic-import/_expected/cjs/main.js @@ -12,12 +12,10 @@ import('to-indirect-relative-external'); import('direct-absolute-external'); import('to-indirect-absolute-external'); -const value = 'existing'; console.log('existing'); var existing = /*#__PURE__*/Object.freeze({ - __proto__: null, - value: value + __proto__: null }); //main diff --git a/test/chunking-form/samples/resolve-dynamic-import/_expected/es/main.js b/test/chunking-form/samples/resolve-dynamic-import/_expected/es/main.js index fb6bacbadd2..821ceee41c6 100644 --- a/test/chunking-form/samples/resolve-dynamic-import/_expected/es/main.js +++ b/test/chunking-form/samples/resolve-dynamic-import/_expected/es/main.js @@ -10,12 +10,10 @@ import('to-indirect-relative-external'); import('direct-absolute-external'); import('to-indirect-absolute-external'); -const value = 'existing'; console.log('existing'); var existing = /*#__PURE__*/Object.freeze({ - __proto__: null, - value: value + __proto__: null }); //main diff --git a/test/chunking-form/samples/resolve-dynamic-import/_expected/system/main.js b/test/chunking-form/samples/resolve-dynamic-import/_expected/system/main.js index db84850fb05..425eeb50564 100644 --- a/test/chunking-form/samples/resolve-dynamic-import/_expected/system/main.js +++ b/test/chunking-form/samples/resolve-dynamic-import/_expected/system/main.js @@ -11,12 +11,10 @@ System.register(['./direct-relative-external', 'to-indirect-relative-external', module.import('direct-absolute-external'); module.import('to-indirect-absolute-external'); - const value = 'existing'; console.log('existing'); var existing = /*#__PURE__*/Object.freeze({ - __proto__: null, - value: value + __proto__: null }); //main diff --git a/test/form/samples/deprecated/dynamic-import-inlining-array/_expected.js b/test/form/samples/deprecated/dynamic-import-inlining-array/_expected.js index ae434046e6c..5a35de939db 100644 --- a/test/form/samples/deprecated/dynamic-import-inlining-array/_expected.js +++ b/test/form/samples/deprecated/dynamic-import-inlining-array/_expected.js @@ -1,11 +1,8 @@ const bar = 2; -Promise.resolve().then(function () { return foo$1; }); +Promise.resolve().then(function () { return foo; }); -const foo = 1; - -var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo +var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); export { bar }; diff --git a/test/form/samples/deprecated/dynamic-import-inlining/_expected.js b/test/form/samples/deprecated/dynamic-import-inlining/_expected.js index ae434046e6c..5a35de939db 100644 --- a/test/form/samples/deprecated/dynamic-import-inlining/_expected.js +++ b/test/form/samples/deprecated/dynamic-import-inlining/_expected.js @@ -1,11 +1,8 @@ const bar = 2; -Promise.resolve().then(function () { return foo$1; }); +Promise.resolve().then(function () { return foo; }); -const foo = 1; - -var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo +var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); export { bar }; diff --git a/test/form/samples/dynamic-import-inlining-array/_expected.js b/test/form/samples/dynamic-import-inlining-array/_expected.js index ae434046e6c..5a35de939db 100644 --- a/test/form/samples/dynamic-import-inlining-array/_expected.js +++ b/test/form/samples/dynamic-import-inlining-array/_expected.js @@ -1,11 +1,8 @@ const bar = 2; -Promise.resolve().then(function () { return foo$1; }); +Promise.resolve().then(function () { return foo; }); -const foo = 1; - -var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo +var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); export { bar }; diff --git a/test/form/samples/dynamic-import-inlining/_expected.js b/test/form/samples/dynamic-import-inlining/_expected.js index ae434046e6c..5a35de939db 100644 --- a/test/form/samples/dynamic-import-inlining/_expected.js +++ b/test/form/samples/dynamic-import-inlining/_expected.js @@ -1,11 +1,8 @@ const bar = 2; -Promise.resolve().then(function () { return foo$1; }); +Promise.resolve().then(function () { return foo; }); -const foo = 1; - -var foo$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - foo: foo +var foo = /*#__PURE__*/Object.freeze({ + __proto__: null }); export { bar }; diff --git a/test/form/samples/no-treeshake/_config.js b/test/form/samples/no-treeshake/_config.js index 76d525b3517..e49231de636 100644 --- a/test/form/samples/no-treeshake/_config.js +++ b/test/form/samples/no-treeshake/_config.js @@ -5,7 +5,8 @@ module.exports = { treeshake: false, output: { globals: { external: 'external' }, - name: /* not shaken, but */ 'stirred' + name: /* not shaken, but */ 'stirred', + inlineDynamicImports: true } } }; diff --git a/test/form/samples/no-treeshake/_expected.js b/test/form/samples/no-treeshake/_expected.js index d0a6b53417a..9ec3c7d3449 100644 --- a/test/form/samples/no-treeshake/_expected.js +++ b/test/form/samples/no-treeshake/_expected.js @@ -54,4 +54,16 @@ try { const x = 1; } catch {} +const { fred: fred$1 } = await Promise.resolve().then(function () { return dynamicImported$1; }); + +const fred = 1; + +var dynamicImported = () => fred; + +var dynamicImported$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + default: dynamicImported, + fred: fred +}); + export { create, getPrototypeOf, quux, quux as strange }; diff --git a/test/form/samples/no-treeshake/dynamic-imported.js b/test/form/samples/no-treeshake/dynamic-imported.js new file mode 100644 index 00000000000..42eb4136650 --- /dev/null +++ b/test/form/samples/no-treeshake/dynamic-imported.js @@ -0,0 +1,3 @@ +export const fred = 1; + +export default () => fred; diff --git a/test/form/samples/no-treeshake/main.js b/test/form/samples/no-treeshake/main.js index 2dde2e93fa0..c60d8840915 100644 --- a/test/form/samples/no-treeshake/main.js +++ b/test/form/samples/no-treeshake/main.js @@ -51,3 +51,5 @@ test({ try { const x = 1; } catch {} + +const { fred } = await import('./dynamic-imported.js'); diff --git a/test/form/samples/treeshake-deterministic-dynamic-import/_config.js b/test/form/samples/treeshake-deterministic-dynamic-import/_config.js new file mode 100644 index 00000000000..e611c1d4186 --- /dev/null +++ b/test/form/samples/treeshake-deterministic-dynamic-import/_config.js @@ -0,0 +1,40 @@ +module.exports = { + description: 'treeshakes dynamic imports when the target is deterministic', + options: { + output: { + inlineDynamicImports: true + }, + external: ['external'], + plugins: [ + { + resolveId(id) { + if (/(bail|effect)-(\d+).js$/.test(id)) { + return id; + } + return null; + }, + load(id) { + const match = /(bail|effect)-(\d+).js$/.exec(id); + if (match) { + if (match[1] === 'bail') + return { + code: [ + `export default '@included-bail-${match[2]}'`, + `export const named${match[2]} = 'bail${match[2]}';` + ].join('\n') + }; + else if (match[1] === 'effect') { + return { + code: [ + 'export function fn() { /* @tree-shaken */ }', + `console.log('@included-effect-${match[2]}');` + ].join('\n') + }; + } + } + return null; + } + } + ] + } +}; diff --git a/test/form/samples/treeshake-deterministic-dynamic-import/_expected.js b/test/form/samples/treeshake-deterministic-dynamic-import/_expected.js new file mode 100644 index 00000000000..8a69e51d073 --- /dev/null +++ b/test/form/samples/treeshake-deterministic-dynamic-import/_expected.js @@ -0,0 +1,239 @@ +import * as external from 'external'; + +function _mergeNamespaces(n, m) { + m.forEach(function (e) { + e && typeof e !== 'string' && !Array.isArray(e) && Object.keys(e).forEach(function (k) { + if (k !== 'default' && !(k in n)) { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + }); + return Object.freeze(n); +} + +async function entry() { + // simple + const { foo1: foo } = await Promise.resolve().then(function () { return sub1; }); + await Promise.resolve().then(function () { return sub1; }); +(await Promise.resolve().then(function () { return sub2; })).bar2(); + await Promise.resolve().then(function () { return sub2; }); + await Promise.resolve().then(function () { return sub2; }); + Promise.resolve().then(function () { return sub2; }).then(({ baz2 }) => baz2); + Promise.resolve().then(function () { return sub2; }).then(function({ reexported }) { }); + + // external with unknown namespace + await Promise.resolve().then(function () { return sub4; }); + + // side-effect only + Promise.resolve().then(function () { return effect1; }); + await Promise.resolve().then(function () { return effect2; }); + Promise.resolve().then(function () { return effect3; }).then(function() { }); + Promise.resolve().then(function () { return effect4; }).then(); + Promise.resolve().then(function () { return effect5; }).catch(() => {}); + Promise.resolve().then(function () { return effect6; }).finally(() => {}); + + // bail out + await Promise.resolve().then(function () { return bail1$1; }); + Promise.resolve().then(function () { return bail1$1; }); // this make it bail out + + await Promise.resolve().then(function () { return bail2$1; }) + + (await Promise.resolve().then(function () { return bail3$1; }))[foo]; + + await Promise.resolve().then(function () { return bail4$1; }).name4; // access on promise, not on export + + Promise.resolve().then(function () { return bail5$1; }).then(foo); + + await Promise.resolve().then(function () { return bail6$1; }).then(function({ named6, ...args }) { }); + + [ + Promise.resolve().then(function () { return bail7$1; }), + Promise.resolve().then(function () { return bail8$1; }), + ]; + + await Promise.resolve().then(function () { return bail9$1; }); + + Promise.resolve().then(function () { return bail10$1; }).then(({ [foo]: bar }) => {}); +} + +function foo1() { + return 'foo1'; +} + +console.log('side-effect1'); + +var sub1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + foo1: foo1 +}); + +function foo3() { + return 'foo3'; +} + +function bar3() { + return 'bar3'; +} + +console.log('side-effect3'); + +function foo2() { + return 'foo2'; +} + +function bar2() { + return 'bar2'; +} + +function baz2() { + return 'baz2'; +} + +var sub2 = /*#__PURE__*/Object.freeze({ + __proto__: null, + bar2: bar2, + bar3: bar3, + baz2: baz2, + foo2: foo2, + foo3: foo3, + reexported: bar3 +}); + +const foo4 = 3; + +var sub4 = /*#__PURE__*/_mergeNamespaces({ + __proto__: null, + foo4: foo4 +}, [external]); + +console.log('@included-effect-1'); + +var effect1 = /*#__PURE__*/Object.freeze({ + __proto__: null +}); + +console.log('@included-effect-2'); + +var effect2 = /*#__PURE__*/Object.freeze({ + __proto__: null +}); + +console.log('@included-effect-3'); + +var effect3 = /*#__PURE__*/Object.freeze({ + __proto__: null +}); + +console.log('@included-effect-4'); + +var effect4 = /*#__PURE__*/Object.freeze({ + __proto__: null +}); + +console.log('@included-effect-5'); + +var effect5 = /*#__PURE__*/Object.freeze({ + __proto__: null +}); + +console.log('@included-effect-6'); + +var effect6 = /*#__PURE__*/Object.freeze({ + __proto__: null +}); + +var bail1 = '@included-bail-1'; +const named1 = 'bail1'; + +var bail1$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + default: bail1, + named1: named1 +}); + +var bail2 = '@included-bail-2'; +const named2 = 'bail2'; + +var bail2$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + default: bail2, + named2: named2 +}); + +var bail3 = '@included-bail-3'; +const named3 = 'bail3'; + +var bail3$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + default: bail3, + named3: named3 +}); + +var bail4 = '@included-bail-4'; +const named4 = 'bail4'; + +var bail4$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + default: bail4, + named4: named4 +}); + +var bail5 = '@included-bail-5'; +const named5 = 'bail5'; + +var bail5$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + default: bail5, + named5: named5 +}); + +var bail6 = '@included-bail-6'; +const named6 = 'bail6'; + +var bail6$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + default: bail6, + named6: named6 +}); + +var bail7 = '@included-bail-7'; +const named7 = 'bail7'; + +var bail7$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + default: bail7, + named7: named7 +}); + +var bail8 = '@included-bail-8'; +const named8 = 'bail8'; + +var bail8$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + default: bail8, + named8: named8 +}); + +var bail9 = '@included-bail-9'; +const named9 = 'bail9'; + +var bail9$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + default: bail9, + named9: named9 +}); + +var bail10 = '@included-bail-10'; +const named10 = 'bail10'; + +var bail10$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + default: bail10, + named10: named10 +}); + +export { entry }; diff --git a/test/form/samples/treeshake-deterministic-dynamic-import/main.js b/test/form/samples/treeshake-deterministic-dynamic-import/main.js new file mode 100644 index 00000000000..48bd026a602 --- /dev/null +++ b/test/form/samples/treeshake-deterministic-dynamic-import/main.js @@ -0,0 +1,46 @@ +export async function entry() { + // simple + const { foo1: foo } = await import('./sub1.js'); + const { doesNotExists } = await import('./sub1.js'); + + // multiple + ;(await import('./sub2.js')).bar2() + const { foo2 } = await import('./sub2.js'); + const { foo3 } = await import('./sub2.js'); + import('./sub2.js').then(({ baz2 }) => baz2) + import('./sub2.js').then(function({ reexported }) { reexported }) + + // external with unknown namespace + const { foo4, x } = await import('./sub4'); + + // side-effect only + import('./effect-1.js') + await import('./effect-2.js') + import('./effect-3.js').then(function() { }) + import('./effect-4.js').then() + import('./effect-5.js').catch(() => {}) + import('./effect-6.js').finally(() => {}) + + // bail out + const { named1 } = await import('./bail-1.js'); + const promise = import('./bail-1.js') // this make it bail out + + const { ...named2 } = await import('./bail-2.js') + + (await import('./bail-3.js'))[foo] + + await import('./bail-4.js').name4 // access on promise, not on export + + import('./bail-5.js').then(foo) + + await import('./bail-6.js').then(function({ named6, ...args }) { }) + + const promises = [ + import('./bail-7.js'), + import('./bail-8.js'), + ] + + const { [foo]: bar } = await import('./bail-9.js') + + import('./bail-10.js').then(({ [foo]: bar }) => {}) +} diff --git a/test/form/samples/treeshake-deterministic-dynamic-import/sub1.js b/test/form/samples/treeshake-deterministic-dynamic-import/sub1.js new file mode 100644 index 00000000000..13f7de36261 --- /dev/null +++ b/test/form/samples/treeshake-deterministic-dynamic-import/sub1.js @@ -0,0 +1,9 @@ +export function foo1() { + return 'foo1'; +} + +export function bar1() { + return 'bar1'; // this should be tree-shaken +} + +console.log('side-effect1'); diff --git a/test/form/samples/treeshake-deterministic-dynamic-import/sub2.js b/test/form/samples/treeshake-deterministic-dynamic-import/sub2.js new file mode 100644 index 00000000000..4ba13873bfc --- /dev/null +++ b/test/form/samples/treeshake-deterministic-dynamic-import/sub2.js @@ -0,0 +1,19 @@ +export function foo2() { + return 'foo2'; +} + +export function bar2() { + return 'bar2'; +} + +export function baz2() { + return 'baz2'; +} + +export function qux2() { + return 'qux2'; // this should be tree-shaken +} + +export { bar3 as reexported } from './sub3.js' + +export * from './sub3.js'; diff --git a/test/form/samples/treeshake-deterministic-dynamic-import/sub3.js b/test/form/samples/treeshake-deterministic-dynamic-import/sub3.js new file mode 100644 index 00000000000..0e0a3c530fb --- /dev/null +++ b/test/form/samples/treeshake-deterministic-dynamic-import/sub3.js @@ -0,0 +1,13 @@ +export function foo3() { + return 'foo3'; +} + +export function bar3() { + return 'bar3'; +} + +export function baz3() { + return 'baz3'; // this should be tree-shaken +} + +console.log('side-effect3') diff --git a/test/form/samples/treeshake-deterministic-dynamic-import/sub4.js b/test/form/samples/treeshake-deterministic-dynamic-import/sub4.js new file mode 100644 index 00000000000..746302805dd --- /dev/null +++ b/test/form/samples/treeshake-deterministic-dynamic-import/sub4.js @@ -0,0 +1,4 @@ +export const foo4 = 3; +export const bar4 = 4; + +export * from 'external'; diff --git a/test/function/samples/perf-adds-plugin-context-to-plugins/foo.js b/test/function/samples/perf-adds-plugin-context-to-plugins/foo.js index d02ba545bd3..aaadd073c11 100644 --- a/test/function/samples/perf-adds-plugin-context-to-plugins/foo.js +++ b/test/function/samples/perf-adds-plugin-context-to-plugins/foo.js @@ -1 +1,3 @@ export default 'foo'; + +console.log('side-effect'); diff --git a/test/function/samples/tree-shake-deterministic-dynamic-import/_config.js b/test/function/samples/tree-shake-deterministic-dynamic-import/_config.js new file mode 100644 index 00000000000..1a8ad6ecf3f --- /dev/null +++ b/test/function/samples/tree-shake-deterministic-dynamic-import/_config.js @@ -0,0 +1,46 @@ +module.exports = { + description: 'treeshakes dynamic imports when the target is deterministic', + options: { + output: { + inlineDynamicImports: true + }, + plugins: [ + { + resolveId(id) { + if (/(bail|effect)-(\d+).js$/.test(id)) { + return id; + } + return null; + }, + load(id) { + const match = /(bail|effect)-(\d+).js$/.exec(id); + if (match) { + if (match[1] === 'bail') + return { + code: [ + `export default '@included-bail-${match[2]}'`, + `export const named${match[2]} = 'bail${match[2]}';` + ].join('\n') + }; + else if (match[1] === 'effect') { + return { + code: [ + 'export function fn() { /* @tree-shaken */ }', + `console.log('@included-effect-${match[2]}');` + ].join('\n') + }; + } + } + return null; + } + } + ] + }, + async exports({ allExports }) { + await allExports(); + }, + context: { + named1: 'named1', + named4: 'named4' + } +}; diff --git a/test/function/samples/tree-shake-deterministic-dynamic-import/main.js b/test/function/samples/tree-shake-deterministic-dynamic-import/main.js new file mode 100644 index 00000000000..32edef50e36 --- /dev/null +++ b/test/function/samples/tree-shake-deterministic-dynamic-import/main.js @@ -0,0 +1,26 @@ +export const allExports = () => Promise.all([ + (async () => { + const value = (await import('./bail-1.js'))[named1] + assert.strictEqual(value, 'bail1'); + })(), + (async () => { + const fn = (exports) => { + assert.deepEqual(exports, { + named2: 'bail2', + default: '@included-bail-2' + }); + } + await import('./bail-2.js').then(fn) + })(), + (async () => { + await import('./bail-3.js').then(({ default: _, ...args }) => { + assert.deepEqual(args, { + named3: 'bail3' + }); + }) + })(), + (async () => { + const { [named4]: value } = await import('./bail-4.js') + assert.strictEqual(value, 'bail4'); + })(), +]);