@fairfox/polly 0.1.1 → 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/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,770 @@
|
|
|
1
|
+
// Structurizr DSL generator
|
|
2
|
+
|
|
3
|
+
import type { ArchitectureAnalysis } from "@fairfox/polly-analysis";
|
|
4
|
+
|
|
5
|
+
export interface StructurizrDSLOptions {
|
|
6
|
+
/** Include dynamic diagrams for message flows */
|
|
7
|
+
includeDynamicDiagrams?: boolean;
|
|
8
|
+
|
|
9
|
+
/** Include component diagrams for contexts */
|
|
10
|
+
includeComponentDiagrams?: boolean;
|
|
11
|
+
|
|
12
|
+
/** Which contexts to generate component diagrams for */
|
|
13
|
+
componentDiagramContexts?: string[];
|
|
14
|
+
|
|
15
|
+
/** Custom theme URL */
|
|
16
|
+
theme?: string;
|
|
17
|
+
|
|
18
|
+
/** Custom styles */
|
|
19
|
+
styles?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class StructurizrDSLGenerator {
|
|
23
|
+
private analysis: ArchitectureAnalysis;
|
|
24
|
+
private options: StructurizrDSLOptions;
|
|
25
|
+
|
|
26
|
+
constructor(analysis: ArchitectureAnalysis, options: StructurizrDSLOptions = {}) {
|
|
27
|
+
this.analysis = analysis;
|
|
28
|
+
this.options = {
|
|
29
|
+
includeDynamicDiagrams: true,
|
|
30
|
+
includeComponentDiagrams: true,
|
|
31
|
+
componentDiagramContexts: ["background"],
|
|
32
|
+
theme: "https://static.structurizr.com/themes/default",
|
|
33
|
+
...options,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate complete Structurizr DSL
|
|
39
|
+
*/
|
|
40
|
+
generate(): string {
|
|
41
|
+
const parts: string[] = [];
|
|
42
|
+
|
|
43
|
+
parts.push(this.generateWorkspaceHeader());
|
|
44
|
+
parts.push(this.generateModel());
|
|
45
|
+
parts.push(this.generateViews());
|
|
46
|
+
parts.push(this.generateWorkspaceFooter());
|
|
47
|
+
|
|
48
|
+
return parts.join("\n\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate workspace header
|
|
53
|
+
*/
|
|
54
|
+
private generateWorkspaceHeader(): string {
|
|
55
|
+
const { name, description } = this.analysis.system;
|
|
56
|
+
|
|
57
|
+
const parts = [`workspace "${this.escape(name)}" "${this.escape(description || "")}" {`];
|
|
58
|
+
parts.push("");
|
|
59
|
+
parts.push(" !identifiers hierarchical");
|
|
60
|
+
|
|
61
|
+
// Add ADRs if present
|
|
62
|
+
if (this.analysis.adrs && this.analysis.adrs.adrs.length > 0) {
|
|
63
|
+
parts.push(" !adrs " + this.analysis.adrs.directory);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return parts.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generate workspace footer
|
|
71
|
+
*/
|
|
72
|
+
private generateWorkspaceFooter(): string {
|
|
73
|
+
return "}";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generate model section
|
|
78
|
+
*/
|
|
79
|
+
private generateModel(): string {
|
|
80
|
+
const parts: string[] = [];
|
|
81
|
+
|
|
82
|
+
parts.push(" model {");
|
|
83
|
+
parts.push(this.generatePeople());
|
|
84
|
+
parts.push(this.generateExternalSystems());
|
|
85
|
+
parts.push(this.generateMainSystem());
|
|
86
|
+
parts.push(" }");
|
|
87
|
+
|
|
88
|
+
return parts.join("\n\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Generate people/actors
|
|
93
|
+
*/
|
|
94
|
+
private generatePeople(): string {
|
|
95
|
+
return ` user = person "User" "Extension user"`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate external systems
|
|
100
|
+
*/
|
|
101
|
+
private generateExternalSystems(): string {
|
|
102
|
+
const parts: string[] = [];
|
|
103
|
+
|
|
104
|
+
for (const integration of this.analysis.integrations) {
|
|
105
|
+
if (integration.type === "api" || integration.type === "websocket") {
|
|
106
|
+
const tech =
|
|
107
|
+
integration.technology || (integration.type === "websocket" ? "WebSocket" : "REST API");
|
|
108
|
+
let desc = integration.description || "";
|
|
109
|
+
|
|
110
|
+
// Generate better description from API calls if available
|
|
111
|
+
if (!desc && integration.calls && integration.calls.length > 0) {
|
|
112
|
+
const endpoints = integration.calls
|
|
113
|
+
.slice(0, 3)
|
|
114
|
+
.map((c) => c.endpoint)
|
|
115
|
+
.join(", ");
|
|
116
|
+
const methods = [...new Set(integration.calls.map((c) => c.method))].join(", ");
|
|
117
|
+
desc = `External API with endpoints: ${endpoints}. Methods: ${methods}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
parts.push(
|
|
121
|
+
` ${this.toId(integration.name)} = softwareSystem "${this.escape(integration.name)}" "${this.escape(desc)}" {`
|
|
122
|
+
);
|
|
123
|
+
parts.push(
|
|
124
|
+
` tags "External System" "${integration.type === "websocket" ? "WebSocket" : "REST API"}"`
|
|
125
|
+
);
|
|
126
|
+
parts.push(` }`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return parts.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Generate main system (the extension)
|
|
135
|
+
*/
|
|
136
|
+
private generateMainSystem(): string {
|
|
137
|
+
const parts: string[] = [];
|
|
138
|
+
|
|
139
|
+
parts.push(` extension = softwareSystem "${this.escape(this.analysis.system.name)}" {`);
|
|
140
|
+
|
|
141
|
+
// Generate containers (contexts)
|
|
142
|
+
for (const [contextType, contextInfo] of Object.entries(this.analysis.contexts)) {
|
|
143
|
+
parts.push(this.generateContainer(contextType, contextInfo));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Generate relationships between containers
|
|
147
|
+
parts.push(this.generateContainerRelationships());
|
|
148
|
+
|
|
149
|
+
parts.push(" }");
|
|
150
|
+
|
|
151
|
+
return parts.join("\n\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generate container (context)
|
|
156
|
+
*/
|
|
157
|
+
private generateContainer(contextType: string, contextInfo: any): string {
|
|
158
|
+
const parts: string[] = [];
|
|
159
|
+
|
|
160
|
+
const technology = this.getContextTechnology(contextType);
|
|
161
|
+
const description = contextInfo.description || `${this.capitalize(contextType)} context`;
|
|
162
|
+
|
|
163
|
+
parts.push(
|
|
164
|
+
` ${contextType} = container "${this.capitalize(contextType)}" "${this.escape(description)}" "${technology}" {`
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Generate components if enabled
|
|
168
|
+
if (
|
|
169
|
+
this.options.includeComponentDiagrams &&
|
|
170
|
+
this.options.componentDiagramContexts?.includes(contextType)
|
|
171
|
+
) {
|
|
172
|
+
parts.push(this.generateComponents(contextType, contextInfo));
|
|
173
|
+
parts.push("");
|
|
174
|
+
parts.push(this.generateComponentRelationships(contextType, contextInfo));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
parts.push(" }");
|
|
178
|
+
|
|
179
|
+
return parts.join("\n");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Generate components within a container
|
|
184
|
+
*/
|
|
185
|
+
private generateComponents(contextType: string, contextInfo: any): string {
|
|
186
|
+
const parts: string[] = [];
|
|
187
|
+
|
|
188
|
+
// Generate components from handlers
|
|
189
|
+
const handlersByType = new Map<string, any[]>();
|
|
190
|
+
for (const handler of contextInfo.handlers) {
|
|
191
|
+
if (!handlersByType.has(handler.messageType)) {
|
|
192
|
+
handlersByType.set(handler.messageType, []);
|
|
193
|
+
}
|
|
194
|
+
handlersByType.get(handler.messageType)!.push(handler);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const [messageType, handlers] of handlersByType) {
|
|
198
|
+
const componentName = this.toComponentName(messageType);
|
|
199
|
+
const description = this.generateComponentDescription(messageType, handlers[0]);
|
|
200
|
+
const tags = this.getComponentTags(messageType, handlers[0]);
|
|
201
|
+
|
|
202
|
+
parts.push(
|
|
203
|
+
` ${this.toId(componentName)} = component "${componentName}" "${description}" {`
|
|
204
|
+
);
|
|
205
|
+
if (tags.length > 0) {
|
|
206
|
+
parts.push(` tags "${tags.join('" "')}"`);
|
|
207
|
+
}
|
|
208
|
+
parts.push(` }`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Generate components from UI components
|
|
212
|
+
if (contextInfo.components) {
|
|
213
|
+
for (const comp of contextInfo.components) {
|
|
214
|
+
parts.push(
|
|
215
|
+
` ${this.toId(comp.name)} = component "${comp.name}" "${this.escape(comp.description || "UI component")}" {`
|
|
216
|
+
);
|
|
217
|
+
parts.push(` tags "UI Component"`);
|
|
218
|
+
parts.push(` }`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Add Chrome API components if used
|
|
223
|
+
if (contextInfo.chromeAPIs && contextInfo.chromeAPIs.length > 0) {
|
|
224
|
+
for (const api of contextInfo.chromeAPIs) {
|
|
225
|
+
const apiId = this.toId(`chrome_${api}`);
|
|
226
|
+
parts.push(
|
|
227
|
+
` ${apiId} = component "Chrome ${this.capitalize(api)} API" "Browser API for ${api}" {`
|
|
228
|
+
);
|
|
229
|
+
parts.push(` tags "Chrome API" "External"`);
|
|
230
|
+
parts.push(` }`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return parts.join("\n");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Generate better component descriptions based on message type
|
|
239
|
+
*/
|
|
240
|
+
private generateComponentDescription(messageType: string, handler: any): string {
|
|
241
|
+
const type = messageType.toLowerCase();
|
|
242
|
+
|
|
243
|
+
// Authentication related
|
|
244
|
+
if (type.includes("login")) {
|
|
245
|
+
return "Authenticates users and establishes sessions";
|
|
246
|
+
}
|
|
247
|
+
if (type.includes("logout")) {
|
|
248
|
+
return "Terminates user sessions and clears credentials";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// CRUD operations
|
|
252
|
+
if (type.includes("add") || type.includes("create")) {
|
|
253
|
+
const entity = type.replace(/_(add|create)/, "").replace(/_/g, " ");
|
|
254
|
+
return `Creates new ${entity} items and persists to storage`;
|
|
255
|
+
}
|
|
256
|
+
if (type.includes("remove") || type.includes("delete")) {
|
|
257
|
+
const entity = type.replace(/_(remove|delete)/, "").replace(/_/g, " ");
|
|
258
|
+
return `Removes ${entity} items from storage`;
|
|
259
|
+
}
|
|
260
|
+
if (type.includes("update") || type.includes("toggle")) {
|
|
261
|
+
const entity = type.replace(/_(update|toggle)/, "").replace(/_/g, " ");
|
|
262
|
+
return `Updates ${entity} item state and syncs with storage`;
|
|
263
|
+
}
|
|
264
|
+
if (type.includes("clear")) {
|
|
265
|
+
const entity = type.replace(/_clear.*/, "").replace(/_/g, " ");
|
|
266
|
+
return `Clears all ${entity} items matching criteria`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Query operations
|
|
270
|
+
if (type.includes("get") || type.includes("fetch") || type.includes("load")) {
|
|
271
|
+
const entity = type.replace(/_(get|fetch|load)/, "").replace(/_/g, " ");
|
|
272
|
+
return `Retrieves ${entity} data from storage`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Default
|
|
276
|
+
return `Processes ${messageType} messages and coordinates business logic`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Determine appropriate tags for a component
|
|
281
|
+
*/
|
|
282
|
+
private getComponentTags(messageType: string, handler: any): string[] {
|
|
283
|
+
const tags: string[] = ["Message Handler"];
|
|
284
|
+
const type = messageType.toLowerCase();
|
|
285
|
+
|
|
286
|
+
// Add functional tags
|
|
287
|
+
if (type.includes("login") || type.includes("logout") || type.includes("auth")) {
|
|
288
|
+
tags.push("Authentication");
|
|
289
|
+
} else if (
|
|
290
|
+
type.includes("add") ||
|
|
291
|
+
type.includes("create") ||
|
|
292
|
+
type.includes("update") ||
|
|
293
|
+
type.includes("delete") ||
|
|
294
|
+
type.includes("remove") ||
|
|
295
|
+
type.includes("toggle")
|
|
296
|
+
) {
|
|
297
|
+
tags.push("CRUD");
|
|
298
|
+
} else if (type.includes("get") || type.includes("fetch") || type.includes("load")) {
|
|
299
|
+
tags.push("Query");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Add domain tags
|
|
303
|
+
if (type.includes("user")) {
|
|
304
|
+
tags.push("User Management");
|
|
305
|
+
}
|
|
306
|
+
if (type.includes("todo")) {
|
|
307
|
+
tags.push("Todo Management");
|
|
308
|
+
}
|
|
309
|
+
if (type.includes("state")) {
|
|
310
|
+
tags.push("State Management");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return tags;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Generate relationships between components within a container
|
|
318
|
+
*/
|
|
319
|
+
private generateComponentRelationships(contextType: string, contextInfo: any): string {
|
|
320
|
+
const parts: string[] = [];
|
|
321
|
+
|
|
322
|
+
// Build a map of handler components
|
|
323
|
+
const handlersByType = new Map<string, any[]>();
|
|
324
|
+
for (const handler of contextInfo.handlers) {
|
|
325
|
+
if (!handlersByType.has(handler.messageType)) {
|
|
326
|
+
handlersByType.set(handler.messageType, []);
|
|
327
|
+
}
|
|
328
|
+
handlersByType.get(handler.messageType)!.push(handler);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Add relationships to Chrome APIs
|
|
332
|
+
if (contextInfo.chromeAPIs && contextInfo.chromeAPIs.length > 0) {
|
|
333
|
+
for (const api of contextInfo.chromeAPIs) {
|
|
334
|
+
const apiId = this.toId(`chrome_${api}`);
|
|
335
|
+
|
|
336
|
+
// Find handlers that use this API
|
|
337
|
+
for (const [messageType, handlers] of handlersByType) {
|
|
338
|
+
const componentId = this.toId(this.toComponentName(messageType));
|
|
339
|
+
|
|
340
|
+
// Infer relationship based on API
|
|
341
|
+
let description = `Uses ${api}`;
|
|
342
|
+
if (api === "storage") {
|
|
343
|
+
if (
|
|
344
|
+
messageType.toLowerCase().includes("get") ||
|
|
345
|
+
messageType.toLowerCase().includes("load")
|
|
346
|
+
) {
|
|
347
|
+
description = "Reads from storage";
|
|
348
|
+
} else {
|
|
349
|
+
description = "Writes to storage";
|
|
350
|
+
}
|
|
351
|
+
} else if (api === "tabs") {
|
|
352
|
+
description = "Manages browser tabs";
|
|
353
|
+
} else if (api === "runtime") {
|
|
354
|
+
description = "Sends messages";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
parts.push(` ${componentId} -> ${apiId} "${description}"`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Add state management relationships (handlers that modify state)
|
|
363
|
+
const stateHandlers: string[] = [];
|
|
364
|
+
const queryHandlers: string[] = [];
|
|
365
|
+
|
|
366
|
+
for (const [messageType, handlers] of handlersByType) {
|
|
367
|
+
const type = messageType.toLowerCase();
|
|
368
|
+
const componentId = this.toId(this.toComponentName(messageType));
|
|
369
|
+
|
|
370
|
+
if (
|
|
371
|
+
type.includes("add") ||
|
|
372
|
+
type.includes("create") ||
|
|
373
|
+
type.includes("update") ||
|
|
374
|
+
type.includes("delete") ||
|
|
375
|
+
type.includes("remove") ||
|
|
376
|
+
type.includes("toggle") ||
|
|
377
|
+
type.includes("clear") ||
|
|
378
|
+
type.includes("login") ||
|
|
379
|
+
type.includes("logout")
|
|
380
|
+
) {
|
|
381
|
+
stateHandlers.push(componentId);
|
|
382
|
+
} else if (type.includes("get") || type.includes("fetch") || type.includes("load")) {
|
|
383
|
+
queryHandlers.push(componentId);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Create implicit state manager if we have state operations
|
|
388
|
+
if (stateHandlers.length > 0 && queryHandlers.length > 0) {
|
|
389
|
+
// Query handlers depend on state set by mutation handlers
|
|
390
|
+
for (const queryHandler of queryHandlers) {
|
|
391
|
+
for (const stateHandler of stateHandlers) {
|
|
392
|
+
if (queryHandler !== stateHandler) {
|
|
393
|
+
parts.push(` ${stateHandler} -> ${queryHandler} "Updates state" {`);
|
|
394
|
+
parts.push(` tags "Implicit"`);
|
|
395
|
+
parts.push(` }`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return parts.join("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Generate relationships between containers
|
|
406
|
+
*/
|
|
407
|
+
private generateContainerRelationships(): string {
|
|
408
|
+
const parts: string[] = [];
|
|
409
|
+
|
|
410
|
+
// Add user relationships
|
|
411
|
+
const uiContexts = ["popup", "options", "devtools"];
|
|
412
|
+
for (const contextType of Object.keys(this.analysis.contexts)) {
|
|
413
|
+
if (uiContexts.includes(contextType)) {
|
|
414
|
+
parts.push(` user -> extension.${contextType} "Uses"`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Add message flow relationships
|
|
419
|
+
for (const flow of this.analysis.messageFlows) {
|
|
420
|
+
const tech = `Sends ${flow.messageType}`;
|
|
421
|
+
for (const to of flow.to) {
|
|
422
|
+
parts.push(` extension.${flow.from} -> extension.${to} "${tech}"`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Add external API relationships
|
|
427
|
+
for (const integration of this.analysis.integrations) {
|
|
428
|
+
if (integration.type === "api" || integration.type === "websocket") {
|
|
429
|
+
// Find which contexts use this integration
|
|
430
|
+
for (const [contextType, contextInfo] of Object.entries(this.analysis.contexts)) {
|
|
431
|
+
const usesIntegration = contextInfo.externalAPIs.some((api: any) =>
|
|
432
|
+
integration.calls?.some((call) => call.endpoint === api.endpoint)
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
if (usesIntegration) {
|
|
436
|
+
const method = integration.type === "websocket" ? "Connects to" : "Calls";
|
|
437
|
+
parts.push(
|
|
438
|
+
` extension.${contextType} -> ${this.toId(integration.name)} "${method}"`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return parts.join("\n");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Generate views section
|
|
450
|
+
*/
|
|
451
|
+
private generateViews(): string {
|
|
452
|
+
const parts: string[] = [];
|
|
453
|
+
|
|
454
|
+
parts.push(" views {");
|
|
455
|
+
parts.push(this.generateSystemContextView());
|
|
456
|
+
parts.push(this.generateContainerView());
|
|
457
|
+
|
|
458
|
+
if (this.options.includeComponentDiagrams) {
|
|
459
|
+
for (const contextType of this.options.componentDiagramContexts || []) {
|
|
460
|
+
if (this.analysis.contexts[contextType]) {
|
|
461
|
+
parts.push(this.generateComponentView(contextType));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (this.options.includeDynamicDiagrams) {
|
|
467
|
+
parts.push(this.generateDynamicViews());
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
parts.push(this.generateStyles());
|
|
471
|
+
parts.push(" }");
|
|
472
|
+
|
|
473
|
+
// Add documentation section if ADRs exist
|
|
474
|
+
if (this.analysis.adrs && this.analysis.adrs.adrs.length > 0) {
|
|
475
|
+
parts.push("");
|
|
476
|
+
parts.push(this.generateDocumentation());
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return parts.join("\n\n");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Generate documentation section for ADRs
|
|
484
|
+
*/
|
|
485
|
+
private generateDocumentation(): string {
|
|
486
|
+
if (!this.analysis.adrs || this.analysis.adrs.adrs.length === 0) {
|
|
487
|
+
return "";
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const parts: string[] = [];
|
|
491
|
+
parts.push(" documentation {");
|
|
492
|
+
|
|
493
|
+
for (const adr of this.analysis.adrs.adrs) {
|
|
494
|
+
parts.push(` decision "${adr.id}" {`);
|
|
495
|
+
parts.push(` title "${this.escape(adr.title)}"`);
|
|
496
|
+
parts.push(` status "${adr.status}"`);
|
|
497
|
+
parts.push(` date "${adr.date}"`);
|
|
498
|
+
parts.push(` content "${this.escape(adr.context)}"`);
|
|
499
|
+
parts.push(" }");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
parts.push(" }");
|
|
503
|
+
|
|
504
|
+
return parts.join("\n");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Generate system context view
|
|
509
|
+
*/
|
|
510
|
+
private generateSystemContextView(): string {
|
|
511
|
+
return ` systemContext extension "SystemContext" {
|
|
512
|
+
include *
|
|
513
|
+
autoLayout lr
|
|
514
|
+
}`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Generate container view
|
|
519
|
+
*/
|
|
520
|
+
private generateContainerView(): string {
|
|
521
|
+
return ` container extension "Containers" {
|
|
522
|
+
include *
|
|
523
|
+
autoLayout lr
|
|
524
|
+
}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Generate component view
|
|
529
|
+
*/
|
|
530
|
+
private generateComponentView(contextType: string): string {
|
|
531
|
+
return ` component extension.${contextType} "Components_${this.capitalize(contextType)}" {
|
|
532
|
+
include *
|
|
533
|
+
autoLayout tb
|
|
534
|
+
}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Generate dynamic views for message flows
|
|
539
|
+
*/
|
|
540
|
+
private generateDynamicViews(): string {
|
|
541
|
+
const parts: string[] = [];
|
|
542
|
+
|
|
543
|
+
// Group flows by domain/feature
|
|
544
|
+
const flowsByDomain = new Map<string, any[]>();
|
|
545
|
+
|
|
546
|
+
for (const flow of this.analysis.messageFlows) {
|
|
547
|
+
// Extract domain from message type (e.g., USER_LOGIN -> user, TODO_ADD -> todo)
|
|
548
|
+
const messageType = flow.messageType.toLowerCase();
|
|
549
|
+
let domain = "general";
|
|
550
|
+
|
|
551
|
+
if (
|
|
552
|
+
messageType.includes("user") ||
|
|
553
|
+
messageType.includes("login") ||
|
|
554
|
+
messageType.includes("logout") ||
|
|
555
|
+
messageType.includes("auth")
|
|
556
|
+
) {
|
|
557
|
+
domain = "authentication";
|
|
558
|
+
} else if (messageType.includes("todo")) {
|
|
559
|
+
domain = "todo";
|
|
560
|
+
} else if (messageType.includes("state")) {
|
|
561
|
+
domain = "state";
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!flowsByDomain.has(domain)) {
|
|
565
|
+
flowsByDomain.set(domain, []);
|
|
566
|
+
}
|
|
567
|
+
flowsByDomain.get(domain)!.push(flow);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Generate a dynamic view for each domain
|
|
571
|
+
let count = 0;
|
|
572
|
+
for (const [domain, flows] of flowsByDomain) {
|
|
573
|
+
if (count >= 5) break; // Limit to avoid too many diagrams
|
|
574
|
+
|
|
575
|
+
const viewName = this.capitalize(domain) + " Flow";
|
|
576
|
+
parts.push(this.generateDynamicView(viewName, flows, domain));
|
|
577
|
+
count++;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return parts.join("\n\n");
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Generate single dynamic view
|
|
585
|
+
*/
|
|
586
|
+
private generateDynamicView(flowName: string, flows: any[], domain: string): string {
|
|
587
|
+
const parts: string[] = [];
|
|
588
|
+
|
|
589
|
+
// Create a user-centric description
|
|
590
|
+
const description = this.getDynamicViewDescription(domain);
|
|
591
|
+
parts.push(` dynamic extension "${flowName}" "${description}" {`);
|
|
592
|
+
|
|
593
|
+
// Add user interaction if this is a UI flow
|
|
594
|
+
const uiContexts = ["popup", "options", "devtools"];
|
|
595
|
+
const hasUIFlow = flows.some((f) => uiContexts.includes(f.from));
|
|
596
|
+
|
|
597
|
+
if (hasUIFlow) {
|
|
598
|
+
// Start with user interaction
|
|
599
|
+
const firstFlow = flows.find((f) => uiContexts.includes(f.from));
|
|
600
|
+
if (firstFlow) {
|
|
601
|
+
const action = this.getUserAction(domain);
|
|
602
|
+
parts.push(` user -> extension.${firstFlow.from} "${action}"`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Generate message flows
|
|
607
|
+
for (const flow of flows) {
|
|
608
|
+
const messageDesc = this.getMessageDescription(flow.messageType);
|
|
609
|
+
|
|
610
|
+
for (const to of flow.to) {
|
|
611
|
+
parts.push(` extension.${flow.from} -> extension.${to} "${messageDesc}"`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
parts.push(" autoLayout lr");
|
|
616
|
+
parts.push(" }");
|
|
617
|
+
|
|
618
|
+
return parts.join("\n");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Get description for dynamic view based on domain
|
|
623
|
+
*/
|
|
624
|
+
private getDynamicViewDescription(domain: string): string {
|
|
625
|
+
const descriptions: Record<string, string> = {
|
|
626
|
+
authentication: "User authentication and session management",
|
|
627
|
+
todo: "Todo item creation, updates, and retrieval",
|
|
628
|
+
state: "Application state synchronization",
|
|
629
|
+
general: "Message flow through the system",
|
|
630
|
+
};
|
|
631
|
+
return descriptions[domain] || descriptions.general;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Get user action description for domain
|
|
636
|
+
*/
|
|
637
|
+
private getUserAction(domain: string): string {
|
|
638
|
+
const actions: Record<string, string> = {
|
|
639
|
+
authentication: "Initiates login",
|
|
640
|
+
todo: "Manages todo items",
|
|
641
|
+
state: "Requests state",
|
|
642
|
+
general: "Interacts",
|
|
643
|
+
};
|
|
644
|
+
return actions[domain] || actions.general;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Get message description based on type
|
|
649
|
+
*/
|
|
650
|
+
private getMessageDescription(messageType: string): string {
|
|
651
|
+
const type = messageType.toLowerCase();
|
|
652
|
+
|
|
653
|
+
if (type.includes("login")) return "Authenticate user";
|
|
654
|
+
if (type.includes("logout")) return "End session";
|
|
655
|
+
if (type.includes("add") || type.includes("create")) return "Create item";
|
|
656
|
+
if (type.includes("remove") || type.includes("delete")) return "Delete item";
|
|
657
|
+
if (type.includes("update") || type.includes("toggle")) return "Update item";
|
|
658
|
+
if (type.includes("get") || type.includes("fetch")) return "Retrieve data";
|
|
659
|
+
if (type.includes("clear")) return "Clear items";
|
|
660
|
+
|
|
661
|
+
return messageType;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Generate styles
|
|
666
|
+
*/
|
|
667
|
+
private generateStyles(): string {
|
|
668
|
+
const parts: string[] = [];
|
|
669
|
+
|
|
670
|
+
// Skip theme directive - causes issues with Structurizr CLI export
|
|
671
|
+
// The inline styles are sufficient for diagram generation
|
|
672
|
+
parts.push(" styles {");
|
|
673
|
+
|
|
674
|
+
// Default styles for containers (contexts)
|
|
675
|
+
const contextStyles: Record<string, string> = {
|
|
676
|
+
background: "#2E7D32",
|
|
677
|
+
content: "#F57C00",
|
|
678
|
+
popup: "#1976D2",
|
|
679
|
+
devtools: "#7B1FA2",
|
|
680
|
+
options: "#0288D1",
|
|
681
|
+
...this.options.styles,
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
for (const [context, color] of Object.entries(contextStyles)) {
|
|
685
|
+
if (this.analysis.contexts[context]) {
|
|
686
|
+
parts.push(` element "extension.${context}" {`);
|
|
687
|
+
parts.push(` background ${color}`);
|
|
688
|
+
parts.push(` color #ffffff`);
|
|
689
|
+
parts.push(" }");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
parts.push(" }");
|
|
694
|
+
|
|
695
|
+
return parts.join("\n");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Get technology label for context
|
|
700
|
+
*/
|
|
701
|
+
private getContextTechnology(contextType: string): string {
|
|
702
|
+
const technologies: Record<string, string> = {
|
|
703
|
+
background: "Service Worker / Background Script",
|
|
704
|
+
content: "Content Script",
|
|
705
|
+
popup: "Browser Action Popup",
|
|
706
|
+
devtools: "DevTools Panel",
|
|
707
|
+
options: "Options Page",
|
|
708
|
+
offscreen: "Offscreen Document",
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
return technologies[contextType] || "Extension Context";
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Convert message type to component name
|
|
716
|
+
*/
|
|
717
|
+
private toComponentName(messageType: string): string {
|
|
718
|
+
return (
|
|
719
|
+
messageType
|
|
720
|
+
.split("_")
|
|
721
|
+
.map((part) => this.capitalize(part.toLowerCase()))
|
|
722
|
+
.join(" ") + " Handler"
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Convert name to identifier
|
|
728
|
+
*/
|
|
729
|
+
private toId(name: string): string {
|
|
730
|
+
return name
|
|
731
|
+
.toLowerCase()
|
|
732
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
733
|
+
.replace(/^_|_$/g, "");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Convert flow name to view name
|
|
738
|
+
*/
|
|
739
|
+
private toViewName(flowName: string): string {
|
|
740
|
+
return flowName
|
|
741
|
+
.split(/[_-]/)
|
|
742
|
+
.map((part) => this.capitalize(part))
|
|
743
|
+
.join(" ");
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Capitalize first letter
|
|
748
|
+
*/
|
|
749
|
+
private capitalize(str: string): string {
|
|
750
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Escape string for DSL
|
|
755
|
+
*/
|
|
756
|
+
private escape(str: string): string {
|
|
757
|
+
return str.replace(/"/g, '\\"');
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Generate Structurizr DSL from architecture analysis
|
|
763
|
+
*/
|
|
764
|
+
export function generateStructurizrDSL(
|
|
765
|
+
analysis: ArchitectureAnalysis,
|
|
766
|
+
options?: StructurizrDSLOptions
|
|
767
|
+
): string {
|
|
768
|
+
const generator = new StructurizrDSLGenerator(analysis, options);
|
|
769
|
+
return generator.generate();
|
|
770
|
+
}
|