11import { createUnplugin } from 'unplugin'
22import { relative } from 'pathe'
3-
3+ import { resolveAlias } from 'pathe/utils'
44import MagicString from 'magic-string'
5- import { genDynamicImport , genImport } from 'knitwork'
6- import { pascalCase , upperFirst } from 'scule'
5+ import { genImport } from 'knitwork'
76import { 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
1011interface 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 (?: c o n s t | l e t | v a r ) \s + ( \w + ) \s * = \s * ) ? d e f i n e L a z y H y d r a t i o n C o m p o n e n t \( \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \s * , \s * \( \s * \) \s * = > \s * i m p o r t \s * \( \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \s * \) \s * \) / g
19- const COMPONENT_NAME = / i m p o r t \( [ " ' ] .* \/ ( [ ^ \\ / ] + ?) \. \w + [ " ' ] \) /
20- const HYDRATION_STRATEGY = [ 'visible' , 'idle' , 'interaction' , 'mediaQuery' , 'if' , 'time' , 'never' ]
19+ const LAZY_HYDRATION_MACRO_RE = / \b d e f i n e L a z y H y d r a t i o n C o m p o n e n t \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
2231export 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