Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tree-shake deterministic dynamic imports #4952

Merged
merged 26 commits into from Apr 23, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/Module.ts
Expand Up @@ -716,6 +716,27 @@ export default class Module {
this.includeAllExports(false);
}

includeExportsByNames(names: readonly string[], includeNamespaceMembers: boolean): void {
antfu marked this conversation as resolved.
Show resolved Hide resolved
if (!this.isExecuted) {
markModuleAndImpureDependenciesAsExecuted(this);
this.graph.needsTreeshakingPass = true;
}

for (const name of names) {
const variable = this.getVariableForExportName(name)[0];
if (variable) {
antfu marked this conversation as resolved.
Show resolved Hide resolved
variable.deoptimizePath(UNKNOWN_PATH);
if (!variable.included) {
this.includeVariable(variable);
}
}
}

if (includeNamespaceMembers) {
antfu marked this conversation as resolved.
Show resolved Hide resolved
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 +1239,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, true);
} 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 }) => {})`
Comment on lines +61 to +63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be doc'ed here too? https://rollupjs.org/introduction/#tree-shaking

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any PR for the docs is welcome! I guess something similar to what Parcel has does not hurt.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍
And we might even include a REPL link to see it in action and play around

*
* 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) {
antfu marked this conversation as resolved.
Show resolved Hide resolved
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')
antfu marked this conversation as resolved.
Show resolved Hide resolved
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