Skip to content

Commit

Permalink
feat: tree-shake deterministic dynamic imports (#4952)
Browse files Browse the repository at this point in the history
* 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 <lukastaegert@users.noreply.github.com>
  • Loading branch information
antfu and lukastaegert committed Apr 23, 2023
1 parent 5af8799 commit b79b73c
Show file tree
Hide file tree
Showing 33 changed files with 661 additions and 90 deletions.
39 changes: 38 additions & 1 deletion src/Module.ts
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
116 changes: 116 additions & 0 deletions src/ast/nodes/ImportExpression.ts
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -296,3 +400,15 @@ const accessedImportGlobals: Record<string, string[]> = {
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;
}
8 changes: 4 additions & 4 deletions src/ast/variables/NamespaceVariable.ts
Expand Up @@ -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,
Expand All @@ -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 } });
Expand Down
@@ -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;
Expand Down
@@ -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;
@@ -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 };
Expand Up @@ -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
});

})
Expand Down
@@ -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;
Expand Down
@@ -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;
@@ -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 };
Expand Up @@ -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
});

})
Expand Down
Expand Up @@ -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
Expand Down
Expand Up @@ -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
Expand Down
Expand Up @@ -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
Expand Down

0 comments on commit b79b73c

Please sign in to comment.