@fairfox/polly 0.2.1 → 0.3.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.
@@ -42,9 +42,22 @@ class ProjectDetector {
42
42
  if (packageJson.dependencies?.electron || packageJson.devDependencies?.electron) {
43
43
  return this.detectElectron(packageJson);
44
44
  }
45
- if (packageJson.dependencies?.ws || packageJson.dependencies?.["socket.io"] || packageJson.dependencies?.elysia || packageJson.dependencies?.express) {
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
- "index.ts"
203
+ "server.js",
204
+ "index.ts",
205
+ "index.js"
186
206
  ];
187
- for (const candidate of serverCandidates) {
188
- const fullPath = path3.join(this.projectRoot, candidate);
189
- if (fs3.existsSync(fullPath)) {
190
- entryPoints.server = fullPath;
191
- break;
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/index.ts"
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
  }
@@ -1168,7 +1348,7 @@ class HandlerExtractor {
1168
1348
  const expression = node.getExpression();
1169
1349
  if (Node4.isPropertyAccessExpression(expression)) {
1170
1350
  const methodName = expression.getName();
1171
- if (methodName === "on") {
1351
+ if (methodName === "on" || methodName === "addEventListener") {
1172
1352
  const handler = this.extractHandler(node, context, filePath);
1173
1353
  if (handler) {
1174
1354
  handlers.push(handler);
@@ -1176,6 +1356,14 @@ class HandlerExtractor {
1176
1356
  }
1177
1357
  }
1178
1358
  }
1359
+ if (Node4.isSwitchStatement(node)) {
1360
+ const switchHandlers = this.extractSwitchCaseHandlers(node, context, filePath);
1361
+ handlers.push(...switchHandlers);
1362
+ }
1363
+ if (Node4.isVariableDeclaration(node)) {
1364
+ const mapHandlers = this.extractHandlerMapPattern(node, context, filePath);
1365
+ handlers.push(...mapHandlers);
1366
+ }
1179
1367
  });
1180
1368
  return handlers;
1181
1369
  }
@@ -1317,6 +1505,75 @@ class HandlerExtractor {
1317
1505
  }
1318
1506
  return;
1319
1507
  }
1508
+ extractSwitchCaseHandlers(switchNode, context, filePath) {
1509
+ const handlers = [];
1510
+ try {
1511
+ const expression = switchNode.getExpression();
1512
+ const expressionText = expression.getText();
1513
+ if (!/\.(type|kind|event|action)/.test(expressionText)) {
1514
+ return handlers;
1515
+ }
1516
+ const caseClauses = switchNode.getClauses();
1517
+ for (const clause of caseClauses) {
1518
+ if (Node4.isCaseClause(clause)) {
1519
+ const caseExpr = clause.getExpression();
1520
+ let messageType = null;
1521
+ if (Node4.isStringLiteral(caseExpr)) {
1522
+ messageType = caseExpr.getLiteralValue();
1523
+ }
1524
+ if (messageType) {
1525
+ const line = clause.getStartLineNumber();
1526
+ handlers.push({
1527
+ messageType,
1528
+ node: context,
1529
+ assignments: [],
1530
+ preconditions: [],
1531
+ postconditions: [],
1532
+ location: { file: filePath, line }
1533
+ });
1534
+ }
1535
+ }
1536
+ }
1537
+ } catch (error) {}
1538
+ return handlers;
1539
+ }
1540
+ extractHandlerMapPattern(varDecl, context, filePath) {
1541
+ const handlers = [];
1542
+ try {
1543
+ const initializer = varDecl.getInitializer();
1544
+ if (!initializer || !Node4.isObjectLiteralExpression(initializer)) {
1545
+ return handlers;
1546
+ }
1547
+ const varName = varDecl.getName().toLowerCase();
1548
+ if (!/(handler|listener|callback|event)s?/.test(varName)) {
1549
+ return handlers;
1550
+ }
1551
+ const properties = initializer.getProperties();
1552
+ for (const prop of properties) {
1553
+ if (Node4.isPropertyAssignment(prop)) {
1554
+ const nameNode = prop.getNameNode();
1555
+ let messageType = null;
1556
+ if (Node4.isStringLiteral(nameNode)) {
1557
+ messageType = nameNode.getLiteralValue();
1558
+ } else if (Node4.isIdentifier(nameNode)) {
1559
+ messageType = nameNode.getText();
1560
+ }
1561
+ if (messageType) {
1562
+ const line = prop.getStartLineNumber();
1563
+ handlers.push({
1564
+ messageType,
1565
+ node: context,
1566
+ assignments: [],
1567
+ preconditions: [],
1568
+ postconditions: [],
1569
+ location: { file: filePath, line }
1570
+ });
1571
+ }
1572
+ }
1573
+ }
1574
+ } catch (error) {}
1575
+ return handlers;
1576
+ }
1320
1577
  inferContext(filePath) {
1321
1578
  const path2 = filePath.toLowerCase();
1322
1579
  if (path2.includes("/background/") || path2.includes("\\background\\")) {
@@ -1337,6 +1594,15 @@ class HandlerExtractor {
1337
1594
  if (path2.includes("/offscreen/") || path2.includes("\\offscreen\\")) {
1338
1595
  return "offscreen";
1339
1596
  }
1597
+ if (path2.includes("/server/") || path2.includes("\\server\\") || path2.includes("/server.")) {
1598
+ return "server";
1599
+ }
1600
+ if (path2.includes("/client/") || path2.includes("\\client\\") || path2.includes("/client.")) {
1601
+ return "client";
1602
+ }
1603
+ if (path2.includes("/worker/") || path2.includes("\\worker\\") || path2.includes("service-worker")) {
1604
+ return "worker";
1605
+ }
1340
1606
  return "unknown";
1341
1607
  }
1342
1608
  }
@@ -1486,10 +1752,8 @@ class ArchitectureAnalyzer {
1486
1752
  let projectConfig;
1487
1753
  let entryPoints = {};
1488
1754
  let systemInfo;
1489
- const manifestPath = path4.join(this.options.projectRoot, "manifest.json");
1490
- const hasManifest = fs4.existsSync(manifestPath);
1491
- if (hasManifest && !this.options.useProjectDetector) {
1492
- const manifestParser = new ManifestParser(this.options.projectRoot);
1755
+ const manifestParser = new ManifestParser(this.options.projectRoot, true);
1756
+ if (manifestParser.hasManifest() && !this.options.useProjectDetector) {
1493
1757
  manifest = manifestParser.parse();
1494
1758
  entryPoints = manifestParser.getContextEntryPoints();
1495
1759
  systemInfo = {
@@ -2256,11 +2520,17 @@ async function generateCommand() {
2256
2520
  console.log(color(` Using: ${tsConfigPath}`, COLORS.gray));
2257
2521
  const projectRoot = findProjectRoot();
2258
2522
  if (!projectRoot) {
2259
- console.error(color("❌ Could not find manifest.json", COLORS.red));
2260
- console.error(" Run this command from your extension project root");
2523
+ console.error(color("❌ Could not find project root", COLORS.red));
2524
+ console.error(" Run this command from a directory with manifest.json, package.json, or tsconfig.json");
2261
2525
  process.exit(1);
2262
2526
  }
2263
2527
  console.log(color(` Project: ${projectRoot}`, COLORS.gray));
2528
+ const hasManifest = fs6.existsSync(path6.join(projectRoot, "manifest.json"));
2529
+ if (hasManifest) {
2530
+ console.log(color(` Type: Chrome Extension`, COLORS.gray));
2531
+ } else {
2532
+ console.log(color(` Type: Detecting from project structure...`, COLORS.gray));
2533
+ }
2264
2534
  const analysis = await analyzeArchitecture({
2265
2535
  tsConfigPath,
2266
2536
  projectRoot
@@ -2447,7 +2717,15 @@ async function serveCommand(args) {
2447
2717
  }
2448
2718
  function showHelp() {
2449
2719
  console.log(`
2450
- ${color("bun visualize", COLORS.blue)} - Architecture visualization for web extensions
2720
+ ${color("bun visualize", COLORS.blue)} - Architecture visualization tool
2721
+
2722
+ ${color("Supports:", COLORS.blue)}
2723
+
2724
+ • Chrome Extensions (manifest.json)
2725
+ • PWAs (public/manifest.json)
2726
+ • WebSocket/Server Apps (ws, socket.io, elysia)
2727
+ • Electron Apps
2728
+ • Generic TypeScript Projects
2451
2729
 
2452
2730
  ${color("Commands:", COLORS.blue)}
2453
2731
 
@@ -2467,14 +2745,14 @@ ${color("Commands:", COLORS.blue)}
2467
2745
 
2468
2746
  ${color("Getting Started:", COLORS.blue)}
2469
2747
 
2470
- 1. Run ${color("bun visualize", COLORS.green)} from your extension project root
2748
+ 1. Run ${color("bun visualize", COLORS.green)} from your project root
2471
2749
  2. Find generated ${color("docs/architecture.dsl", COLORS.blue)}
2472
2750
  3. View with Structurizr Lite (see instructions after generation)
2473
2751
 
2474
2752
  ${color("What gets generated:", COLORS.blue)}
2475
2753
 
2476
- • System Context diagram - Extension + external systems
2477
- • Container diagram - Extension contexts (background, content, popup, etc.)
2754
+ • System Context diagram - Your app + external systems
2755
+ • Container diagram - App contexts (background, content, server, client, etc.)
2478
2756
  • Component diagrams - Internal components within contexts
2479
2757
  • Dynamic diagrams - Message flows between contexts
2480
2758
 
@@ -2500,8 +2778,7 @@ function findTsConfig() {
2500
2778
  function findProjectRoot() {
2501
2779
  const locations = [process.cwd(), path6.join(process.cwd(), "..")];
2502
2780
  for (const loc of locations) {
2503
- const manifestPath = path6.join(loc, "manifest.json");
2504
- if (fs6.existsSync(manifestPath)) {
2781
+ if (fs6.existsSync(path6.join(loc, "manifest.json")) || fs6.existsSync(path6.join(loc, "package.json")) || fs6.existsSync(path6.join(loc, "tsconfig.json"))) {
2505
2782
  return loc;
2506
2783
  }
2507
2784
  }
@@ -2519,4 +2796,4 @@ Stack trace:`, COLORS.gray));
2519
2796
  process.exit(1);
2520
2797
  });
2521
2798
 
2522
- //# debugId=42A0D5368C5C715B64756E2164756E21
2799
+ //# debugId=37FBF573C9C57A4C64756E2164756E21