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): allow dynamic import treeshaking when injecting preload #14221

Merged
merged 10 commits into from
Jun 6, 2024
55 changes: 54 additions & 1 deletion packages/vite/src/node/plugins/importAnalysisBuild.ts
Original file line number Diff line number Diff line change
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\([^)]+\)(\.then\([^{]*\{([^}]+)\})/g
bluwy marked this conversation as resolved.
Show resolved Hide resolved

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
bluwy marked this conversation as resolved.
Show resolved Hide resolved
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,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${
Expand Down
28 changes: 27 additions & 1 deletion playground/dynamic-import/__tests__/dynamic-import.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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')
}
Loading