Skip to content

Commit

Permalink
feat: support more case
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Apr 20, 2023
1 parent 913bb4c commit 9ac9832
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 117 deletions.
14 changes: 9 additions & 5 deletions src/Module.ts
Expand Up @@ -716,19 +716,23 @@ export default class Module {
this.includeAllExports(false);
}

includeExportsByNames(names: string[]): void {
includeExportsByNames(names: string[], includeNamespaceMembers: boolean): void {
for (const name of names) {
const variable = this.getVariableForExportName(name)[0];
if (variable) {
variable.deoptimizePath(UNKNOWN_PATH);
if (!variable.included) {
this.includeVariable(variable);
}
if (variable instanceof ExternalVariable) {
variable.module.reexported = true;
}
// if (variable instanceof ExternalVariable) {
// variable.module.reexported = true;
// }
}
}

if (includeNamespaceMembers) {
this.namespace.setMergedNamespaces(this.includeAndGetAdditionalMergedNamespaces());
}
}

isIncluded(): boolean | null {
Expand Down Expand Up @@ -1243,7 +1247,7 @@ export default class Module {
: undefined;

if (importedNames) {
resolution.includeExportsByNames(importedNames);
resolution.includeExportsByNames(importedNames, true);
} else {
resolution.includeAllExports(true);
}
Expand Down
77 changes: 52 additions & 25 deletions src/ast/nodes/ImportExpression.ts
Expand Up @@ -12,7 +12,10 @@ import { findFirstOccurrenceOutsideComment, type RenderOptions } from '../../uti
import type { InclusionContext } from '../ExecutionContext';
import type ChildScope from '../scopes/ChildScope';
import type NamespaceVariable from '../variables/NamespaceVariable';
import type ArrowFunctionExpression from './ArrowFunctionExpression';
import type AwaitExpression from './AwaitExpression';
import type CallExpression from './CallExpression';
import type FunctionExpression from './FunctionExpression';
import type Identifier from './Identifier';
import type MemberExpression from './MemberExpression';
import type * as NodeType from './NodeType';
Expand Down Expand Up @@ -55,43 +58,55 @@ export default class ImportExpression extends NodeBase {
*
* - `const { foo } = await import('bar')`.
* - `(await import('bar')).foo`
* - `import('bar').then(({ foo }) => {})`
*
* Returns undefined if it's not deterministic.
*/
getDeterministicImportedNames(): string[] | undefined {
if (this.parent?.type !== 'AwaitExpression') return;
if (this.parent?.type === 'AwaitExpression') {
const awaitExpression = this.parent as AwaitExpression;

const awaitExpression = this.parent as AwaitExpression;
// Case 1: const { foo } = await import('bar')
if (awaitExpression.parent?.type === 'VariableDeclarator') {
const variableDeclarator = awaitExpression.parent as VariableDeclarator;
if (variableDeclarator.id?.type !== 'ObjectPattern') return;

// Case 1: const { foo } = await import('bar')
if (awaitExpression.parent?.type === 'VariableDeclarator') {
const variableDeclarator = awaitExpression.parent as VariableDeclarator;
if (variableDeclarator.id?.type !== 'ObjectPattern') return;

const objectPattern = variableDeclarator.id as ObjectPattern;
return getDeterministicObjectDestructure(variableDeclarator.id as ObjectPattern);
}
// Case 2: (await import('bar')).foo
else {
if (awaitExpression.parent?.type !== 'MemberExpression') return;

const variables: string[] = [];
const memberExpression = awaitExpression.parent as MemberExpression;
if (memberExpression.computed || memberExpression.property.type !== 'Identifier') return;

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 [(memberExpression.property as Identifier).name];
}

return variables;
}
// Case 2: (await import('bar')).foo
else {
if (awaitExpression.parent?.type !== 'MemberExpression') return;
// Case 3: import('bar').then(({ foo }) => {})
else if (this.parent?.type === 'MemberExpression') {
const memberExpression = this.parent as MemberExpression;
if (
memberExpression.property.type !== 'Identifier' ||
(memberExpression.property as Identifier).name !== 'then' ||
memberExpression.parent?.type !== 'CallExpression'
)
return;

const callExpression = memberExpression.parent as CallExpression;

if (
callExpression.arguments.length !== 1 ||
!['ArrowFunctionExpression', 'FunctionExpression'].includes(
callExpression.arguments[0].type
)
)
return;

const memberExpression = awaitExpression.parent as MemberExpression;
if (memberExpression.computed || memberExpression.property.type !== 'Identifier') return;
const callback = callExpression.arguments[0] as ArrowFunctionExpression | FunctionExpression;
if (callback.params.length !== 1 || callback.params[0].type !== 'ObjectPattern') return;

return [(memberExpression.property as Identifier).name];
return getDeterministicObjectDestructure(callback.params[0] as ObjectPattern);
}
}

Expand Down Expand Up @@ -346,3 +361,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;
}
Expand Up @@ -3,6 +3,28 @@ module.exports = {
options: {
output: {
inlineDynamicImports: true
}
},
plugins: [
{
resolveId(id) {
if (/bail-(\d+).js$/.test(id)) {
return id;
}
return null;
},
load(id) {
const match = /bail-(\d+).js$/.exec(id);
if (match) {
return {
code: [
`export default 'should be included ${match[1]}'`,
`export const named${match[1]} = 'bail${match[1]}';`
].join('\n')
};
}
return null;
}
}
]
}
};
111 changes: 68 additions & 43 deletions test/form/samples/treeshake-deterministic-dynamic-import/_expected.js
@@ -1,19 +1,25 @@
async function entry() {
// simple
const { foo1: foo } = await Promise.resolve().then(function () { return sub1; });
(await Promise.resolve().then(function () { return sub2; })).bar3();
(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 }) { });

// bail out
await Promise.resolve().then(function () { return bail1$1; });
Promise.resolve().then(function () { return bail1$1; }); // this should make full1.js not be tree-shaken
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; }).bail4;
await Promise.resolve().then(function () { return bail4$1; }).name4;

Promise.resolve().then(function () { return bail5$1; }).then(foo);

await Promise.resolve().then(function () { return bail6$1; }).then(function({ named6, ...args }) { });
}

