@fairfox/polly 0.1.3 → 0.1.4
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 → dist/cli/polly.js} +100 -206
- package/dist/cli/polly.js.map +10 -0
- package/dist/scripts/build-extension.js +137 -0
- package/dist/scripts/build-extension.js.map +10 -0
- package/dist/vendor/verify/src/cli.js +2089 -0
- package/dist/vendor/verify/src/cli.js.map +16 -0
- package/dist/vendor/visualize/src/cli.js +2204 -0
- package/dist/vendor/visualize/src/cli.js.map +19 -0
- package/package.json +12 -12
- package/vendor/analysis/src/extract/adr.ts +0 -212
- package/vendor/analysis/src/extract/architecture.ts +0 -160
- package/vendor/analysis/src/extract/contexts.ts +0 -298
- package/vendor/analysis/src/extract/flows.ts +0 -309
- package/vendor/analysis/src/extract/handlers.ts +0 -321
- package/vendor/analysis/src/extract/index.ts +0 -9
- package/vendor/analysis/src/extract/integrations.ts +0 -329
- package/vendor/analysis/src/extract/manifest.ts +0 -298
- package/vendor/analysis/src/extract/types.ts +0 -389
- package/vendor/analysis/src/index.ts +0 -7
- package/vendor/analysis/src/types/adr.ts +0 -53
- package/vendor/analysis/src/types/architecture.ts +0 -245
- package/vendor/analysis/src/types/core.ts +0 -210
- package/vendor/analysis/src/types/index.ts +0 -18
- package/vendor/verify/src/adapters/base.ts +0 -164
- package/vendor/verify/src/adapters/detection.ts +0 -281
- package/vendor/verify/src/adapters/event-bus/index.ts +0 -480
- package/vendor/verify/src/adapters/web-extension/index.ts +0 -508
- package/vendor/verify/src/adapters/websocket/index.ts +0 -486
- package/vendor/verify/src/cli.ts +0 -430
- package/vendor/verify/src/codegen/config.ts +0 -354
- package/vendor/verify/src/codegen/tla.ts +0 -719
- package/vendor/verify/src/config/parser.ts +0 -303
- package/vendor/verify/src/config/types.ts +0 -113
- package/vendor/verify/src/core/model.ts +0 -267
- package/vendor/verify/src/core/primitives.ts +0 -106
- package/vendor/verify/src/extract/handlers.ts +0 -2
- package/vendor/verify/src/extract/types.ts +0 -2
- package/vendor/verify/src/index.ts +0 -150
- package/vendor/verify/src/primitives/index.ts +0 -102
- package/vendor/verify/src/runner/docker.ts +0 -283
- package/vendor/verify/src/types.ts +0 -51
- package/vendor/visualize/src/cli.ts +0 -365
- package/vendor/visualize/src/codegen/structurizr.ts +0 -770
- package/vendor/visualize/src/index.ts +0 -13
- package/vendor/visualize/src/runner/export.ts +0 -235
- package/vendor/visualize/src/viewer/server.ts +0 -485
- /package/dist/{background → src/background}/index.js +0 -0
- /package/dist/{background → src/background}/index.js.map +0 -0
- /package/dist/{background → src/background}/message-router.js +0 -0
- /package/dist/{background → src/background}/message-router.js.map +0 -0
- /package/dist/{index.js → src/index.js} +0 -0
- /package/dist/{index.js.map → src/index.js.map} +0 -0
- /package/dist/{shared → src/shared}/adapters/index.js +0 -0
- /package/dist/{shared → src/shared}/adapters/index.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/context-helpers.js +0 -0
- /package/dist/{shared → src/shared}/lib/context-helpers.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/errors.js +0 -0
- /package/dist/{shared → src/shared}/lib/errors.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/message-bus.js +0 -0
- /package/dist/{shared → src/shared}/lib/message-bus.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/state.js +0 -0
- /package/dist/{shared → src/shared}/lib/state.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/test-helpers.js +0 -0
- /package/dist/{shared → src/shared}/lib/test-helpers.js.map +0 -0
- /package/dist/{shared → src/shared}/state/app-state.js +0 -0
- /package/dist/{shared → src/shared}/state/app-state.js.map +0 -0
- /package/dist/{shared → src/shared}/types/messages.js +0 -0
- /package/dist/{shared → src/shared}/types/messages.js.map +0 -0
|
@@ -0,0 +1,2204 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
+
for (let key of __getOwnPropNames(mod))
|
|
12
|
+
if (!__hasOwnProp.call(to, key))
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: () => mod[key],
|
|
15
|
+
enumerable: true
|
|
16
|
+
});
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, {
|
|
22
|
+
get: all[name],
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
set: (newValue) => all[name] = () => newValue
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
29
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
30
|
+
|
|
31
|
+
// vendor/visualize/src/cli.ts
|
|
32
|
+
import * as fs5 from "node:fs";
|
|
33
|
+
import * as path5 from "node:path";
|
|
34
|
+
|
|
35
|
+
// vendor/analysis/src/extract/architecture.ts
|
|
36
|
+
import * as fs3 from "node:fs";
|
|
37
|
+
import * as path3 from "node:path";
|
|
38
|
+
|
|
39
|
+
// vendor/analysis/src/extract/manifest.ts
|
|
40
|
+
import * as fs from "node:fs";
|
|
41
|
+
import * as path from "node:path";
|
|
42
|
+
|
|
43
|
+
class ManifestParser {
|
|
44
|
+
manifestPath;
|
|
45
|
+
manifestData;
|
|
46
|
+
baseDir;
|
|
47
|
+
constructor(projectRoot) {
|
|
48
|
+
this.baseDir = projectRoot;
|
|
49
|
+
this.manifestPath = path.join(projectRoot, "manifest.json");
|
|
50
|
+
if (!fs.existsSync(this.manifestPath)) {
|
|
51
|
+
throw new Error(`manifest.json not found at ${this.manifestPath}`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(this.manifestPath, "utf-8");
|
|
55
|
+
this.manifestData = JSON.parse(content);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(`Failed to parse manifest.json: ${error}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
parse() {
|
|
61
|
+
const manifest = this.manifestData;
|
|
62
|
+
return {
|
|
63
|
+
name: manifest.name || "Unknown Extension",
|
|
64
|
+
version: manifest.version || "0.0.0",
|
|
65
|
+
description: manifest.description,
|
|
66
|
+
manifestVersion: manifest.manifest_version || 2,
|
|
67
|
+
background: this.parseBackground(),
|
|
68
|
+
contentScripts: this.parseContentScripts(),
|
|
69
|
+
popup: this.parsePopup(),
|
|
70
|
+
options: this.parseOptions(),
|
|
71
|
+
devtools: this.parseDevtools(),
|
|
72
|
+
permissions: manifest.permissions || [],
|
|
73
|
+
hostPermissions: manifest.host_permissions || []
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
getContextEntryPoints() {
|
|
77
|
+
const entryPoints = {};
|
|
78
|
+
const background = this.parseBackground();
|
|
79
|
+
if (background) {
|
|
80
|
+
const entryFile = background.files[0];
|
|
81
|
+
if (entryFile) {
|
|
82
|
+
entryPoints.background = this.findSourceFile(entryFile);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const contentScripts = this.parseContentScripts();
|
|
86
|
+
if (contentScripts && contentScripts.length > 0) {
|
|
87
|
+
const firstScript = contentScripts[0].js[0];
|
|
88
|
+
if (firstScript) {
|
|
89
|
+
entryPoints.content = this.findSourceFile(firstScript);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const popup = this.parsePopup();
|
|
93
|
+
if (popup) {
|
|
94
|
+
const htmlPath = path.join(this.baseDir, popup.html);
|
|
95
|
+
const jsPath = this.findAssociatedJS(htmlPath);
|
|
96
|
+
if (jsPath) {
|
|
97
|
+
entryPoints.popup = jsPath;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const options = this.parseOptions();
|
|
101
|
+
if (options) {
|
|
102
|
+
const htmlPath = path.join(this.baseDir, options.page);
|
|
103
|
+
const jsPath = this.findAssociatedJS(htmlPath);
|
|
104
|
+
if (jsPath) {
|
|
105
|
+
entryPoints.options = jsPath;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const devtools = this.parseDevtools();
|
|
109
|
+
if (devtools) {
|
|
110
|
+
const htmlPath = path.join(this.baseDir, devtools.page);
|
|
111
|
+
const jsPath = this.findAssociatedJS(htmlPath);
|
|
112
|
+
if (jsPath) {
|
|
113
|
+
entryPoints.devtools = jsPath;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return entryPoints;
|
|
117
|
+
}
|
|
118
|
+
parseBackground() {
|
|
119
|
+
const bg = this.manifestData.background;
|
|
120
|
+
if (!bg)
|
|
121
|
+
return;
|
|
122
|
+
if (bg.service_worker) {
|
|
123
|
+
return {
|
|
124
|
+
type: "service_worker",
|
|
125
|
+
files: [bg.service_worker]
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (bg.scripts) {
|
|
129
|
+
return {
|
|
130
|
+
type: "script",
|
|
131
|
+
files: bg.scripts
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (bg.page) {
|
|
135
|
+
return {
|
|
136
|
+
type: "script",
|
|
137
|
+
files: [bg.page]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
parseContentScripts() {
|
|
143
|
+
const cs = this.manifestData.content_scripts;
|
|
144
|
+
if (!cs || !Array.isArray(cs))
|
|
145
|
+
return;
|
|
146
|
+
return cs.map((script) => ({
|
|
147
|
+
matches: script.matches || [],
|
|
148
|
+
js: script.js || [],
|
|
149
|
+
css: script.css
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
parsePopup() {
|
|
153
|
+
const action = this.manifestData.action || this.manifestData.browser_action;
|
|
154
|
+
if (!action)
|
|
155
|
+
return;
|
|
156
|
+
if (action.default_popup) {
|
|
157
|
+
return {
|
|
158
|
+
html: action.default_popup,
|
|
159
|
+
default: true
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
parseOptions() {
|
|
165
|
+
const options = this.manifestData.options_ui || this.manifestData.options_page;
|
|
166
|
+
if (!options)
|
|
167
|
+
return;
|
|
168
|
+
if (typeof options === "string") {
|
|
169
|
+
return {
|
|
170
|
+
page: options,
|
|
171
|
+
openInTab: false
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
page: options.page,
|
|
176
|
+
openInTab: options.open_in_tab
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
parseDevtools() {
|
|
180
|
+
const devtools = this.manifestData.devtools_page;
|
|
181
|
+
if (!devtools)
|
|
182
|
+
return;
|
|
183
|
+
return {
|
|
184
|
+
page: devtools
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
findSourceFile(manifestPath) {
|
|
188
|
+
const candidates = [
|
|
189
|
+
path.join(this.baseDir, manifestPath),
|
|
190
|
+
path.join(this.baseDir, manifestPath.replace(/\.js$/, ".ts")),
|
|
191
|
+
path.join(this.baseDir, "src", manifestPath),
|
|
192
|
+
path.join(this.baseDir, "src", manifestPath.replace(/\.js$/, ".ts")),
|
|
193
|
+
path.join(this.baseDir, "src", manifestPath.replace(/\.js$/, ".tsx"))
|
|
194
|
+
];
|
|
195
|
+
for (const candidate of candidates) {
|
|
196
|
+
if (fs.existsSync(candidate)) {
|
|
197
|
+
return candidate;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return path.join(this.baseDir, manifestPath);
|
|
201
|
+
}
|
|
202
|
+
findAssociatedJS(htmlPath) {
|
|
203
|
+
if (!fs.existsSync(htmlPath)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const html = fs.readFileSync(htmlPath, "utf-8");
|
|
207
|
+
const scriptMatch = html.match(/<script[^>]+src=["']([^"']+)["']/i);
|
|
208
|
+
if (scriptMatch && scriptMatch[1]) {
|
|
209
|
+
const scriptPath = scriptMatch[1];
|
|
210
|
+
const fullPath = path.resolve(path.dirname(htmlPath), scriptPath);
|
|
211
|
+
if (fs.existsSync(fullPath))
|
|
212
|
+
return fullPath;
|
|
213
|
+
if (fs.existsSync(fullPath.replace(/\.js$/, ".ts"))) {
|
|
214
|
+
return fullPath.replace(/\.js$/, ".ts");
|
|
215
|
+
}
|
|
216
|
+
if (fs.existsSync(fullPath.replace(/\.js$/, ".tsx"))) {
|
|
217
|
+
return fullPath.replace(/\.js$/, ".tsx");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const baseName = path.basename(htmlPath, ".html");
|
|
221
|
+
const dir = path.dirname(htmlPath);
|
|
222
|
+
const candidates = [
|
|
223
|
+
path.join(dir, `${baseName}.ts`),
|
|
224
|
+
path.join(dir, `${baseName}.tsx`),
|
|
225
|
+
path.join(dir, `${baseName}.js`),
|
|
226
|
+
path.join(dir, "index.ts"),
|
|
227
|
+
path.join(dir, "index.tsx"),
|
|
228
|
+
path.join(dir, "index.js")
|
|
229
|
+
];
|
|
230
|
+
for (const candidate of candidates) {
|
|
231
|
+
if (fs.existsSync(candidate)) {
|
|
232
|
+
return candidate;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// vendor/analysis/src/extract/contexts.ts
|
|
240
|
+
import { Project, Node } from "ts-morph";
|
|
241
|
+
|
|
242
|
+
class ContextAnalyzer {
|
|
243
|
+
project;
|
|
244
|
+
constructor(tsConfigPath) {
|
|
245
|
+
this.project = new Project({
|
|
246
|
+
tsConfigFilePath: tsConfigPath
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
analyzeContext(contextType, entryPoint, handlers) {
|
|
250
|
+
const sourceFile = this.project.getSourceFile(entryPoint);
|
|
251
|
+
if (!sourceFile) {
|
|
252
|
+
throw new Error(`Could not find source file: ${entryPoint}`);
|
|
253
|
+
}
|
|
254
|
+
const chromeAPIs = this.extractChromeAPIs(sourceFile);
|
|
255
|
+
const dependencies = this.extractDependencies(sourceFile);
|
|
256
|
+
const description = this.extractDescription(sourceFile);
|
|
257
|
+
const components = this.isUIContext(contextType) ? this.extractComponents(sourceFile) : undefined;
|
|
258
|
+
const contextHandlers = handlers.filter((h) => h.node === contextType);
|
|
259
|
+
return {
|
|
260
|
+
type: contextType,
|
|
261
|
+
entryPoint,
|
|
262
|
+
handlers: contextHandlers,
|
|
263
|
+
chromeAPIs,
|
|
264
|
+
externalAPIs: [],
|
|
265
|
+
components,
|
|
266
|
+
dependencies,
|
|
267
|
+
description
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
extractChromeAPIs(sourceFile) {
|
|
271
|
+
const apis = new Set;
|
|
272
|
+
sourceFile.forEachDescendant((node) => {
|
|
273
|
+
if (Node.isPropertyAccessExpression(node)) {
|
|
274
|
+
const text = node.getText();
|
|
275
|
+
if (text.startsWith("chrome.")) {
|
|
276
|
+
const match = text.match(/^chrome\.([^.(]+(?:\.[^.(]+)?)/);
|
|
277
|
+
if (match) {
|
|
278
|
+
apis.add(match[1]);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (text.startsWith("browser.")) {
|
|
282
|
+
const match = text.match(/^browser\.([^.(]+(?:\.[^.(]+)?)/);
|
|
283
|
+
if (match) {
|
|
284
|
+
apis.add(match[1]);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (text.includes("bus.adapters.")) {
|
|
288
|
+
const match = text.match(/bus\.adapters\.([^.(]+)/);
|
|
289
|
+
if (match) {
|
|
290
|
+
apis.add(match[1]);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
return Array.from(apis).sort();
|
|
296
|
+
}
|
|
297
|
+
extractDependencies(sourceFile) {
|
|
298
|
+
const deps = [];
|
|
299
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
300
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
301
|
+
deps.push(moduleSpecifier);
|
|
302
|
+
}
|
|
303
|
+
return deps;
|
|
304
|
+
}
|
|
305
|
+
extractDescription(sourceFile) {
|
|
306
|
+
const firstStatement = sourceFile.getStatements()[0];
|
|
307
|
+
if (!firstStatement)
|
|
308
|
+
return;
|
|
309
|
+
const leadingComments = firstStatement.getLeadingCommentRanges();
|
|
310
|
+
if (leadingComments.length === 0)
|
|
311
|
+
return;
|
|
312
|
+
const comment = leadingComments[0].getText();
|
|
313
|
+
const descMatch = comment.match(/@description\s+(.+?)(?:\n|$)/s);
|
|
314
|
+
if (descMatch) {
|
|
315
|
+
return descMatch[1].trim();
|
|
316
|
+
}
|
|
317
|
+
const lines = comment.split(`
|
|
318
|
+
`).map((l) => l.replace(/^[\s*]+/, "").trim()).filter((l) => l && !l.startsWith("@"));
|
|
319
|
+
return lines[0] || undefined;
|
|
320
|
+
}
|
|
321
|
+
extractComponents(sourceFile) {
|
|
322
|
+
const components = [];
|
|
323
|
+
sourceFile.forEachDescendant((node) => {
|
|
324
|
+
if (Node.isFunctionDeclaration(node)) {
|
|
325
|
+
const name = node.getName();
|
|
326
|
+
if (name && this.looksLikeComponent(name, node)) {
|
|
327
|
+
components.push({
|
|
328
|
+
name,
|
|
329
|
+
type: "function",
|
|
330
|
+
filePath: sourceFile.getFilePath(),
|
|
331
|
+
line: node.getStartLineNumber(),
|
|
332
|
+
props: this.extractProps(node),
|
|
333
|
+
description: this.extractJSDocDescription(node)
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (Node.isVariableDeclaration(node)) {
|
|
338
|
+
const name = node.getName();
|
|
339
|
+
const initializer = node.getInitializer();
|
|
340
|
+
if (name && initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))) {
|
|
341
|
+
if (this.looksLikeComponent(name, initializer)) {
|
|
342
|
+
components.push({
|
|
343
|
+
name,
|
|
344
|
+
type: "function",
|
|
345
|
+
filePath: sourceFile.getFilePath(),
|
|
346
|
+
line: node.getStartLineNumber(),
|
|
347
|
+
props: this.extractProps(initializer),
|
|
348
|
+
description: this.extractJSDocDescription(node)
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (Node.isClassDeclaration(node)) {
|
|
354
|
+
const name = node.getName();
|
|
355
|
+
if (name && this.looksLikeClassComponent(node)) {
|
|
356
|
+
components.push({
|
|
357
|
+
name,
|
|
358
|
+
type: "class",
|
|
359
|
+
filePath: sourceFile.getFilePath(),
|
|
360
|
+
line: node.getStartLineNumber(),
|
|
361
|
+
props: this.extractPropsFromClass(node),
|
|
362
|
+
description: this.extractJSDocDescription(node)
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
return components;
|
|
368
|
+
}
|
|
369
|
+
looksLikeComponent(name, node) {
|
|
370
|
+
if (!/^[A-Z]/.test(name))
|
|
371
|
+
return false;
|
|
372
|
+
const body = node.getBody();
|
|
373
|
+
if (!body)
|
|
374
|
+
return false;
|
|
375
|
+
let hasJSX = false;
|
|
376
|
+
if (Node.isBlock(body)) {
|
|
377
|
+
body.forEachDescendant((child) => {
|
|
378
|
+
if (Node.isJsxElement(child) || Node.isJsxSelfClosingElement(child)) {
|
|
379
|
+
hasJSX = true;
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
} else {
|
|
383
|
+
if (Node.isJsxElement(body) || Node.isJsxSelfClosingElement(body)) {
|
|
384
|
+
hasJSX = true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return hasJSX;
|
|
388
|
+
}
|
|
389
|
+
looksLikeClassComponent(node) {
|
|
390
|
+
const extendedTypes = node.getExtends();
|
|
391
|
+
if (!extendedTypes)
|
|
392
|
+
return false;
|
|
393
|
+
const extendsText = extendedTypes.getText();
|
|
394
|
+
return /Component|PureComponent/.test(extendsText);
|
|
395
|
+
}
|
|
396
|
+
extractProps(node) {
|
|
397
|
+
const params = node.getParameters();
|
|
398
|
+
if (params.length === 0)
|
|
399
|
+
return [];
|
|
400
|
+
const propsParam = params[0];
|
|
401
|
+
const type = propsParam.getType();
|
|
402
|
+
const props = [];
|
|
403
|
+
for (const prop of type.getProperties()) {
|
|
404
|
+
props.push(prop.getName());
|
|
405
|
+
}
|
|
406
|
+
return props;
|
|
407
|
+
}
|
|
408
|
+
extractPropsFromClass(node) {
|
|
409
|
+
const extendedTypes = node.getExtends();
|
|
410
|
+
if (!extendedTypes)
|
|
411
|
+
return [];
|
|
412
|
+
const typeArgs = extendedTypes.getType().getTypeArguments();
|
|
413
|
+
if (typeArgs.length === 0)
|
|
414
|
+
return [];
|
|
415
|
+
const propsType = typeArgs[0];
|
|
416
|
+
const props = [];
|
|
417
|
+
for (const prop of propsType.getProperties()) {
|
|
418
|
+
props.push(prop.getName());
|
|
419
|
+
}
|
|
420
|
+
return props;
|
|
421
|
+
}
|
|
422
|
+
extractJSDocDescription(node) {
|
|
423
|
+
const jsDocs = node.getJsDocs();
|
|
424
|
+
if (jsDocs.length === 0)
|
|
425
|
+
return;
|
|
426
|
+
const description = jsDocs[0].getDescription().trim();
|
|
427
|
+
return description || undefined;
|
|
428
|
+
}
|
|
429
|
+
isUIContext(contextType) {
|
|
430
|
+
return ["popup", "options", "devtools"].includes(contextType);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// vendor/analysis/src/extract/flows.ts
|
|
435
|
+
import { Project as Project2, Node as Node2 } from "ts-morph";
|
|
436
|
+
|
|
437
|
+
class FlowAnalyzer {
|
|
438
|
+
project;
|
|
439
|
+
handlers;
|
|
440
|
+
constructor(tsConfigPath, handlers) {
|
|
441
|
+
this.project = new Project2({
|
|
442
|
+
tsConfigFilePath: tsConfigPath
|
|
443
|
+
});
|
|
444
|
+
this.handlers = handlers;
|
|
445
|
+
}
|
|
446
|
+
analyzeFlows() {
|
|
447
|
+
const flows = [];
|
|
448
|
+
const handlersByType = new Map;
|
|
449
|
+
for (const handler of this.handlers) {
|
|
450
|
+
if (!handlersByType.has(handler.messageType)) {
|
|
451
|
+
handlersByType.set(handler.messageType, []);
|
|
452
|
+
}
|
|
453
|
+
handlersByType.get(handler.messageType).push(handler);
|
|
454
|
+
}
|
|
455
|
+
for (const [messageType, handlers] of handlersByType) {
|
|
456
|
+
const senders = this.findMessageSenders(messageType);
|
|
457
|
+
for (const sender of senders) {
|
|
458
|
+
const recipients = handlers.map((h) => h.node);
|
|
459
|
+
const sequence = this.buildSequence(messageType, sender, handlers);
|
|
460
|
+
const flowMetadata = this.extractFlowMetadata(sender.file, sender.line);
|
|
461
|
+
flows.push({
|
|
462
|
+
messageType,
|
|
463
|
+
from: sender.context,
|
|
464
|
+
to: recipients,
|
|
465
|
+
trigger: flowMetadata.trigger,
|
|
466
|
+
flowName: flowMetadata.flowName,
|
|
467
|
+
description: flowMetadata.description,
|
|
468
|
+
sequence
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return flows;
|
|
473
|
+
}
|
|
474
|
+
findMessageSenders(messageType) {
|
|
475
|
+
const senders = [];
|
|
476
|
+
for (const sourceFile of this.project.getSourceFiles()) {
|
|
477
|
+
const filePath = sourceFile.getFilePath();
|
|
478
|
+
const context = this.inferContext(filePath);
|
|
479
|
+
sourceFile.forEachDescendant((node) => {
|
|
480
|
+
if (Node2.isCallExpression(node)) {
|
|
481
|
+
const expression = node.getExpression();
|
|
482
|
+
if (Node2.isPropertyAccessExpression(expression)) {
|
|
483
|
+
const methodName = expression.getName();
|
|
484
|
+
if (methodName === "send" || methodName === "emit") {
|
|
485
|
+
const args = node.getArguments();
|
|
486
|
+
if (args.length > 0) {
|
|
487
|
+
const firstArg = args[0];
|
|
488
|
+
let msgType;
|
|
489
|
+
if (Node2.isStringLiteral(firstArg)) {
|
|
490
|
+
msgType = firstArg.getLiteralValue();
|
|
491
|
+
} else if (Node2.isObjectLiteralExpression(firstArg)) {
|
|
492
|
+
const typeProperty = firstArg.getProperty("type");
|
|
493
|
+
if (typeProperty && Node2.isPropertyAssignment(typeProperty)) {
|
|
494
|
+
const initializer = typeProperty.getInitializer();
|
|
495
|
+
if (initializer && Node2.isStringLiteral(initializer)) {
|
|
496
|
+
msgType = initializer.getLiteralValue();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (msgType === messageType) {
|
|
501
|
+
senders.push({
|
|
502
|
+
context,
|
|
503
|
+
file: filePath,
|
|
504
|
+
line: node.getStartLineNumber()
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return senders;
|
|
514
|
+
}
|
|
515
|
+
buildSequence(messageType, sender, handlers) {
|
|
516
|
+
const steps = [];
|
|
517
|
+
let stepNumber = 1;
|
|
518
|
+
steps.push({
|
|
519
|
+
step: stepNumber++,
|
|
520
|
+
action: `${sender.context}.send(${messageType})`,
|
|
521
|
+
context: sender.context,
|
|
522
|
+
location: {
|
|
523
|
+
file: sender.file,
|
|
524
|
+
line: sender.line
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
for (const handler of handlers) {
|
|
528
|
+
steps.push({
|
|
529
|
+
step: stepNumber++,
|
|
530
|
+
action: `${handler.node}.handle(${messageType})`,
|
|
531
|
+
context: handler.node,
|
|
532
|
+
location: handler.location
|
|
533
|
+
});
|
|
534
|
+
const subsends = this.findMessagesInHandler(handler);
|
|
535
|
+
for (const subsend of subsends) {
|
|
536
|
+
steps.push({
|
|
537
|
+
step: stepNumber++,
|
|
538
|
+
action: `${handler.node}.send(${subsend.messageType})`,
|
|
539
|
+
context: handler.node,
|
|
540
|
+
location: subsend.location
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return steps;
|
|
545
|
+
}
|
|
546
|
+
findMessagesInHandler(handler) {
|
|
547
|
+
const sends = [];
|
|
548
|
+
const sourceFile = this.project.getSourceFile(handler.location.file);
|
|
549
|
+
if (!sourceFile)
|
|
550
|
+
return sends;
|
|
551
|
+
const targetLine = handler.location.line;
|
|
552
|
+
sourceFile.forEachDescendant((node) => {
|
|
553
|
+
if (Node2.isCallExpression(node)) {
|
|
554
|
+
const line = node.getStartLineNumber();
|
|
555
|
+
if (Math.abs(line - targetLine) < 20) {
|
|
556
|
+
const expression = node.getExpression();
|
|
557
|
+
if (Node2.isPropertyAccessExpression(expression)) {
|
|
558
|
+
const methodName = expression.getName();
|
|
559
|
+
if (methodName === "send" || methodName === "emit") {
|
|
560
|
+
const args = node.getArguments();
|
|
561
|
+
if (args.length > 0) {
|
|
562
|
+
const firstArg = args[0];
|
|
563
|
+
let messageType;
|
|
564
|
+
if (Node2.isStringLiteral(firstArg)) {
|
|
565
|
+
messageType = firstArg.getLiteralValue();
|
|
566
|
+
} else if (Node2.isObjectLiteralExpression(firstArg)) {
|
|
567
|
+
const typeProperty = firstArg.getProperty("type");
|
|
568
|
+
if (typeProperty && Node2.isPropertyAssignment(typeProperty)) {
|
|
569
|
+
const initializer = typeProperty.getInitializer();
|
|
570
|
+
if (initializer && Node2.isStringLiteral(initializer)) {
|
|
571
|
+
messageType = initializer.getLiteralValue();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (messageType) {
|
|
576
|
+
sends.push({
|
|
577
|
+
messageType,
|
|
578
|
+
location: {
|
|
579
|
+
file: handler.location.file,
|
|
580
|
+
line
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
return sends;
|
|
591
|
+
}
|
|
592
|
+
extractFlowMetadata(filePath, lineNumber) {
|
|
593
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
594
|
+
if (!sourceFile)
|
|
595
|
+
return {};
|
|
596
|
+
let targetNode = null;
|
|
597
|
+
sourceFile.forEachDescendant((node) => {
|
|
598
|
+
if (node.getStartLineNumber() === lineNumber) {
|
|
599
|
+
targetNode = node;
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
if (!targetNode)
|
|
603
|
+
return {};
|
|
604
|
+
const jsDocs = targetNode.getJsDocs?.() || [];
|
|
605
|
+
if (jsDocs.length === 0)
|
|
606
|
+
return {};
|
|
607
|
+
const comment = jsDocs[0].getText();
|
|
608
|
+
const flowMatch = comment.match(/@flow\s+([^\s]+)/);
|
|
609
|
+
const flowName = flowMatch ? flowMatch[1] : undefined;
|
|
610
|
+
const triggerMatch = comment.match(/@trigger\s+(.+?)(?:\n|$)/);
|
|
611
|
+
const trigger = triggerMatch ? triggerMatch[1].trim() : undefined;
|
|
612
|
+
const descMatch = comment.match(/@description\s+(.+?)(?:\n|$)/s);
|
|
613
|
+
const description = descMatch ? descMatch[1].trim() : undefined;
|
|
614
|
+
return { trigger, flowName, description };
|
|
615
|
+
}
|
|
616
|
+
inferContext(filePath) {
|
|
617
|
+
const path2 = filePath.toLowerCase();
|
|
618
|
+
if (path2.includes("/background/") || path2.includes("\\background\\")) {
|
|
619
|
+
return "background";
|
|
620
|
+
}
|
|
621
|
+
if (path2.includes("/content/") || path2.includes("\\content\\")) {
|
|
622
|
+
return "content";
|
|
623
|
+
}
|
|
624
|
+
if (path2.includes("/popup/") || path2.includes("\\popup\\")) {
|
|
625
|
+
return "popup";
|
|
626
|
+
}
|
|
627
|
+
if (path2.includes("/devtools/") || path2.includes("\\devtools\\")) {
|
|
628
|
+
return "devtools";
|
|
629
|
+
}
|
|
630
|
+
if (path2.includes("/options/") || path2.includes("\\options\\")) {
|
|
631
|
+
return "options";
|
|
632
|
+
}
|
|
633
|
+
if (path2.includes("/offscreen/") || path2.includes("\\offscreen\\")) {
|
|
634
|
+
return "offscreen";
|
|
635
|
+
}
|
|
636
|
+
return "unknown";
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// vendor/analysis/src/extract/integrations.ts
|
|
641
|
+
import { Project as Project3, Node as Node3 } from "ts-morph";
|
|
642
|
+
|
|
643
|
+
class IntegrationAnalyzer {
|
|
644
|
+
project;
|
|
645
|
+
constructor(tsConfigPath) {
|
|
646
|
+
this.project = new Project3({
|
|
647
|
+
tsConfigFilePath: tsConfigPath
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
analyzeIntegrations() {
|
|
651
|
+
const integrations = new Map;
|
|
652
|
+
const fetchCalls = this.findFetchCalls();
|
|
653
|
+
for (const call of fetchCalls) {
|
|
654
|
+
this.addOrMergeIntegration(integrations, this.createAPIIntegration(call));
|
|
655
|
+
}
|
|
656
|
+
const websockets = this.findWebSockets();
|
|
657
|
+
for (const ws of websockets) {
|
|
658
|
+
this.addOrMergeIntegration(integrations, this.createWebSocketIntegration(ws));
|
|
659
|
+
}
|
|
660
|
+
const externalScripts = this.findExternalScripts();
|
|
661
|
+
for (const script of externalScripts) {
|
|
662
|
+
this.addOrMergeIntegration(integrations, script);
|
|
663
|
+
}
|
|
664
|
+
return Array.from(integrations.values());
|
|
665
|
+
}
|
|
666
|
+
findFetchCalls() {
|
|
667
|
+
const calls = [];
|
|
668
|
+
for (const sourceFile of this.project.getSourceFiles()) {
|
|
669
|
+
sourceFile.forEachDescendant((node) => {
|
|
670
|
+
if (Node3.isCallExpression(node)) {
|
|
671
|
+
const expression = node.getExpression();
|
|
672
|
+
if (Node3.isIdentifier(expression) && expression.getText() === "fetch") {
|
|
673
|
+
const args = node.getArguments();
|
|
674
|
+
if (args.length > 0) {
|
|
675
|
+
const urlArg = args[0];
|
|
676
|
+
let url = null;
|
|
677
|
+
if (Node3.isStringLiteral(urlArg)) {
|
|
678
|
+
url = urlArg.getLiteralValue();
|
|
679
|
+
} else if (Node3.isTemplateExpression(urlArg)) {
|
|
680
|
+
url = this.extractBaseURL(urlArg.getText());
|
|
681
|
+
}
|
|
682
|
+
if (url) {
|
|
683
|
+
let method = "GET";
|
|
684
|
+
if (args.length > 1 && Node3.isObjectLiteralExpression(args[1])) {
|
|
685
|
+
const options = args[1];
|
|
686
|
+
const methodProp = options.getProperty("method");
|
|
687
|
+
if (methodProp && Node3.isPropertyAssignment(methodProp)) {
|
|
688
|
+
const initializer = methodProp.getInitializer();
|
|
689
|
+
if (initializer && Node3.isStringLiteral(initializer)) {
|
|
690
|
+
method = initializer.getLiteralValue().toUpperCase();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const description = this.extractJSDocDescription(node);
|
|
695
|
+
calls.push({
|
|
696
|
+
url,
|
|
697
|
+
method,
|
|
698
|
+
file: sourceFile.getFilePath(),
|
|
699
|
+
line: node.getStartLineNumber(),
|
|
700
|
+
description
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
return calls;
|
|
709
|
+
}
|
|
710
|
+
findWebSockets() {
|
|
711
|
+
const websockets = [];
|
|
712
|
+
for (const sourceFile of this.project.getSourceFiles()) {
|
|
713
|
+
sourceFile.forEachDescendant((node) => {
|
|
714
|
+
if (Node3.isNewExpression(node)) {
|
|
715
|
+
const expression = node.getExpression();
|
|
716
|
+
if (Node3.isIdentifier(expression) && expression.getText() === "WebSocket") {
|
|
717
|
+
const args = node.getArguments();
|
|
718
|
+
if (args.length > 0 && Node3.isStringLiteral(args[0])) {
|
|
719
|
+
const url = args[0].getLiteralValue();
|
|
720
|
+
const description = this.extractJSDocDescription(node);
|
|
721
|
+
websockets.push({
|
|
722
|
+
url,
|
|
723
|
+
file: sourceFile.getFilePath(),
|
|
724
|
+
line: node.getStartLineNumber(),
|
|
725
|
+
description
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
return websockets;
|
|
733
|
+
}
|
|
734
|
+
findExternalScripts() {
|
|
735
|
+
const scripts = [];
|
|
736
|
+
const seen = new Set;
|
|
737
|
+
for (const sourceFile of this.project.getSourceFiles()) {
|
|
738
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
739
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
740
|
+
if (!moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
|
|
741
|
+
const packageName = moduleSpecifier.startsWith("@") ? moduleSpecifier.split("/").slice(0, 2).join("/") : moduleSpecifier.split("/")[0];
|
|
742
|
+
if (!seen.has(packageName)) {
|
|
743
|
+
seen.add(packageName);
|
|
744
|
+
scripts.push({
|
|
745
|
+
type: "external-script",
|
|
746
|
+
name: packageName,
|
|
747
|
+
technology: "npm package",
|
|
748
|
+
usedIn: [sourceFile.getFilePath()],
|
|
749
|
+
description: `External dependency: ${packageName}`
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return scripts;
|
|
756
|
+
}
|
|
757
|
+
createAPIIntegration(call) {
|
|
758
|
+
const baseURL = this.extractBaseURL(call.url);
|
|
759
|
+
const name = this.inferAPIName(baseURL);
|
|
760
|
+
return {
|
|
761
|
+
type: "api",
|
|
762
|
+
name,
|
|
763
|
+
technology: "REST API",
|
|
764
|
+
url: baseURL,
|
|
765
|
+
usedIn: [call.file],
|
|
766
|
+
description: call.description || `External API: ${name}`,
|
|
767
|
+
calls: [
|
|
768
|
+
{
|
|
769
|
+
method: call.method,
|
|
770
|
+
endpoint: call.url,
|
|
771
|
+
location: {
|
|
772
|
+
file: call.file,
|
|
773
|
+
line: call.line
|
|
774
|
+
},
|
|
775
|
+
description: call.description
|
|
776
|
+
}
|
|
777
|
+
]
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
createWebSocketIntegration(ws) {
|
|
781
|
+
const name = this.inferAPIName(ws.url);
|
|
782
|
+
return {
|
|
783
|
+
type: "websocket",
|
|
784
|
+
name,
|
|
785
|
+
technology: "WebSocket",
|
|
786
|
+
url: ws.url,
|
|
787
|
+
usedIn: [ws.file],
|
|
788
|
+
description: ws.description || `WebSocket connection: ${name}`
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
extractBaseURL(url) {
|
|
792
|
+
url = url.replace(/\$\{[^}]+\}/g, "");
|
|
793
|
+
url = url.replace(/`/g, "");
|
|
794
|
+
try {
|
|
795
|
+
const parsed = new URL(url);
|
|
796
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
797
|
+
} catch {
|
|
798
|
+
const match = url.match(/https?:\/\/([^/]+)/);
|
|
799
|
+
return match ? match[0] : url;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
inferAPIName(url) {
|
|
803
|
+
try {
|
|
804
|
+
const parsed = new URL(url);
|
|
805
|
+
const hostname = parsed.hostname;
|
|
806
|
+
const cleanHost = hostname.replace(/^www\./, "");
|
|
807
|
+
const parts = cleanHost.split(".");
|
|
808
|
+
if (parts.length > 0) {
|
|
809
|
+
return parts[0].charAt(0).toUpperCase() + parts[0].slice(1) + " API";
|
|
810
|
+
}
|
|
811
|
+
} catch {}
|
|
812
|
+
return "External API";
|
|
813
|
+
}
|
|
814
|
+
addOrMergeIntegration(map, integration) {
|
|
815
|
+
const key = `${integration.type}:${integration.name}`;
|
|
816
|
+
if (map.has(key)) {
|
|
817
|
+
const existing = map.get(key);
|
|
818
|
+
existing.usedIn = [...new Set([...existing.usedIn, ...integration.usedIn])];
|
|
819
|
+
if (integration.calls && existing.calls) {
|
|
820
|
+
existing.calls.push(...integration.calls);
|
|
821
|
+
} else if (integration.calls) {
|
|
822
|
+
existing.calls = integration.calls;
|
|
823
|
+
}
|
|
824
|
+
} else {
|
|
825
|
+
map.set(key, integration);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
extractJSDocDescription(node) {
|
|
829
|
+
const jsDocs = node.getJsDocs?.() || [];
|
|
830
|
+
if (jsDocs.length === 0)
|
|
831
|
+
return;
|
|
832
|
+
const comment = jsDocs[0].getDescription().trim();
|
|
833
|
+
return comment || undefined;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// vendor/analysis/src/extract/handlers.ts
|
|
838
|
+
import { Project as Project4, SyntaxKind as SyntaxKind2, Node as Node4 } from "ts-morph";
|
|
839
|
+
|
|
840
|
+
class HandlerExtractor {
|
|
841
|
+
project;
|
|
842
|
+
constructor(tsConfigPath) {
|
|
843
|
+
this.project = new Project4({
|
|
844
|
+
tsConfigFilePath: tsConfigPath
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
extractHandlers() {
|
|
848
|
+
const handlers = [];
|
|
849
|
+
const messageTypes = new Set;
|
|
850
|
+
const sourceFiles = this.project.getSourceFiles();
|
|
851
|
+
for (const sourceFile of sourceFiles) {
|
|
852
|
+
const fileHandlers = this.extractFromFile(sourceFile);
|
|
853
|
+
handlers.push(...fileHandlers);
|
|
854
|
+
for (const handler of fileHandlers) {
|
|
855
|
+
messageTypes.add(handler.messageType);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
handlers,
|
|
860
|
+
messageTypes
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
extractFromFile(sourceFile) {
|
|
864
|
+
const handlers = [];
|
|
865
|
+
const filePath = sourceFile.getFilePath();
|
|
866
|
+
const context = this.inferContext(filePath);
|
|
867
|
+
sourceFile.forEachDescendant((node) => {
|
|
868
|
+
if (Node4.isCallExpression(node)) {
|
|
869
|
+
const expression = node.getExpression();
|
|
870
|
+
if (Node4.isPropertyAccessExpression(expression)) {
|
|
871
|
+
const methodName = expression.getName();
|
|
872
|
+
if (methodName === "on") {
|
|
873
|
+
const handler = this.extractHandler(node, context, filePath);
|
|
874
|
+
if (handler) {
|
|
875
|
+
handlers.push(handler);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
return handlers;
|
|
882
|
+
}
|
|
883
|
+
extractHandler(callExpr, context, filePath) {
|
|
884
|
+
const args = callExpr.getArguments();
|
|
885
|
+
if (args.length < 2) {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
const messageTypeArg = args[0];
|
|
889
|
+
let messageType = null;
|
|
890
|
+
if (Node4.isStringLiteral(messageTypeArg)) {
|
|
891
|
+
messageType = messageTypeArg.getLiteralValue();
|
|
892
|
+
} else if (Node4.isTemplateExpression(messageTypeArg)) {
|
|
893
|
+
messageType = messageTypeArg.getText().replace(/[`'"]/g, "");
|
|
894
|
+
}
|
|
895
|
+
if (!messageType) {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
const handlerArg = args[1];
|
|
899
|
+
const assignments = [];
|
|
900
|
+
const preconditions = [];
|
|
901
|
+
const postconditions = [];
|
|
902
|
+
if (Node4.isArrowFunction(handlerArg) || Node4.isFunctionExpression(handlerArg)) {
|
|
903
|
+
this.extractAssignments(handlerArg, assignments);
|
|
904
|
+
this.extractVerificationConditions(handlerArg, preconditions, postconditions);
|
|
905
|
+
}
|
|
906
|
+
const line = callExpr.getStartLineNumber();
|
|
907
|
+
return {
|
|
908
|
+
messageType,
|
|
909
|
+
node: context,
|
|
910
|
+
assignments,
|
|
911
|
+
preconditions,
|
|
912
|
+
postconditions,
|
|
913
|
+
location: {
|
|
914
|
+
file: filePath,
|
|
915
|
+
line
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
extractAssignments(funcNode, assignments) {
|
|
920
|
+
funcNode.forEachDescendant((node) => {
|
|
921
|
+
if (Node4.isBinaryExpression(node)) {
|
|
922
|
+
const operator = node.getOperatorToken().getText();
|
|
923
|
+
if (operator === "=") {
|
|
924
|
+
const left = node.getLeft();
|
|
925
|
+
const right = node.getRight();
|
|
926
|
+
if (Node4.isPropertyAccessExpression(left)) {
|
|
927
|
+
const fieldPath = this.getPropertyPath(left);
|
|
928
|
+
if (fieldPath.startsWith("state.")) {
|
|
929
|
+
const field = fieldPath.substring(6);
|
|
930
|
+
const value = this.extractValue(right);
|
|
931
|
+
if (value !== undefined) {
|
|
932
|
+
assignments.push({
|
|
933
|
+
field,
|
|
934
|
+
value
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
extractVerificationConditions(funcNode, preconditions, postconditions) {
|
|
944
|
+
const body = funcNode.getBody();
|
|
945
|
+
const statements = Node4.isBlock(body) ? body.getStatements() : [body];
|
|
946
|
+
statements.forEach((statement, index) => {
|
|
947
|
+
if (Node4.isExpressionStatement(statement)) {
|
|
948
|
+
const expr = statement.getExpression();
|
|
949
|
+
if (Node4.isCallExpression(expr)) {
|
|
950
|
+
const callee = expr.getExpression();
|
|
951
|
+
if (Node4.isIdentifier(callee)) {
|
|
952
|
+
const functionName = callee.getText();
|
|
953
|
+
if (functionName === "requires") {
|
|
954
|
+
const condition = this.extractCondition(expr);
|
|
955
|
+
if (condition) {
|
|
956
|
+
preconditions.push(condition);
|
|
957
|
+
}
|
|
958
|
+
} else if (functionName === "ensures") {
|
|
959
|
+
const condition = this.extractCondition(expr);
|
|
960
|
+
if (condition) {
|
|
961
|
+
postconditions.push(condition);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
extractCondition(callExpr) {
|
|
970
|
+
const args = callExpr.getArguments();
|
|
971
|
+
if (args.length === 0) {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
const conditionArg = args[0];
|
|
975
|
+
const expression = conditionArg.getText();
|
|
976
|
+
let message;
|
|
977
|
+
if (args.length >= 2 && Node4.isStringLiteral(args[1])) {
|
|
978
|
+
message = args[1].getLiteralValue();
|
|
979
|
+
}
|
|
980
|
+
const line = callExpr.getStartLineNumber();
|
|
981
|
+
const column = callExpr.getStartLinePos();
|
|
982
|
+
return {
|
|
983
|
+
expression,
|
|
984
|
+
message,
|
|
985
|
+
location: {
|
|
986
|
+
line,
|
|
987
|
+
column
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
getPropertyPath(node) {
|
|
992
|
+
const parts = [];
|
|
993
|
+
let current = node;
|
|
994
|
+
while (Node4.isPropertyAccessExpression(current)) {
|
|
995
|
+
parts.unshift(current.getName());
|
|
996
|
+
current = current.getExpression();
|
|
997
|
+
}
|
|
998
|
+
if (Node4.isIdentifier(current)) {
|
|
999
|
+
parts.unshift(current.getText());
|
|
1000
|
+
}
|
|
1001
|
+
return parts.join(".");
|
|
1002
|
+
}
|
|
1003
|
+
extractValue(node) {
|
|
1004
|
+
if (Node4.isStringLiteral(node)) {
|
|
1005
|
+
return node.getLiteralValue();
|
|
1006
|
+
}
|
|
1007
|
+
if (Node4.isNumericLiteral(node)) {
|
|
1008
|
+
return node.getLiteralValue();
|
|
1009
|
+
}
|
|
1010
|
+
if (node.getKind() === SyntaxKind2.TrueKeyword) {
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
if (node.getKind() === SyntaxKind2.FalseKeyword) {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
if (node.getKind() === SyntaxKind2.NullKeyword) {
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
inferContext(filePath) {
|
|
1022
|
+
const path2 = filePath.toLowerCase();
|
|
1023
|
+
if (path2.includes("/background/") || path2.includes("\\background\\")) {
|
|
1024
|
+
return "background";
|
|
1025
|
+
}
|
|
1026
|
+
if (path2.includes("/content/") || path2.includes("\\content\\")) {
|
|
1027
|
+
return "content";
|
|
1028
|
+
}
|
|
1029
|
+
if (path2.includes("/popup/") || path2.includes("\\popup\\")) {
|
|
1030
|
+
return "popup";
|
|
1031
|
+
}
|
|
1032
|
+
if (path2.includes("/devtools/") || path2.includes("\\devtools\\")) {
|
|
1033
|
+
return "devtools";
|
|
1034
|
+
}
|
|
1035
|
+
if (path2.includes("/options/") || path2.includes("\\options\\")) {
|
|
1036
|
+
return "options";
|
|
1037
|
+
}
|
|
1038
|
+
if (path2.includes("/offscreen/") || path2.includes("\\offscreen\\")) {
|
|
1039
|
+
return "offscreen";
|
|
1040
|
+
}
|
|
1041
|
+
return "unknown";
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// vendor/analysis/src/extract/adr.ts
|
|
1046
|
+
import * as fs2 from "node:fs";
|
|
1047
|
+
import * as path2 from "node:path";
|
|
1048
|
+
|
|
1049
|
+
class ADRExtractor {
|
|
1050
|
+
projectRoot;
|
|
1051
|
+
constructor(projectRoot) {
|
|
1052
|
+
this.projectRoot = projectRoot;
|
|
1053
|
+
}
|
|
1054
|
+
extract() {
|
|
1055
|
+
const adrDir = this.findADRDirectory();
|
|
1056
|
+
if (!adrDir || !fs2.existsSync(adrDir)) {
|
|
1057
|
+
return {
|
|
1058
|
+
adrs: [],
|
|
1059
|
+
directory: adrDir || path2.join(this.projectRoot, "docs", "adr")
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
const files = fs2.readdirSync(adrDir).filter((file) => file.endsWith(".md")).map((file) => path2.join(adrDir, file));
|
|
1063
|
+
const adrs = [];
|
|
1064
|
+
for (const file of files) {
|
|
1065
|
+
try {
|
|
1066
|
+
const adr = this.parseADR(file);
|
|
1067
|
+
if (adr) {
|
|
1068
|
+
adrs.push(adr);
|
|
1069
|
+
}
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
console.warn(`Failed to parse ADR: ${file}`, error);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
adrs.sort((a, b) => a.id.localeCompare(b.id));
|
|
1075
|
+
return {
|
|
1076
|
+
adrs,
|
|
1077
|
+
directory: adrDir
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
findADRDirectory() {
|
|
1081
|
+
const candidates = [
|
|
1082
|
+
path2.join(this.projectRoot, "docs", "adr"),
|
|
1083
|
+
path2.join(this.projectRoot, "docs", "architecture", "decisions"),
|
|
1084
|
+
path2.join(this.projectRoot, "adr"),
|
|
1085
|
+
path2.join(this.projectRoot, "architecture", "decisions")
|
|
1086
|
+
];
|
|
1087
|
+
for (const candidate of candidates) {
|
|
1088
|
+
if (fs2.existsSync(candidate)) {
|
|
1089
|
+
return candidate;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
parseADR(filePath) {
|
|
1095
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
1096
|
+
const fileName = path2.basename(filePath, ".md");
|
|
1097
|
+
const idMatch = fileName.match(/^(\d+)/);
|
|
1098
|
+
const id = idMatch ? idMatch[1] : fileName;
|
|
1099
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1100
|
+
const title = titleMatch ? titleMatch[1].trim() : fileName;
|
|
1101
|
+
const status = this.extractStatus(content);
|
|
1102
|
+
const date = this.extractDate(content);
|
|
1103
|
+
const context = this.extractSection(content, "Context");
|
|
1104
|
+
const decision = this.extractSection(content, "Decision");
|
|
1105
|
+
const consequences = this.extractSection(content, "Consequences");
|
|
1106
|
+
const alternativesSection = this.extractSection(content, "Alternatives");
|
|
1107
|
+
const alternatives = alternativesSection ? alternativesSection.split(`
|
|
1108
|
+
`).filter((line) => line.trim().startsWith("-")).map((line) => line.replace(/^-\s*/, "").trim()) : undefined;
|
|
1109
|
+
const links = this.extractLinks(content);
|
|
1110
|
+
if (!context || !decision || !consequences) {
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
id,
|
|
1115
|
+
title,
|
|
1116
|
+
status,
|
|
1117
|
+
date,
|
|
1118
|
+
context,
|
|
1119
|
+
decision,
|
|
1120
|
+
consequences,
|
|
1121
|
+
alternatives,
|
|
1122
|
+
links,
|
|
1123
|
+
source: filePath
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
extractStatus(content) {
|
|
1127
|
+
const statusMatch = content.match(/Status:\s*(\w+)/i);
|
|
1128
|
+
if (!statusMatch)
|
|
1129
|
+
return "accepted";
|
|
1130
|
+
const status = statusMatch[1].toLowerCase();
|
|
1131
|
+
if (["proposed", "accepted", "deprecated", "superseded"].includes(status)) {
|
|
1132
|
+
return status;
|
|
1133
|
+
}
|
|
1134
|
+
return "accepted";
|
|
1135
|
+
}
|
|
1136
|
+
extractDate(content) {
|
|
1137
|
+
const dateMatch = content.match(/Date:\s*(\d{4}-\d{2}-\d{2})/i) || content.match(/(\d{4}-\d{2}-\d{2})/i);
|
|
1138
|
+
return dateMatch ? dateMatch[1] : new Date().toISOString().split("T")[0];
|
|
1139
|
+
}
|
|
1140
|
+
extractSection(content, sectionName) {
|
|
1141
|
+
const regex = new RegExp(`##\\s+${sectionName}\\s*\\n([\\s\\S]*?)(?=\\n##|$)`, "i");
|
|
1142
|
+
const match = content.match(regex);
|
|
1143
|
+
return match ? match[1].trim() : "";
|
|
1144
|
+
}
|
|
1145
|
+
extractLinks(content) {
|
|
1146
|
+
const links = [];
|
|
1147
|
+
const supersedesMatch = content.match(/Supersedes:\s*ADR-(\d+)/gi);
|
|
1148
|
+
if (supersedesMatch) {
|
|
1149
|
+
for (const match of supersedesMatch) {
|
|
1150
|
+
const idMatch = match.match(/ADR-(\d+)/);
|
|
1151
|
+
if (idMatch) {
|
|
1152
|
+
links.push({
|
|
1153
|
+
type: "supersedes",
|
|
1154
|
+
adrId: idMatch[1]
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const supersededByMatch = content.match(/Superseded by:\s*ADR-(\d+)/gi);
|
|
1160
|
+
if (supersededByMatch) {
|
|
1161
|
+
for (const match of supersededByMatch) {
|
|
1162
|
+
const idMatch = match.match(/ADR-(\d+)/);
|
|
1163
|
+
if (idMatch) {
|
|
1164
|
+
links.push({
|
|
1165
|
+
type: "superseded-by",
|
|
1166
|
+
adrId: idMatch[1]
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return links;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function extractADRs(projectRoot) {
|
|
1175
|
+
const extractor = new ADRExtractor(projectRoot);
|
|
1176
|
+
return extractor.extract();
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// vendor/analysis/src/extract/architecture.ts
|
|
1180
|
+
class ArchitectureAnalyzer {
|
|
1181
|
+
options;
|
|
1182
|
+
constructor(options) {
|
|
1183
|
+
this.options = options;
|
|
1184
|
+
}
|
|
1185
|
+
async analyze() {
|
|
1186
|
+
const manifestParser = new ManifestParser(this.options.projectRoot);
|
|
1187
|
+
const manifest = manifestParser.parse();
|
|
1188
|
+
const entryPoints = manifestParser.getContextEntryPoints();
|
|
1189
|
+
const handlerExtractor = new HandlerExtractor(this.options.tsConfigPath);
|
|
1190
|
+
const { handlers } = handlerExtractor.extractHandlers();
|
|
1191
|
+
const contextAnalyzer = new ContextAnalyzer(this.options.tsConfigPath);
|
|
1192
|
+
const contexts = {};
|
|
1193
|
+
for (const [contextType, entryPoint] of Object.entries(entryPoints)) {
|
|
1194
|
+
try {
|
|
1195
|
+
const contextInfo = contextAnalyzer.analyzeContext(contextType, entryPoint, handlers);
|
|
1196
|
+
contexts[contextType] = contextInfo;
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
console.warn(`Failed to analyze context ${contextType}: ${error}`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const flowAnalyzer = new FlowAnalyzer(this.options.tsConfigPath, handlers);
|
|
1202
|
+
const messageFlows = flowAnalyzer.analyzeFlows();
|
|
1203
|
+
const integrationAnalyzer = new IntegrationAnalyzer(this.options.tsConfigPath);
|
|
1204
|
+
const integrations = integrationAnalyzer.analyzeIntegrations();
|
|
1205
|
+
this.mergeExternalAPIsIntoContexts(contexts, integrations);
|
|
1206
|
+
const adrs = extractADRs(this.options.projectRoot);
|
|
1207
|
+
const repository = this.extractRepositoryInfo();
|
|
1208
|
+
return {
|
|
1209
|
+
system: {
|
|
1210
|
+
name: manifest.name,
|
|
1211
|
+
version: manifest.version,
|
|
1212
|
+
description: manifest.description
|
|
1213
|
+
},
|
|
1214
|
+
manifest,
|
|
1215
|
+
contexts,
|
|
1216
|
+
messageFlows,
|
|
1217
|
+
integrations,
|
|
1218
|
+
adrs: adrs.adrs.length > 0 ? adrs : undefined,
|
|
1219
|
+
repository
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
mergeExternalAPIsIntoContexts(contexts, integrations) {
|
|
1223
|
+
for (const integration of integrations) {
|
|
1224
|
+
if (integration.calls) {
|
|
1225
|
+
for (const call of integration.calls) {
|
|
1226
|
+
for (const [contextType, contextInfo] of Object.entries(contexts)) {
|
|
1227
|
+
if (call.location.file.includes(`/${contextType}/`)) {
|
|
1228
|
+
contextInfo.externalAPIs.push(call);
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
extractRepositoryInfo() {
|
|
1237
|
+
const packageJsonPath = path3.join(this.options.projectRoot, "package.json");
|
|
1238
|
+
if (!fs3.existsSync(packageJsonPath)) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
const content = fs3.readFileSync(packageJsonPath, "utf-8");
|
|
1243
|
+
const packageJson = JSON.parse(content);
|
|
1244
|
+
if (packageJson.repository) {
|
|
1245
|
+
if (typeof packageJson.repository === "string") {
|
|
1246
|
+
return {
|
|
1247
|
+
url: packageJson.repository,
|
|
1248
|
+
type: "git"
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
return {
|
|
1252
|
+
url: packageJson.repository.url || packageJson.repository,
|
|
1253
|
+
type: packageJson.repository.type || "git"
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
console.warn(`Failed to parse package.json: ${error}`);
|
|
1258
|
+
}
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
async function analyzeArchitecture(options) {
|
|
1263
|
+
const analyzer = new ArchitectureAnalyzer(options);
|
|
1264
|
+
return analyzer.analyze();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// vendor/visualize/src/codegen/structurizr.ts
|
|
1268
|
+
class StructurizrDSLGenerator {
|
|
1269
|
+
analysis;
|
|
1270
|
+
options;
|
|
1271
|
+
constructor(analysis, options = {}) {
|
|
1272
|
+
this.analysis = analysis;
|
|
1273
|
+
this.options = {
|
|
1274
|
+
includeDynamicDiagrams: true,
|
|
1275
|
+
includeComponentDiagrams: true,
|
|
1276
|
+
componentDiagramContexts: ["background"],
|
|
1277
|
+
theme: "https://static.structurizr.com/themes/default",
|
|
1278
|
+
...options
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
generate() {
|
|
1282
|
+
const parts = [];
|
|
1283
|
+
parts.push(this.generateWorkspaceHeader());
|
|
1284
|
+
parts.push(this.generateModel());
|
|
1285
|
+
parts.push(this.generateViews());
|
|
1286
|
+
parts.push(this.generateWorkspaceFooter());
|
|
1287
|
+
return parts.join(`
|
|
1288
|
+
|
|
1289
|
+
`);
|
|
1290
|
+
}
|
|
1291
|
+
generateWorkspaceHeader() {
|
|
1292
|
+
const { name, description } = this.analysis.system;
|
|
1293
|
+
const parts = [`workspace "${this.escape(name)}" "${this.escape(description || "")}" {`];
|
|
1294
|
+
parts.push("");
|
|
1295
|
+
parts.push(" !identifiers hierarchical");
|
|
1296
|
+
if (this.analysis.adrs && this.analysis.adrs.adrs.length > 0) {
|
|
1297
|
+
parts.push(" !adrs " + this.analysis.adrs.directory);
|
|
1298
|
+
}
|
|
1299
|
+
return parts.join(`
|
|
1300
|
+
`);
|
|
1301
|
+
}
|
|
1302
|
+
generateWorkspaceFooter() {
|
|
1303
|
+
return "}";
|
|
1304
|
+
}
|
|
1305
|
+
generateModel() {
|
|
1306
|
+
const parts = [];
|
|
1307
|
+
parts.push(" model {");
|
|
1308
|
+
parts.push(this.generatePeople());
|
|
1309
|
+
parts.push(this.generateExternalSystems());
|
|
1310
|
+
parts.push(this.generateMainSystem());
|
|
1311
|
+
parts.push(" }");
|
|
1312
|
+
return parts.join(`
|
|
1313
|
+
|
|
1314
|
+
`);
|
|
1315
|
+
}
|
|
1316
|
+
generatePeople() {
|
|
1317
|
+
return ` user = person "User" "Extension user"`;
|
|
1318
|
+
}
|
|
1319
|
+
generateExternalSystems() {
|
|
1320
|
+
const parts = [];
|
|
1321
|
+
for (const integration of this.analysis.integrations) {
|
|
1322
|
+
if (integration.type === "api" || integration.type === "websocket") {
|
|
1323
|
+
const tech = integration.technology || (integration.type === "websocket" ? "WebSocket" : "REST API");
|
|
1324
|
+
let desc = integration.description || "";
|
|
1325
|
+
if (!desc && integration.calls && integration.calls.length > 0) {
|
|
1326
|
+
const endpoints = integration.calls.slice(0, 3).map((c) => c.endpoint).join(", ");
|
|
1327
|
+
const methods = [...new Set(integration.calls.map((c) => c.method))].join(", ");
|
|
1328
|
+
desc = `External API with endpoints: ${endpoints}. Methods: ${methods}`;
|
|
1329
|
+
}
|
|
1330
|
+
parts.push(` ${this.toId(integration.name)} = softwareSystem "${this.escape(integration.name)}" "${this.escape(desc)}" {`);
|
|
1331
|
+
parts.push(` tags "External System" "${integration.type === "websocket" ? "WebSocket" : "REST API"}"`);
|
|
1332
|
+
parts.push(` }`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return parts.join(`
|
|
1336
|
+
`);
|
|
1337
|
+
}
|
|
1338
|
+
generateMainSystem() {
|
|
1339
|
+
const parts = [];
|
|
1340
|
+
parts.push(` extension = softwareSystem "${this.escape(this.analysis.system.name)}" {`);
|
|
1341
|
+
for (const [contextType, contextInfo] of Object.entries(this.analysis.contexts)) {
|
|
1342
|
+
parts.push(this.generateContainer(contextType, contextInfo));
|
|
1343
|
+
}
|
|
1344
|
+
parts.push(this.generateContainerRelationships());
|
|
1345
|
+
parts.push(" }");
|
|
1346
|
+
return parts.join(`
|
|
1347
|
+
|
|
1348
|
+
`);
|
|
1349
|
+
}
|
|
1350
|
+
generateContainer(contextType, contextInfo) {
|
|
1351
|
+
const parts = [];
|
|
1352
|
+
const technology = this.getContextTechnology(contextType);
|
|
1353
|
+
const description = contextInfo.description || `${this.capitalize(contextType)} context`;
|
|
1354
|
+
parts.push(` ${contextType} = container "${this.capitalize(contextType)}" "${this.escape(description)}" "${technology}" {`);
|
|
1355
|
+
if (this.options.includeComponentDiagrams && this.options.componentDiagramContexts?.includes(contextType)) {
|
|
1356
|
+
parts.push(this.generateComponents(contextType, contextInfo));
|
|
1357
|
+
parts.push("");
|
|
1358
|
+
parts.push(this.generateComponentRelationships(contextType, contextInfo));
|
|
1359
|
+
}
|
|
1360
|
+
parts.push(" }");
|
|
1361
|
+
return parts.join(`
|
|
1362
|
+
`);
|
|
1363
|
+
}
|
|
1364
|
+
generateComponents(contextType, contextInfo) {
|
|
1365
|
+
const parts = [];
|
|
1366
|
+
const handlersByType = new Map;
|
|
1367
|
+
for (const handler of contextInfo.handlers) {
|
|
1368
|
+
if (!handlersByType.has(handler.messageType)) {
|
|
1369
|
+
handlersByType.set(handler.messageType, []);
|
|
1370
|
+
}
|
|
1371
|
+
handlersByType.get(handler.messageType).push(handler);
|
|
1372
|
+
}
|
|
1373
|
+
for (const [messageType, handlers] of handlersByType) {
|
|
1374
|
+
const componentName = this.toComponentName(messageType);
|
|
1375
|
+
const description = this.generateComponentDescription(messageType, handlers[0]);
|
|
1376
|
+
const tags = this.getComponentTags(messageType, handlers[0]);
|
|
1377
|
+
parts.push(` ${this.toId(componentName)} = component "${componentName}" "${description}" {`);
|
|
1378
|
+
if (tags.length > 0) {
|
|
1379
|
+
parts.push(` tags "${tags.join('" "')}"`);
|
|
1380
|
+
}
|
|
1381
|
+
parts.push(` }`);
|
|
1382
|
+
}
|
|
1383
|
+
if (contextInfo.components) {
|
|
1384
|
+
for (const comp of contextInfo.components) {
|
|
1385
|
+
parts.push(` ${this.toId(comp.name)} = component "${comp.name}" "${this.escape(comp.description || "UI component")}" {`);
|
|
1386
|
+
parts.push(` tags "UI Component"`);
|
|
1387
|
+
parts.push(` }`);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (contextInfo.chromeAPIs && contextInfo.chromeAPIs.length > 0) {
|
|
1391
|
+
for (const api of contextInfo.chromeAPIs) {
|
|
1392
|
+
const apiId = this.toId(`chrome_${api}`);
|
|
1393
|
+
parts.push(` ${apiId} = component "Chrome ${this.capitalize(api)} API" "Browser API for ${api}" {`);
|
|
1394
|
+
parts.push(` tags "Chrome API" "External"`);
|
|
1395
|
+
parts.push(` }`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return parts.join(`
|
|
1399
|
+
`);
|
|
1400
|
+
}
|
|
1401
|
+
generateComponentDescription(messageType, handler) {
|
|
1402
|
+
const type = messageType.toLowerCase();
|
|
1403
|
+
if (type.includes("login")) {
|
|
1404
|
+
return "Authenticates users and establishes sessions";
|
|
1405
|
+
}
|
|
1406
|
+
if (type.includes("logout")) {
|
|
1407
|
+
return "Terminates user sessions and clears credentials";
|
|
1408
|
+
}
|
|
1409
|
+
if (type.includes("add") || type.includes("create")) {
|
|
1410
|
+
const entity = type.replace(/_(add|create)/, "").replace(/_/g, " ");
|
|
1411
|
+
return `Creates new ${entity} items and persists to storage`;
|
|
1412
|
+
}
|
|
1413
|
+
if (type.includes("remove") || type.includes("delete")) {
|
|
1414
|
+
const entity = type.replace(/_(remove|delete)/, "").replace(/_/g, " ");
|
|
1415
|
+
return `Removes ${entity} items from storage`;
|
|
1416
|
+
}
|
|
1417
|
+
if (type.includes("update") || type.includes("toggle")) {
|
|
1418
|
+
const entity = type.replace(/_(update|toggle)/, "").replace(/_/g, " ");
|
|
1419
|
+
return `Updates ${entity} item state and syncs with storage`;
|
|
1420
|
+
}
|
|
1421
|
+
if (type.includes("clear")) {
|
|
1422
|
+
const entity = type.replace(/_clear.*/, "").replace(/_/g, " ");
|
|
1423
|
+
return `Clears all ${entity} items matching criteria`;
|
|
1424
|
+
}
|
|
1425
|
+
if (type.includes("get") || type.includes("fetch") || type.includes("load")) {
|
|
1426
|
+
const entity = type.replace(/_(get|fetch|load)/, "").replace(/_/g, " ");
|
|
1427
|
+
return `Retrieves ${entity} data from storage`;
|
|
1428
|
+
}
|
|
1429
|
+
return `Processes ${messageType} messages and coordinates business logic`;
|
|
1430
|
+
}
|
|
1431
|
+
getComponentTags(messageType, handler) {
|
|
1432
|
+
const tags = ["Message Handler"];
|
|
1433
|
+
const type = messageType.toLowerCase();
|
|
1434
|
+
if (type.includes("login") || type.includes("logout") || type.includes("auth")) {
|
|
1435
|
+
tags.push("Authentication");
|
|
1436
|
+
} else if (type.includes("add") || type.includes("create") || type.includes("update") || type.includes("delete") || type.includes("remove") || type.includes("toggle")) {
|
|
1437
|
+
tags.push("CRUD");
|
|
1438
|
+
} else if (type.includes("get") || type.includes("fetch") || type.includes("load")) {
|
|
1439
|
+
tags.push("Query");
|
|
1440
|
+
}
|
|
1441
|
+
if (type.includes("user")) {
|
|
1442
|
+
tags.push("User Management");
|
|
1443
|
+
}
|
|
1444
|
+
if (type.includes("todo")) {
|
|
1445
|
+
tags.push("Todo Management");
|
|
1446
|
+
}
|
|
1447
|
+
if (type.includes("state")) {
|
|
1448
|
+
tags.push("State Management");
|
|
1449
|
+
}
|
|
1450
|
+
return tags;
|
|
1451
|
+
}
|
|
1452
|
+
generateComponentRelationships(contextType, contextInfo) {
|
|
1453
|
+
const parts = [];
|
|
1454
|
+
const handlersByType = new Map;
|
|
1455
|
+
for (const handler of contextInfo.handlers) {
|
|
1456
|
+
if (!handlersByType.has(handler.messageType)) {
|
|
1457
|
+
handlersByType.set(handler.messageType, []);
|
|
1458
|
+
}
|
|
1459
|
+
handlersByType.get(handler.messageType).push(handler);
|
|
1460
|
+
}
|
|
1461
|
+
if (contextInfo.chromeAPIs && contextInfo.chromeAPIs.length > 0) {
|
|
1462
|
+
for (const api of contextInfo.chromeAPIs) {
|
|
1463
|
+
const apiId = this.toId(`chrome_${api}`);
|
|
1464
|
+
for (const [messageType, handlers] of handlersByType) {
|
|
1465
|
+
const componentId = this.toId(this.toComponentName(messageType));
|
|
1466
|
+
let description = `Uses ${api}`;
|
|
1467
|
+
if (api === "storage") {
|
|
1468
|
+
if (messageType.toLowerCase().includes("get") || messageType.toLowerCase().includes("load")) {
|
|
1469
|
+
description = "Reads from storage";
|
|
1470
|
+
} else {
|
|
1471
|
+
description = "Writes to storage";
|
|
1472
|
+
}
|
|
1473
|
+
} else if (api === "tabs") {
|
|
1474
|
+
description = "Manages browser tabs";
|
|
1475
|
+
} else if (api === "runtime") {
|
|
1476
|
+
description = "Sends messages";
|
|
1477
|
+
}
|
|
1478
|
+
parts.push(` ${componentId} -> ${apiId} "${description}"`);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
const stateHandlers = [];
|
|
1483
|
+
const queryHandlers = [];
|
|
1484
|
+
for (const [messageType, handlers] of handlersByType) {
|
|
1485
|
+
const type = messageType.toLowerCase();
|
|
1486
|
+
const componentId = this.toId(this.toComponentName(messageType));
|
|
1487
|
+
if (type.includes("add") || type.includes("create") || type.includes("update") || type.includes("delete") || type.includes("remove") || type.includes("toggle") || type.includes("clear") || type.includes("login") || type.includes("logout")) {
|
|
1488
|
+
stateHandlers.push(componentId);
|
|
1489
|
+
} else if (type.includes("get") || type.includes("fetch") || type.includes("load")) {
|
|
1490
|
+
queryHandlers.push(componentId);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (stateHandlers.length > 0 && queryHandlers.length > 0) {
|
|
1494
|
+
for (const queryHandler of queryHandlers) {
|
|
1495
|
+
for (const stateHandler of stateHandlers) {
|
|
1496
|
+
if (queryHandler !== stateHandler) {
|
|
1497
|
+
parts.push(` ${stateHandler} -> ${queryHandler} "Updates state" {`);
|
|
1498
|
+
parts.push(` tags "Implicit"`);
|
|
1499
|
+
parts.push(` }`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return parts.join(`
|
|
1505
|
+
`);
|
|
1506
|
+
}
|
|
1507
|
+
generateContainerRelationships() {
|
|
1508
|
+
const parts = [];
|
|
1509
|
+
const uiContexts = ["popup", "options", "devtools"];
|
|
1510
|
+
for (const contextType of Object.keys(this.analysis.contexts)) {
|
|
1511
|
+
if (uiContexts.includes(contextType)) {
|
|
1512
|
+
parts.push(` user -> extension.${contextType} "Uses"`);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
for (const flow of this.analysis.messageFlows) {
|
|
1516
|
+
const tech = `Sends ${flow.messageType}`;
|
|
1517
|
+
for (const to of flow.to) {
|
|
1518
|
+
parts.push(` extension.${flow.from} -> extension.${to} "${tech}"`);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
for (const integration of this.analysis.integrations) {
|
|
1522
|
+
if (integration.type === "api" || integration.type === "websocket") {
|
|
1523
|
+
for (const [contextType, contextInfo] of Object.entries(this.analysis.contexts)) {
|
|
1524
|
+
const usesIntegration = contextInfo.externalAPIs.some((api) => integration.calls?.some((call) => call.endpoint === api.endpoint));
|
|
1525
|
+
if (usesIntegration) {
|
|
1526
|
+
const method = integration.type === "websocket" ? "Connects to" : "Calls";
|
|
1527
|
+
parts.push(` extension.${contextType} -> ${this.toId(integration.name)} "${method}"`);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return parts.join(`
|
|
1533
|
+
`);
|
|
1534
|
+
}
|
|
1535
|
+
generateViews() {
|
|
1536
|
+
const parts = [];
|
|
1537
|
+
parts.push(" views {");
|
|
1538
|
+
parts.push(this.generateSystemContextView());
|
|
1539
|
+
parts.push(this.generateContainerView());
|
|
1540
|
+
if (this.options.includeComponentDiagrams) {
|
|
1541
|
+
for (const contextType of this.options.componentDiagramContexts || []) {
|
|
1542
|
+
if (this.analysis.contexts[contextType]) {
|
|
1543
|
+
parts.push(this.generateComponentView(contextType));
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (this.options.includeDynamicDiagrams) {
|
|
1548
|
+
parts.push(this.generateDynamicViews());
|
|
1549
|
+
}
|
|
1550
|
+
parts.push(this.generateStyles());
|
|
1551
|
+
parts.push(" }");
|
|
1552
|
+
if (this.analysis.adrs && this.analysis.adrs.adrs.length > 0) {
|
|
1553
|
+
parts.push("");
|
|
1554
|
+
parts.push(this.generateDocumentation());
|
|
1555
|
+
}
|
|
1556
|
+
return parts.join(`
|
|
1557
|
+
|
|
1558
|
+
`);
|
|
1559
|
+
}
|
|
1560
|
+
generateDocumentation() {
|
|
1561
|
+
if (!this.analysis.adrs || this.analysis.adrs.adrs.length === 0) {
|
|
1562
|
+
return "";
|
|
1563
|
+
}
|
|
1564
|
+
const parts = [];
|
|
1565
|
+
parts.push(" documentation {");
|
|
1566
|
+
for (const adr of this.analysis.adrs.adrs) {
|
|
1567
|
+
parts.push(` decision "${adr.id}" {`);
|
|
1568
|
+
parts.push(` title "${this.escape(adr.title)}"`);
|
|
1569
|
+
parts.push(` status "${adr.status}"`);
|
|
1570
|
+
parts.push(` date "${adr.date}"`);
|
|
1571
|
+
parts.push(` content "${this.escape(adr.context)}"`);
|
|
1572
|
+
parts.push(" }");
|
|
1573
|
+
}
|
|
1574
|
+
parts.push(" }");
|
|
1575
|
+
return parts.join(`
|
|
1576
|
+
`);
|
|
1577
|
+
}
|
|
1578
|
+
generateSystemContextView() {
|
|
1579
|
+
return ` systemContext extension "SystemContext" {
|
|
1580
|
+
include *
|
|
1581
|
+
autoLayout lr
|
|
1582
|
+
}`;
|
|
1583
|
+
}
|
|
1584
|
+
generateContainerView() {
|
|
1585
|
+
return ` container extension "Containers" {
|
|
1586
|
+
include *
|
|
1587
|
+
autoLayout lr
|
|
1588
|
+
}`;
|
|
1589
|
+
}
|
|
1590
|
+
generateComponentView(contextType) {
|
|
1591
|
+
return ` component extension.${contextType} "Components_${this.capitalize(contextType)}" {
|
|
1592
|
+
include *
|
|
1593
|
+
autoLayout tb
|
|
1594
|
+
}`;
|
|
1595
|
+
}
|
|
1596
|
+
generateDynamicViews() {
|
|
1597
|
+
const parts = [];
|
|
1598
|
+
const flowsByDomain = new Map;
|
|
1599
|
+
for (const flow of this.analysis.messageFlows) {
|
|
1600
|
+
const messageType = flow.messageType.toLowerCase();
|
|
1601
|
+
let domain = "general";
|
|
1602
|
+
if (messageType.includes("user") || messageType.includes("login") || messageType.includes("logout") || messageType.includes("auth")) {
|
|
1603
|
+
domain = "authentication";
|
|
1604
|
+
} else if (messageType.includes("todo")) {
|
|
1605
|
+
domain = "todo";
|
|
1606
|
+
} else if (messageType.includes("state")) {
|
|
1607
|
+
domain = "state";
|
|
1608
|
+
}
|
|
1609
|
+
if (!flowsByDomain.has(domain)) {
|
|
1610
|
+
flowsByDomain.set(domain, []);
|
|
1611
|
+
}
|
|
1612
|
+
flowsByDomain.get(domain).push(flow);
|
|
1613
|
+
}
|
|
1614
|
+
let count = 0;
|
|
1615
|
+
for (const [domain, flows] of flowsByDomain) {
|
|
1616
|
+
if (count >= 5)
|
|
1617
|
+
break;
|
|
1618
|
+
const viewName = this.capitalize(domain) + " Flow";
|
|
1619
|
+
parts.push(this.generateDynamicView(viewName, flows, domain));
|
|
1620
|
+
count++;
|
|
1621
|
+
}
|
|
1622
|
+
return parts.join(`
|
|
1623
|
+
|
|
1624
|
+
`);
|
|
1625
|
+
}
|
|
1626
|
+
generateDynamicView(flowName, flows, domain) {
|
|
1627
|
+
const parts = [];
|
|
1628
|
+
const description = this.getDynamicViewDescription(domain);
|
|
1629
|
+
parts.push(` dynamic extension "${flowName}" "${description}" {`);
|
|
1630
|
+
const uiContexts = ["popup", "options", "devtools"];
|
|
1631
|
+
const hasUIFlow = flows.some((f) => uiContexts.includes(f.from));
|
|
1632
|
+
if (hasUIFlow) {
|
|
1633
|
+
const firstFlow = flows.find((f) => uiContexts.includes(f.from));
|
|
1634
|
+
if (firstFlow) {
|
|
1635
|
+
const action = this.getUserAction(domain);
|
|
1636
|
+
parts.push(` user -> extension.${firstFlow.from} "${action}"`);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
for (const flow of flows) {
|
|
1640
|
+
const messageDesc = this.getMessageDescription(flow.messageType);
|
|
1641
|
+
for (const to of flow.to) {
|
|
1642
|
+
parts.push(` extension.${flow.from} -> extension.${to} "${messageDesc}"`);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
parts.push(" autoLayout lr");
|
|
1646
|
+
parts.push(" }");
|
|
1647
|
+
return parts.join(`
|
|
1648
|
+
`);
|
|
1649
|
+
}
|
|
1650
|
+
getDynamicViewDescription(domain) {
|
|
1651
|
+
const descriptions = {
|
|
1652
|
+
authentication: "User authentication and session management",
|
|
1653
|
+
todo: "Todo item creation, updates, and retrieval",
|
|
1654
|
+
state: "Application state synchronization",
|
|
1655
|
+
general: "Message flow through the system"
|
|
1656
|
+
};
|
|
1657
|
+
return descriptions[domain] || descriptions.general;
|
|
1658
|
+
}
|
|
1659
|
+
getUserAction(domain) {
|
|
1660
|
+
const actions = {
|
|
1661
|
+
authentication: "Initiates login",
|
|
1662
|
+
todo: "Manages todo items",
|
|
1663
|
+
state: "Requests state",
|
|
1664
|
+
general: "Interacts"
|
|
1665
|
+
};
|
|
1666
|
+
return actions[domain] || actions.general;
|
|
1667
|
+
}
|
|
1668
|
+
getMessageDescription(messageType) {
|
|
1669
|
+
const type = messageType.toLowerCase();
|
|
1670
|
+
if (type.includes("login"))
|
|
1671
|
+
return "Authenticate user";
|
|
1672
|
+
if (type.includes("logout"))
|
|
1673
|
+
return "End session";
|
|
1674
|
+
if (type.includes("add") || type.includes("create"))
|
|
1675
|
+
return "Create item";
|
|
1676
|
+
if (type.includes("remove") || type.includes("delete"))
|
|
1677
|
+
return "Delete item";
|
|
1678
|
+
if (type.includes("update") || type.includes("toggle"))
|
|
1679
|
+
return "Update item";
|
|
1680
|
+
if (type.includes("get") || type.includes("fetch"))
|
|
1681
|
+
return "Retrieve data";
|
|
1682
|
+
if (type.includes("clear"))
|
|
1683
|
+
return "Clear items";
|
|
1684
|
+
return messageType;
|
|
1685
|
+
}
|
|
1686
|
+
generateStyles() {
|
|
1687
|
+
const parts = [];
|
|
1688
|
+
parts.push(" styles {");
|
|
1689
|
+
const contextStyles = {
|
|
1690
|
+
background: "#2E7D32",
|
|
1691
|
+
content: "#F57C00",
|
|
1692
|
+
popup: "#1976D2",
|
|
1693
|
+
devtools: "#7B1FA2",
|
|
1694
|
+
options: "#0288D1",
|
|
1695
|
+
...this.options.styles
|
|
1696
|
+
};
|
|
1697
|
+
for (const [context, color] of Object.entries(contextStyles)) {
|
|
1698
|
+
if (this.analysis.contexts[context]) {
|
|
1699
|
+
parts.push(` element "extension.${context}" {`);
|
|
1700
|
+
parts.push(` background ${color}`);
|
|
1701
|
+
parts.push(` color #ffffff`);
|
|
1702
|
+
parts.push(" }");
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
parts.push(" }");
|
|
1706
|
+
return parts.join(`
|
|
1707
|
+
`);
|
|
1708
|
+
}
|
|
1709
|
+
getContextTechnology(contextType) {
|
|
1710
|
+
const technologies = {
|
|
1711
|
+
background: "Service Worker / Background Script",
|
|
1712
|
+
content: "Content Script",
|
|
1713
|
+
popup: "Browser Action Popup",
|
|
1714
|
+
devtools: "DevTools Panel",
|
|
1715
|
+
options: "Options Page",
|
|
1716
|
+
offscreen: "Offscreen Document"
|
|
1717
|
+
};
|
|
1718
|
+
return technologies[contextType] || "Extension Context";
|
|
1719
|
+
}
|
|
1720
|
+
toComponentName(messageType) {
|
|
1721
|
+
return messageType.split("_").map((part) => this.capitalize(part.toLowerCase())).join(" ") + " Handler";
|
|
1722
|
+
}
|
|
1723
|
+
toId(name) {
|
|
1724
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
1725
|
+
}
|
|
1726
|
+
toViewName(flowName) {
|
|
1727
|
+
return flowName.split(/[_-]/).map((part) => this.capitalize(part)).join(" ");
|
|
1728
|
+
}
|
|
1729
|
+
capitalize(str) {
|
|
1730
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1731
|
+
}
|
|
1732
|
+
escape(str) {
|
|
1733
|
+
return str.replace(/"/g, "\\\"");
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
function generateStructurizrDSL(analysis, options) {
|
|
1737
|
+
const generator = new StructurizrDSLGenerator(analysis, options);
|
|
1738
|
+
return generator.generate();
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// vendor/visualize/src/runner/export.ts
|
|
1742
|
+
import * as fs4 from "node:fs";
|
|
1743
|
+
import * as path4 from "node:path";
|
|
1744
|
+
import { spawn } from "node:child_process";
|
|
1745
|
+
|
|
1746
|
+
class DiagramExporter {
|
|
1747
|
+
static DOCKER_IMAGE = "structurizr/cli:latest";
|
|
1748
|
+
static DEFAULT_TIMEOUT = 120000;
|
|
1749
|
+
async export(options) {
|
|
1750
|
+
const { dslPath, outputDir, timeout = DiagramExporter.DEFAULT_TIMEOUT } = options;
|
|
1751
|
+
if (!fs4.existsSync(dslPath)) {
|
|
1752
|
+
return {
|
|
1753
|
+
success: false,
|
|
1754
|
+
siteDir: "",
|
|
1755
|
+
error: `DSL file not found: ${dslPath}`
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
if (!fs4.existsSync(outputDir)) {
|
|
1759
|
+
fs4.mkdirSync(outputDir, { recursive: true });
|
|
1760
|
+
}
|
|
1761
|
+
const dockerAvailable = await this.isDockerAvailable();
|
|
1762
|
+
if (!dockerAvailable) {
|
|
1763
|
+
return {
|
|
1764
|
+
success: false,
|
|
1765
|
+
siteDir: "",
|
|
1766
|
+
error: "Docker is not available. Please install Docker to export the site."
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
options.onProgress?.("Checking Structurizr CLI image...");
|
|
1770
|
+
const hasImage = await this.hasImage();
|
|
1771
|
+
if (!hasImage) {
|
|
1772
|
+
options.onProgress?.("Pulling Structurizr CLI image (this may take a moment)...");
|
|
1773
|
+
await this.pullImage((line) => options.onProgress?.(line));
|
|
1774
|
+
}
|
|
1775
|
+
options.onProgress?.("Generating static site...");
|
|
1776
|
+
try {
|
|
1777
|
+
await this.exportStaticSite(dslPath, outputDir, timeout);
|
|
1778
|
+
options.onProgress?.("✓ Static site generated successfully");
|
|
1779
|
+
return {
|
|
1780
|
+
success: true,
|
|
1781
|
+
siteDir: outputDir
|
|
1782
|
+
};
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
return {
|
|
1785
|
+
success: false,
|
|
1786
|
+
siteDir: "",
|
|
1787
|
+
error: `Failed to export static site: ${error}`
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
async exportStaticSite(dslPath, outputDir, timeout) {
|
|
1792
|
+
const dslDir = path4.dirname(dslPath);
|
|
1793
|
+
const dslFileName = path4.basename(dslPath);
|
|
1794
|
+
const workspaceMount = `${dslDir}:/usr/local/structurizr`;
|
|
1795
|
+
const outputMount = `${outputDir}:/output`;
|
|
1796
|
+
const args = [
|
|
1797
|
+
"run",
|
|
1798
|
+
"--rm",
|
|
1799
|
+
"-v",
|
|
1800
|
+
workspaceMount,
|
|
1801
|
+
"-v",
|
|
1802
|
+
outputMount,
|
|
1803
|
+
DiagramExporter.DOCKER_IMAGE,
|
|
1804
|
+
"export",
|
|
1805
|
+
"-workspace",
|
|
1806
|
+
`/usr/local/structurizr/${dslFileName}`,
|
|
1807
|
+
"-format",
|
|
1808
|
+
"static",
|
|
1809
|
+
"-output",
|
|
1810
|
+
"/output"
|
|
1811
|
+
];
|
|
1812
|
+
await this.runDocker(args, timeout);
|
|
1813
|
+
}
|
|
1814
|
+
async isDockerAvailable() {
|
|
1815
|
+
try {
|
|
1816
|
+
await this.runCommand("docker", ["--version"], 5000);
|
|
1817
|
+
return true;
|
|
1818
|
+
} catch {
|
|
1819
|
+
return false;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
async hasImage() {
|
|
1823
|
+
try {
|
|
1824
|
+
const output = await this.runCommand("docker", ["images", "-q", DiagramExporter.DOCKER_IMAGE], 5000);
|
|
1825
|
+
return output.trim().length > 0;
|
|
1826
|
+
} catch {
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
async pullImage(onProgress) {
|
|
1831
|
+
return new Promise((resolve2, reject) => {
|
|
1832
|
+
const proc = spawn("docker", ["pull", DiagramExporter.DOCKER_IMAGE]);
|
|
1833
|
+
proc.stdout.on("data", (data) => {
|
|
1834
|
+
onProgress?.(data.toString().trim());
|
|
1835
|
+
});
|
|
1836
|
+
proc.stderr.on("data", (data) => {
|
|
1837
|
+
onProgress?.(data.toString().trim());
|
|
1838
|
+
});
|
|
1839
|
+
proc.on("close", (code) => {
|
|
1840
|
+
if (code === 0) {
|
|
1841
|
+
resolve2();
|
|
1842
|
+
} else {
|
|
1843
|
+
reject(new Error(`Failed to pull image, exit code: ${code}`));
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
proc.on("error", (error) => {
|
|
1847
|
+
reject(error);
|
|
1848
|
+
});
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
async runDocker(args, timeout) {
|
|
1852
|
+
return this.runCommand("docker", args, timeout);
|
|
1853
|
+
}
|
|
1854
|
+
async runCommand(command, args, timeout) {
|
|
1855
|
+
return new Promise((resolve2, reject) => {
|
|
1856
|
+
const proc = spawn(command, args);
|
|
1857
|
+
let stdout = "";
|
|
1858
|
+
let stderr = "";
|
|
1859
|
+
const timer = setTimeout(() => {
|
|
1860
|
+
proc.kill();
|
|
1861
|
+
reject(new Error(`Command timed out after ${timeout}ms`));
|
|
1862
|
+
}, timeout);
|
|
1863
|
+
proc.stdout.on("data", (data) => {
|
|
1864
|
+
stdout += data.toString();
|
|
1865
|
+
});
|
|
1866
|
+
proc.stderr.on("data", (data) => {
|
|
1867
|
+
stderr += data.toString();
|
|
1868
|
+
});
|
|
1869
|
+
proc.on("close", (code) => {
|
|
1870
|
+
clearTimeout(timer);
|
|
1871
|
+
if (code === 0) {
|
|
1872
|
+
resolve2(stdout);
|
|
1873
|
+
} else {
|
|
1874
|
+
reject(new Error(`Command failed with exit code ${code}: ${stderr}`));
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
proc.on("error", (error) => {
|
|
1878
|
+
clearTimeout(timer);
|
|
1879
|
+
reject(error);
|
|
1880
|
+
});
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
async function exportDiagrams(options) {
|
|
1885
|
+
const exporter = new DiagramExporter;
|
|
1886
|
+
return exporter.export(options);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// vendor/visualize/src/cli.ts
|
|
1890
|
+
var COLORS = {
|
|
1891
|
+
reset: "\x1B[0m",
|
|
1892
|
+
red: "\x1B[31m",
|
|
1893
|
+
green: "\x1B[32m",
|
|
1894
|
+
yellow: "\x1B[33m",
|
|
1895
|
+
blue: "\x1B[34m",
|
|
1896
|
+
gray: "\x1B[90m"
|
|
1897
|
+
};
|
|
1898
|
+
function color(text, colorCode) {
|
|
1899
|
+
return `${colorCode}${text}${COLORS.reset}`;
|
|
1900
|
+
}
|
|
1901
|
+
async function main() {
|
|
1902
|
+
const args = process.argv.slice(2);
|
|
1903
|
+
const command = args[0];
|
|
1904
|
+
switch (command) {
|
|
1905
|
+
case "--generate":
|
|
1906
|
+
case "generate":
|
|
1907
|
+
await generateCommand();
|
|
1908
|
+
break;
|
|
1909
|
+
case "--export":
|
|
1910
|
+
case "export":
|
|
1911
|
+
await exportCommand(args.slice(1));
|
|
1912
|
+
break;
|
|
1913
|
+
case "--serve":
|
|
1914
|
+
case "serve":
|
|
1915
|
+
case "--view":
|
|
1916
|
+
case "view":
|
|
1917
|
+
await serveCommand(args.slice(1));
|
|
1918
|
+
break;
|
|
1919
|
+
case "--help":
|
|
1920
|
+
case "help":
|
|
1921
|
+
showHelp();
|
|
1922
|
+
break;
|
|
1923
|
+
default:
|
|
1924
|
+
await generateCommand();
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
async function generateCommand() {
|
|
1928
|
+
console.log(color(`
|
|
1929
|
+
\uD83D\uDCCA Analyzing architecture...
|
|
1930
|
+
`, COLORS.blue));
|
|
1931
|
+
try {
|
|
1932
|
+
const tsConfigPath = findTsConfig();
|
|
1933
|
+
if (!tsConfigPath) {
|
|
1934
|
+
console.error(color("❌ Could not find tsconfig.json", COLORS.red));
|
|
1935
|
+
console.error(" Run this command from your project root");
|
|
1936
|
+
process.exit(1);
|
|
1937
|
+
}
|
|
1938
|
+
console.log(color(` Using: ${tsConfigPath}`, COLORS.gray));
|
|
1939
|
+
const projectRoot = findProjectRoot();
|
|
1940
|
+
if (!projectRoot) {
|
|
1941
|
+
console.error(color("❌ Could not find manifest.json", COLORS.red));
|
|
1942
|
+
console.error(" Run this command from your extension project root");
|
|
1943
|
+
process.exit(1);
|
|
1944
|
+
}
|
|
1945
|
+
console.log(color(` Project: ${projectRoot}`, COLORS.gray));
|
|
1946
|
+
const analysis = await analyzeArchitecture({
|
|
1947
|
+
tsConfigPath,
|
|
1948
|
+
projectRoot
|
|
1949
|
+
});
|
|
1950
|
+
console.log(color(`
|
|
1951
|
+
✓ Found ${Object.keys(analysis.contexts).length} context(s)`, COLORS.green));
|
|
1952
|
+
console.log(color(`✓ Found ${analysis.messageFlows.length} message flow(s)`, COLORS.green));
|
|
1953
|
+
console.log(color(`✓ Found ${analysis.integrations.length} integration(s)`, COLORS.green));
|
|
1954
|
+
console.log(color(`
|
|
1955
|
+
\uD83D\uDCCB Architecture Summary:
|
|
1956
|
+
`, COLORS.blue));
|
|
1957
|
+
console.log(color(" Contexts:", COLORS.blue));
|
|
1958
|
+
for (const [contextType, contextInfo] of Object.entries(analysis.contexts)) {
|
|
1959
|
+
console.log(color(` • ${contextType}`, COLORS.gray));
|
|
1960
|
+
console.log(color(` - ${contextInfo.handlers.length} handler(s)`, COLORS.gray));
|
|
1961
|
+
console.log(color(` - ${contextInfo.chromeAPIs.length} Chrome API(s)`, COLORS.gray));
|
|
1962
|
+
if (contextInfo.components) {
|
|
1963
|
+
console.log(color(` - ${contextInfo.components.length} component(s)`, COLORS.gray));
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (analysis.integrations.length > 0) {
|
|
1967
|
+
console.log(color(`
|
|
1968
|
+
External Integrations:`, COLORS.blue));
|
|
1969
|
+
for (const integration of analysis.integrations) {
|
|
1970
|
+
console.log(color(` • ${integration.name} (${integration.type})`, COLORS.gray));
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
console.log(color(`
|
|
1974
|
+
\uD83D\uDCDD Generating Structurizr DSL...
|
|
1975
|
+
`, COLORS.blue));
|
|
1976
|
+
const dsl = generateStructurizrDSL(analysis, {
|
|
1977
|
+
includeDynamicDiagrams: true,
|
|
1978
|
+
includeComponentDiagrams: true,
|
|
1979
|
+
componentDiagramContexts: ["background"]
|
|
1980
|
+
});
|
|
1981
|
+
const outputDir = path5.join(process.cwd(), "docs");
|
|
1982
|
+
if (!fs5.existsSync(outputDir)) {
|
|
1983
|
+
fs5.mkdirSync(outputDir, { recursive: true });
|
|
1984
|
+
}
|
|
1985
|
+
const dslPath = path5.join(outputDir, "architecture.dsl");
|
|
1986
|
+
fs5.writeFileSync(dslPath, dsl, "utf-8");
|
|
1987
|
+
console.log(color(`✅ Architecture documentation generated!
|
|
1988
|
+
`, COLORS.green));
|
|
1989
|
+
console.log(` File: ${color(dslPath, COLORS.blue)}`);
|
|
1990
|
+
console.log();
|
|
1991
|
+
console.log(color("\uD83D\uDCDD Next steps:", COLORS.blue));
|
|
1992
|
+
console.log();
|
|
1993
|
+
console.log(" 1. Export diagrams:");
|
|
1994
|
+
console.log(" bun visualize --export");
|
|
1995
|
+
console.log();
|
|
1996
|
+
console.log(" 2. View in browser:");
|
|
1997
|
+
console.log(" bun visualize --serve");
|
|
1998
|
+
console.log();
|
|
1999
|
+
console.log(color("\uD83D\uDCA1 Alternative: Structurizr Lite", COLORS.gray));
|
|
2000
|
+
console.log(color(" docker run -it --rm -p 8080:8080 \\", COLORS.gray));
|
|
2001
|
+
console.log(color(` -v ${outputDir}:/usr/local/structurizr \\`, COLORS.gray));
|
|
2002
|
+
console.log(color(" structurizr/lite", COLORS.gray));
|
|
2003
|
+
console.log();
|
|
2004
|
+
console.log(color("\uD83D\uDCA1 Upload to Structurizr Cloud:", COLORS.gray));
|
|
2005
|
+
console.log(color(" 1. Sign up at https://structurizr.com", COLORS.gray));
|
|
2006
|
+
console.log(color(" 2. Create a workspace and get API credentials", COLORS.gray));
|
|
2007
|
+
console.log(color(" 3. docker run -it --rm -v $(pwd)/docs:/usr/local/structurizr \\", COLORS.gray));
|
|
2008
|
+
console.log(color(" structurizr/cli push -id WORKSPACE_ID -key KEY -secret SECRET \\", COLORS.gray));
|
|
2009
|
+
console.log(color(" -workspace /usr/local/structurizr/architecture.dsl", COLORS.gray));
|
|
2010
|
+
console.log();
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
console.error(color(`
|
|
2013
|
+
❌ Generation failed:`, COLORS.red));
|
|
2014
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
2015
|
+
if (error instanceof Error && error.stack) {
|
|
2016
|
+
console.error(color(`
|
|
2017
|
+
Stack trace:`, COLORS.gray));
|
|
2018
|
+
console.error(color(error.stack, COLORS.gray));
|
|
2019
|
+
}
|
|
2020
|
+
process.exit(1);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
async function exportCommand(args) {
|
|
2024
|
+
console.log(color(`
|
|
2025
|
+
\uD83D\uDCE4 Generating static site...
|
|
2026
|
+
`, COLORS.blue));
|
|
2027
|
+
try {
|
|
2028
|
+
const dslPath = path5.join(process.cwd(), "docs", "architecture.dsl");
|
|
2029
|
+
if (!fs5.existsSync(dslPath)) {
|
|
2030
|
+
console.error(color("❌ DSL file not found", COLORS.red));
|
|
2031
|
+
console.error(" Expected: docs/architecture.dsl");
|
|
2032
|
+
console.error(" Run 'bun visualize' first to generate the DSL");
|
|
2033
|
+
process.exit(1);
|
|
2034
|
+
}
|
|
2035
|
+
const outputDir = path5.join(process.cwd(), "docs", "site");
|
|
2036
|
+
console.log(color(` DSL: ${dslPath}`, COLORS.gray));
|
|
2037
|
+
console.log(color(` Output: ${outputDir}`, COLORS.gray));
|
|
2038
|
+
console.log();
|
|
2039
|
+
const result = await exportDiagrams({
|
|
2040
|
+
dslPath,
|
|
2041
|
+
outputDir,
|
|
2042
|
+
onProgress: (message) => {
|
|
2043
|
+
console.log(color(` ${message}`, COLORS.gray));
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
if (!result.success) {
|
|
2047
|
+
console.error(color(`
|
|
2048
|
+
❌ Export failed:`, COLORS.red));
|
|
2049
|
+
console.error(` ${result.error}`);
|
|
2050
|
+
process.exit(1);
|
|
2051
|
+
}
|
|
2052
|
+
console.log(color(`
|
|
2053
|
+
✅ Static site generated!
|
|
2054
|
+
`, COLORS.green));
|
|
2055
|
+
console.log(color("\uD83D\uDCC1 Location:", COLORS.blue));
|
|
2056
|
+
console.log(` ${outputDir}`);
|
|
2057
|
+
console.log();
|
|
2058
|
+
console.log(color("\uD83D\uDCA1 Next steps:", COLORS.gray));
|
|
2059
|
+
console.log(color(" • View: bun visualize --serve", COLORS.gray));
|
|
2060
|
+
console.log(color(" • Or open: docs/site/index.html", COLORS.gray));
|
|
2061
|
+
console.log();
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
console.error(color(`
|
|
2064
|
+
❌ Export failed:`, COLORS.red));
|
|
2065
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
2066
|
+
if (error instanceof Error && error.stack) {
|
|
2067
|
+
console.error(color(`
|
|
2068
|
+
Stack trace:`, COLORS.gray));
|
|
2069
|
+
console.error(color(error.stack, COLORS.gray));
|
|
2070
|
+
}
|
|
2071
|
+
process.exit(1);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
async function serveCommand(args) {
|
|
2075
|
+
console.log(color(`
|
|
2076
|
+
\uD83C\uDF10 Starting static site server...
|
|
2077
|
+
`, COLORS.blue));
|
|
2078
|
+
try {
|
|
2079
|
+
const siteDir = path5.join(process.cwd(), "docs", "site");
|
|
2080
|
+
const indexPath = path5.join(siteDir, "index.html");
|
|
2081
|
+
if (!fs5.existsSync(indexPath)) {
|
|
2082
|
+
console.error(color("❌ Static site not found", COLORS.red));
|
|
2083
|
+
console.error(" Expected: docs/site/index.html");
|
|
2084
|
+
console.error(" Run 'bun visualize --export' first to generate the site");
|
|
2085
|
+
process.exit(1);
|
|
2086
|
+
}
|
|
2087
|
+
const portArg = args.find((arg) => arg.startsWith("--port="));
|
|
2088
|
+
const port = portArg ? parseInt(portArg.replace("--port=", "")) : 3000;
|
|
2089
|
+
console.log(color(` Site: ${siteDir}`, COLORS.gray));
|
|
2090
|
+
console.log(color(` Port: ${port}`, COLORS.gray));
|
|
2091
|
+
console.log();
|
|
2092
|
+
const BunGlobal = globalThis.Bun;
|
|
2093
|
+
if (!BunGlobal) {
|
|
2094
|
+
throw new Error("Bun runtime is required to run the server");
|
|
2095
|
+
}
|
|
2096
|
+
const server = BunGlobal.serve({
|
|
2097
|
+
port,
|
|
2098
|
+
fetch(req) {
|
|
2099
|
+
const url = new URL(req.url);
|
|
2100
|
+
let filePath = path5.join(siteDir, url.pathname === "/" ? "index.html" : url.pathname);
|
|
2101
|
+
if (fs5.existsSync(filePath) && fs5.statSync(filePath).isFile()) {
|
|
2102
|
+
const file = BunGlobal.file(filePath);
|
|
2103
|
+
return new Response(file);
|
|
2104
|
+
}
|
|
2105
|
+
return new Response("Not found", { status: 404 });
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
console.log(color(`
|
|
2109
|
+
✅ Server started!
|
|
2110
|
+
`, COLORS.green));
|
|
2111
|
+
console.log(` ${color(`http://localhost:${port}`, COLORS.blue)}`);
|
|
2112
|
+
console.log();
|
|
2113
|
+
console.log(color("Press Ctrl+C to stop the server", COLORS.gray));
|
|
2114
|
+
console.log();
|
|
2115
|
+
if (process.platform === "darwin") {
|
|
2116
|
+
await BunGlobal.spawn(["open", `http://localhost:${port}`]);
|
|
2117
|
+
} else if (process.platform === "linux") {
|
|
2118
|
+
await BunGlobal.spawn(["xdg-open", `http://localhost:${port}`]);
|
|
2119
|
+
} else if (process.platform === "win32") {
|
|
2120
|
+
await BunGlobal.spawn(["cmd", "/c", "start", `http://localhost:${port}`]);
|
|
2121
|
+
}
|
|
2122
|
+
await new Promise(() => {});
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
console.error(color(`
|
|
2125
|
+
❌ Failed to start server:`, COLORS.red));
|
|
2126
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
2127
|
+
process.exit(1);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
function showHelp() {
|
|
2131
|
+
console.log(`
|
|
2132
|
+
${color("bun visualize", COLORS.blue)} - Architecture visualization for web extensions
|
|
2133
|
+
|
|
2134
|
+
${color("Commands:", COLORS.blue)}
|
|
2135
|
+
|
|
2136
|
+
${color("bun visualize", COLORS.green)}
|
|
2137
|
+
${color("bun visualize --generate", COLORS.green)}
|
|
2138
|
+
Analyze codebase and generate Structurizr DSL
|
|
2139
|
+
|
|
2140
|
+
${color("bun visualize --export", COLORS.green)}
|
|
2141
|
+
Generate static HTML site with interactive diagrams (requires Docker)
|
|
2142
|
+
|
|
2143
|
+
${color("bun visualize --serve", COLORS.green)}
|
|
2144
|
+
${color("bun visualize --serve --port=3000", COLORS.green)}
|
|
2145
|
+
Serve the static site in browser
|
|
2146
|
+
|
|
2147
|
+
${color("bun visualize --help", COLORS.green)}
|
|
2148
|
+
Show this help message
|
|
2149
|
+
|
|
2150
|
+
${color("Getting Started:", COLORS.blue)}
|
|
2151
|
+
|
|
2152
|
+
1. Run ${color("bun visualize", COLORS.green)} from your extension project root
|
|
2153
|
+
2. Find generated ${color("docs/architecture.dsl", COLORS.blue)}
|
|
2154
|
+
3. View with Structurizr Lite (see instructions after generation)
|
|
2155
|
+
|
|
2156
|
+
${color("What gets generated:", COLORS.blue)}
|
|
2157
|
+
|
|
2158
|
+
• System Context diagram - Extension + external systems
|
|
2159
|
+
• Container diagram - Extension contexts (background, content, popup, etc.)
|
|
2160
|
+
• Component diagrams - Internal components within contexts
|
|
2161
|
+
• Dynamic diagrams - Message flows between contexts
|
|
2162
|
+
|
|
2163
|
+
${color("Learn More:", COLORS.blue)}
|
|
2164
|
+
|
|
2165
|
+
Documentation: https://github.com/fairfox/web-ext
|
|
2166
|
+
Structurizr: https://structurizr.com
|
|
2167
|
+
C4 Model: https://c4model.com
|
|
2168
|
+
`);
|
|
2169
|
+
}
|
|
2170
|
+
function findTsConfig() {
|
|
2171
|
+
const locations = [
|
|
2172
|
+
path5.join(process.cwd(), "tsconfig.json"),
|
|
2173
|
+
path5.join(process.cwd(), "..", "tsconfig.json")
|
|
2174
|
+
];
|
|
2175
|
+
for (const loc of locations) {
|
|
2176
|
+
if (fs5.existsSync(loc)) {
|
|
2177
|
+
return loc;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
return null;
|
|
2181
|
+
}
|
|
2182
|
+
function findProjectRoot() {
|
|
2183
|
+
const locations = [process.cwd(), path5.join(process.cwd(), "..")];
|
|
2184
|
+
for (const loc of locations) {
|
|
2185
|
+
const manifestPath = path5.join(loc, "manifest.json");
|
|
2186
|
+
if (fs5.existsSync(manifestPath)) {
|
|
2187
|
+
return loc;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
return null;
|
|
2191
|
+
}
|
|
2192
|
+
main().catch((error) => {
|
|
2193
|
+
console.error(color(`
|
|
2194
|
+
❌ Fatal error:`, COLORS.red));
|
|
2195
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
2196
|
+
if (error instanceof Error && error.stack) {
|
|
2197
|
+
console.error(color(`
|
|
2198
|
+
Stack trace:`, COLORS.gray));
|
|
2199
|
+
console.error(color(error.stack, COLORS.gray));
|
|
2200
|
+
}
|
|
2201
|
+
process.exit(1);
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2204
|
+
//# debugId=A7BBE9A42DC131F564756E2164756E21
|