@@ -21,17 +21,18 @@ interface CachedAnalysis {
2121 signature : string
2222}
2323
24+ interface CachedDirective {
25+ kind : Exclude < ComponentKind , 'unknown' >
26+ signature : string
27+ }
28+
2429interface FileAnalysis {
25- imports : Map < string , ImportBinding >
30+ imports : Map < string , string >
2631 jsxTags : JsxTagReference [ ]
2732 localComponentNames : Set < string >
2833 ownComponentKind : Exclude < ComponentKind , 'unknown' >
2934}
3035
31- interface ImportBinding {
32- source : string
33- }
34-
3536interface JsxTagReference {
3637 lookupName : string
3738 ranges : DecorationSegment [ ]
@@ -40,6 +41,7 @@ interface JsxTagReference {
4041
4142export class ComponentLensAnalyzer {
4243 private readonly analysisCache = new Map < string , CachedAnalysis > ( )
44+ private readonly directiveCache = new Map < string , CachedDirective > ( )
4345
4446 public constructor (
4547 private readonly host : SourceHost ,
@@ -48,11 +50,13 @@ export class ComponentLensAnalyzer {
4850
4951 public clear ( ) : void {
5052 this . analysisCache . clear ( )
53+ this . directiveCache . clear ( )
5154 this . resolver . clear ( )
5255 }
5356
5457 public invalidateFile ( filePath : string ) : void {
5558 this . analysisCache . delete ( filePath )
59+ this . directiveCache . delete ( filePath )
5660 }
5761
5862 public async analyzeDocument (
@@ -65,41 +69,41 @@ export class ComponentLensAnalyzer {
6569 return [ ]
6670 }
6771
68- const tagResolutions = new Map < JsxTagReference , string > ( )
72+ const resolvedPaths = new Map < string , string > ( )
6973 const uniqueFilePaths = new Set < string > ( )
7074
7175 for ( const jsxTag of analysis . jsxTags ) {
72- if ( analysis . localComponentNames . has ( jsxTag . lookupName ) ) {
76+ const lookupName = jsxTag . lookupName
77+ if (
78+ analysis . localComponentNames . has ( lookupName ) ||
79+ resolvedPaths . has ( lookupName )
80+ ) {
7381 continue
7482 }
7583
76- const importBinding = analysis . imports . get ( jsxTag . lookupName )
77- if ( ! importBinding ) {
84+ const importSource = analysis . imports . get ( lookupName )
85+ if ( ! importSource ) {
7886 continue
7987 }
8088
8189 const resolvedFilePath = this . resolver . resolveImport (
8290 filePath ,
83- importBinding . source ,
91+ importSource ,
8492 )
85- if ( ! resolvedFilePath ) {
86- continue
93+ if ( resolvedFilePath ) {
94+ resolvedPaths . set ( lookupName , resolvedFilePath )
95+ uniqueFilePaths . add ( resolvedFilePath )
8796 }
88-
89- tagResolutions . set ( jsxTag , resolvedFilePath )
90- uniqueFilePaths . add ( resolvedFilePath )
9197 }
9298
9399 const componentKinds = new Map < string , ComponentKind > ( )
94- const kindPromises : Promise < void > [ ] = [ ]
95- for ( const resolvedPath of uniqueFilePaths ) {
96- kindPromises . push (
100+ await Promise . all (
101+ Array . from ( uniqueFilePaths , ( resolvedPath ) =>
97102 this . getFileComponentKind ( resolvedPath ) . then ( ( kind ) => {
98103 componentKinds . set ( resolvedPath , kind )
99104 } ) ,
100- )
101- }
102- await Promise . all ( kindPromises )
105+ ) ,
106+ )
103107
104108 const usages : ComponentUsage [ ] = [ ]
105109
@@ -114,7 +118,7 @@ export class ComponentLensAnalyzer {
114118 continue
115119 }
116120
117- const resolvedFilePath = tagResolutions . get ( jsxTag )
121+ const resolvedFilePath = resolvedPaths . get ( jsxTag . lookupName )
118122 if ( ! resolvedFilePath ) {
119123 continue
120124 }
@@ -153,8 +157,18 @@ export class ComponentLensAnalyzer {
153157 return 'unknown'
154158 }
155159
156- const analysis = this . getAnalysis ( filePath , sourceText , signature )
157- return analysis ?. ownComponentKind ?? 'unknown'
160+ const cached = this . directiveCache . get ( filePath )
161+ if ( cached && cached . signature === signature ) {
162+ return cached . kind
163+ }
164+
165+ const kind : Exclude < ComponentKind , 'unknown' > = hasUseClientDirective (
166+ sourceText ,
167+ )
168+ ? 'client'
169+ : 'server'
170+ this . directiveCache . set ( filePath , { kind, signature } )
171+ return kind
158172 }
159173
160174 private getAnalysis (
@@ -182,7 +196,7 @@ function parseFileAnalysis(filePath: string, sourceText: string): FileAnalysis {
182196 getScriptKind ( filePath ) ,
183197 )
184198
185- const imports = new Map < string , ImportBinding > ( )
199+ const imports = new Map < string , string > ( )
186200 const localComponentNames = new Set < string > ( )
187201 let ownComponentKind : Exclude < ComponentKind , 'unknown' > = 'server'
188202 let statementIndex = 0
@@ -204,94 +218,67 @@ function parseFileAnalysis(filePath: string, sourceText: string): FileAnalysis {
204218
205219 for ( ; statementIndex < sourceFile . statements . length ; statementIndex ++ ) {
206220 const statement = sourceFile . statements [ statementIndex ] !
207- collectImportBindings ( statement , imports )
208- collectLocalComponentName ( statement , localComponentNames )
209- }
210-
211- return {
212- imports,
213- jsxTags : collectJsxTags ( sourceFile ) ,
214- localComponentNames,
215- ownComponentKind,
216- }
217- }
218-
219- function collectImportBindings (
220- statement : ts . Statement ,
221- imports : Map < string , ImportBinding > ,
222- ) : void {
223- if (
224- ! ts . isImportDeclaration ( statement ) ||
225- ! ts . isStringLiteral ( statement . moduleSpecifier )
226- ) {
227- return
228- }
229-
230- const importClause = statement . importClause
231- if ( ! importClause ) {
232- return
233- }
234-
235- const binding : ImportBinding = { source : statement . moduleSpecifier . text }
236-
237- if ( importClause . name ) {
238- imports . set ( importClause . name . text , binding )
239- }
240-
241- const namedBindings = importClause . namedBindings
242- if ( ! namedBindings ) {
243- return
244- }
245-
246- if ( ts . isNamespaceImport ( namedBindings ) ) {
247- imports . set ( namedBindings . name . text , binding )
248- return
249- }
250-
251- for ( const element of namedBindings . elements ) {
252- imports . set ( element . name . text , binding )
253- }
254- }
255-
256- function collectLocalComponentName (
257- statement : ts . Statement ,
258- localComponentNames : Set < string > ,
259- ) : void {
260- if (
261- ts . isFunctionDeclaration ( statement ) &&
262- statement . name &&
263- isComponentIdentifier ( statement . name . text )
264- ) {
265- localComponentNames . add ( statement . name . text )
266- return
267- }
268-
269- if (
270- ts . isClassDeclaration ( statement ) &&
271- statement . name &&
272- isComponentIdentifier ( statement . name . text )
273- ) {
274- localComponentNames . add ( statement . name . text )
275- return
276- }
277221
278- if ( ! ts . isVariableStatement ( statement ) ) {
279- return
280- }
222+ if (
223+ ts . isImportDeclaration ( statement ) &&
224+ ts . isStringLiteral ( statement . moduleSpecifier )
225+ ) {
226+ const source = statement . moduleSpecifier . text
227+ const importClause = statement . importClause
228+ if ( importClause ) {
229+ if ( importClause . name ) {
230+ imports . set ( importClause . name . text , source )
231+ }
232+ const namedBindings = importClause . namedBindings
233+ if ( namedBindings ) {
234+ if ( ts . isNamespaceImport ( namedBindings ) ) {
235+ imports . set ( namedBindings . name . text , source )
236+ } else {
237+ for ( const element of namedBindings . elements ) {
238+ imports . set ( element . name . text , source )
239+ }
240+ }
241+ }
242+ }
243+ continue
244+ }
281245
282- for ( const declaration of statement . declarationList . declarations ) {
283- if ( ! ts . isIdentifier ( declaration . name ) ) {
246+ if (
247+ ts . isFunctionDeclaration ( statement ) &&
248+ statement . name &&
249+ isComponentIdentifier ( statement . name . text )
250+ ) {
251+ localComponentNames . add ( statement . name . text )
284252 continue
285253 }
286254
287255 if (
288- ! isComponentIdentifier ( declaration . name . text ) ||
289- ! isComponentInitializer ( declaration . initializer )
256+ ts . isClassDeclaration ( statement ) &&
257+ statement . name &&
258+ isComponentIdentifier ( statement . name . text )
290259 ) {
260+ localComponentNames . add ( statement . name . text )
291261 continue
292262 }
293263
294- localComponentNames . add ( declaration . name . text )
264+ if ( ts . isVariableStatement ( statement ) ) {
265+ for ( const declaration of statement . declarationList . declarations ) {
266+ if (
267+ ts . isIdentifier ( declaration . name ) &&
268+ isComponentIdentifier ( declaration . name . text ) &&
269+ isComponentInitializer ( declaration . initializer )
270+ ) {
271+ localComponentNames . add ( declaration . name . text )
272+ }
273+ }
274+ }
275+ }
276+
277+ return {
278+ imports,
279+ jsxTags : collectJsxTags ( sourceFile ) ,
280+ localComponentNames,
281+ ownComponentKind,
295282 }
296283}
297284
@@ -326,9 +313,7 @@ function isComponentInitializer(
326313 return false
327314 }
328315
329- const callee = initializer . expression
330- const calleeName = ts . isIdentifier ( callee ) ? callee . text : callee . getText ( )
331- if ( ! COMPONENT_WRAPPER_NAMES . has ( calleeName ) ) {
316+ if ( ! COMPONENT_WRAPPER_NAMES . has ( getCalleeText ( initializer . expression ) ) ) {
332317 return false
333318 }
334319
@@ -338,6 +323,21 @@ function isComponentInitializer(
338323 )
339324}
340325
326+ function getCalleeText ( expression : ts . Expression ) : string {
327+ if ( ts . isIdentifier ( expression ) ) {
328+ return expression . text
329+ }
330+
331+ if (
332+ ts . isPropertyAccessExpression ( expression ) &&
333+ ts . isIdentifier ( expression . expression )
334+ ) {
335+ return `${ expression . expression . text } .${ expression . name . text } `
336+ }
337+
338+ return ''
339+ }
340+
341341function collectJsxTags ( sourceFile : ts . SourceFile ) : JsxTagReference [ ] {
342342 const jsxTags : JsxTagReference [ ] = [ ]
343343 const visit = ( node : ts . Node ) : void => {
@@ -456,3 +456,59 @@ function getScriptKind(filePath: string): ts.ScriptKind {
456456
457457 return ts . ScriptKind . TS
458458}
459+
460+ function hasUseClientDirective ( sourceText : string ) : boolean {
461+ const len = sourceText . length
462+ let i = 0
463+
464+ while ( i < len ) {
465+ const ch = sourceText . charCodeAt ( i )
466+
467+ if ( ch <= 32 || ch === 59 || ch === 0xfeff ) {
468+ i ++
469+ continue
470+ }
471+
472+ if ( ch === 47 && i + 1 < len ) {
473+ const next = sourceText . charCodeAt ( i + 1 )
474+ if ( next === 47 ) {
475+ i += 2
476+ while ( i < len && sourceText . charCodeAt ( i ) !== 10 ) i ++
477+ continue
478+ }
479+ if ( next === 42 ) {
480+ i += 2
481+ while ( i + 1 < len ) {
482+ if (
483+ sourceText . charCodeAt ( i ) === 42 &&
484+ sourceText . charCodeAt ( i + 1 ) === 47
485+ ) {
486+ i += 2
487+ break
488+ }
489+ i ++
490+ }
491+ continue
492+ }
493+ }
494+
495+ if ( ch === 34 && sourceText . startsWith ( '"use client"' , i ) ) {
496+ return true
497+ }
498+
499+ if ( ch === 39 && sourceText . startsWith ( "'use client'" , i ) ) {
500+ return true
501+ }
502+
503+ if ( ch === 34 || ch === 39 ) {
504+ i ++
505+ while ( i < len && sourceText . charCodeAt ( i ) !== ch ) i ++
506+ if ( i < len ) i ++
507+ continue
508+ }
509+
510+ return false
511+ }
512+
513+ return false
514+ }
0 commit comments