@rayburst/cli 0.2.0 → 0.2.2
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 +10 -1
- package/dist/chunk-ZAT3D23Y.js +806 -0
- package/dist/index.d.ts +54 -6
- package/dist/index.js +24 -6
- package/dist/vite-plugin.d.ts +6 -3
- package/dist/vite-plugin.js +6 -109
- package/package.json +5 -4
- package/dist/analysis/analyze-project.d.ts +0 -9
- package/dist/analysis/analyze-project.js +0 -440
- package/dist/git-utils.d.ts +0 -33
- package/dist/git-utils.js +0 -96
- package/dist/incremental-analyzer.d.ts +0 -128
- package/dist/incremental-analyzer.js +0 -259
- package/dist/local-storage.d.ts +0 -39
- package/dist/local-storage.js +0 -117
- package/dist/registry.d.ts +0 -89
- package/dist/registry.js +0 -287
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
// src/analysis/analyze-project.ts
|
|
2
|
+
import { Project, SyntaxKind, Node } from "ts-morph";
|
|
3
|
+
import { execSync as execSync2 } from "child_process";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path2 from "path";
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
|
|
8
|
+
// src/git-utils.ts
|
|
9
|
+
import { execSync } from "child_process";
|
|
10
|
+
import path from "path";
|
|
11
|
+
function isGitRepository(projectPath) {
|
|
12
|
+
try {
|
|
13
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
14
|
+
cwd: projectPath,
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
17
|
+
});
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function getChangedFilesVsRemote(projectPath, remoteBranch = void 0) {
|
|
24
|
+
try {
|
|
25
|
+
if (!isGitRepository(projectPath)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
let currentBranch;
|
|
29
|
+
try {
|
|
30
|
+
currentBranch = execSync("git branch --show-current", {
|
|
31
|
+
cwd: projectPath,
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
34
|
+
}).trim();
|
|
35
|
+
} catch {
|
|
36
|
+
const branchOutput = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
37
|
+
cwd: projectPath,
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
40
|
+
}).trim();
|
|
41
|
+
currentBranch = branchOutput;
|
|
42
|
+
}
|
|
43
|
+
const remote = remoteBranch || `origin/${currentBranch}`;
|
|
44
|
+
try {
|
|
45
|
+
execSync("git fetch origin --quiet", {
|
|
46
|
+
cwd: projectPath,
|
|
47
|
+
stdio: "ignore",
|
|
48
|
+
timeout: 5e3
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
execSync(`git rev-parse --verify ${remote}`, {
|
|
54
|
+
cwd: projectPath,
|
|
55
|
+
stdio: "ignore"
|
|
56
|
+
});
|
|
57
|
+
} catch {
|
|
58
|
+
if (remote !== "origin/main") {
|
|
59
|
+
try {
|
|
60
|
+
execSync("git rev-parse --verify origin/main", {
|
|
61
|
+
cwd: projectPath,
|
|
62
|
+
stdio: "ignore"
|
|
63
|
+
});
|
|
64
|
+
return getChangedFilesVsRemote(projectPath, "origin/main");
|
|
65
|
+
} catch {
|
|
66
|
+
console.warn(`No remote branch found (tried ${remote} and origin/main)`);
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const output = execSync(`git diff --name-only ${remote}...HEAD`, {
|
|
73
|
+
cwd: projectPath,
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
76
|
+
});
|
|
77
|
+
return output.split("\n").filter(Boolean).map((file) => path.resolve(projectPath, file.trim()));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.warn("Failed to get changed files vs remote:", error.message);
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/analysis/analyze-project.ts
|
|
85
|
+
async function analyzeProject(projectPath, projectId, onLog) {
|
|
86
|
+
const logs = [];
|
|
87
|
+
const log = (message) => {
|
|
88
|
+
console.log(message);
|
|
89
|
+
logs.push(message);
|
|
90
|
+
if (onLog) {
|
|
91
|
+
onLog(message);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
log(`Analyzing project at: ${projectPath}`);
|
|
95
|
+
const gitHash = getGitHash(projectPath);
|
|
96
|
+
const gitInfo = getGitInfo(projectPath);
|
|
97
|
+
log(` Git hash: ${gitHash}`);
|
|
98
|
+
log(` Current branch: ${gitInfo.branch}`);
|
|
99
|
+
const tsconfigPath = path2.join(projectPath, "tsconfig.json");
|
|
100
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
101
|
+
log(` Warning: No tsconfig.json found, skipping TypeScript analysis`);
|
|
102
|
+
const emptyAnalysis = createEmptyAnalysis(projectPath, gitInfo);
|
|
103
|
+
emptyAnalysis.logs = logs;
|
|
104
|
+
return emptyAnalysis;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const project = new Project({
|
|
108
|
+
tsConfigFilePath: tsconfigPath
|
|
109
|
+
});
|
|
110
|
+
const nodes = [];
|
|
111
|
+
const edges = [];
|
|
112
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
113
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
114
|
+
const idCounters = /* @__PURE__ */ new Map();
|
|
115
|
+
let fileIndex = 0;
|
|
116
|
+
const filePositions = /* @__PURE__ */ new Map();
|
|
117
|
+
const sourceFiles = project.getSourceFiles().filter((sf) => {
|
|
118
|
+
const filePath = sf.getFilePath();
|
|
119
|
+
return !filePath.includes("node_modules") && !filePath.includes(".test.") && !filePath.includes(".spec.") && (filePath.endsWith(".ts") || filePath.endsWith(".tsx"));
|
|
120
|
+
});
|
|
121
|
+
log(` Analyzing ${sourceFiles.length} source files...`);
|
|
122
|
+
for (let i = 0; i < sourceFiles.length; i++) {
|
|
123
|
+
const sourceFile = sourceFiles[i];
|
|
124
|
+
const filePath = sourceFile.getFilePath();
|
|
125
|
+
const relativePath = filePath.replace(projectPath + "/", "");
|
|
126
|
+
if (!filePositions.has(relativePath)) {
|
|
127
|
+
filePositions.set(relativePath, fileIndex++);
|
|
128
|
+
}
|
|
129
|
+
const baseX = (filePositions.get(relativePath) || 0) * 400;
|
|
130
|
+
let nodeY = 100;
|
|
131
|
+
const components = extractComponents(sourceFile, relativePath, gitHash, baseX, nodeY, nodes, nodeMap, usedIds, idCounters);
|
|
132
|
+
nodeY += components * 150;
|
|
133
|
+
const functions = extractFunctions(sourceFile, relativePath, gitHash, baseX, nodeY, nodes, nodeMap, usedIds, idCounters);
|
|
134
|
+
nodeY += functions * 150;
|
|
135
|
+
const states = extractState(sourceFile, relativePath, gitHash, baseX, nodeY, nodes, nodeMap, usedIds, idCounters);
|
|
136
|
+
nodeY += states * 150;
|
|
137
|
+
if (i % 10 === 0 && i > 0) {
|
|
138
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
log(` Extracted ${nodes.length} nodes`);
|
|
142
|
+
for (const sourceFile of sourceFiles) {
|
|
143
|
+
generateEdges(sourceFile, nodeMap, edges);
|
|
144
|
+
}
|
|
145
|
+
log(` Generated ${edges.length} edges`);
|
|
146
|
+
const packageJsonPath = path2.join(projectPath, "package.json");
|
|
147
|
+
let packageJson = { name: path2.basename(projectPath) };
|
|
148
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
149
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
150
|
+
}
|
|
151
|
+
const branchId = gitInfo.branch.replace(/[^a-zA-Z0-9]/g, "-");
|
|
152
|
+
const projectMetadata = {
|
|
153
|
+
id: projectId || crypto.createHash("md5").update(projectPath).digest("hex").substring(0, 12),
|
|
154
|
+
title: packageJson.name || path2.basename(projectPath),
|
|
155
|
+
subtitle: (/* @__PURE__ */ new Date()).toLocaleDateString(),
|
|
156
|
+
value: `${nodes.length} nodes`,
|
|
157
|
+
trend: 0,
|
|
158
|
+
chartData: [30, 40, 35, 50, 45, 60, 55, 70],
|
|
159
|
+
chartColor: "#22c55e"
|
|
160
|
+
};
|
|
161
|
+
const branchMetadata = {
|
|
162
|
+
id: branchId,
|
|
163
|
+
name: gitInfo.branch,
|
|
164
|
+
lastCommit: gitInfo.lastCommit,
|
|
165
|
+
lastCommitDate: "just now",
|
|
166
|
+
author: gitInfo.author,
|
|
167
|
+
status: "active",
|
|
168
|
+
commitCount: gitInfo.commitCount,
|
|
169
|
+
pullRequests: 0
|
|
170
|
+
};
|
|
171
|
+
const planData = {
|
|
172
|
+
nodes,
|
|
173
|
+
edges
|
|
174
|
+
};
|
|
175
|
+
log(" Checking for changed files vs remote...");
|
|
176
|
+
const changedFiles = getChangedFilesVsRemote(projectPath);
|
|
177
|
+
log(` Found ${changedFiles.length} changed files`);
|
|
178
|
+
const result = {
|
|
179
|
+
project: projectMetadata,
|
|
180
|
+
branches: [branchMetadata],
|
|
181
|
+
planData: {
|
|
182
|
+
[branchId]: planData
|
|
183
|
+
},
|
|
184
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
185
|
+
logs,
|
|
186
|
+
changedFilesVsRemote: changedFiles.length > 0 ? {
|
|
187
|
+
[branchId]: changedFiles
|
|
188
|
+
} : void 0
|
|
189
|
+
};
|
|
190
|
+
return result;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(` Error analyzing project:`, error.message);
|
|
193
|
+
log(` Error: ${error.message}`);
|
|
194
|
+
const emptyAnalysis = createEmptyAnalysis(projectPath, gitInfo);
|
|
195
|
+
emptyAnalysis.logs = logs;
|
|
196
|
+
return emptyAnalysis;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function createEmptyAnalysis(projectPath, gitInfo) {
|
|
200
|
+
const packageJsonPath = path2.join(projectPath, "package.json");
|
|
201
|
+
let packageJson = { name: path2.basename(projectPath) };
|
|
202
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
203
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
204
|
+
}
|
|
205
|
+
const branchId = gitInfo.branch.replace(/[^a-zA-Z0-9]/g, "-");
|
|
206
|
+
return {
|
|
207
|
+
project: {
|
|
208
|
+
id: crypto.createHash("md5").update(projectPath).digest("hex").substring(0, 12),
|
|
209
|
+
title: packageJson.name || path2.basename(projectPath),
|
|
210
|
+
subtitle: (/* @__PURE__ */ new Date()).toLocaleDateString(),
|
|
211
|
+
value: "0 nodes",
|
|
212
|
+
trend: 0,
|
|
213
|
+
chartData: [],
|
|
214
|
+
chartColor: "#6b7280"
|
|
215
|
+
},
|
|
216
|
+
branches: [{
|
|
217
|
+
id: branchId,
|
|
218
|
+
name: gitInfo.branch,
|
|
219
|
+
lastCommit: gitInfo.lastCommit,
|
|
220
|
+
lastCommitDate: "just now",
|
|
221
|
+
author: gitInfo.author,
|
|
222
|
+
status: "active",
|
|
223
|
+
commitCount: gitInfo.commitCount,
|
|
224
|
+
pullRequests: 0
|
|
225
|
+
}],
|
|
226
|
+
planData: {},
|
|
227
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function getGitHash(projectPath) {
|
|
231
|
+
try {
|
|
232
|
+
return execSync2("git rev-parse --short HEAD", { cwd: projectPath, encoding: "utf-8" }).trim();
|
|
233
|
+
} catch {
|
|
234
|
+
return "nogit";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function getGitInfo(projectPath) {
|
|
238
|
+
try {
|
|
239
|
+
const branch = execSync2("git branch --show-current", { cwd: projectPath, encoding: "utf-8" }).trim() || "main";
|
|
240
|
+
const lastCommit = execSync2("git log -1 --pretty=%s", { cwd: projectPath, encoding: "utf-8" }).trim() || "No commits";
|
|
241
|
+
const author = execSync2("git log -1 --pretty=%an", { cwd: projectPath, encoding: "utf-8" }).trim() || "Unknown";
|
|
242
|
+
const commitCount = parseInt(execSync2("git rev-list --count HEAD", { cwd: projectPath, encoding: "utf-8" }).trim() || "0");
|
|
243
|
+
return { branch, lastCommit, author, commitCount };
|
|
244
|
+
} catch {
|
|
245
|
+
return {
|
|
246
|
+
branch: "main",
|
|
247
|
+
lastCommit: "No commits",
|
|
248
|
+
author: "Unknown",
|
|
249
|
+
commitCount: 0
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function createNodeId(filePath, nodeName, gitHash, counter = 0) {
|
|
254
|
+
const suffix = counter > 0 ? `-${counter}` : "";
|
|
255
|
+
return `${filePath}::${nodeName}${suffix}::${gitHash}`;
|
|
256
|
+
}
|
|
257
|
+
function getUniqueNodeId(filePath, nodeName, gitHash, usedIds, idCounters) {
|
|
258
|
+
const baseKey = `${filePath}::${nodeName}`;
|
|
259
|
+
const counter = idCounters.get(baseKey) || 0;
|
|
260
|
+
const id = createNodeId(filePath, nodeName, gitHash, counter);
|
|
261
|
+
if (usedIds.has(id)) {
|
|
262
|
+
idCounters.set(baseKey, counter + 1);
|
|
263
|
+
return getUniqueNodeId(filePath, nodeName, gitHash, usedIds, idCounters);
|
|
264
|
+
}
|
|
265
|
+
usedIds.add(id);
|
|
266
|
+
idCounters.set(baseKey, counter + 1);
|
|
267
|
+
return id;
|
|
268
|
+
}
|
|
269
|
+
function isReactComponent(func) {
|
|
270
|
+
const body = func.getBody();
|
|
271
|
+
if (!body) return false;
|
|
272
|
+
const text = body.getText();
|
|
273
|
+
return text.includes("return") && (text.includes("</") || text.includes("<>") || text.includes("jsx"));
|
|
274
|
+
}
|
|
275
|
+
function extractComponents(sourceFile, relativePath, gitHash, baseX, startY, nodes, nodeMap, usedIds, idCounters) {
|
|
276
|
+
let count = 0;
|
|
277
|
+
let y = startY;
|
|
278
|
+
const functions = sourceFile.getFunctions();
|
|
279
|
+
for (const func of functions) {
|
|
280
|
+
if (isReactComponent(func)) {
|
|
281
|
+
const name = func.getName() || "AnonymousComponent";
|
|
282
|
+
const id = getUniqueNodeId(relativePath, name, gitHash, usedIds, idCounters);
|
|
283
|
+
const params = func.getParameters();
|
|
284
|
+
const propsParam = params[0];
|
|
285
|
+
const propsType = propsParam ? propsParam.getType().getText() : "any";
|
|
286
|
+
const node = {
|
|
287
|
+
id,
|
|
288
|
+
type: "component",
|
|
289
|
+
position: { x: baseX, y },
|
|
290
|
+
data: {
|
|
291
|
+
componentName: name,
|
|
292
|
+
props: propsType,
|
|
293
|
+
label: name,
|
|
294
|
+
description: `Component in ${relativePath}`
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
nodes.push(node);
|
|
298
|
+
nodeMap.set(id, node);
|
|
299
|
+
nodeMap.set(name, node);
|
|
300
|
+
count++;
|
|
301
|
+
y += 150;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const variables = sourceFile.getVariableDeclarations();
|
|
305
|
+
for (const variable of variables) {
|
|
306
|
+
const init = variable.getInitializer();
|
|
307
|
+
if (init && (Node.isArrowFunction(init) || Node.isFunctionExpression(init))) {
|
|
308
|
+
if (isReactComponent(init)) {
|
|
309
|
+
const name = variable.getName();
|
|
310
|
+
const id = getUniqueNodeId(relativePath, name, gitHash, usedIds, idCounters);
|
|
311
|
+
const params = init.getParameters();
|
|
312
|
+
const propsParam = params[0];
|
|
313
|
+
const propsType = propsParam ? propsParam.getType().getText() : "any";
|
|
314
|
+
const node = {
|
|
315
|
+
id,
|
|
316
|
+
type: "component",
|
|
317
|
+
position: { x: baseX, y },
|
|
318
|
+
data: {
|
|
319
|
+
componentName: name,
|
|
320
|
+
props: propsType,
|
|
321
|
+
label: name,
|
|
322
|
+
description: `Component in ${relativePath}`
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
nodes.push(node);
|
|
326
|
+
nodeMap.set(id, node);
|
|
327
|
+
nodeMap.set(name, node);
|
|
328
|
+
count++;
|
|
329
|
+
y += 150;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return count;
|
|
334
|
+
}
|
|
335
|
+
function extractFunctions(sourceFile, relativePath, gitHash, baseX, startY, nodes, nodeMap, usedIds, idCounters) {
|
|
336
|
+
let count = 0;
|
|
337
|
+
let y = startY;
|
|
338
|
+
const functions = sourceFile.getFunctions();
|
|
339
|
+
for (const func of functions) {
|
|
340
|
+
if (!isReactComponent(func)) {
|
|
341
|
+
const name = func.getName() || "anonymous";
|
|
342
|
+
if (name === "anonymous") continue;
|
|
343
|
+
const id = getUniqueNodeId(relativePath, name, gitHash, usedIds, idCounters);
|
|
344
|
+
const params = func.getParameters().map((p) => {
|
|
345
|
+
const paramName = p.getName();
|
|
346
|
+
const paramType = p.getType().getText();
|
|
347
|
+
return `${paramName}: ${paramType}`;
|
|
348
|
+
}).join(", ");
|
|
349
|
+
const returnType = func.getReturnType().getText();
|
|
350
|
+
const node = {
|
|
351
|
+
id,
|
|
352
|
+
type: "function",
|
|
353
|
+
position: { x: baseX, y },
|
|
354
|
+
data: {
|
|
355
|
+
functionName: name,
|
|
356
|
+
parameters: params,
|
|
357
|
+
returnType,
|
|
358
|
+
label: name,
|
|
359
|
+
description: `Function in ${relativePath}`
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
nodes.push(node);
|
|
363
|
+
nodeMap.set(id, node);
|
|
364
|
+
nodeMap.set(name, node);
|
|
365
|
+
count++;
|
|
366
|
+
y += 150;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return count;
|
|
370
|
+
}
|
|
371
|
+
function extractState(sourceFile, relativePath, gitHash, baseX, startY, nodes, nodeMap, usedIds, idCounters) {
|
|
372
|
+
let count = 0;
|
|
373
|
+
let y = startY;
|
|
374
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((callExpr) => {
|
|
375
|
+
const expr = callExpr.getExpression();
|
|
376
|
+
if (expr.getText() === "useState") {
|
|
377
|
+
const parent = callExpr.getParent();
|
|
378
|
+
if (Node.isVariableDeclaration(parent)) {
|
|
379
|
+
const nameNode = parent.getNameNode();
|
|
380
|
+
if (Node.isArrayBindingPattern(nameNode)) {
|
|
381
|
+
const elements = nameNode.getElements();
|
|
382
|
+
if (elements.length > 0) {
|
|
383
|
+
const stateName = elements[0].getText();
|
|
384
|
+
const id = getUniqueNodeId(relativePath, stateName, gitHash, usedIds, idCounters);
|
|
385
|
+
const args = callExpr.getArguments();
|
|
386
|
+
const initialValue = args.length > 0 ? args[0].getText() : "undefined";
|
|
387
|
+
const stateType = parent.getType().getText();
|
|
388
|
+
const node = {
|
|
389
|
+
id,
|
|
390
|
+
type: "state",
|
|
391
|
+
position: { x: baseX, y },
|
|
392
|
+
data: {
|
|
393
|
+
stateName,
|
|
394
|
+
stateType,
|
|
395
|
+
initialValue,
|
|
396
|
+
label: stateName,
|
|
397
|
+
description: `State in ${relativePath}`
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
nodes.push(node);
|
|
401
|
+
nodeMap.set(id, node);
|
|
402
|
+
nodeMap.set(stateName, node);
|
|
403
|
+
count++;
|
|
404
|
+
y += 150;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
return count;
|
|
411
|
+
}
|
|
412
|
+
function generateEdges(sourceFile, nodeMap, edges) {
|
|
413
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach((jsxElement) => {
|
|
414
|
+
const openingElement = jsxElement.getOpeningElement();
|
|
415
|
+
const tagName = openingElement.getTagNameNode().getText();
|
|
416
|
+
const containingFunc = jsxElement.getFirstAncestorByKind(SyntaxKind.FunctionDeclaration) || jsxElement.getFirstAncestorByKind(SyntaxKind.ArrowFunction);
|
|
417
|
+
if (containingFunc) {
|
|
418
|
+
const parentName = containingFunc.getName?.() || containingFunc.getParent()?.asKind(SyntaxKind.VariableDeclaration)?.getName() || null;
|
|
419
|
+
if (parentName && nodeMap.has(parentName) && nodeMap.has(tagName)) {
|
|
420
|
+
const sourceNode = nodeMap.get(parentName);
|
|
421
|
+
const targetNode = nodeMap.get(tagName);
|
|
422
|
+
if (sourceNode && targetNode) {
|
|
423
|
+
const edgeId = `e-${sourceNode.id}-${targetNode.id}`;
|
|
424
|
+
if (!edges.find((e) => e.id === edgeId)) {
|
|
425
|
+
edges.push({
|
|
426
|
+
id: edgeId,
|
|
427
|
+
source: sourceNode.id,
|
|
428
|
+
target: targetNode.id,
|
|
429
|
+
type: "floating"
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach((jsxElement) => {
|
|
437
|
+
const tagName = jsxElement.getTagNameNode().getText();
|
|
438
|
+
const containingFunc = jsxElement.getFirstAncestorByKind(SyntaxKind.FunctionDeclaration) || jsxElement.getFirstAncestorByKind(SyntaxKind.ArrowFunction);
|
|
439
|
+
if (containingFunc) {
|
|
440
|
+
const parentName = containingFunc.getName?.() || containingFunc.getParent()?.asKind(SyntaxKind.VariableDeclaration)?.getName() || null;
|
|
441
|
+
if (parentName && nodeMap.has(parentName) && nodeMap.has(tagName)) {
|
|
442
|
+
const sourceNode = nodeMap.get(parentName);
|
|
443
|
+
const targetNode = nodeMap.get(tagName);
|
|
444
|
+
if (sourceNode && targetNode) {
|
|
445
|
+
const edgeId = `e-${sourceNode.id}-${targetNode.id}`;
|
|
446
|
+
if (!edges.find((e) => e.id === edgeId)) {
|
|
447
|
+
edges.push({
|
|
448
|
+
id: edgeId,
|
|
449
|
+
source: sourceNode.id,
|
|
450
|
+
target: targetNode.id,
|
|
451
|
+
type: "floating"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((callExpr) => {
|
|
459
|
+
const expr = callExpr.getExpression();
|
|
460
|
+
const calledName = expr.getText().split(".").pop();
|
|
461
|
+
if (calledName && nodeMap.has(calledName)) {
|
|
462
|
+
const containingFunc = callExpr.getFirstAncestorByKind(SyntaxKind.FunctionDeclaration) || callExpr.getFirstAncestorByKind(SyntaxKind.ArrowFunction);
|
|
463
|
+
if (containingFunc) {
|
|
464
|
+
const parentName = containingFunc.getName?.() || containingFunc.getParent()?.asKind(SyntaxKind.VariableDeclaration)?.getName() || null;
|
|
465
|
+
if (parentName && nodeMap.has(parentName)) {
|
|
466
|
+
const sourceNode = nodeMap.get(parentName);
|
|
467
|
+
const targetNode = nodeMap.get(calledName);
|
|
468
|
+
if (sourceNode && targetNode && sourceNode.id !== targetNode.id) {
|
|
469
|
+
const edgeId = `e-${sourceNode.id}-${targetNode.id}`;
|
|
470
|
+
if (!edges.find((e) => e.id === edgeId)) {
|
|
471
|
+
edges.push({
|
|
472
|
+
id: edgeId,
|
|
473
|
+
source: sourceNode.id,
|
|
474
|
+
target: targetNode.id,
|
|
475
|
+
type: "floating"
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/local-storage.ts
|
|
486
|
+
import { promises as fs2 } from "fs";
|
|
487
|
+
import path3 from "path";
|
|
488
|
+
async function ensureRayburstDir(projectPath) {
|
|
489
|
+
const rayburstDir = path3.join(projectPath, ".rayburst");
|
|
490
|
+
await fs2.mkdir(rayburstDir, { recursive: true });
|
|
491
|
+
return rayburstDir;
|
|
492
|
+
}
|
|
493
|
+
async function readLocalAnalysis(projectPath) {
|
|
494
|
+
const analysisPath = path3.join(projectPath, ".rayburst", "analysis.json");
|
|
495
|
+
try {
|
|
496
|
+
const data = await fs2.readFile(analysisPath, "utf-8");
|
|
497
|
+
return JSON.parse(data);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function writeLocalAnalysis(projectPath, analysis) {
|
|
503
|
+
await ensureRayburstDir(projectPath);
|
|
504
|
+
const analysisPath = path3.join(projectPath, ".rayburst", "analysis.json");
|
|
505
|
+
await fs2.writeFile(analysisPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
506
|
+
}
|
|
507
|
+
async function readLocalMeta(projectPath) {
|
|
508
|
+
const metaPath = path3.join(projectPath, ".rayburst", "meta.json");
|
|
509
|
+
try {
|
|
510
|
+
const data = await fs2.readFile(metaPath, "utf-8");
|
|
511
|
+
return JSON.parse(data);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function writeLocalMeta(projectPath, meta) {
|
|
517
|
+
await ensureRayburstDir(projectPath);
|
|
518
|
+
const metaPath = path3.join(projectPath, ".rayburst", "meta.json");
|
|
519
|
+
await fs2.writeFile(metaPath, JSON.stringify(meta, null, 2), "utf-8");
|
|
520
|
+
}
|
|
521
|
+
async function addGitignoreEntry(projectPath) {
|
|
522
|
+
const gitignorePath = path3.join(projectPath, ".gitignore");
|
|
523
|
+
let gitignoreContent = "";
|
|
524
|
+
try {
|
|
525
|
+
gitignoreContent = await fs2.readFile(gitignorePath, "utf-8");
|
|
526
|
+
} catch (error) {
|
|
527
|
+
}
|
|
528
|
+
if (gitignoreContent.includes(".rayburst")) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const entry = "\n# Rayburst analysis data\n.rayburst/\n";
|
|
532
|
+
gitignoreContent += entry;
|
|
533
|
+
await fs2.writeFile(gitignorePath, gitignoreContent, "utf-8");
|
|
534
|
+
}
|
|
535
|
+
async function isProjectInitialized(projectPath) {
|
|
536
|
+
const rayburstDir = path3.join(projectPath, ".rayburst");
|
|
537
|
+
try {
|
|
538
|
+
const stats = await fs2.stat(rayburstDir);
|
|
539
|
+
return stats.isDirectory();
|
|
540
|
+
} catch (error) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async function initializeProject(projectPath, projectName, cliVersion) {
|
|
545
|
+
await ensureRayburstDir(projectPath);
|
|
546
|
+
const projectId = generateProjectId(projectPath);
|
|
547
|
+
const meta = {
|
|
548
|
+
projectId,
|
|
549
|
+
projectName,
|
|
550
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
551
|
+
cliVersion
|
|
552
|
+
};
|
|
553
|
+
await writeLocalMeta(projectPath, meta);
|
|
554
|
+
await addGitignoreEntry(projectPath);
|
|
555
|
+
return meta;
|
|
556
|
+
}
|
|
557
|
+
function generateProjectId(projectPath) {
|
|
558
|
+
const baseName = path3.basename(projectPath);
|
|
559
|
+
const timestamp = Date.now().toString(36);
|
|
560
|
+
return `${baseName}-${timestamp}`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/registry.ts
|
|
564
|
+
import path4 from "path";
|
|
565
|
+
import { fileURLToPath } from "url";
|
|
566
|
+
import os from "os";
|
|
567
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
568
|
+
var __dirname = path4.dirname(__filename);
|
|
569
|
+
var RAYBURST_DIR = path4.join(os.homedir(), ".rayburst");
|
|
570
|
+
var PROJECTS_FILE = path4.join(RAYBURST_DIR, "projects.json");
|
|
571
|
+
var ANALYZED_DIR = path4.join(RAYBURST_DIR, "analyzed");
|
|
572
|
+
|
|
573
|
+
// src/incremental-analyzer.ts
|
|
574
|
+
import chalk from "chalk";
|
|
575
|
+
function generateDiff(oldAnalysis, newAnalysis) {
|
|
576
|
+
const update = {
|
|
577
|
+
added: {
|
|
578
|
+
nodes: [],
|
|
579
|
+
edges: []
|
|
580
|
+
},
|
|
581
|
+
removed: {
|
|
582
|
+
nodeIds: [],
|
|
583
|
+
edgeIds: []
|
|
584
|
+
},
|
|
585
|
+
modified: {
|
|
586
|
+
nodes: [],
|
|
587
|
+
edges: []
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
const branches = /* @__PURE__ */ new Set([
|
|
591
|
+
...Object.keys(oldAnalysis.planData || {}),
|
|
592
|
+
...Object.keys(newAnalysis.planData || {})
|
|
593
|
+
]);
|
|
594
|
+
for (const branchId of branches) {
|
|
595
|
+
const oldPlanData = oldAnalysis.planData?.[branchId];
|
|
596
|
+
const newPlanData = newAnalysis.planData?.[branchId];
|
|
597
|
+
if (!oldPlanData && newPlanData) {
|
|
598
|
+
update.added.nodes.push(...newPlanData.nodes || []);
|
|
599
|
+
update.added.edges.push(...newPlanData.edges || []);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (oldPlanData && !newPlanData) {
|
|
603
|
+
update.removed.nodeIds.push(...(oldPlanData.nodes || []).map((n) => n.id));
|
|
604
|
+
update.removed.edgeIds.push(...(oldPlanData.edges || []).map((e) => e.id));
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const branchDiff = comparePlanData(oldPlanData, newPlanData);
|
|
608
|
+
update.added.nodes.push(...branchDiff.added.nodes);
|
|
609
|
+
update.added.edges.push(...branchDiff.added.edges);
|
|
610
|
+
update.removed.nodeIds.push(...branchDiff.removed.nodeIds);
|
|
611
|
+
update.removed.edgeIds.push(...branchDiff.removed.edgeIds);
|
|
612
|
+
update.modified.nodes.push(...branchDiff.modified.nodes);
|
|
613
|
+
update.modified.edges.push(...branchDiff.modified.edges);
|
|
614
|
+
}
|
|
615
|
+
return update;
|
|
616
|
+
}
|
|
617
|
+
function comparePlanData(oldPlanData, newPlanData) {
|
|
618
|
+
const oldNodes = oldPlanData?.nodes || [];
|
|
619
|
+
const newNodes = newPlanData?.nodes || [];
|
|
620
|
+
const oldEdges = oldPlanData?.edges || [];
|
|
621
|
+
const newEdges = newPlanData?.edges || [];
|
|
622
|
+
const oldNodeMap = new Map(oldNodes.map((n) => [n.id, n]));
|
|
623
|
+
const newNodeMap = new Map(newNodes.map((n) => [n.id, n]));
|
|
624
|
+
const oldEdgeMap = new Map(oldEdges.map((e) => [e.id, e]));
|
|
625
|
+
const newEdgeMap = new Map(newEdges.map((e) => [e.id, e]));
|
|
626
|
+
const diff = {
|
|
627
|
+
added: { nodes: [], edges: [] },
|
|
628
|
+
removed: { nodeIds: [], edgeIds: [] },
|
|
629
|
+
modified: { nodes: [], edges: [] }
|
|
630
|
+
};
|
|
631
|
+
for (const [nodeId, newNode] of newNodeMap) {
|
|
632
|
+
const oldNode = oldNodeMap.get(nodeId);
|
|
633
|
+
if (!oldNode) {
|
|
634
|
+
diff.added.nodes.push(newNode);
|
|
635
|
+
} else if (hasNodeChanged(oldNode, newNode)) {
|
|
636
|
+
diff.modified.nodes.push({
|
|
637
|
+
id: nodeId,
|
|
638
|
+
...getNodeChanges(oldNode, newNode)
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
for (const nodeId of oldNodeMap.keys()) {
|
|
643
|
+
if (!newNodeMap.has(nodeId)) {
|
|
644
|
+
diff.removed.nodeIds.push(nodeId);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
for (const [edgeId, newEdge] of newEdgeMap) {
|
|
648
|
+
const oldEdge = oldEdgeMap.get(edgeId);
|
|
649
|
+
if (!oldEdge) {
|
|
650
|
+
diff.added.edges.push(newEdge);
|
|
651
|
+
} else if (hasEdgeChanged(oldEdge, newEdge)) {
|
|
652
|
+
diff.modified.edges.push({
|
|
653
|
+
id: edgeId,
|
|
654
|
+
...getEdgeChanges(oldEdge, newEdge)
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
for (const edgeId of oldEdgeMap.keys()) {
|
|
659
|
+
if (!newEdgeMap.has(edgeId)) {
|
|
660
|
+
diff.removed.edgeIds.push(edgeId);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return diff;
|
|
664
|
+
}
|
|
665
|
+
function hasNodeChanged(oldNode, newNode) {
|
|
666
|
+
const oldData = JSON.stringify({ ...oldNode.data, type: oldNode.type });
|
|
667
|
+
const newData = JSON.stringify({ ...newNode.data, type: newNode.type });
|
|
668
|
+
return oldData !== newData;
|
|
669
|
+
}
|
|
670
|
+
function getNodeChanges(oldNode, newNode) {
|
|
671
|
+
const changes = {};
|
|
672
|
+
if (oldNode.type !== newNode.type) {
|
|
673
|
+
changes.type = newNode.type;
|
|
674
|
+
}
|
|
675
|
+
if (JSON.stringify(oldNode.data) !== JSON.stringify(newNode.data)) {
|
|
676
|
+
changes.data = newNode.data;
|
|
677
|
+
}
|
|
678
|
+
return changes;
|
|
679
|
+
}
|
|
680
|
+
function hasEdgeChanged(oldEdge, newEdge) {
|
|
681
|
+
return oldEdge.source !== newEdge.source || oldEdge.target !== newEdge.target || oldEdge.type !== newEdge.type || JSON.stringify(oldEdge.data) !== JSON.stringify(newEdge.data);
|
|
682
|
+
}
|
|
683
|
+
function getEdgeChanges(oldEdge, newEdge) {
|
|
684
|
+
const changes = {};
|
|
685
|
+
if (oldEdge.source !== newEdge.source) changes.source = newEdge.source;
|
|
686
|
+
if (oldEdge.target !== newEdge.target) changes.target = newEdge.target;
|
|
687
|
+
if (oldEdge.type !== newEdge.type) changes.type = newEdge.type;
|
|
688
|
+
if (JSON.stringify(oldEdge.data) !== JSON.stringify(newEdge.data)) {
|
|
689
|
+
changes.data = newEdge.data;
|
|
690
|
+
}
|
|
691
|
+
return changes;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/vite-plugin.ts
|
|
695
|
+
import chalk2 from "chalk";
|
|
696
|
+
function rayburstPlugin(options = {}) {
|
|
697
|
+
const {
|
|
698
|
+
enabled = process.env.CI !== "true",
|
|
699
|
+
// Disabled in CI by default
|
|
700
|
+
debounceMs = 1500
|
|
701
|
+
} = options;
|
|
702
|
+
let config;
|
|
703
|
+
let debounceTimer = null;
|
|
704
|
+
let isAnalyzing = false;
|
|
705
|
+
const runAnalysis = async (triggerFile) => {
|
|
706
|
+
if (isAnalyzing) return;
|
|
707
|
+
isAnalyzing = true;
|
|
708
|
+
try {
|
|
709
|
+
const projectPath = config.root;
|
|
710
|
+
const previousAnalysis = await readLocalAnalysis(projectPath);
|
|
711
|
+
const newAnalysis = await analyzeProject(projectPath);
|
|
712
|
+
await writeLocalAnalysis(projectPath, newAnalysis);
|
|
713
|
+
if (previousAnalysis) {
|
|
714
|
+
const diff = generateDiff(previousAnalysis, newAnalysis);
|
|
715
|
+
console.log(chalk2.green("[Rayburst] Analysis updated:"));
|
|
716
|
+
console.log(
|
|
717
|
+
chalk2.gray(
|
|
718
|
+
` Added: ${diff.added.nodes.length} nodes, ${diff.added.edges.length} edges`
|
|
719
|
+
)
|
|
720
|
+
);
|
|
721
|
+
console.log(
|
|
722
|
+
chalk2.gray(
|
|
723
|
+
` Removed: ${diff.removed.nodeIds.length} nodes, ${diff.removed.edgeIds.length} edges`
|
|
724
|
+
)
|
|
725
|
+
);
|
|
726
|
+
if (diff.modified.nodes.length > 0) {
|
|
727
|
+
console.log(chalk2.gray(` Modified: ${diff.modified.nodes.length} nodes`));
|
|
728
|
+
}
|
|
729
|
+
} else {
|
|
730
|
+
const firstBranch = Object.keys(newAnalysis.planData)[0];
|
|
731
|
+
const nodeCount = firstBranch ? newAnalysis.planData[firstBranch].nodes.length : 0;
|
|
732
|
+
const edgeCount = firstBranch ? newAnalysis.planData[firstBranch].edges.length : 0;
|
|
733
|
+
console.log(
|
|
734
|
+
chalk2.green(`[Rayburst] Initial analysis complete: ${nodeCount} nodes, ${edgeCount} edges`)
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
} catch (error) {
|
|
738
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
739
|
+
console.error(chalk2.red("[Rayburst] Analysis failed:"), message);
|
|
740
|
+
} finally {
|
|
741
|
+
isAnalyzing = false;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
return {
|
|
745
|
+
name: "rayburst-analyzer",
|
|
746
|
+
// Hook 1: Store resolved config
|
|
747
|
+
configResolved(resolvedConfig) {
|
|
748
|
+
config = resolvedConfig;
|
|
749
|
+
},
|
|
750
|
+
// Hook 2: Run initial analysis when dev server starts
|
|
751
|
+
async buildStart() {
|
|
752
|
+
if (!enabled || config.command !== "serve") {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const projectPath = config.root;
|
|
756
|
+
await ensureRayburstDir(projectPath);
|
|
757
|
+
await addGitignoreEntry(projectPath);
|
|
758
|
+
console.log(chalk2.blue("[Rayburst] Starting code analysis..."));
|
|
759
|
+
await runAnalysis();
|
|
760
|
+
},
|
|
761
|
+
// Hook 3: Watch for changes using Vite's built-in watcher
|
|
762
|
+
configureServer(server) {
|
|
763
|
+
if (!enabled) return;
|
|
764
|
+
const handleFileChange = (filePath) => {
|
|
765
|
+
if (!filePath.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (filePath.includes("node_modules") || filePath.includes(".rayburst") || filePath.includes(".test.") || filePath.includes(".spec.")) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (debounceTimer) {
|
|
772
|
+
clearTimeout(debounceTimer);
|
|
773
|
+
}
|
|
774
|
+
debounceTimer = setTimeout(() => {
|
|
775
|
+
const relativePath = filePath.replace(config.root, "").replace(/^\//, "");
|
|
776
|
+
console.log(chalk2.dim(`[Rayburst] File changed: ${relativePath}`));
|
|
777
|
+
runAnalysis(filePath);
|
|
778
|
+
}, debounceMs);
|
|
779
|
+
};
|
|
780
|
+
server.watcher.on("change", handleFileChange);
|
|
781
|
+
server.watcher.on("add", handleFileChange);
|
|
782
|
+
server.watcher.on("unlink", handleFileChange);
|
|
783
|
+
return () => {
|
|
784
|
+
server.watcher.off("change", handleFileChange);
|
|
785
|
+
server.watcher.off("add", handleFileChange);
|
|
786
|
+
server.watcher.off("unlink", handleFileChange);
|
|
787
|
+
if (debounceTimer) {
|
|
788
|
+
clearTimeout(debounceTimer);
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export {
|
|
796
|
+
analyzeProject,
|
|
797
|
+
ensureRayburstDir,
|
|
798
|
+
readLocalAnalysis,
|
|
799
|
+
writeLocalAnalysis,
|
|
800
|
+
readLocalMeta,
|
|
801
|
+
writeLocalMeta,
|
|
802
|
+
addGitignoreEntry,
|
|
803
|
+
isProjectInitialized,
|
|
804
|
+
initializeProject,
|
|
805
|
+
rayburstPlugin
|
|
806
|
+
};
|