Skip to content

Commit 88ba9c9

Browse files
committed
Optimize
1 parent 2f26a0d commit 88ba9c9

7 files changed

Lines changed: 240 additions & 117 deletions

File tree

bun.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,13 @@
9090
"typescript": "5.9.3"
9191
},
9292
"devDependencies": {
93-
"eslint-plugin-devup": "^2.0.17",
94-
"husky": "^9.1.7",
95-
"oxlint": "^1.55",
93+
"@types/bun": "^1.3.10",
9694
"@types/node": "25.5.0",
9795
"@types/vscode": "1.110.0",
9896
"@vscode/vsce": "3.7.1",
99-
"globals": "17.4.0"
97+
"eslint-plugin-devup": "^2.0.17",
98+
"globals": "17.4.0",
99+
"husky": "^9.1.7",
100+
"oxlint": "^1.55"
100101
}
101102
}

src/analyzer.ts

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,26 +51,25 @@ export class ComponentLensAnalyzer {
5151
this.resolver.clear()
5252
}
5353

54-
public analyzeDocument(
54+
public invalidateFile(filePath: string): void {
55+
this.analysisCache.delete(filePath)
56+
}
57+
58+
public async analyzeDocument(
5559
filePath: string,
5660
sourceText: string,
5761
signature: string,
58-
): ComponentUsage[] {
62+
): Promise<ComponentUsage[]> {
5963
const analysis = this.getAnalysis(filePath, sourceText, signature)
6064
if (!analysis) {
6165
return []
6266
}
6367

64-
const usages: ComponentUsage[] = []
68+
const tagResolutions = new Map<JsxTagReference, string>()
69+
const uniqueFilePaths = new Set<string>()
6570

6671
for (const jsxTag of analysis.jsxTags) {
6772
if (analysis.localComponentNames.has(jsxTag.lookupName)) {
68-
usages.push({
69-
kind: analysis.ownComponentKind,
70-
ranges: jsxTag.ranges,
71-
sourceFilePath: filePath,
72-
tagName: jsxTag.tagName,
73-
})
7473
continue
7574
}
7675

@@ -87,8 +86,40 @@ export class ComponentLensAnalyzer {
8786
continue
8887
}
8988

90-
const componentKind = this.getFileComponentKind(resolvedFilePath)
91-
if (componentKind === 'unknown') {
89+
tagResolutions.set(jsxTag, resolvedFilePath)
90+
uniqueFilePaths.add(resolvedFilePath)
91+
}
92+
93+
const componentKinds = new Map<string, ComponentKind>()
94+
await Promise.all(
95+
[...uniqueFilePaths].map(async (resolvedPath) => {
96+
componentKinds.set(
97+
resolvedPath,
98+
await this.getFileComponentKind(resolvedPath),
99+
)
100+
}),
101+
)
102+
103+
const usages: ComponentUsage[] = []
104+
105+
for (const jsxTag of analysis.jsxTags) {
106+
if (analysis.localComponentNames.has(jsxTag.lookupName)) {
107+
usages.push({
108+
kind: analysis.ownComponentKind,
109+
ranges: jsxTag.ranges,
110+
sourceFilePath: filePath,
111+
tagName: jsxTag.tagName,
112+
})
113+
continue
114+
}
115+
116+
const resolvedFilePath = tagResolutions.get(jsxTag)
117+
if (!resolvedFilePath) {
118+
continue
119+
}
120+
121+
const componentKind = componentKinds.get(resolvedFilePath)
122+
if (!componentKind || componentKind === 'unknown') {
92123
continue
93124
}
94125

@@ -103,9 +134,15 @@ export class ComponentLensAnalyzer {
103134
return usages
104135
}
105136

106-
private getFileComponentKind(filePath: string): ComponentKind {
107-
const sourceText = this.host.readFile(filePath)
108-
const signature = this.host.getSignature(filePath)
137+
private async getFileComponentKind(filePath: string): Promise<ComponentKind> {
138+
const [sourceText, signature] = await Promise.all([
139+
this.host.readFileAsync
140+
? this.host.readFileAsync(filePath)
141+
: this.host.readFile(filePath),
142+
this.host.getSignatureAsync
143+
? this.host.getSignatureAsync(filePath)
144+
: this.host.getSignature(filePath),
145+
])
109146
if (sourceText === undefined || signature === undefined) {
110147
return 'unknown'
111148
}
@@ -253,8 +290,16 @@ function hasUseClientDirective(sourceFile: ts.SourceFile): boolean {
253290
return false
254291
}
255292

293+
const COMPONENT_NAME_RE = /^[A-Z]/u
294+
const COMPONENT_WRAPPER_NAMES = new Set([
295+
'forwardRef',
296+
'memo',
297+
'React.forwardRef',
298+
'React.memo',
299+
])
300+
256301
function isComponentIdentifier(name: string): boolean {
257-
return /^[A-Z]/u.test(name)
302+
return COMPONENT_NAME_RE.test(name)
258303
}
259304

260305
function isComponentInitializer(
@@ -277,11 +322,7 @@ function isComponentInitializer(
277322
}
278323

279324
const calleeName = initializer.expression.getText()
280-
if (
281-
!['forwardRef', 'memo', 'React.forwardRef', 'React.memo'].includes(
282-
calleeName,
283-
)
284-
) {
325+
if (!COMPONENT_WRAPPER_NAMES.has(calleeName)) {
285326
return false
286327
}
287328

src/extension.ts

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,49 +15,47 @@ const DEFAULT_HIGHLIGHT_COLORS: HighlightColors = {
1515
}
1616

1717
export function activate(context: vscode.ExtensionContext): void {
18-
const initialConfiguration = getConfiguration()
18+
let config = getConfiguration()
1919
const sourceHost = new WorkspaceSourceHost()
2020
const resolver = new ImportResolver(sourceHost)
2121
const analyzer = new ComponentLensAnalyzer(sourceHost, resolver)
22-
const decorations = new LensDecorations(initialConfiguration.highlightColors)
22+
const decorations = new LensDecorations(config.highlightColors)
2323

2424
context.subscriptions.push(decorations)
2525

2626
let refreshTimer: NodeJS.Timeout | undefined
2727
let watcherDisposables: vscode.Disposable[] = []
2828

29-
const clearCachesAndRefresh = (
30-
delay = getConfiguration().debounceMs,
31-
): void => {
29+
const clearCachesAndRefresh = (delay = config.debounceMs): void => {
3230
analyzer.clear()
3331
scheduleRefresh(delay)
3432
}
3533

36-
const refreshVisibleEditors = (): void => {
37-
for (const editor of vscode.window.visibleTextEditors) {
38-
refreshEditor(editor)
39-
}
34+
const refreshVisibleEditors = async (): Promise<void> => {
35+
await Promise.all(
36+
vscode.window.visibleTextEditors.map((editor) => refreshEditor(editor)),
37+
)
4038
}
4139

42-
const scheduleRefresh = (delay = getConfiguration().debounceMs): void => {
40+
const scheduleRefresh = (delay = config.debounceMs): void => {
4341
if (refreshTimer) {
4442
clearTimeout(refreshTimer)
4543
}
4644

4745
refreshTimer = setTimeout(() => {
4846
refreshTimer = undefined
49-
refreshVisibleEditors()
47+
void refreshVisibleEditors()
5048
}, delay)
5149
}
5250

53-
const refreshEditor = (editor: vscode.TextEditor): void => {
54-
if (!getConfiguration().enabled || !isSupportedDocument(editor.document)) {
51+
const refreshEditor = async (editor: vscode.TextEditor): Promise<void> => {
52+
if (!config.enabled || !isSupportedDocument(editor.document)) {
5553
decorations.clear(editor)
5654
return
5755
}
5856

5957
const signature = createOpenSignature(editor.document.version)
60-
const usages = analyzer.analyzeDocument(
58+
const usages = await analyzer.analyzeDocument(
6159
editor.document.fileName,
6260
editor.document.getText(),
6361
signature,
@@ -99,9 +97,9 @@ export function activate(context: vscode.ExtensionContext): void {
9997
}
10098

10199
context.subscriptions.push(
102-
vscode.commands.registerCommand('reactComponentLens.refresh', () => {
100+
vscode.commands.registerCommand('reactComponentLens.refresh', async () => {
103101
analyzer.clear()
104-
refreshVisibleEditors()
102+
await refreshVisibleEditors()
105103
void vscode.window.showInformationMessage(
106104
'React Component Lens refreshed.',
107105
)
@@ -114,14 +112,17 @@ export function activate(context: vscode.ExtensionContext): void {
114112
vscode.window.onDidChangeVisibleTextEditors(() => {
115113
scheduleRefresh(0)
116114
}),
117-
vscode.workspace.onDidChangeTextDocument(() => {
118-
clearCachesAndRefresh()
115+
vscode.workspace.onDidChangeTextDocument((event) => {
116+
analyzer.invalidateFile(event.document.fileName)
117+
scheduleRefresh()
119118
}),
120-
vscode.workspace.onDidOpenTextDocument(() => {
121-
clearCachesAndRefresh()
119+
vscode.workspace.onDidOpenTextDocument((document) => {
120+
analyzer.invalidateFile(document.fileName)
121+
scheduleRefresh()
122122
}),
123-
vscode.workspace.onDidSaveTextDocument(() => {
124-
clearCachesAndRefresh(0)
123+
vscode.workspace.onDidSaveTextDocument((document) => {
124+
analyzer.invalidateFile(document.fileName)
125+
scheduleRefresh(0)
125126
}),
126127
vscode.workspace.onDidChangeWorkspaceFolders(() => {
127128
analyzer.clear()
@@ -133,8 +134,10 @@ export function activate(context: vscode.ExtensionContext): void {
133134
return
134135
}
135136

137+
config = getConfiguration()
138+
136139
if (event.affectsConfiguration('reactComponentLens.highlightColors')) {
137-
decorations.updateColors(getConfiguration().highlightColors)
140+
decorations.updateColors(config.highlightColors)
138141
}
139142

140143
scheduleRefresh(0)
@@ -202,12 +205,12 @@ class WorkspaceSourceHost implements SourceHost {
202205
return createOpenSignature(openDocument.version)
203206
}
204207

205-
if (!fs.existsSync(filePath)) {
208+
try {
209+
const stats = fs.statSync(filePath)
210+
return createDiskSignature(stats.mtimeMs, stats.size)
211+
} catch {
206212
return undefined
207213
}
208-
209-
const stats = fs.statSync(filePath)
210-
return createDiskSignature(stats.mtimeMs, stats.size)
211214
}
212215

213216
public readFile(filePath: string): string | undefined {
@@ -216,11 +219,40 @@ class WorkspaceSourceHost implements SourceHost {
216219
return openDocument.getText()
217220
}
218221

219-
if (!fs.existsSync(filePath)) {
222+
try {
223+
return fs.readFileSync(filePath, 'utf8')
224+
} catch {
225+
return undefined
226+
}
227+
}
228+
229+
public async readFileAsync(filePath: string): Promise<string | undefined> {
230+
const openDocument = this.getOpenDocument(filePath)
231+
if (openDocument) {
232+
return openDocument.getText()
233+
}
234+
235+
try {
236+
return await fs.promises.readFile(filePath, 'utf8')
237+
} catch {
220238
return undefined
221239
}
240+
}
222241

223-
return fs.readFileSync(filePath, 'utf8')
242+
public async getSignatureAsync(
243+
filePath: string,
244+
): Promise<string | undefined> {
245+
const openDocument = this.getOpenDocument(filePath)
246+
if (openDocument) {
247+
return createOpenSignature(openDocument.version)
248+
}
249+
250+
try {
251+
const stats = await fs.promises.stat(filePath)
252+
return createDiskSignature(stats.mtimeMs, stats.size)
253+
} catch {
254+
return undefined
255+
}
224256
}
225257

226258
private getOpenDocument(filePath: string): vscode.TextDocument | undefined {

src/resolver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export interface SourceHost {
66
fileExists(filePath: string): boolean
77
getSignature(filePath: string): string | undefined
88
readFile(filePath: string): string | undefined
9+
getSignatureAsync?(filePath: string): Promise<string | undefined>
10+
readFileAsync?(filePath: string): Promise<string | undefined>
911
}
1012

1113
const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = {

0 commit comments

Comments
 (0)