@fairfox/polly 0.1.0 → 0.1.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 +9 -9
- package/cli/polly.ts +9 -3
- package/package.json +2 -2
- package/vendor/analysis/src/extract/adr.ts +212 -0
- package/vendor/analysis/src/extract/architecture.ts +160 -0
- package/vendor/analysis/src/extract/contexts.ts +298 -0
- package/vendor/analysis/src/extract/flows.ts +309 -0
- package/vendor/analysis/src/extract/handlers.ts +321 -0
- package/vendor/analysis/src/extract/index.ts +9 -0
- package/vendor/analysis/src/extract/integrations.ts +329 -0
- package/vendor/analysis/src/extract/manifest.ts +298 -0
- package/vendor/analysis/src/extract/types.ts +389 -0
- package/vendor/analysis/src/index.ts +7 -0
- package/vendor/analysis/src/types/adr.ts +53 -0
- package/vendor/analysis/src/types/architecture.ts +245 -0
- package/vendor/analysis/src/types/core.ts +210 -0
- package/vendor/analysis/src/types/index.ts +18 -0
- package/vendor/verify/src/adapters/base.ts +164 -0
- package/vendor/verify/src/adapters/detection.ts +281 -0
- package/vendor/verify/src/adapters/event-bus/index.ts +480 -0
- package/vendor/verify/src/adapters/web-extension/index.ts +508 -0
- package/vendor/verify/src/adapters/websocket/index.ts +486 -0
- package/vendor/verify/src/cli.ts +430 -0
- package/vendor/verify/src/codegen/config.ts +354 -0
- package/vendor/verify/src/codegen/tla.ts +719 -0
- package/vendor/verify/src/config/parser.ts +303 -0
- package/vendor/verify/src/config/types.ts +113 -0
- package/vendor/verify/src/core/model.ts +267 -0
- package/vendor/verify/src/core/primitives.ts +106 -0
- package/vendor/verify/src/extract/handlers.ts +2 -0
- package/vendor/verify/src/extract/types.ts +2 -0
- package/vendor/verify/src/index.ts +150 -0
- package/vendor/verify/src/primitives/index.ts +102 -0
- package/vendor/verify/src/runner/docker.ts +283 -0
- package/vendor/verify/src/types.ts +51 -0
- package/vendor/visualize/src/cli.ts +365 -0
- package/vendor/visualize/src/codegen/structurizr.ts +770 -0
- package/vendor/visualize/src/index.ts +13 -0
- package/vendor/visualize/src/runner/export.ts +235 -0
- package/vendor/visualize/src/viewer/server.ts +485 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// Handler extraction from TypeScript code
|
|
2
|
+
// Extracts message handlers and their state mutations
|
|
3
|
+
|
|
4
|
+
import { Project, type SourceFile, SyntaxKind, Node } from "ts-morph";
|
|
5
|
+
import type { MessageHandler, StateAssignment, VerificationCondition } from "../types";
|
|
6
|
+
|
|
7
|
+
export interface HandlerAnalysis {
|
|
8
|
+
handlers: MessageHandler[];
|
|
9
|
+
messageTypes: Set<string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class HandlerExtractor {
|
|
13
|
+
private project: Project;
|
|
14
|
+
|
|
15
|
+
constructor(tsConfigPath: string) {
|
|
16
|
+
this.project = new Project({
|
|
17
|
+
tsConfigFilePath: tsConfigPath,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract all message handlers from the codebase
|
|
23
|
+
*/
|
|
24
|
+
extractHandlers(): HandlerAnalysis {
|
|
25
|
+
const handlers: MessageHandler[] = [];
|
|
26
|
+
const messageTypes = new Set<string>();
|
|
27
|
+
|
|
28
|
+
// Find all source files
|
|
29
|
+
const sourceFiles = this.project.getSourceFiles();
|
|
30
|
+
|
|
31
|
+
for (const sourceFile of sourceFiles) {
|
|
32
|
+
const fileHandlers = this.extractFromFile(sourceFile);
|
|
33
|
+
handlers.push(...fileHandlers);
|
|
34
|
+
|
|
35
|
+
for (const handler of fileHandlers) {
|
|
36
|
+
messageTypes.add(handler.messageType);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
handlers,
|
|
42
|
+
messageTypes,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract handlers from a single source file
|
|
48
|
+
*/
|
|
49
|
+
private extractFromFile(sourceFile: SourceFile): MessageHandler[] {
|
|
50
|
+
const handlers: MessageHandler[] = [];
|
|
51
|
+
const filePath = sourceFile.getFilePath();
|
|
52
|
+
|
|
53
|
+
// Determine context from file path
|
|
54
|
+
const context = this.inferContext(filePath);
|
|
55
|
+
|
|
56
|
+
// Find all .on() call expressions
|
|
57
|
+
sourceFile.forEachDescendant((node) => {
|
|
58
|
+
if (Node.isCallExpression(node)) {
|
|
59
|
+
const expression = node.getExpression();
|
|
60
|
+
|
|
61
|
+
// Check if this is a .on() call
|
|
62
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
63
|
+
const methodName = expression.getName();
|
|
64
|
+
|
|
65
|
+
if (methodName === "on") {
|
|
66
|
+
const handler = this.extractHandler(node, context, filePath);
|
|
67
|
+
if (handler) {
|
|
68
|
+
handlers.push(handler);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return handlers;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Extract handler details from a .on() call expression
|
|
80
|
+
*/
|
|
81
|
+
private extractHandler(callExpr: any, context: string, filePath: string): MessageHandler | null {
|
|
82
|
+
const args = callExpr.getArguments();
|
|
83
|
+
|
|
84
|
+
if (args.length < 2) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// First argument should be the message type (string literal)
|
|
89
|
+
const messageTypeArg = args[0];
|
|
90
|
+
let messageType: string | null = null;
|
|
91
|
+
|
|
92
|
+
if (Node.isStringLiteral(messageTypeArg)) {
|
|
93
|
+
messageType = messageTypeArg.getLiteralValue();
|
|
94
|
+
} else if (Node.isTemplateExpression(messageTypeArg)) {
|
|
95
|
+
// Handle template literals if needed
|
|
96
|
+
messageType = messageTypeArg.getText().replace(/[`'"]/g, "");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!messageType) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Second argument is the handler function
|
|
104
|
+
const handlerArg = args[1];
|
|
105
|
+
const assignments: StateAssignment[] = [];
|
|
106
|
+
const preconditions: VerificationCondition[] = [];
|
|
107
|
+
const postconditions: VerificationCondition[] = [];
|
|
108
|
+
|
|
109
|
+
// Parse the handler function for state assignments and verification conditions
|
|
110
|
+
if (Node.isArrowFunction(handlerArg) || Node.isFunctionExpression(handlerArg)) {
|
|
111
|
+
this.extractAssignments(handlerArg, assignments);
|
|
112
|
+
this.extractVerificationConditions(handlerArg, preconditions, postconditions);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const line = callExpr.getStartLineNumber();
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
messageType,
|
|
119
|
+
node: context, // Renamed from 'context' to 'node' for generalization
|
|
120
|
+
assignments,
|
|
121
|
+
preconditions,
|
|
122
|
+
postconditions,
|
|
123
|
+
location: {
|
|
124
|
+
file: filePath,
|
|
125
|
+
line,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extract state assignments from a handler function
|
|
132
|
+
*/
|
|
133
|
+
private extractAssignments(funcNode: any, assignments: StateAssignment[]): void {
|
|
134
|
+
funcNode.forEachDescendant((node: any) => {
|
|
135
|
+
// Look for assignment expressions: state.field = value
|
|
136
|
+
if (Node.isBinaryExpression(node)) {
|
|
137
|
+
const operator = node.getOperatorToken().getText();
|
|
138
|
+
|
|
139
|
+
if (operator === "=") {
|
|
140
|
+
const left = node.getLeft();
|
|
141
|
+
const right = node.getRight();
|
|
142
|
+
|
|
143
|
+
// Check if left side is a state property access
|
|
144
|
+
if (Node.isPropertyAccessExpression(left)) {
|
|
145
|
+
const fieldPath = this.getPropertyPath(left);
|
|
146
|
+
|
|
147
|
+
// Check if this is a state access
|
|
148
|
+
if (fieldPath.startsWith("state.")) {
|
|
149
|
+
const field = fieldPath.substring(6); // Remove "state." prefix
|
|
150
|
+
const value = this.extractValue(right);
|
|
151
|
+
|
|
152
|
+
if (value !== undefined) {
|
|
153
|
+
assignments.push({
|
|
154
|
+
field,
|
|
155
|
+
value,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract verification conditions (requires/ensures) from a handler function
|
|
167
|
+
*/
|
|
168
|
+
private extractVerificationConditions(
|
|
169
|
+
funcNode: any,
|
|
170
|
+
preconditions: VerificationCondition[],
|
|
171
|
+
postconditions: VerificationCondition[]
|
|
172
|
+
): void {
|
|
173
|
+
const body = funcNode.getBody();
|
|
174
|
+
|
|
175
|
+
// Get all statements in the function body
|
|
176
|
+
const statements = Node.isBlock(body) ? body.getStatements() : [body];
|
|
177
|
+
|
|
178
|
+
statements.forEach((statement: any, index: number) => {
|
|
179
|
+
// Look for expression statements that are function calls
|
|
180
|
+
if (Node.isExpressionStatement(statement)) {
|
|
181
|
+
const expr = statement.getExpression();
|
|
182
|
+
|
|
183
|
+
if (Node.isCallExpression(expr)) {
|
|
184
|
+
const callee = expr.getExpression();
|
|
185
|
+
|
|
186
|
+
if (Node.isIdentifier(callee)) {
|
|
187
|
+
const functionName = callee.getText();
|
|
188
|
+
|
|
189
|
+
if (functionName === "requires") {
|
|
190
|
+
// Extract precondition
|
|
191
|
+
const condition = this.extractCondition(expr);
|
|
192
|
+
if (condition) {
|
|
193
|
+
preconditions.push(condition);
|
|
194
|
+
}
|
|
195
|
+
} else if (functionName === "ensures") {
|
|
196
|
+
// Extract postcondition
|
|
197
|
+
const condition = this.extractCondition(expr);
|
|
198
|
+
if (condition) {
|
|
199
|
+
postconditions.push(condition);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Extract condition from a requires() or ensures() call
|
|
210
|
+
*/
|
|
211
|
+
private extractCondition(callExpr: any): VerificationCondition | null {
|
|
212
|
+
const args = callExpr.getArguments();
|
|
213
|
+
|
|
214
|
+
if (args.length === 0) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// First argument is the condition expression
|
|
219
|
+
const conditionArg = args[0];
|
|
220
|
+
const expression = conditionArg.getText();
|
|
221
|
+
|
|
222
|
+
// Second argument (optional) is the message
|
|
223
|
+
let message: string | undefined;
|
|
224
|
+
if (args.length >= 2 && Node.isStringLiteral(args[1])) {
|
|
225
|
+
message = args[1].getLiteralValue();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const line = callExpr.getStartLineNumber();
|
|
229
|
+
const column = callExpr.getStartLinePos();
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
expression,
|
|
233
|
+
message,
|
|
234
|
+
location: {
|
|
235
|
+
line,
|
|
236
|
+
column,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get the full property access path (e.g., "state.user.loggedIn")
|
|
243
|
+
*/
|
|
244
|
+
private getPropertyPath(node: any): string {
|
|
245
|
+
const parts: string[] = [];
|
|
246
|
+
|
|
247
|
+
let current = node;
|
|
248
|
+
while (Node.isPropertyAccessExpression(current)) {
|
|
249
|
+
parts.unshift(current.getName());
|
|
250
|
+
current = current.getExpression();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Add the base identifier
|
|
254
|
+
if (Node.isIdentifier(current)) {
|
|
255
|
+
parts.unshift(current.getText());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return parts.join(".");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Extract a literal value from an expression
|
|
263
|
+
*/
|
|
264
|
+
private extractValue(node: any): string | boolean | number | null | undefined {
|
|
265
|
+
if (Node.isStringLiteral(node)) {
|
|
266
|
+
return node.getLiteralValue();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (Node.isNumericLiteral(node)) {
|
|
270
|
+
return node.getLiteralValue();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (node.getKind() === SyntaxKind.TrueKeyword) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (node.getKind() === SyntaxKind.FalseKeyword) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (node.getKind() === SyntaxKind.NullKeyword) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// For complex expressions, return undefined (can't extract)
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Infer the context (background, content, popup, etc.) from file path
|
|
291
|
+
*/
|
|
292
|
+
private inferContext(filePath: string): string {
|
|
293
|
+
const path = filePath.toLowerCase();
|
|
294
|
+
|
|
295
|
+
if (path.includes("/background/") || path.includes("\\background\\")) {
|
|
296
|
+
return "background";
|
|
297
|
+
}
|
|
298
|
+
if (path.includes("/content/") || path.includes("\\content\\")) {
|
|
299
|
+
return "content";
|
|
300
|
+
}
|
|
301
|
+
if (path.includes("/popup/") || path.includes("\\popup\\")) {
|
|
302
|
+
return "popup";
|
|
303
|
+
}
|
|
304
|
+
if (path.includes("/devtools/") || path.includes("\\devtools\\")) {
|
|
305
|
+
return "devtools";
|
|
306
|
+
}
|
|
307
|
+
if (path.includes("/options/") || path.includes("\\options\\")) {
|
|
308
|
+
return "options";
|
|
309
|
+
}
|
|
310
|
+
if (path.includes("/offscreen/") || path.includes("\\offscreen\\")) {
|
|
311
|
+
return "offscreen";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return "unknown";
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function extractHandlers(tsConfigPath: string): HandlerAnalysis {
|
|
319
|
+
const extractor = new HandlerExtractor(tsConfigPath);
|
|
320
|
+
return extractor.extractHandlers();
|
|
321
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Export all extraction utilities
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export * from "./handlers";
|
|
4
|
+
export * from "./manifest";
|
|
5
|
+
export * from "./contexts";
|
|
6
|
+
export * from "./flows";
|
|
7
|
+
export * from "./integrations";
|
|
8
|
+
export * from "./adr";
|
|
9
|
+
export * from "./architecture";
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// External integration detection - find external APIs, services, etc.
|
|
2
|
+
|
|
3
|
+
import { Project, Node } from "ts-morph";
|
|
4
|
+
import type { ExternalIntegration, ExternalAPICall } from "../types/architecture";
|
|
5
|
+
|
|
6
|
+
export class IntegrationAnalyzer {
|
|
7
|
+
private project: Project;
|
|
8
|
+
|
|
9
|
+
constructor(tsConfigPath: string) {
|
|
10
|
+
this.project = new Project({
|
|
11
|
+
tsConfigFilePath: tsConfigPath,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Analyze external integrations in the codebase
|
|
17
|
+
*/
|
|
18
|
+
analyzeIntegrations(): ExternalIntegration[] {
|
|
19
|
+
const integrations = new Map<string, ExternalIntegration>();
|
|
20
|
+
|
|
21
|
+
// Find all fetch() calls
|
|
22
|
+
const fetchCalls = this.findFetchCalls();
|
|
23
|
+
for (const call of fetchCalls) {
|
|
24
|
+
this.addOrMergeIntegration(integrations, this.createAPIIntegration(call));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Find WebSocket connections
|
|
28
|
+
const websockets = this.findWebSockets();
|
|
29
|
+
for (const ws of websockets) {
|
|
30
|
+
this.addOrMergeIntegration(integrations, this.createWebSocketIntegration(ws));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Find external script imports
|
|
34
|
+
const externalScripts = this.findExternalScripts();
|
|
35
|
+
for (const script of externalScripts) {
|
|
36
|
+
this.addOrMergeIntegration(integrations, script);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Array.from(integrations.values());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Find all fetch() API calls
|
|
44
|
+
*/
|
|
45
|
+
private findFetchCalls(): Array<{
|
|
46
|
+
url: string;
|
|
47
|
+
method: string;
|
|
48
|
+
file: string;
|
|
49
|
+
line: number;
|
|
50
|
+
description?: string;
|
|
51
|
+
}> {
|
|
52
|
+
const calls: Array<{
|
|
53
|
+
url: string;
|
|
54
|
+
method: string;
|
|
55
|
+
file: string;
|
|
56
|
+
line: number;
|
|
57
|
+
description?: string;
|
|
58
|
+
}> = [];
|
|
59
|
+
|
|
60
|
+
for (const sourceFile of this.project.getSourceFiles()) {
|
|
61
|
+
sourceFile.forEachDescendant((node) => {
|
|
62
|
+
if (Node.isCallExpression(node)) {
|
|
63
|
+
const expression = node.getExpression();
|
|
64
|
+
|
|
65
|
+
// Check for fetch() calls
|
|
66
|
+
if (Node.isIdentifier(expression) && expression.getText() === "fetch") {
|
|
67
|
+
const args = node.getArguments();
|
|
68
|
+
if (args.length > 0) {
|
|
69
|
+
// Extract URL
|
|
70
|
+
const urlArg = args[0];
|
|
71
|
+
let url: string | null = null;
|
|
72
|
+
|
|
73
|
+
if (Node.isStringLiteral(urlArg)) {
|
|
74
|
+
url = urlArg.getLiteralValue();
|
|
75
|
+
} else if (Node.isTemplateExpression(urlArg)) {
|
|
76
|
+
// Try to extract base URL from template
|
|
77
|
+
url = this.extractBaseURL(urlArg.getText());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (url) {
|
|
81
|
+
// Extract method
|
|
82
|
+
let method = "GET"; // Default
|
|
83
|
+
if (args.length > 1 && Node.isObjectLiteralExpression(args[1])) {
|
|
84
|
+
const options = args[1];
|
|
85
|
+
const methodProp = options.getProperty("method");
|
|
86
|
+
if (methodProp && Node.isPropertyAssignment(methodProp)) {
|
|
87
|
+
const initializer = methodProp.getInitializer();
|
|
88
|
+
if (initializer && Node.isStringLiteral(initializer)) {
|
|
89
|
+
method = initializer.getLiteralValue().toUpperCase();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Extract description from JSDoc
|
|
95
|
+
const description = this.extractJSDocDescription(node);
|
|
96
|
+
|
|
97
|
+
calls.push({
|
|
98
|
+
url,
|
|
99
|
+
method,
|
|
100
|
+
file: sourceFile.getFilePath(),
|
|
101
|
+
line: node.getStartLineNumber(),
|
|
102
|
+
description,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return calls;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Find WebSocket connections
|
|
116
|
+
*/
|
|
117
|
+
private findWebSockets(): Array<{
|
|
118
|
+
url: string;
|
|
119
|
+
file: string;
|
|
120
|
+
line: number;
|
|
121
|
+
description?: string;
|
|
122
|
+
}> {
|
|
123
|
+
const websockets: Array<{
|
|
124
|
+
url: string;
|
|
125
|
+
file: string;
|
|
126
|
+
line: number;
|
|
127
|
+
description?: string;
|
|
128
|
+
}> = [];
|
|
129
|
+
|
|
130
|
+
for (const sourceFile of this.project.getSourceFiles()) {
|
|
131
|
+
sourceFile.forEachDescendant((node) => {
|
|
132
|
+
if (Node.isNewExpression(node)) {
|
|
133
|
+
const expression = node.getExpression();
|
|
134
|
+
|
|
135
|
+
// Check for new WebSocket()
|
|
136
|
+
if (Node.isIdentifier(expression) && expression.getText() === "WebSocket") {
|
|
137
|
+
const args = node.getArguments();
|
|
138
|
+
if (args.length > 0 && Node.isStringLiteral(args[0])) {
|
|
139
|
+
const url = args[0].getLiteralValue();
|
|
140
|
+
const description = this.extractJSDocDescription(node);
|
|
141
|
+
|
|
142
|
+
websockets.push({
|
|
143
|
+
url,
|
|
144
|
+
file: sourceFile.getFilePath(),
|
|
145
|
+
line: node.getStartLineNumber(),
|
|
146
|
+
description,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return websockets;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Find external script dependencies (from imports)
|
|
159
|
+
*/
|
|
160
|
+
private findExternalScripts(): ExternalIntegration[] {
|
|
161
|
+
const scripts: ExternalIntegration[] = [];
|
|
162
|
+
const seen = new Set<string>();
|
|
163
|
+
|
|
164
|
+
for (const sourceFile of this.project.getSourceFiles()) {
|
|
165
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
166
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
167
|
+
|
|
168
|
+
// Only consider external packages (not relative imports)
|
|
169
|
+
if (!moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
|
|
170
|
+
// Extract package name (handle scoped packages)
|
|
171
|
+
const packageName = moduleSpecifier.startsWith("@")
|
|
172
|
+
? moduleSpecifier.split("/").slice(0, 2).join("/")
|
|
173
|
+
: moduleSpecifier.split("/")[0];
|
|
174
|
+
|
|
175
|
+
if (!seen.has(packageName)) {
|
|
176
|
+
seen.add(packageName);
|
|
177
|
+
|
|
178
|
+
scripts.push({
|
|
179
|
+
type: "external-script",
|
|
180
|
+
name: packageName,
|
|
181
|
+
technology: "npm package",
|
|
182
|
+
usedIn: [sourceFile.getFilePath()],
|
|
183
|
+
description: `External dependency: ${packageName}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return scripts;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create API integration from fetch call
|
|
195
|
+
*/
|
|
196
|
+
private createAPIIntegration(call: {
|
|
197
|
+
url: string;
|
|
198
|
+
method: string;
|
|
199
|
+
file: string;
|
|
200
|
+
line: number;
|
|
201
|
+
description?: string;
|
|
202
|
+
}): ExternalIntegration {
|
|
203
|
+
// Extract base URL
|
|
204
|
+
const baseURL = this.extractBaseURL(call.url);
|
|
205
|
+
|
|
206
|
+
// Infer name from URL
|
|
207
|
+
const name = this.inferAPIName(baseURL);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
type: "api",
|
|
211
|
+
name,
|
|
212
|
+
technology: "REST API",
|
|
213
|
+
url: baseURL,
|
|
214
|
+
usedIn: [call.file],
|
|
215
|
+
description: call.description || `External API: ${name}`,
|
|
216
|
+
calls: [
|
|
217
|
+
{
|
|
218
|
+
method: call.method,
|
|
219
|
+
endpoint: call.url,
|
|
220
|
+
location: {
|
|
221
|
+
file: call.file,
|
|
222
|
+
line: call.line,
|
|
223
|
+
},
|
|
224
|
+
description: call.description,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create WebSocket integration
|
|
232
|
+
*/
|
|
233
|
+
private createWebSocketIntegration(ws: {
|
|
234
|
+
url: string;
|
|
235
|
+
file: string;
|
|
236
|
+
line: number;
|
|
237
|
+
description?: string;
|
|
238
|
+
}): ExternalIntegration {
|
|
239
|
+
const name = this.inferAPIName(ws.url);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
type: "websocket",
|
|
243
|
+
name,
|
|
244
|
+
technology: "WebSocket",
|
|
245
|
+
url: ws.url,
|
|
246
|
+
usedIn: [ws.file],
|
|
247
|
+
description: ws.description || `WebSocket connection: ${name}`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Extract base URL from full URL or template
|
|
253
|
+
*/
|
|
254
|
+
private extractBaseURL(url: string): string {
|
|
255
|
+
// Remove template parts
|
|
256
|
+
url = url.replace(/\$\{[^}]+\}/g, "");
|
|
257
|
+
url = url.replace(/`/g, "");
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const parsed = new URL(url);
|
|
261
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
262
|
+
} catch {
|
|
263
|
+
// If parsing fails, try to extract domain
|
|
264
|
+
const match = url.match(/https?:\/\/([^/]+)/);
|
|
265
|
+
return match ? match[0] : url;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Infer API name from URL
|
|
271
|
+
*/
|
|
272
|
+
private inferAPIName(url: string): string {
|
|
273
|
+
try {
|
|
274
|
+
const parsed = new URL(url);
|
|
275
|
+
const hostname = parsed.hostname;
|
|
276
|
+
|
|
277
|
+
// Remove www. prefix
|
|
278
|
+
const cleanHost = hostname.replace(/^www\./, "");
|
|
279
|
+
|
|
280
|
+
// Take first part of domain
|
|
281
|
+
const parts = cleanHost.split(".");
|
|
282
|
+
if (parts.length > 0) {
|
|
283
|
+
// Capitalize first letter
|
|
284
|
+
return parts[0].charAt(0).toUpperCase() + parts[0].slice(1) + " API";
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// Fallback
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return "External API";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Add or merge integration into map
|
|
295
|
+
*/
|
|
296
|
+
private addOrMergeIntegration(
|
|
297
|
+
map: Map<string, ExternalIntegration>,
|
|
298
|
+
integration: ExternalIntegration
|
|
299
|
+
): void {
|
|
300
|
+
const key = `${integration.type}:${integration.name}`;
|
|
301
|
+
|
|
302
|
+
if (map.has(key)) {
|
|
303
|
+
const existing = map.get(key)!;
|
|
304
|
+
|
|
305
|
+
// Merge usedIn
|
|
306
|
+
existing.usedIn = [...new Set([...existing.usedIn, ...integration.usedIn])];
|
|
307
|
+
|
|
308
|
+
// Merge calls
|
|
309
|
+
if (integration.calls && existing.calls) {
|
|
310
|
+
existing.calls.push(...integration.calls);
|
|
311
|
+
} else if (integration.calls) {
|
|
312
|
+
existing.calls = integration.calls;
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
map.set(key, integration);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Extract JSDoc description from node
|
|
321
|
+
*/
|
|
322
|
+
private extractJSDocDescription(node: any): string | undefined {
|
|
323
|
+
const jsDocs = node.getJsDocs?.() || [];
|
|
324
|
+
if (jsDocs.length === 0) return undefined;
|
|
325
|
+
|
|
326
|
+
const comment = jsDocs[0].getDescription().trim();
|
|
327
|
+
return comment || undefined;
|
|
328
|
+
}
|
|
329
|
+
}
|