@fairfox/polly 0.2.1 → 0.3.1
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.
|
@@ -42,9 +42,22 @@ class ProjectDetector {
|
|
|
42
42
|
if (packageJson.dependencies?.electron || packageJson.devDependencies?.electron) {
|
|
43
43
|
return this.detectElectron(packageJson);
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
const deps = {
|
|
46
|
+
...packageJson.dependencies,
|
|
47
|
+
...packageJson.devDependencies
|
|
48
|
+
};
|
|
49
|
+
if (deps?.ws || deps?.["socket.io"] || deps?.elysia || deps?.express || deps?.fastify || deps?.hono || deps?.koa) {
|
|
46
50
|
return this.detectWebSocketApp(packageJson);
|
|
47
51
|
}
|
|
52
|
+
const serverResult = this.detectWebSocketApp(packageJson);
|
|
53
|
+
if (Object.keys(serverResult.entryPoints).length > 0) {
|
|
54
|
+
return serverResult;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
const serverResult = this.detectWebSocketApp({});
|
|
58
|
+
if (Object.keys(serverResult.entryPoints).length > 0) {
|
|
59
|
+
return serverResult;
|
|
60
|
+
}
|
|
48
61
|
}
|
|
49
62
|
return this.detectGenericProject();
|
|
50
63
|
}
|
|
@@ -176,37 +189,53 @@ class ProjectDetector {
|
|
|
176
189
|
}
|
|
177
190
|
detectWebSocketApp(packageJson) {
|
|
178
191
|
const entryPoints = {};
|
|
192
|
+
const contextMapping = {};
|
|
179
193
|
const serverCandidates = [
|
|
180
194
|
"src/server.ts",
|
|
195
|
+
"src/server.js",
|
|
181
196
|
"src/index.ts",
|
|
197
|
+
"src/index.js",
|
|
182
198
|
"src/main.ts",
|
|
199
|
+
"src/main.js",
|
|
183
200
|
"src/app.ts",
|
|
201
|
+
"src/app.js",
|
|
184
202
|
"server.ts",
|
|
185
|
-
"
|
|
203
|
+
"server.js",
|
|
204
|
+
"index.ts",
|
|
205
|
+
"index.js"
|
|
186
206
|
];
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
const scoredServers = this.scoreServerCandidates(serverCandidates);
|
|
208
|
+
if (scoredServers.length > 0) {
|
|
209
|
+
const best = scoredServers[0];
|
|
210
|
+
entryPoints.server = best.path;
|
|
211
|
+
if (best.hasWebSocket) {
|
|
212
|
+
contextMapping.server = "WebSocket Server";
|
|
213
|
+
} else if (best.hasHTTP) {
|
|
214
|
+
contextMapping.server = "HTTP Server";
|
|
215
|
+
} else {
|
|
216
|
+
contextMapping.server = "Server";
|
|
192
217
|
}
|
|
193
218
|
}
|
|
194
219
|
const clientCandidates = [
|
|
195
220
|
"src/client/index.ts",
|
|
221
|
+
"src/client/index.js",
|
|
196
222
|
"src/client.ts",
|
|
197
|
-
"client
|
|
223
|
+
"src/client.js",
|
|
224
|
+
"client/index.ts",
|
|
225
|
+
"client/index.js"
|
|
198
226
|
];
|
|
199
227
|
for (const candidate of clientCandidates) {
|
|
200
228
|
const fullPath = path3.join(this.projectRoot, candidate);
|
|
201
229
|
if (fs3.existsSync(fullPath)) {
|
|
202
230
|
entryPoints.client = fullPath;
|
|
231
|
+
contextMapping.client = "Client";
|
|
203
232
|
break;
|
|
204
233
|
}
|
|
205
234
|
}
|
|
206
235
|
return {
|
|
207
236
|
type: "websocket-app",
|
|
208
237
|
entryPoints,
|
|
209
|
-
contextMapping: {
|
|
238
|
+
contextMapping: Object.keys(contextMapping).length > 0 ? contextMapping : {
|
|
210
239
|
server: "Server",
|
|
211
240
|
client: "Client"
|
|
212
241
|
},
|
|
@@ -217,6 +246,98 @@ class ProjectDetector {
|
|
|
217
246
|
}
|
|
218
247
|
};
|
|
219
248
|
}
|
|
249
|
+
scoreServerCandidates(candidates) {
|
|
250
|
+
const scored = [];
|
|
251
|
+
for (const candidate of candidates) {
|
|
252
|
+
const fullPath = path3.join(this.projectRoot, candidate);
|
|
253
|
+
if (!fs3.existsSync(fullPath))
|
|
254
|
+
continue;
|
|
255
|
+
try {
|
|
256
|
+
const content = fs3.readFileSync(fullPath, "utf-8");
|
|
257
|
+
let score = 0;
|
|
258
|
+
let hasWebSocket = false;
|
|
259
|
+
let hasHTTP = false;
|
|
260
|
+
let framework = null;
|
|
261
|
+
const patterns = {
|
|
262
|
+
bunWebSocket: /Bun\.serve\s*\([^)]*websocket\s*:/i,
|
|
263
|
+
bunHTTP: /Bun\.serve\s*\(/i,
|
|
264
|
+
wsServer: /new\s+WebSocket\.Server/i,
|
|
265
|
+
wsImport: /from\s+['"]ws['"]/,
|
|
266
|
+
socketIO: /io\s*\(|require\s*\(\s*['"]socket\.io['"]\s*\)/i,
|
|
267
|
+
elysia: /new\s+Elysia\s*\(|\.ws\s*\(/i,
|
|
268
|
+
elysiaImport: /from\s+['"]elysia['"]/,
|
|
269
|
+
express: /express\s*\(\)|app\.listen/i,
|
|
270
|
+
expressWs: /expressWs\s*\(/i,
|
|
271
|
+
httpServer: /createServer|app\.listen|server\.listen/i,
|
|
272
|
+
webSocket: /WebSocket|websocket|\.ws\s*\(|wss\s*:/i
|
|
273
|
+
};
|
|
274
|
+
if (patterns.bunWebSocket.test(content)) {
|
|
275
|
+
score += 15;
|
|
276
|
+
hasWebSocket = true;
|
|
277
|
+
hasHTTP = true;
|
|
278
|
+
framework = "Bun";
|
|
279
|
+
} else if (patterns.bunHTTP.test(content)) {
|
|
280
|
+
score += 10;
|
|
281
|
+
hasHTTP = true;
|
|
282
|
+
framework = "Bun";
|
|
283
|
+
}
|
|
284
|
+
if (patterns.wsServer.test(content) || patterns.wsImport.test(content)) {
|
|
285
|
+
score += 12;
|
|
286
|
+
hasWebSocket = true;
|
|
287
|
+
framework = framework || "ws";
|
|
288
|
+
}
|
|
289
|
+
if (patterns.socketIO.test(content)) {
|
|
290
|
+
score += 12;
|
|
291
|
+
hasWebSocket = true;
|
|
292
|
+
framework = framework || "socket.io";
|
|
293
|
+
}
|
|
294
|
+
if (patterns.elysia.test(content) || patterns.elysiaImport.test(content)) {
|
|
295
|
+
score += 10;
|
|
296
|
+
hasHTTP = true;
|
|
297
|
+
framework = framework || "Elysia";
|
|
298
|
+
}
|
|
299
|
+
if (patterns.elysiaImport.test(content) && patterns.webSocket.test(content)) {
|
|
300
|
+
score += 8;
|
|
301
|
+
hasWebSocket = true;
|
|
302
|
+
}
|
|
303
|
+
if (patterns.express.test(content)) {
|
|
304
|
+
score += 8;
|
|
305
|
+
hasHTTP = true;
|
|
306
|
+
framework = framework || "Express";
|
|
307
|
+
}
|
|
308
|
+
if (patterns.expressWs.test(content)) {
|
|
309
|
+
score += 5;
|
|
310
|
+
hasWebSocket = true;
|
|
311
|
+
}
|
|
312
|
+
if (patterns.httpServer.test(content) && !hasHTTP) {
|
|
313
|
+
score += 5;
|
|
314
|
+
hasHTTP = true;
|
|
315
|
+
}
|
|
316
|
+
if (patterns.webSocket.test(content) && !hasWebSocket) {
|
|
317
|
+
score += 3;
|
|
318
|
+
hasWebSocket = true;
|
|
319
|
+
}
|
|
320
|
+
if (/\.listen\s*\(/.test(content)) {
|
|
321
|
+
score += 5;
|
|
322
|
+
}
|
|
323
|
+
if (/export\s+default/.test(content)) {
|
|
324
|
+
score += 3;
|
|
325
|
+
}
|
|
326
|
+
if (candidate.includes("server")) {
|
|
327
|
+
score += 3;
|
|
328
|
+
}
|
|
329
|
+
if (candidate === "src/index.ts" || candidate === "src/index.js") {
|
|
330
|
+
score += 2;
|
|
331
|
+
}
|
|
332
|
+
if (score > 0) {
|
|
333
|
+
scored.push({ path: fullPath, score, hasWebSocket, hasHTTP, framework });
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return scored.sort((a, b) => b.score - a.score);
|
|
340
|
+
}
|
|
220
341
|
detectGenericProject() {
|
|
221
342
|
const entryPoints = {};
|
|
222
343
|
const tsConfigPath = path3.join(this.projectRoot, "tsconfig.json");
|
|
@@ -343,10 +464,14 @@ class ManifestParser {
|
|
|
343
464
|
manifestPath;
|
|
344
465
|
manifestData;
|
|
345
466
|
baseDir;
|
|
346
|
-
constructor(projectRoot) {
|
|
467
|
+
constructor(projectRoot, optional = false) {
|
|
347
468
|
this.baseDir = projectRoot;
|
|
348
469
|
this.manifestPath = path.join(projectRoot, "manifest.json");
|
|
349
470
|
if (!fs.existsSync(this.manifestPath)) {
|
|
471
|
+
if (optional) {
|
|
472
|
+
this.manifestData = null;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
350
475
|
throw new Error(`manifest.json not found at ${this.manifestPath}`);
|
|
351
476
|
}
|
|
352
477
|
try {
|
|
@@ -356,7 +481,13 @@ class ManifestParser {
|
|
|
356
481
|
throw new Error(`Failed to parse manifest.json: ${error}`);
|
|
357
482
|
}
|
|
358
483
|
}
|
|
484
|
+
hasManifest() {
|
|
485
|
+
return this.manifestData !== null;
|
|
486
|
+
}
|
|
359
487
|
parse() {
|
|
488
|
+
if (!this.manifestData) {
|
|
489
|
+
throw new Error("Cannot parse manifest: manifest.json not loaded. Use hasManifest() to check availability.");
|
|
490
|
+
}
|
|
360
491
|
const manifest = this.manifestData;
|
|
361
492
|
return {
|
|
362
493
|
name: manifest.name || "Unknown Extension",
|
|
@@ -373,6 +504,9 @@ class ManifestParser {
|
|
|
373
504
|
};
|
|
374
505
|
}
|
|
375
506
|
getContextEntryPoints() {
|
|
507
|
+
if (!this.manifestData) {
|
|
508
|
+
return {};
|
|
509
|
+
}
|
|
376
510
|
const entryPoints = {};
|
|
377
511
|
const background = this.parseBackground();
|
|
378
512
|
if (background) {
|
|
@@ -415,6 +549,8 @@ class ManifestParser {
|
|
|
415
549
|
return entryPoints;
|
|
416
550
|
}
|
|
417
551
|
parseBackground() {
|
|
552
|
+
if (!this.manifestData)
|
|
553
|
+
return;
|
|
418
554
|
const bg = this.manifestData.background;
|
|
419
555
|
if (!bg)
|
|
420
556
|
return;
|
|
@@ -439,6 +575,8 @@ class ManifestParser {
|
|
|
439
575
|
return;
|
|
440
576
|
}
|
|
441
577
|
parseContentScripts() {
|
|
578
|
+
if (!this.manifestData)
|
|
579
|
+
return;
|
|
442
580
|
const cs = this.manifestData.content_scripts;
|
|
443
581
|
if (!cs || !Array.isArray(cs))
|
|
444
582
|
return;
|
|
@@ -449,6 +587,8 @@ class ManifestParser {
|
|
|
449
587
|
}));
|
|
450
588
|
}
|
|
451
589
|
parsePopup() {
|
|
590
|
+
if (!this.manifestData)
|
|
591
|
+
return;
|
|
452
592
|
const action = this.manifestData.action || this.manifestData.browser_action;
|
|
453
593
|
if (!action)
|
|
454
594
|
return;
|
|
@@ -461,6 +601,8 @@ class ManifestParser {
|
|
|
461
601
|
return;
|
|
462
602
|
}
|
|
463
603
|
parseOptions() {
|
|
604
|
+
if (!this.manifestData)
|
|
605
|
+
return;
|
|
464
606
|
const options = this.manifestData.options_ui || this.manifestData.options_page;
|
|
465
607
|
if (!options)
|
|
466
608
|
return;
|
|
@@ -476,6 +618,8 @@ class ManifestParser {
|
|
|
476
618
|
};
|
|
477
619
|
}
|
|
478
620
|
parseDevtools() {
|
|
621
|
+
if (!this.manifestData)
|
|
622
|
+
return;
|
|
479
623
|
const devtools = this.manifestData.devtools_page;
|
|
480
624
|
if (!devtools)
|
|
481
625
|
return;
|
|
@@ -780,7 +924,34 @@ class FlowAnalyzer {
|
|
|
780
924
|
const expression = node.getExpression();
|
|
781
925
|
if (Node2.isPropertyAccessExpression(expression)) {
|
|
782
926
|
const methodName = expression.getName();
|
|
783
|
-
if (methodName === "send" || methodName === "emit") {
|
|
927
|
+
if (methodName === "send" || methodName === "emit" || methodName === "postMessage" || methodName === "broadcast") {
|
|
928
|
+
const args = node.getArguments();
|
|
929
|
+
if (args.length > 0) {
|
|
930
|
+
const firstArg = args[0];
|
|
931
|
+
let msgType;
|
|
932
|
+
if (Node2.isStringLiteral(firstArg)) {
|
|
933
|
+
msgType = firstArg.getLiteralValue();
|
|
934
|
+
} else if (Node2.isObjectLiteralExpression(firstArg)) {
|
|
935
|
+
const typeProperty = firstArg.getProperty("type");
|
|
936
|
+
if (typeProperty && Node2.isPropertyAssignment(typeProperty)) {
|
|
937
|
+
const initializer = typeProperty.getInitializer();
|
|
938
|
+
if (initializer && Node2.isStringLiteral(initializer)) {
|
|
939
|
+
msgType = initializer.getLiteralValue();
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (msgType === messageType) {
|
|
944
|
+
senders.push({
|
|
945
|
+
context,
|
|
946
|
+
file: filePath,
|
|
947
|
+
line: node.getStartLineNumber()
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (Node2.isIdentifier(expression)) {
|
|
954
|
+
if (expression.getText() === "postMessage") {
|
|
784
955
|
const args = node.getArguments();
|
|
785
956
|
if (args.length > 0) {
|
|
786
957
|
const firstArg = args[0];
|
|
@@ -932,6 +1103,15 @@ class FlowAnalyzer {
|
|
|
932
1103
|
if (path2.includes("/offscreen/") || path2.includes("\\offscreen\\")) {
|
|
933
1104
|
return "offscreen";
|
|
934
1105
|
}
|
|
1106
|
+
if (path2.includes("/server/") || path2.includes("\\server\\") || path2.includes("/server.")) {
|
|
1107
|
+
return "server";
|
|
1108
|
+
}
|
|
1109
|
+
if (path2.includes("/client/") || path2.includes("\\client\\") || path2.includes("/client.")) {
|
|
1110
|
+
return "client";
|
|
1111
|
+
}
|
|
1112
|
+
if (path2.includes("/worker/") || path2.includes("\\worker\\") || path2.includes("service-worker")) {
|
|
1113
|
+
return "worker";
|
|
1114
|
+
}
|
|
935
1115
|
return "unknown";
|
|
936
1116
|
}
|
|
937
1117
|
}
|
|
@@ -1138,10 +1318,12 @@ import { Project as Project4, SyntaxKind as SyntaxKind2, Node as Node4 } from "t
|
|
|
1138
1318
|
|
|
1139
1319
|
class HandlerExtractor {
|
|
1140
1320
|
project;
|
|
1321
|
+
typeGuardCache;
|
|
1141
1322
|
constructor(tsConfigPath) {
|
|
1142
1323
|
this.project = new Project4({
|
|
1143
1324
|
tsConfigFilePath: tsConfigPath
|
|
1144
1325
|
});
|
|
1326
|
+
this.typeGuardCache = new WeakMap;
|
|
1145
1327
|
}
|
|
1146
1328
|
extractHandlers() {
|
|
1147
1329
|
const handlers = [];
|
|
@@ -1168,7 +1350,7 @@ class HandlerExtractor {
|
|
|
1168
1350
|
const expression = node.getExpression();
|
|
1169
1351
|
if (Node4.isPropertyAccessExpression(expression)) {
|
|
1170
1352
|
const methodName = expression.getName();
|
|
1171
|
-
if (methodName === "on") {
|
|
1353
|
+
if (methodName === "on" || methodName === "addEventListener") {
|
|
1172
1354
|
const handler = this.extractHandler(node, context, filePath);
|
|
1173
1355
|
if (handler) {
|
|
1174
1356
|
handlers.push(handler);
|
|
@@ -1176,6 +1358,18 @@ class HandlerExtractor {
|
|
|
1176
1358
|
}
|
|
1177
1359
|
}
|
|
1178
1360
|
}
|
|
1361
|
+
if (Node4.isSwitchStatement(node)) {
|
|
1362
|
+
const switchHandlers = this.extractSwitchCaseHandlers(node, context, filePath);
|
|
1363
|
+
handlers.push(...switchHandlers);
|
|
1364
|
+
}
|
|
1365
|
+
if (Node4.isVariableDeclaration(node)) {
|
|
1366
|
+
const mapHandlers = this.extractHandlerMapPattern(node, context, filePath);
|
|
1367
|
+
handlers.push(...mapHandlers);
|
|
1368
|
+
}
|
|
1369
|
+
if (Node4.isIfStatement(node)) {
|
|
1370
|
+
const typeGuardHandlers = this.extractTypeGuardHandlers(node, context, filePath);
|
|
1371
|
+
handlers.push(...typeGuardHandlers);
|
|
1372
|
+
}
|
|
1179
1373
|
});
|
|
1180
1374
|
return handlers;
|
|
1181
1375
|
}
|
|
@@ -1317,6 +1511,182 @@ class HandlerExtractor {
|
|
|
1317
1511
|
}
|
|
1318
1512
|
return;
|
|
1319
1513
|
}
|
|
1514
|
+
extractSwitchCaseHandlers(switchNode, context, filePath) {
|
|
1515
|
+
const handlers = [];
|
|
1516
|
+
try {
|
|
1517
|
+
const expression = switchNode.getExpression();
|
|
1518
|
+
const expressionText = expression.getText();
|
|
1519
|
+
if (!/\.(type|kind|event|action)/.test(expressionText)) {
|
|
1520
|
+
return handlers;
|
|
1521
|
+
}
|
|
1522
|
+
const caseClauses = switchNode.getClauses();
|
|
1523
|
+
for (const clause of caseClauses) {
|
|
1524
|
+
if (Node4.isCaseClause(clause)) {
|
|
1525
|
+
const caseExpr = clause.getExpression();
|
|
1526
|
+
let messageType = null;
|
|
1527
|
+
if (Node4.isStringLiteral(caseExpr)) {
|
|
1528
|
+
messageType = caseExpr.getLiteralValue();
|
|
1529
|
+
}
|
|
1530
|
+
if (messageType) {
|
|
1531
|
+
const line = clause.getStartLineNumber();
|
|
1532
|
+
handlers.push({
|
|
1533
|
+
messageType,
|
|
1534
|
+
node: context,
|
|
1535
|
+
assignments: [],
|
|
1536
|
+
preconditions: [],
|
|
1537
|
+
postconditions: [],
|
|
1538
|
+
location: { file: filePath, line }
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
} catch (error) {}
|
|
1544
|
+
return handlers;
|
|
1545
|
+
}
|
|
1546
|
+
extractHandlerMapPattern(varDecl, context, filePath) {
|
|
1547
|
+
const handlers = [];
|
|
1548
|
+
try {
|
|
1549
|
+
const initializer = varDecl.getInitializer();
|
|
1550
|
+
if (!initializer || !Node4.isObjectLiteralExpression(initializer)) {
|
|
1551
|
+
return handlers;
|
|
1552
|
+
}
|
|
1553
|
+
const varName = varDecl.getName().toLowerCase();
|
|
1554
|
+
if (!/(handler|listener|callback|event)s?/.test(varName)) {
|
|
1555
|
+
return handlers;
|
|
1556
|
+
}
|
|
1557
|
+
const properties = initializer.getProperties();
|
|
1558
|
+
for (const prop of properties) {
|
|
1559
|
+
if (Node4.isPropertyAssignment(prop)) {
|
|
1560
|
+
const nameNode = prop.getNameNode();
|
|
1561
|
+
let messageType = null;
|
|
1562
|
+
if (Node4.isStringLiteral(nameNode)) {
|
|
1563
|
+
messageType = nameNode.getLiteralValue();
|
|
1564
|
+
} else if (Node4.isIdentifier(nameNode)) {
|
|
1565
|
+
messageType = nameNode.getText();
|
|
1566
|
+
}
|
|
1567
|
+
if (messageType) {
|
|
1568
|
+
const line = prop.getStartLineNumber();
|
|
1569
|
+
handlers.push({
|
|
1570
|
+
messageType,
|
|
1571
|
+
node: context,
|
|
1572
|
+
assignments: [],
|
|
1573
|
+
preconditions: [],
|
|
1574
|
+
postconditions: [],
|
|
1575
|
+
location: { file: filePath, line }
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
} catch (error) {}
|
|
1581
|
+
return handlers;
|
|
1582
|
+
}
|
|
1583
|
+
extractTypeGuardHandlers(ifNode, context, filePath) {
|
|
1584
|
+
const handlers = [];
|
|
1585
|
+
try {
|
|
1586
|
+
const sourceFile = ifNode.getSourceFile();
|
|
1587
|
+
let typeGuards = this.typeGuardCache.get(sourceFile);
|
|
1588
|
+
if (!typeGuards) {
|
|
1589
|
+
typeGuards = this.findTypePredicateFunctions(sourceFile);
|
|
1590
|
+
this.typeGuardCache.set(sourceFile, typeGuards);
|
|
1591
|
+
}
|
|
1592
|
+
if (typeGuards.size === 0) {
|
|
1593
|
+
return handlers;
|
|
1594
|
+
}
|
|
1595
|
+
let currentIf = ifNode;
|
|
1596
|
+
while (currentIf) {
|
|
1597
|
+
const handler = this.extractHandlerFromIfClause(currentIf, typeGuards, context, filePath);
|
|
1598
|
+
if (handler) {
|
|
1599
|
+
handlers.push(handler);
|
|
1600
|
+
}
|
|
1601
|
+
const elseStatement = currentIf.getElseStatement();
|
|
1602
|
+
if (elseStatement && Node4.isIfStatement(elseStatement)) {
|
|
1603
|
+
currentIf = elseStatement;
|
|
1604
|
+
} else {
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
} catch (error) {}
|
|
1609
|
+
return handlers;
|
|
1610
|
+
}
|
|
1611
|
+
extractHandlerFromIfClause(ifNode, typeGuards, context, filePath) {
|
|
1612
|
+
try {
|
|
1613
|
+
const condition = ifNode.getExpression();
|
|
1614
|
+
if (!Node4.isCallExpression(condition)) {
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
const funcExpr = condition.getExpression();
|
|
1618
|
+
let funcName;
|
|
1619
|
+
if (Node4.isIdentifier(funcExpr)) {
|
|
1620
|
+
funcName = funcExpr.getText();
|
|
1621
|
+
}
|
|
1622
|
+
if (!funcName || !typeGuards.has(funcName)) {
|
|
1623
|
+
return null;
|
|
1624
|
+
}
|
|
1625
|
+
const messageType = typeGuards.get(funcName);
|
|
1626
|
+
const line = ifNode.getStartLineNumber();
|
|
1627
|
+
return {
|
|
1628
|
+
messageType,
|
|
1629
|
+
node: context,
|
|
1630
|
+
assignments: [],
|
|
1631
|
+
preconditions: [],
|
|
1632
|
+
postconditions: [],
|
|
1633
|
+
location: { file: filePath, line }
|
|
1634
|
+
};
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
findTypePredicateFunctions(sourceFile) {
|
|
1640
|
+
const typeGuards = new Map;
|
|
1641
|
+
sourceFile.forEachDescendant((node) => {
|
|
1642
|
+
if (Node4.isFunctionDeclaration(node) || Node4.isFunctionExpression(node) || Node4.isArrowFunction(node)) {
|
|
1643
|
+
const returnType = node.getReturnType();
|
|
1644
|
+
const returnTypeText = returnType.getText();
|
|
1645
|
+
if (/is\s+\w+/.test(returnTypeText)) {
|
|
1646
|
+
let functionName;
|
|
1647
|
+
if (Node4.isFunctionDeclaration(node)) {
|
|
1648
|
+
functionName = node.getName();
|
|
1649
|
+
} else if (Node4.isFunctionExpression(node)) {
|
|
1650
|
+
const parent = node.getParent();
|
|
1651
|
+
if (Node4.isVariableDeclaration(parent)) {
|
|
1652
|
+
functionName = parent.getName();
|
|
1653
|
+
}
|
|
1654
|
+
} else if (Node4.isArrowFunction(node)) {
|
|
1655
|
+
const parent = node.getParent();
|
|
1656
|
+
if (Node4.isVariableDeclaration(parent)) {
|
|
1657
|
+
functionName = parent.getName();
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
if (functionName) {
|
|
1661
|
+
let messageType = null;
|
|
1662
|
+
const typeMatch = returnTypeText.match(/is\s+(\w+)/);
|
|
1663
|
+
if (typeMatch) {
|
|
1664
|
+
const typeName = typeMatch[1];
|
|
1665
|
+
messageType = this.extractMessageTypeFromTypeName(typeName);
|
|
1666
|
+
}
|
|
1667
|
+
if (!messageType) {
|
|
1668
|
+
const body = node.getBody();
|
|
1669
|
+
if (body) {
|
|
1670
|
+
const bodyText = body.getText();
|
|
1671
|
+
const typeValueMatch = bodyText.match(/\.type\s*===?\s*['"](\w+)['"]/);
|
|
1672
|
+
if (typeValueMatch) {
|
|
1673
|
+
messageType = typeValueMatch[1];
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
if (messageType) {
|
|
1678
|
+
typeGuards.set(functionName, messageType);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
return typeGuards;
|
|
1685
|
+
}
|
|
1686
|
+
extractMessageTypeFromTypeName(typeName) {
|
|
1687
|
+
const messageType = typeName.replace(/Message$/, "").replace(/Event$/, "").replace(/Request$/, "").replace(/Command$/, "").replace(/Query$/, "").toLowerCase();
|
|
1688
|
+
return messageType;
|
|
1689
|
+
}
|
|
1320
1690
|
inferContext(filePath) {
|
|
1321
1691
|
const path2 = filePath.toLowerCase();
|
|
1322
1692
|
if (path2.includes("/background/") || path2.includes("\\background\\")) {
|
|
@@ -1337,6 +1707,15 @@ class HandlerExtractor {
|
|
|
1337
1707
|
if (path2.includes("/offscreen/") || path2.includes("\\offscreen\\")) {
|
|
1338
1708
|
return "offscreen";
|
|
1339
1709
|
}
|
|
1710
|
+
if (path2.includes("/server/") || path2.includes("\\server\\") || path2.includes("/server.")) {
|
|
1711
|
+
return "server";
|
|
1712
|
+
}
|
|
1713
|
+
if (path2.includes("/client/") || path2.includes("\\client\\") || path2.includes("/client.")) {
|
|
1714
|
+
return "client";
|
|
1715
|
+
}
|
|
1716
|
+
if (path2.includes("/worker/") || path2.includes("\\worker\\") || path2.includes("service-worker")) {
|
|
1717
|
+
return "worker";
|
|
1718
|
+
}
|
|
1340
1719
|
return "unknown";
|
|
1341
1720
|
}
|
|
1342
1721
|
}
|
|
@@ -1486,10 +1865,8 @@ class ArchitectureAnalyzer {
|
|
|
1486
1865
|
let projectConfig;
|
|
1487
1866
|
let entryPoints = {};
|
|
1488
1867
|
let systemInfo;
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1491
|
-
if (hasManifest && !this.options.useProjectDetector) {
|
|
1492
|
-
const manifestParser = new ManifestParser(this.options.projectRoot);
|
|
1868
|
+
const manifestParser = new ManifestParser(this.options.projectRoot, true);
|
|
1869
|
+
if (manifestParser.hasManifest() && !this.options.useProjectDetector) {
|
|
1493
1870
|
manifest = manifestParser.parse();
|
|
1494
1871
|
entryPoints = manifestParser.getContextEntryPoints();
|
|
1495
1872
|
systemInfo = {
|
|
@@ -2256,11 +2633,17 @@ async function generateCommand() {
|
|
|
2256
2633
|
console.log(color(` Using: ${tsConfigPath}`, COLORS.gray));
|
|
2257
2634
|
const projectRoot = findProjectRoot();
|
|
2258
2635
|
if (!projectRoot) {
|
|
2259
|
-
console.error(color("❌ Could not find
|
|
2260
|
-
console.error(" Run this command from
|
|
2636
|
+
console.error(color("❌ Could not find project root", COLORS.red));
|
|
2637
|
+
console.error(" Run this command from a directory with manifest.json, package.json, or tsconfig.json");
|
|
2261
2638
|
process.exit(1);
|
|
2262
2639
|
}
|
|
2263
2640
|
console.log(color(` Project: ${projectRoot}`, COLORS.gray));
|
|
2641
|
+
const hasManifest = fs6.existsSync(path6.join(projectRoot, "manifest.json"));
|
|
2642
|
+
if (hasManifest) {
|
|
2643
|
+
console.log(color(` Type: Chrome Extension`, COLORS.gray));
|
|
2644
|
+
} else {
|
|
2645
|
+
console.log(color(` Type: Detecting from project structure...`, COLORS.gray));
|
|
2646
|
+
}
|
|
2264
2647
|
const analysis = await analyzeArchitecture({
|
|
2265
2648
|
tsConfigPath,
|
|
2266
2649
|
projectRoot
|
|
@@ -2447,7 +2830,15 @@ async function serveCommand(args) {
|
|
|
2447
2830
|
}
|
|
2448
2831
|
function showHelp() {
|
|
2449
2832
|
console.log(`
|
|
2450
|
-
${color("bun visualize", COLORS.blue)} - Architecture visualization
|
|
2833
|
+
${color("bun visualize", COLORS.blue)} - Architecture visualization tool
|
|
2834
|
+
|
|
2835
|
+
${color("Supports:", COLORS.blue)}
|
|
2836
|
+
|
|
2837
|
+
• Chrome Extensions (manifest.json)
|
|
2838
|
+
• PWAs (public/manifest.json)
|
|
2839
|
+
• WebSocket/Server Apps (ws, socket.io, elysia)
|
|
2840
|
+
• Electron Apps
|
|
2841
|
+
• Generic TypeScript Projects
|
|
2451
2842
|
|
|
2452
2843
|
${color("Commands:", COLORS.blue)}
|
|
2453
2844
|
|
|
@@ -2467,14 +2858,14 @@ ${color("Commands:", COLORS.blue)}
|
|
|
2467
2858
|
|
|
2468
2859
|
${color("Getting Started:", COLORS.blue)}
|
|
2469
2860
|
|
|
2470
|
-
1. Run ${color("bun visualize", COLORS.green)} from your
|
|
2861
|
+
1. Run ${color("bun visualize", COLORS.green)} from your project root
|
|
2471
2862
|
2. Find generated ${color("docs/architecture.dsl", COLORS.blue)}
|
|
2472
2863
|
3. View with Structurizr Lite (see instructions after generation)
|
|
2473
2864
|
|
|
2474
2865
|
${color("What gets generated:", COLORS.blue)}
|
|
2475
2866
|
|
|
2476
|
-
• System Context diagram -
|
|
2477
|
-
• Container diagram -
|
|
2867
|
+
• System Context diagram - Your app + external systems
|
|
2868
|
+
• Container diagram - App contexts (background, content, server, client, etc.)
|
|
2478
2869
|
• Component diagrams - Internal components within contexts
|
|
2479
2870
|
• Dynamic diagrams - Message flows between contexts
|
|
2480
2871
|
|
|
@@ -2500,8 +2891,7 @@ function findTsConfig() {
|
|
|
2500
2891
|
function findProjectRoot() {
|
|
2501
2892
|
const locations = [process.cwd(), path6.join(process.cwd(), "..")];
|
|
2502
2893
|
for (const loc of locations) {
|
|
2503
|
-
|
|
2504
|
-
if (fs6.existsSync(manifestPath)) {
|
|
2894
|
+
if (fs6.existsSync(path6.join(loc, "manifest.json")) || fs6.existsSync(path6.join(loc, "package.json")) || fs6.existsSync(path6.join(loc, "tsconfig.json"))) {
|
|
2505
2895
|
return loc;
|
|
2506
2896
|
}
|
|
2507
2897
|
}
|
|
@@ -2519,4 +2909,4 @@ Stack trace:`, COLORS.gray));
|
|
|
2519
2909
|
process.exit(1);
|
|
2520
2910
|
});
|
|
2521
2911
|
|
|
2522
|
-
//# debugId=
|
|
2912
|
+
//# debugId=2DAA9097DE2E4CFB64756E2164756E21
|