function foo1() {
Expand All @@ -27,71 +33,90 @@ var sub1 = /*#__PURE__*/Object.freeze({
foo1: foo1
});

function foo4() {
return 'foo4';
function foo3() {
return 'foo3';
}

console.log('side-effect4');
function bar3() {
return 'bar3';
}

var sub2 = /*#__PURE__*/Object.freeze({
__proto__: null,
foo4: foo4
});
console.log('side-effect3');

function bail1() {
return 'bail1';
function foo2() {
return 'foo2';
}

function bailout1() {
return 'bailout1';
function bar2() {
return 'bar2';
}

var bail1$1 = /*#__PURE__*/Object.freeze({
function baz2() {
return 'baz2';
}

var sub2 = /*#__PURE__*/Object.freeze({
__proto__: null,
bail1: bail1,
bailout1: bailout1
bar2: bar2,
bar3: bar3,
baz2: baz2,
foo2: foo2,
foo3: foo3,
reexported: bar3
});

function bail2() {
return 'bail2';
}
var bail1 = 'should be included 1';
const named1 = 'bail1';

function bailout2() {
return 'bailout2';
}
var bail1$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
default: bail1,
named1: named1
});

var bail2 = 'should be included 2';
const named2 = 'bail2';

var bail2$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
bail2: bail2,
bailout2: bailout2
default: bail2,
named2: named2
});

function bail3() {
return 'bail3';
}

function bailout3() {
return 'bailout3';
}
var bail3 = 'should be included 3';
const named3 = 'bail3';

var bail3$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
bail3: bail3,
bailout3: bailout3
default: bail3,
named3: named3
});

function bail4() {
return 'bail4';
}

function bailout4() {
return 'bailout4';
}
var bail4 = 'should be included 4';
const named4 = 'bail4';

var bail4$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
bail4: bail4,
bailout4: bailout4
default: bail4,
named4: named4
});

var bail5 = 'should be included 5';
const named5 = 'bail5';

var bail5$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
default: bail5,
named5: named5
});

var bail6 = 'should be included 6';
const named6 = 'bail6';

var bail6$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
default: bail6,
named6: named6
});

export { entry };

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

22 changes: 14 additions & 8 deletions test/form/samples/treeshake-deterministic-dynamic-import/main.js
Expand Up @@ -3,17 +3,23 @@ export async function entry() {
const { foo1: foo } = await import('./sub1.js');

// multiple
;(await import('./sub2.js')).bar3()
const { foo3, baz3 } = await import('./sub2.js');
const { foo4 } = await import('./sub2.js');
;(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 })

// bail out
const { bail1 } = await import('./bail1.js');
const promise = import('./bail1.js') // this should make full1.js not be tree-shaken
const { named1 } = await import('./bail-1.js');
const promise = import('./bail-1.js') // this make it bail out

const { ...bail2 } = await import('./bail2.js')
const { ...named2 } = await import('./bail-2.js')

(await import('./bail3.js'))[foo]
(await import('./bail-3.js'))[foo]

await import('./bail4.js').bail4
await import('./bail-4.js').name4

import('./bail-5.js').then(foo)

await import('./bail-6.js').then(function({ named6, ...args }) { })
}

0 comments on commit 9ac9832

Please sign in to comment.