Skip to content

Commit 04dda84

Browse files
TheAlexLichterMini-ghost
authored andcommitted
feat(nuxt): lazy hydration macros without auto-imports (#33037)
Co-authored-by: Alex Liu <[email protected]>
1 parent 9ea90fc commit 04dda84

File tree

4 files changed

+286
-90
lines changed

4 files changed

+286
-90
lines changed

packages/nuxt/src/components/module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export default defineNuxtModule<ComponentsOptions>({
237237
addBuildPlugin(LazyHydrationMacroTransformPlugin({
238238
...sharedLoaderOptions,
239239
sourcemap: !!(nuxt.options.sourcemap.server || nuxt.options.sourcemap.client),
240+
alias: nuxt.options.alias,
240241
}))
241242

242243
addImportsSources(lazyHydrationMacroPreset)
Lines changed: 81 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
import { createUnplugin } from 'unplugin'
22
import { relative } from 'pathe'
3-
3+
import { resolveAlias } from 'pathe/utils'
44
import MagicString from 'magic-string'
5-
import { genDynamicImport, genImport } from 'knitwork'
6-
import { pascalCase, upperFirst } from 'scule'
5+
import { genImport } from 'knitwork'
76
import { isJS, isVue } from '../../core/utils'
8-
import type { Component, ComponentsOptions } from 'nuxt/schema'
7+
import type { ComponentsOptions } from 'nuxt/schema'
8+
import { parseAndWalk } from 'oxc-walker'
9+
import type { Argument, Expression, FunctionBody, ImportExpression } from 'oxc-parser'
910

1011
interface LoaderOptions {
11-
getComponents (): Component[]
1212
srcDir: string
1313
sourcemap?: boolean
1414
transform?: ComponentsOptions['transform']
1515
clientDelayedComponentRuntime: string
16+
alias: Record<string, string>
1617
}
1718

18-
const LAZY_HYDRATION_MACRO_RE = /(?:\b(?:const|let|var)\s+(\w+)\s*=\s*)?defineLazyHydrationComponent\(\s*['"]([^'"]+)['"]\s*,\s*\(\s*\)\s*=>\s*import\s*\(\s*['"]([^'"]+)['"]\s*\)\s*\)/g
19-
const COMPONENT_NAME = /import\(["'].*\/([^\\/]+?)\.\w+["']\)/
20-
const HYDRATION_STRATEGY = ['visible', 'idle', 'interaction', 'mediaQuery', 'if', 'time', 'never']
19+
const LAZY_HYDRATION_MACRO_RE = /\bdefineLazyHydrationComponent\s*\(/
20+
21+
const HYDRATION_TO_FACTORY = new Map<string, string>([
22+
['visible', 'createLazyVisibleComponent'],
23+
['idle', 'createLazyIdleComponent'],
24+
['interaction', 'createLazyInteractionComponent'],
25+
['mediaQuery', 'createLazyMediaQueryComponent'],
26+
['if', 'createLazyIfComponent'],
27+
['time', 'createLazyTimeComponent'],
28+
['never', 'createLazyNeverComponent'],
29+
])
2130

2231
export const LazyHydrationMacroTransformPlugin = (options: LoaderOptions) => createUnplugin(() => {
2332
const exclude = options.transform?.exclude || []
@@ -38,54 +47,51 @@ export const LazyHydrationMacroTransformPlugin = (options: LoaderOptions) => cre
3847

3948
transform: {
4049
filter: {
41-
code: { include: LAZY_HYDRATION_MACRO_RE },
50+
code: {
51+
include: LAZY_HYDRATION_MACRO_RE,
52+
},
4253
},
43-
44-
handler (code) {
45-
const matches = Array.from(code.matchAll(LAZY_HYDRATION_MACRO_RE))
46-
if (!matches.length) { return }
47-
54+
handler (code, id) {
4855
const s = new MagicString(code)
4956
const names = new Set<string>()
57+
type Edit = { start: number, end: number, replacement: string }
58+
const edits: Edit[] = []
5059

51-
const components = options.getComponents()
60+
parseAndWalk(code, id, (node, parent) => {
61+
if (node.type !== 'CallExpression') { return }
62+
if (node.callee?.type !== 'Identifier') { return }
63+
if (node.callee.name !== 'defineLazyHydrationComponent') { return }
5264

53-
for (const match of matches) {
54-
const [matchedString, variableName, hydrationStrategy] = match
65+
if (parent?.type !== 'VariableDeclarator') { return }
66+
if (parent.id.type !== 'Identifier') { return }
5567

56-
const startIndex = match.index
57-
const endIndex = startIndex + matchedString.length
68+
if (node.arguments.length < 2) { return }
69+
const [strategyArgument, loaderArgument] = node.arguments
5870

59-
if (!variableName) {
60-
s.remove(startIndex, endIndex)
61-
continue
62-
}
71+
if (!isStringLiteral(strategyArgument)) { return }
72+
const strategy: string = strategyArgument.value
6373

64-
if (!hydrationStrategy || !HYDRATION_STRATEGY.includes(hydrationStrategy)) {
65-
s.remove(startIndex, endIndex)
66-
continue
67-
}
74+
const functionName = HYDRATION_TO_FACTORY.get(strategy)
75+
if (!functionName) { return }
6876

69-
const componentNameMatch = matchedString.match(COMPONENT_NAME)
70-
if (!componentNameMatch || !componentNameMatch[1]) {
71-
s.remove(startIndex, endIndex)
72-
continue
73-
}
77+
if (loaderArgument?.type !== 'ArrowFunctionExpression') { return }
7478

75-
const name = componentNameMatch[1]
76-
const component = findComponent(components, name)
77-
if (!component) {
78-
s.remove(startIndex, endIndex)
79-
continue
80-
}
79+
const { importExpression, importLiteral } = findImportExpression(loaderArgument.body)
80+
if (!importExpression || !isStringLiteral(importLiteral)) { return }
81+
82+
const rawPath = importLiteral.value
83+
const filePath = resolveAlias(rawPath, options.alias || {})
84+
const relativePath = relative(options.srcDir, filePath)
85+
86+
const originalLoader = code.slice(loaderArgument.start, loaderArgument.end)
87+
const replacement = `__${functionName}(${JSON.stringify(relativePath)}, ${originalLoader})`
8188

82-
const relativePath = relative(options.srcDir, component.filePath)
83-
const dynamicImport = `${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c)`
84-
const replaceFunctionName = `createLazy${upperFirst(hydrationStrategy)}Component`
85-
const replacement = `const ${variableName} = __${replaceFunctionName}(${JSON.stringify(relativePath)}, ${dynamicImport})`
89+
edits.push({ start: node.start, end: node.end, replacement })
90+
names.add(functionName)
91+
})
8692

87-
s.overwrite(startIndex, endIndex, replacement)
88-
names.add(replaceFunctionName)
93+
for (const edit of edits) {
94+
s.overwrite(edit.start, edit.end, edit.replacement)
8995
}
9096

9197
if (names.size) {
@@ -106,7 +112,35 @@ export const LazyHydrationMacroTransformPlugin = (options: LoaderOptions) => cre
106112
}
107113
})
108114

109-
function findComponent (components: Component[], name: string) {
110-
const id = pascalCase(name)
111-
return components.find(c => c.pascalName === id)
115+
function isStringLiteral (node: Argument | undefined) {
116+
return !!node && node.type === 'Literal' && typeof node.value === 'string'
117+
}
118+
119+
function findImportExpression (node: Expression | FunctionBody): { importExpression?: ImportExpression, importLiteral?: Expression } {
120+
if (node.type === 'ImportExpression') {
121+
return { importExpression: node, importLiteral: node.source }
122+
}
123+
if (node.type === 'BlockStatement') {
124+
const returnStmt = node.body.find(stmt => stmt.type === 'ReturnStatement')
125+
if (returnStmt && returnStmt.argument) {
126+
return findImportExpression(returnStmt.argument)
127+
}
128+
return {}
129+
}
130+
if (node.type === 'ParenthesizedExpression') {
131+
return findImportExpression(node.expression)
132+
}
133+
if (node.type === 'AwaitExpression') {
134+
return findImportExpression(node.argument)
135+
}
136+
if (node.type === 'ConditionalExpression') {
137+
return findImportExpression(node.consequent) || findImportExpression(node.alternate)
138+
}
139+
if (node.type === 'MemberExpression') {
140+
return findImportExpression(node.object)
141+
}
142+
if (node.type === 'CallExpression') {
143+
return findImportExpression(node.callee)
144+
}
145+
return {}
112146
}

0 commit comments

Comments
 (0)