Skip to content

Commit 0e55ce7

Browse files
committed
Optimize
1 parent 3625285 commit 0e55ce7

File tree

3 files changed

+193
-133
lines changed

3 files changed

+193
-133
lines changed

src/analyzer.ts

Lines changed: 162 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@ interface CachedAnalysis {
2121
signature: string
2222
}
2323

24+
interface CachedDirective {
25+
kind: Exclude<ComponentKind, 'unknown'>
26+
signature: string
27+
}
28+
2429
interface 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-
3536
interface JsxTagReference {
3637
lookupName: string
3738
ranges: DecorationSegment[]
@@ -40,6 +41,7 @@ interface JsxTagReference {
4041

4142
export 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+
341341
function 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

Comments
 (0)