From f43f71f22436832abaa0cac74f4e35e4f9c16e17 Mon Sep 17 00:00:00 2001 From: sun0day Date: Thu, 6 Jun 2024 22:29:24 +0800 Subject: [PATCH] fix(build): allow dynamic import treeshaking when injecting preload (#14221) Co-authored-by: bluwy --- .../src/node/plugins/importAnalysisBuild.ts | 84 ++++++++++++++++++- .../__tests__/dynamic-import.spec.ts | 19 +++++ playground/dynamic-import/nested/index.js | 23 +++++ .../nested/treeshaken/treeshaken.js | 31 +++++++ 4 files changed, 156 insertions(+), 1 deletion(-) 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 ed424d56c49102..3386b1a5a4948b 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -41,6 +41,9 @@ const preloadMarkerRE = new RegExp(preloadMarker, 'g') const dynamicImportPrefixRE = /import\s*\(/ +const dynamicImportTreeshakenRE = + /(\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.posix.relative(path.posix.dirname(importer), filename) return relPath[0] === '.' ? relPath : `./${relPath}` @@ -235,6 +238,66 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { return null } + // 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 } + > = {} + + if (insertPreload) { + let match + while ((match = dynamicImportTreeshakenRE.exec(source))) { + /* 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]}`, + names: match[3]?.trim(), + } + continue + } + + /* handle `(await import('foo')).foo` + * + * match[4]: `(await import('foo')).foo` + * match[5]: `.foo` + * import end: `(await import('foo'))` + * ^ + */ + if (match[4]) { + 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} }` } + continue + } + + /* 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 + ] = { declaration: `const {${names}}`, names: `{ ${names} }` } + } + } + let s: MagicString | undefined const str = () => s || (s = new MagicString(source)) let needPreloadHelper = false @@ -265,7 +328,26 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { source[start] === '`') ) { needPreloadHelper = true - str().prependLeft(expStart, `${preloadMethod}(() => `) + 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 { + 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 56f0fbc294661f..217211eb85d286 100644 --- a/playground/dynamic-import/__tests__/dynamic-import.spec.ts +++ b/playground/dynamic-import/__tests__/dynamic-import.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from 'vitest' import { + browserLogs, findAssetFile, getColor, isBuild, @@ -178,6 +179,24 @@ 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') +}) + test.runIf(isBuild)('should not preload for non-analyzable urls', () => { const js = findAssetFile(/index-[-\w]{8}\.js$/) // should match e.g. await import(e.jss);o(".view",p===i) diff --git a/playground/dynamic-import/nested/index.js b/playground/dynamic-import/nested/index.js index 22b7e68c2c7f20..175dd3969d6d7f 100644 --- a/playground/dynamic-import/nested/index.js +++ b/playground/dynamic-import/nested/index.js @@ -135,6 +135,29 @@ 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() +})() import(`../nested/static.js`).then((mod) => { text('.dynamic-import-static', mod.self) 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') +}