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

fix(build-import-analysis): treeshaken dynamic import when injecting preload #14221

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
89 changes: 88 additions & 1 deletion packages/vite/src/node/plugins/importAnalysisBuild.ts
Expand Up @@ -45,6 +45,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\([^)]+\)(\s*\.then\([^{]*?\(\s*\{([^}.]+)\})/g

function toRelativePath(filename: string, importer: string) {
const relPath = path.relative(path.dirname(importer), filename)
return relPath[0] === '.' ? relPath : `./${relPath}`
Expand Down Expand Up @@ -284,6 +287,61 @@ 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 }
> = {}

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(),
sun0day marked this conversation as resolved.
Show resolved Hide resolved
}
continue
}

/* 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
] = { chains: match[5] }
sun0day marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

Does it make sense to make this:

const names = match[5].match(/\.([^.?]+)/)?.[1] || ''
... = { declarations: `const {${names}}`, names: : `{ ${names} }`  }

The regex is borrowed from line 386, which maybe could be simplified and extracted.

This way all cases are consistently typed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that would be better 👍

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} }` }
sun0day marked this conversation as resolved.
Show resolved Hide resolved
}
}

let s: MagicString | undefined
const str = () => s || (s = new MagicString(source))
let needPreloadHelper = false
Expand All @@ -308,7 +366,36 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {

if (isDynamicImport && insertPreload) {
needPreloadHelper = true
str().prependLeft(expStart, `${preloadMethod}(() => `)
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,
`${preloadMethod}(async () => { const __vite_temp__ = (await `,
)
str().appendRight(
expEnd,
`).${name}; return { ${name}: __vite_temp__ }}`,
)
} else {
str().prependLeft(expStart, `${preloadMethod}(() => `)
}

str().appendRight(
expEnd,
`,${isModernFlag}?"${preloadMarker}":void 0${
Expand Down
28 changes: 27 additions & 1 deletion 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')
Expand Down Expand Up @@ -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')
})
23 changes: 23 additions & 0 deletions playground/dynamic-import/nested/index.js
Expand Up @@ -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')
31 changes: 31 additions & 0 deletions 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')
}