From 3f34cf6448ac1d843fdd91a92c52b513caf3b630 Mon Sep 17 00:00:00 2001 From: sun0day Date: Mon, 28 Aug 2023 16:42:47 +0800 Subject: [PATCH 1/7] fix(build-import-analysis): treeshaken dynamic import when injecting preload --- .../src/node/plugins/importAnalysisBuild.ts | 55 ++++++++++++++++++- .../__tests__/dynamic-import.spec.ts | 28 +++++++++- playground/dynamic-import/nested/index.js | 23 ++++++++ .../nested/treeshaken/treeshaken.js | 31 +++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 playground/dynamic-import/nested/treeshaken/treeshaken.js diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 1d046eb49f581e..5df82da83eadaa 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -46,6 +46,9 @@ const dynamicImportPrefixRE = /import\s*\(/ const optimizedDepChunkRE = /\/chunk-[A-Z\d]{8}\.js/ const optimizedDepDynamicRE = /-[A-Z\d]{8}\.js/ +const dynamicImportTreeshakenRE = + /(\b(const|let|var)\s+(\{[^}.]+\})\s*=\s*await\s+import\([^)]+\))|(\(\s*await\s+import\([^)]+\)\s*\)(\??\.[^;[\s]+)+)|\bimport\([^)]+\)(\.then\([^{]*\{([^}]+)\})/g + function toRelativePath(filename: string, importer: string) { const relPath = path.relative(path.dirname(importer), filename) return relPath[0] === '.' ? relPath : `./${relPath}` @@ -285,6 +288,39 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { return [url, resolved.id] } + const dynamicImports: Record< + number, + { declaration?: string; names?: string; chains?: string } + > = {} + + if (insertPreload) { + let match + while ((match = dynamicImportTreeshakenRE.exec(source))) { + // handle `const {foo} = await import('foo')` + if (match[1]) { + dynamicImports[dynamicImportTreeshakenRE.lastIndex] = { + declaration: `${match[2]} ${match[3]}`, + names: match[3]?.trim(), + } + continue + } + + // handle `(await import('foo')).foo` + if (match[4]) { + dynamicImports[ + dynamicImportTreeshakenRE.lastIndex - match[5]?.length - 1 + ] = { chains: match[5] } + continue + } + + // handle `import('foo').then(({foo})=>{})` + const names = match[7]?.trim() + dynamicImports[ + dynamicImportTreeshakenRE.lastIndex - match[6]?.length + ] = { declaration: `const {${names}}`, names: `{ ${names} }` } + } + } + let s: MagicString | undefined const str = () => s || (s = new MagicString(source)) let needPreloadHelper = false @@ -309,7 +345,24 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { if (isDynamicImport && insertPreload) { needPreloadHelper = true - str().prependLeft(expStart, `${preloadMethod}(() => `) + const { declaration, names, chains } = dynamicImports[expEnd] || {} + if (names) { + str().prependLeft( + expStart, + `${preloadMethod}(async () => { ${declaration} = await `, + ) + str().appendRight(expEnd, `;return ${names}}`) + } else if (chains) { + const name = chains.match(/\.([^.?]+)/)?.[1] || '' + str().prependLeft( + expStart, + `${preloadMethod}(async () => { const ${name} = (await `, + ) + str().appendRight(expEnd, `).${name}; return { ${name} }}`) + } else { + str().prependLeft(expStart, `${preloadMethod}(() => `) + } + str().appendRight( expEnd, `,${isModernFlag}?"${preloadMarker}":void 0${ diff --git a/playground/dynamic-import/__tests__/dynamic-import.spec.ts b/playground/dynamic-import/__tests__/dynamic-import.spec.ts index d81317678bc6f3..0fc0647ade9bf8 100644 --- a/playground/dynamic-import/__tests__/dynamic-import.spec.ts +++ b/playground/dynamic-import/__tests__/dynamic-import.spec.ts @@ -1,5 +1,13 @@ import { expect, test } from 'vitest' -import { getColor, isBuild, page, serverLogs, untilUpdated } from '~utils' +import { + browserLogs, + findAssetFile, + getColor, + isBuild, + page, + serverLogs, + untilUpdated, +} from '~utils' test('should load literal dynamic import', async () => { await page.click('.baz') @@ -162,3 +170,21 @@ test.runIf(isBuild)( ) }, ) + +test('dynamic import treeshaken log', async () => { + const log = browserLogs.join('\n') + expect(log).toContain('treeshaken foo') + expect(log).toContain('treeshaken bar') + expect(log).toContain('treeshaken baz1') + expect(log).toContain('treeshaken baz2') + expect(log).toContain('treeshaken baz3') + expect(log).toContain('treeshaken baz4') + expect(log).toContain('treeshaken baz5') + expect(log).toContain('treeshaken default') + + expect(log).not.toContain('treeshaken removed') +}) + +test.runIf(isBuild)('dynamic import treeshaken file', async () => { + expect(findAssetFile(/treeshaken.+\.js$/)).not.toContain('treeshaken removed') +}) diff --git a/playground/dynamic-import/nested/index.js b/playground/dynamic-import/nested/index.js index 180dd6b5ad3b6b..99f87b642aee29 100644 --- a/playground/dynamic-import/nested/index.js +++ b/playground/dynamic-import/nested/index.js @@ -130,5 +130,28 @@ import(`../nested/${base}.js`).then((mod) => { import(`../nested/nested/${base}.js`).then((mod) => { text('.dynamic-import-nested-self', mod.self) }) +;(async function () { + const { foo } = await import('./treeshaken/treeshaken.js') + const { bar, default: tree } = await import('./treeshaken/treeshaken.js') + const baz1 = (await import('./treeshaken/treeshaken.js')).baz1 + const baz2 = (await import('./treeshaken/treeshaken.js')).baz2.log + const baz3 = (await import('./treeshaken/treeshaken.js')).baz3?.log + const baz4 = await import('./treeshaken/treeshaken.js').then( + ({ baz4 }) => baz4, + ) + const baz5 = await import('./treeshaken/treeshaken.js').then(function ({ + baz5, + }) { + return baz5 + }) + foo() + bar() + tree() + baz1() + baz2() + baz3() + baz4() + baz5() +})() console.log('index.js') diff --git a/playground/dynamic-import/nested/treeshaken/treeshaken.js b/playground/dynamic-import/nested/treeshaken/treeshaken.js new file mode 100644 index 00000000000000..f56824fc996240 --- /dev/null +++ b/playground/dynamic-import/nested/treeshaken/treeshaken.js @@ -0,0 +1,31 @@ +export const foo = () => { + console.log('treeshaken foo') +} +export const bar = () => { + console.log('treeshaken bar') +} +export const baz1 = () => { + console.log('treeshaken baz1') +} +export const baz2 = { + log: () => { + console.log('treeshaken baz2') + }, +} +export const baz3 = { + log: () => { + console.log('treeshaken baz3') + }, +} +export const baz4 = () => { + console.log('treeshaken baz4') +} +export const baz5 = () => { + console.log('treeshaken baz5') +} +export const removed = () => { + console.log('treeshaken removed') +} +export default () => { + console.log('treeshaken default') +} From 3f052e2792a83d70741cfa64d3e15b3d132c2840 Mon Sep 17 00:00:00 2001 From: sun0day Date: Mon, 28 Aug 2023 17:46:34 +0800 Subject: [PATCH 2/7] fix(build-import-analysis): more accurate dynamicImportTreeshakenRE --- packages/vite/src/node/plugins/importAnalysisBuild.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 5df82da83eadaa..325b2160f1a212 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -47,7 +47,7 @@ const optimizedDepChunkRE = /\/chunk-[A-Z\d]{8}\.js/ const optimizedDepDynamicRE = /-[A-Z\d]{8}\.js/ const dynamicImportTreeshakenRE = - /(\b(const|let|var)\s+(\{[^}.]+\})\s*=\s*await\s+import\([^)]+\))|(\(\s*await\s+import\([^)]+\)\s*\)(\??\.[^;[\s]+)+)|\bimport\([^)]+\)(\.then\([^{]*\{([^}]+)\})/g + /(\b(const|let|var)\s+(\{[^}.]+\})\s*=\s*await\s+import\([^)]+\))|(\(\s*await\s+import\([^)]+\)\s*\)(\??\.[^;[\s]+)+)|\bimport\([^)]+\)(\.then\([^{]*?\(\s*\{([^}.]+)\})/g function toRelativePath(filename: string, importer: string) { const relPath = path.relative(path.dirname(importer), filename) @@ -356,9 +356,12 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const name = chains.match(/\.([^.?]+)/)?.[1] || '' str().prependLeft( expStart, - `${preloadMethod}(async () => { const ${name} = (await `, + `${preloadMethod}(async () => { const __vite_temp__ = (await `, + ) + str().appendRight( + expEnd, + `).${name}; return { ${name}: __vite_temp__ }}`, ) - str().appendRight(expEnd, `).${name}; return { ${name} }}`) } else { str().prependLeft(expStart, `${preloadMethod}(() => `) } From 6e98abf5631cd967b48e24156ccf1ae6f8373532 Mon Sep 17 00:00:00 2001 From: sun0day Date: Mon, 28 Aug 2023 19:57:02 +0800 Subject: [PATCH 3/7] fix(build-import-analysis): support \s*\.then --- packages/vite/src/node/plugins/importAnalysisBuild.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 325b2160f1a212..3ddbe98cae4b0e 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -47,7 +47,7 @@ const optimizedDepChunkRE = /\/chunk-[A-Z\d]{8}\.js/ const optimizedDepDynamicRE = /-[A-Z\d]{8}\.js/ const dynamicImportTreeshakenRE = - /(\b(const|let|var)\s+(\{[^}.]+\})\s*=\s*await\s+import\([^)]+\))|(\(\s*await\s+import\([^)]+\)\s*\)(\??\.[^;[\s]+)+)|\bimport\([^)]+\)(\.then\([^{]*?\(\s*\{([^}.]+)\})/g + /(\b(const|let|var)\s+(\{[^}.]+\})\s*=\s*await\s+import\([^)]+\))|(\(\s*await\s+import\([^)]+\)\s*\)(\??\.[^;[\s]+)+)|\bimport\([^)]+\)(\s*\.then\([^{]*?\(\s*\{([^}.]+)\})/g function toRelativePath(filename: string, importer: string) { const relPath = path.relative(path.dirname(importer), filename) From 83bb05f723a0bbefbbf1b3aa2193ff0044346b80 Mon Sep 17 00:00:00 2001 From: sun0day Date: Tue, 19 Sep 2023 10:46:07 +0800 Subject: [PATCH 4/7] chore: add comment --- .../src/node/plugins/importAnalysisBuild.ts | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index e360029c19cac5..ffa957b7ecc473 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -295,7 +295,14 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { if (insertPreload) { let match while ((match = dynamicImportTreeshakenRE.exec(source))) { - // handle `const {foo} = await import('foo')` + /* handle `const {foo} = await import('foo')` + * + * match[1]: `const {foo} = await import('foo')` + * match[2]: `const` + * match[3]: `{foo}` + * import end: `const {foo} = await import('foo')_` + * ^ + */ if (match[1]) { dynamicImports[dynamicImportTreeshakenRE.lastIndex] = { declaration: `${match[2]} ${match[3]}`, @@ -304,7 +311,13 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { continue } - // handle `(await import('foo')).foo` + /* handle `(await import('foo')).foo` + * + * match[4]: `(await import('foo')).foo` + * match[5]: `.foo` + * import end: `(await import('foo'))` + * ^ + */ if (match[4]) { dynamicImports[ dynamicImportTreeshakenRE.lastIndex - match[5]?.length - 1 @@ -312,7 +325,13 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { continue } - // handle `import('foo').then(({foo})=>{})` + /* handle `import('foo').then(({foo})=>{})` + * + * match[6]: `.then(({foo}` + * match[7]: `foo` + * import end: `import('foo').` + * ^ + */ const names = match[7]?.trim() dynamicImports[ dynamicImportTreeshakenRE.lastIndex - match[6]?.length @@ -346,12 +365,21 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { needPreloadHelper = true const { declaration, names, chains } = dynamicImports[expEnd] || {} if (names) { + /* transform `const {foo} = await import('foo')` + * to `const {foo} = await __vitePreload(async () => { const {foo} = await import('foo');return {foo}}, ...)` + * + * transform `import('foo').then(({foo})=>{})` + * to `__vitePreload(async () => { const {foo} = await import('foo');return { foo }},...).then(({foo})=>{})` + */ str().prependLeft( expStart, `${preloadMethod}(async () => { ${declaration} = await `, ) str().appendRight(expEnd, `;return ${names}}`) } else if (chains) { + /* transform `(await import('foo')).foo` + * to `__vitePreload(async () => { const __vite_temp__ = (await import('foo')).foo; return { foo: __vite_temp__ }},...)).foo` + */ const name = chains.match(/\.([^.?]+)/)?.[1] || '' str().prependLeft( expStart, From b8c2f1ac8db58f6cb2e543489b0b0fe5656361f9 Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 19 Sep 2023 21:20:50 +0800 Subject: [PATCH 5/7] chore: add additional comments --- packages/vite/src/node/plugins/importAnalysisBuild.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index ffa957b7ecc473..7855283c58dbb3 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -287,6 +287,9 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { return [url, resolved.id] } + // when wrapping dynamic imports with a preload helper, Rollup is unable to analyze the + // accessed variables for treeshaking. This below tries to match common accessed syntax + // to "copy" it over to the dynamic import wrapped by the preload helper. const dynamicImports: Record< number, { declaration?: string; names?: string; chains?: string } From a919be5e3b36476c9b507d1d4428304cebf83add Mon Sep 17 00:00:00 2001 From: sun0day Date: Tue, 19 Sep 2023 22:04:30 +0800 Subject: [PATCH 6/7] chore: consistent type --- .../src/node/plugins/importAnalysisBuild.ts | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index ffa957b7ecc473..ecebdf98a9a4c3 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -289,7 +289,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const dynamicImports: Record< number, - { declaration?: string; names?: string; chains?: string } + { declaration?: string; names?: string } > = {} if (insertPreload) { @@ -319,9 +319,10 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { * ^ */ if (match[4]) { + const names = match[5].match(/\.([^.?]+)/)?.[1] || '' dynamicImports[ dynamicImportTreeshakenRE.lastIndex - match[5]?.length - 1 - ] = { chains: match[5] } + ] = { declaration: `const {${names}}`, names: `{ ${names} }` } continue } @@ -363,32 +364,22 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { if (isDynamicImport && insertPreload) { needPreloadHelper = true - const { declaration, names, chains } = dynamicImports[expEnd] || {} + const { declaration, names } = dynamicImports[expEnd] || {} if (names) { /* transform `const {foo} = await import('foo')` * to `const {foo} = await __vitePreload(async () => { const {foo} = await import('foo');return {foo}}, ...)` * * transform `import('foo').then(({foo})=>{})` * to `__vitePreload(async () => { const {foo} = await import('foo');return { foo }},...).then(({foo})=>{})` + * + * transform `(await import('foo')).foo` + * to `__vitePreload(async () => { const {foo} = (await import('foo')).foo; return { foo }},...)).foo` */ str().prependLeft( expStart, `${preloadMethod}(async () => { ${declaration} = await `, ) str().appendRight(expEnd, `;return ${names}}`) - } else if (chains) { - /* transform `(await import('foo')).foo` - * to `__vitePreload(async () => { const __vite_temp__ = (await import('foo')).foo; return { foo: __vite_temp__ }},...)).foo` - */ - const name = chains.match(/\.([^.?]+)/)?.[1] || '' - str().prependLeft( - expStart, - `${preloadMethod}(async () => { const __vite_temp__ = (await `, - ) - str().appendRight( - expEnd, - `).${name}; return { ${name}: __vite_temp__ }}`, - ) } else { str().prependLeft(expStart, `${preloadMethod}(() => `) } From a1d3562cfbdb91bacfa9768c908e82dbca217636 Mon Sep 17 00:00:00 2001 From: sun0day Date: Tue, 19 Sep 2023 22:22:16 +0800 Subject: [PATCH 7/7] fix: default keyword --- packages/vite/src/node/plugins/importAnalysisBuild.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 9ee671327ee925..7b1faf8e8b7cde 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -322,7 +322,11 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { * ^ */ if (match[4]) { - const names = match[5].match(/\.([^.?]+)/)?.[1] || '' + let names = match[5].match(/\.([^.?]+)/)?.[1] || '' + // avoid `default` keyword error + if (names === 'default') { + names = 'default: __vite_default__' + } dynamicImports[ dynamicImportTreeshakenRE.lastIndex - match[5]?.length - 1 ] = { declaration: `const {${names}}`, names: `{ ${names} }` }