@jonathan0823/pi-sonarqube 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -3
- package/dist/api.d.ts +39 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +443 -0
- package/dist/api.js.map +1 -0
- package/dist/commands.d.ts +69 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +248 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +275 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -54
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +315 -975
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/ui.d.ts +40 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +234 -0
- package/dist/ui.js.map +1 -0
- package/package.json +1 -1
- package/src/api.ts +587 -0
- package/src/commands.ts +314 -0
- package/src/config.ts +336 -0
- package/src/index.ts +477 -1262
- package/src/types.ts +112 -0
- package/src/ui.ts +320 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// ── Public types ──────────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export type SonarAction = "analyze" | "issues" | "open" | "init";
|
|
4
|
+
|
|
5
|
+
export interface SonarIssue {
|
|
6
|
+
key: string;
|
|
7
|
+
rule: string;
|
|
8
|
+
ruleName?: string;
|
|
9
|
+
severity: string;
|
|
10
|
+
message: string;
|
|
11
|
+
filePath: string;
|
|
12
|
+
line: number | null;
|
|
13
|
+
status?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SonarDuplicationMeasures {
|
|
17
|
+
duplicatedLinesDensity: number;
|
|
18
|
+
duplicatedLines: number;
|
|
19
|
+
duplicatedBlocks: number;
|
|
20
|
+
duplicatedFiles: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IssueSeverityCounts {
|
|
24
|
+
blocker: number;
|
|
25
|
+
critical: number;
|
|
26
|
+
major: number;
|
|
27
|
+
minor: number;
|
|
28
|
+
info: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FileDuplication {
|
|
32
|
+
filePath: string;
|
|
33
|
+
fileKey: string;
|
|
34
|
+
duplicatedLines: number;
|
|
35
|
+
duplicatedBlocks: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DuplicationBlock {
|
|
39
|
+
from: number;
|
|
40
|
+
size: number;
|
|
41
|
+
filePath: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DuplicationBlockGroup {
|
|
45
|
+
blocks: DuplicationBlock[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SonarAnalysisState {
|
|
49
|
+
version: 1;
|
|
50
|
+
analyzedAt: string;
|
|
51
|
+
baseDir: string;
|
|
52
|
+
serverUrl: string;
|
|
53
|
+
projectKey: string;
|
|
54
|
+
dashboardUrl?: string;
|
|
55
|
+
ceTaskUrl?: string;
|
|
56
|
+
analysisId?: string;
|
|
57
|
+
filters?: SonarIssueFetchOptions;
|
|
58
|
+
measures?: SonarDuplicationMeasures;
|
|
59
|
+
totalIssues: number;
|
|
60
|
+
issues: SonarIssue[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SonarProjectConfig {
|
|
64
|
+
baseDir: string;
|
|
65
|
+
serverUrl: string;
|
|
66
|
+
projectKey: string;
|
|
67
|
+
token?: string;
|
|
68
|
+
hasProperties: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SonarIssueFetchOptions {
|
|
72
|
+
severities?: string[];
|
|
73
|
+
statuses?: string[];
|
|
74
|
+
types?: string[];
|
|
75
|
+
rules?: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SonarInitConfig {
|
|
79
|
+
serverUrl: string;
|
|
80
|
+
projectKey: string;
|
|
81
|
+
token?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Internal types shared by adapter and extension ──────────────────────────
|
|
85
|
+
|
|
86
|
+
export interface SonarWorkspaceRegistry {
|
|
87
|
+
version: 1;
|
|
88
|
+
workspaces: Record<string, string>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ResolvedTarget {
|
|
92
|
+
baseDir: string;
|
|
93
|
+
repoRoot: string;
|
|
94
|
+
alias?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ParsedSonarIssueArgs {
|
|
98
|
+
targetInput?: string;
|
|
99
|
+
issueIndex?: number;
|
|
100
|
+
filters?: SonarIssueFetchOptions;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface InitCommandOptions {
|
|
104
|
+
alias?: string;
|
|
105
|
+
targetInput?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Constants shared by adapter and extension ───────────────────────────────
|
|
109
|
+
|
|
110
|
+
export const SONAR_SEVERITIES = ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"] as const;
|
|
111
|
+
export const SONAR_STATUSES = ["OPEN", "CONFIRMED", "REOPENED", "RESOLVED", "CLOSED"] as const;
|
|
112
|
+
export const SONAR_TYPES = ["BUG", "VULNERABILITY", "CODE_SMELL"] as const;
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import type { Theme, ExtensionContext, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import type { SonarIssue, SonarAnalysisState, SonarIssueFetchOptions, FileDuplication } from "./types.js";
|
|
6
|
+
import { issueFilterLabel, fetchIssues, createAnalysisState, normalizeIssueFilters } from "./api.js";
|
|
7
|
+
import { resolveConfig, resolveTarget } from "./config.js";
|
|
8
|
+
|
|
9
|
+
// ── Issue preview ───────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export async function buildIssuePreview(baseDir: string, issue: SonarIssue, radius = 3): Promise<string> {
|
|
12
|
+
const filePath = resolve(baseDir, issue.filePath);
|
|
13
|
+
const content = await readFile(filePath, "utf8");
|
|
14
|
+
const lines = content.split(/\r?\n/);
|
|
15
|
+
const target = issue.line ?? 1;
|
|
16
|
+
const start = Math.max(1, target - radius);
|
|
17
|
+
const end = Math.min(lines.length, target + radius);
|
|
18
|
+
const width = String(end).length;
|
|
19
|
+
|
|
20
|
+
const rendered: string[] = [];
|
|
21
|
+
for (let line = start; line <= end; line++) {
|
|
22
|
+
const marker = line === target ? ">" : " ";
|
|
23
|
+
const num = String(line).padStart(width, " ");
|
|
24
|
+
rendered.push(`${marker} ${num} | ${lines[line - 1] ?? ""}`);
|
|
25
|
+
}
|
|
26
|
+
return rendered.join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Analysis UI ─────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const ANALYSIS_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
32
|
+
const ANALYSIS_WIDGET_KEY = "sonarqube-analysis";
|
|
33
|
+
|
|
34
|
+
export interface AnalysisUiHandle {
|
|
35
|
+
setPhase(phase: string): void;
|
|
36
|
+
stop(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function startAnalysisUi(ctx: ExtensionContext, projectKey: string): AnalysisUiHandle {
|
|
40
|
+
let frameIndex = 0;
|
|
41
|
+
let phase = "Starting...";
|
|
42
|
+
const render = () => {
|
|
43
|
+
const frame = ANALYSIS_SPINNER[frameIndex];
|
|
44
|
+
ctx.ui.setWidget(ANALYSIS_WIDGET_KEY, [`${frame} SonarQube ${projectKey}`, phase]);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
ctx.ui.setWorkingMessage(`Analyzing ${projectKey}...`);
|
|
48
|
+
ctx.ui.setWorkingIndicator({ frames: ANALYSIS_SPINNER as unknown as string[], intervalMs: 80 });
|
|
49
|
+
render();
|
|
50
|
+
|
|
51
|
+
const timer = setInterval(() => {
|
|
52
|
+
frameIndex = (frameIndex + 1) % ANALYSIS_SPINNER.length;
|
|
53
|
+
render();
|
|
54
|
+
}, 80);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
setPhase(nextPhase: string) {
|
|
58
|
+
phase = nextPhase;
|
|
59
|
+
render();
|
|
60
|
+
},
|
|
61
|
+
stop() {
|
|
62
|
+
clearInterval(timer);
|
|
63
|
+
ctx.ui.setWidget(ANALYSIS_WIDGET_KEY, undefined);
|
|
64
|
+
ctx.ui.setWorkingMessage();
|
|
65
|
+
ctx.ui.setWorkingIndicator();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Issue Browser UI component ──────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export class IssueBrowser {
|
|
73
|
+
private selected = 0;
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
private readonly state: SonarAnalysisState,
|
|
77
|
+
private readonly theme: Theme,
|
|
78
|
+
private readonly done: (result: number | null) => void,
|
|
79
|
+
) {}
|
|
80
|
+
|
|
81
|
+
invalidate(): void {} // NOSONAR - required by TUI interface
|
|
82
|
+
|
|
83
|
+
handleInput(data: string): void {
|
|
84
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, "ctrl+c")) {
|
|
85
|
+
this.done(null);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (matchesKey(data, Key.up)) {
|
|
89
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (matchesKey(data, Key.down)) {
|
|
93
|
+
this.selected = Math.min(this.state.issues.length - 1, this.selected + 1);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (matchesKey(data, Key.enter)) {
|
|
97
|
+
this.done(this.selected);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
render(width: number): string[] {
|
|
102
|
+
const lines: string[] = [];
|
|
103
|
+
const title = this.theme.fg("accent", this.theme.bold(" SonarQube Issues "));
|
|
104
|
+
const filterSuffix = this.state.filters ? ` • ${issueFilterLabel(this.state.filters)}` : "";
|
|
105
|
+
const subtitle = this.theme.fg(
|
|
106
|
+
"dim",
|
|
107
|
+
`${this.state.projectKey} • ${this.state.totalIssues} issue(s)${filterSuffix}`,
|
|
108
|
+
);
|
|
109
|
+
lines.push(
|
|
110
|
+
truncateToWidth(title, width),
|
|
111
|
+
truncateToWidth(subtitle, width),
|
|
112
|
+
"",
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const pageSize = Math.min(20, this.state.issues.length);
|
|
116
|
+
const halfWindow = Math.floor(pageSize / 2);
|
|
117
|
+
const maxStart = Math.max(0, this.state.issues.length - pageSize);
|
|
118
|
+
const start = Math.max(0, Math.min(this.selected - halfWindow, maxStart));
|
|
119
|
+
const end = Math.min(this.state.issues.length, start + pageSize);
|
|
120
|
+
const visibleIssues = this.state.issues.slice(start, end);
|
|
121
|
+
|
|
122
|
+
if (visibleIssues.length === 0) {
|
|
123
|
+
lines.push(
|
|
124
|
+
truncateToWidth(this.theme.fg("success", "No open issues found."), width),
|
|
125
|
+
"",
|
|
126
|
+
truncateToWidth(
|
|
127
|
+
this.theme.fg("dim", "Up/Down to move, Enter to preview, Esc to close"),
|
|
128
|
+
width,
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
return lines;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (start > 0) {
|
|
135
|
+
lines.push(truncateToWidth(this.theme.fg("dim", `... ${start} more above`), width));
|
|
136
|
+
}
|
|
137
|
+
for (const [offset, issue] of visibleIssues.entries()) {
|
|
138
|
+
const issueIndex = start + offset;
|
|
139
|
+
const isSelected = issueIndex === this.selected;
|
|
140
|
+
const marker = isSelected ? this.theme.fg("accent", ">") : this.theme.fg("dim", " ");
|
|
141
|
+
const location = issue.line ? `${issue.filePath}:${issue.line}` : issue.filePath;
|
|
142
|
+
const rule = issue.ruleName ? `${issue.rule} (${issue.ruleName})` : issue.rule;
|
|
143
|
+
const severity = severityColor(this.theme, issue.severity);
|
|
144
|
+
const summary = [
|
|
145
|
+
marker,
|
|
146
|
+
String(issueIndex + 1).padStart(2, " "),
|
|
147
|
+
".",
|
|
148
|
+
severity,
|
|
149
|
+
this.theme.fg("accent", location),
|
|
150
|
+
this.theme.fg("muted", rule),
|
|
151
|
+
this.theme.fg("text", `— ${issue.message}`),
|
|
152
|
+
].join(" ");
|
|
153
|
+
lines.push(truncateToWidth(summary, width));
|
|
154
|
+
}
|
|
155
|
+
if (end < this.state.issues.length) {
|
|
156
|
+
lines.push(
|
|
157
|
+
truncateToWidth(
|
|
158
|
+
this.theme.fg("dim", `... ${this.state.issues.length - end} more below`),
|
|
159
|
+
width,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
lines.push(
|
|
165
|
+
"",
|
|
166
|
+
truncateToWidth(
|
|
167
|
+
this.theme.fg("dim", "Up/Down to move, Enter to preview, Esc to close"),
|
|
168
|
+
width,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return lines;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Severity colors ─────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function severityColor(theme: Theme, severity: string): string {
|
|
179
|
+
switch (severity.toLowerCase()) {
|
|
180
|
+
case "blocker":
|
|
181
|
+
case "critical":
|
|
182
|
+
return theme.fg("error", severity);
|
|
183
|
+
case "major":
|
|
184
|
+
return theme.fg("warning", severity);
|
|
185
|
+
case "minor":
|
|
186
|
+
return theme.fg("accent", severity);
|
|
187
|
+
default:
|
|
188
|
+
return theme.fg("muted", severity);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Duplication browser (used by index.ts) ─────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export class DuplicationBrowser {
|
|
195
|
+
private selected = 0;
|
|
196
|
+
|
|
197
|
+
constructor(
|
|
198
|
+
private readonly files: FileDuplication[],
|
|
199
|
+
private readonly theme: Theme,
|
|
200
|
+
private readonly done: (result: number | null) => void,
|
|
201
|
+
) {}
|
|
202
|
+
|
|
203
|
+
invalidate(): void {} // NOSONAR - required by TUI interface
|
|
204
|
+
|
|
205
|
+
handleInput(data: string): void {
|
|
206
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, "ctrl+c")) {
|
|
207
|
+
this.done(null);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (matchesKey(data, Key.up)) {
|
|
211
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (matchesKey(data, Key.down)) {
|
|
215
|
+
this.selected = Math.min(this.files.length - 1, this.selected + 1);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (matchesKey(data, Key.enter)) {
|
|
219
|
+
this.done(this.selected);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
render(width: number): string[] {
|
|
224
|
+
const lines: string[] = [
|
|
225
|
+
this.theme.fg("accent", this.theme.bold(" SonarQube Duplications ")),
|
|
226
|
+
this.theme.fg("dim", `${this.files.length} file(s) with duplicate code`),
|
|
227
|
+
"",
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const pageSize = Math.min(15, this.files.length);
|
|
231
|
+
const halfWindow = Math.floor(pageSize / 2);
|
|
232
|
+
const maxStart = Math.max(0, this.files.length - pageSize);
|
|
233
|
+
const start = Math.max(0, Math.min(this.selected - halfWindow, maxStart));
|
|
234
|
+
const end = Math.min(this.files.length, start + pageSize);
|
|
235
|
+
const visible = this.files.slice(start, end);
|
|
236
|
+
|
|
237
|
+
for (const [offset, file] of visible.entries()) {
|
|
238
|
+
const index = start + offset;
|
|
239
|
+
const isSelected = index === this.selected;
|
|
240
|
+
const marker = isSelected ? this.theme.fg("accent", ">") : this.theme.fg("dim", " ");
|
|
241
|
+
const detail = `blocks=${file.duplicatedBlocks} lines=${file.duplicatedLines}`;
|
|
242
|
+
const line = `${marker} ${String(index + 1).padStart(2, " ")}. ${this.theme.fg("accent", file.filePath)} ${this.theme.fg("dim", detail)}`;
|
|
243
|
+
lines.push(truncateToWidth(line, width));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
lines.push(
|
|
247
|
+
"",
|
|
248
|
+
this.theme.fg("dim", "Up/Down to move, Enter for details, Esc to close"),
|
|
249
|
+
);
|
|
250
|
+
return lines;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function showDuplicationBrowser(
|
|
255
|
+
ctx: ExtensionCommandContext,
|
|
256
|
+
files: FileDuplication[],
|
|
257
|
+
): Promise<number | null> {
|
|
258
|
+
if (ctx.mode !== "tui" || files.length === 0) return null;
|
|
259
|
+
return await ctx.ui.custom<number | null>((_tui, theme, _kb, done) => new DuplicationBrowser(files, theme, done));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Issue browser / preview helpers (used by index.ts) ──────────────────────
|
|
263
|
+
|
|
264
|
+
export async function showIssueBrowser(
|
|
265
|
+
ctx: ExtensionCommandContext,
|
|
266
|
+
state: SonarAnalysisState,
|
|
267
|
+
): Promise<number | null> {
|
|
268
|
+
if (ctx.mode !== "tui") return null;
|
|
269
|
+
if (state.issues.length === 0) return null;
|
|
270
|
+
return await ctx.ui.custom<number | null>((_tui, theme, _kb, done) => new IssueBrowser(state, theme, done));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function openIssuePreview(
|
|
274
|
+
ctx: ExtensionCommandContext,
|
|
275
|
+
state: SonarAnalysisState,
|
|
276
|
+
issue: SonarIssue,
|
|
277
|
+
): Promise<void> {
|
|
278
|
+
const preview = await buildIssuePreview(state.baseDir, issue);
|
|
279
|
+
const title = issue.line ? `${issue.filePath}:${issue.line}` : issue.filePath;
|
|
280
|
+
if (ctx.mode === "tui") {
|
|
281
|
+
await ctx.ui.editor(title, preview);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
ctx.ui.notify(`${title}\n${preview}`, "info");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Issue loading / target resolution (orchestration) ───────────────────────
|
|
288
|
+
|
|
289
|
+
export async function loadProjectIssuesFromApi(
|
|
290
|
+
ctx: { signal?: AbortSignal; cwd: string },
|
|
291
|
+
inputPath?: string,
|
|
292
|
+
filters?: SonarIssueFetchOptions,
|
|
293
|
+
): Promise<SonarAnalysisState> {
|
|
294
|
+
const config = await resolveConfig(ctx, inputPath);
|
|
295
|
+
const normalizedFilters = normalizeIssueFilters(filters);
|
|
296
|
+
const issues = await fetchIssues(
|
|
297
|
+
config.serverUrl,
|
|
298
|
+
config.token,
|
|
299
|
+
config.projectKey,
|
|
300
|
+
ctx.signal,
|
|
301
|
+
normalizedFilters,
|
|
302
|
+
);
|
|
303
|
+
return createAnalysisState(config, issues, { filters: normalizedFilters });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function resolveTargetState(
|
|
307
|
+
ctx: { signal?: AbortSignal; cwd: string },
|
|
308
|
+
statesByBaseDir: Map<string, SonarAnalysisState>,
|
|
309
|
+
targetInput?: string,
|
|
310
|
+
filters?: SonarIssueFetchOptions,
|
|
311
|
+
): Promise<SonarAnalysisState | undefined> {
|
|
312
|
+
const resolvedTarget = await resolveTarget(ctx, targetInput);
|
|
313
|
+
try {
|
|
314
|
+
return await loadProjectIssuesFromApi(ctx, targetInput, filters);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
const cached = statesByBaseDir.get(resolvedTarget.baseDir);
|
|
317
|
+
if (cached) return cached;
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
}
|