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
58 changes: 57 additions & 1 deletion packages/vite/src/node/plugins/importAnalysisBuild.ts
Expand Up @@ -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\([^)]+\)(\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 @@ -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(),
sun0day marked this conversation as resolved.
Show resolved Hide resolved
}
continue
}

// handle `(await import('foo')).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})=>{})`
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 @@ -309,7 +345,27 @@ 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 __vite_temp__ = (await `,
)
str().appendRight(
expEnd,
`).${name}; return { ${name}: __vite_temp__ }}`,
)
} else {
str().prependLeft(expStart, `${preloadMethod}(() => `)
}
sun0day marked this conversation as resolved.
Show resolved Hide resolved

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')
}