@launchsecure/launch-kit 0.0.8 → 0.0.10
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/dist/chart-client/assets/index-DIPKWwEJ.js +404 -0
- package/dist/chart-client/assets/index-DjnSR-Hf.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-BIpeUMYR.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +608 -336
- package/dist/server/cli.js +700 -325
- package/dist/server/graph/queries/db-calls.scm +8 -0
- package/dist/server/graph/queries/deep/conditions.scm +10 -0
- package/dist/server/graph/queries/deep/jsx-semantic.scm +21 -0
- package/dist/server/graph/queries/deep/request-params.scm +24 -0
- package/dist/server/graph/queries/deep/responses.scm +26 -0
- package/dist/server/graph/queries/deep/state-hooks.scm +23 -0
- package/dist/server/graph/queries/deep/variables.scm +17 -0
- package/dist/server/graph/queries/exports.scm +66 -0
- package/dist/server/graph/queries/fetch-calls.scm +57 -0
- package/dist/server/graph/queries/imports.scm +14 -0
- package/dist/server/graph/queries/jsx-elements.scm +14 -0
- package/dist/server/graph/queries/navigations.scm +85 -0
- package/dist/server/graph/queries/wrappers.scm +8 -0
- package/dist/server/graph-mcp-entry.js +722 -342
- package/package.json +5 -2
- package/dist/chart-client/assets/index-0Xm1mXjM.js +0 -379
- package/dist/chart-client/assets/index-C-OUsIfD.css +0 -1
- package/dist/client/assets/index-DbqEe7we.css +0 -32
- /package/dist/client/assets/{index-Ci95xk2_.js → index-qzK1AD2G.js} +0 -0
|
@@ -155,37 +155,134 @@ var init_config = __esm({
|
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
// src/server/graph/core/
|
|
159
|
-
function
|
|
160
|
-
if (
|
|
161
|
-
|
|
158
|
+
// src/server/graph/core/ts-extractor.ts
|
|
159
|
+
async function initTreeSitter() {
|
|
160
|
+
if (initialized) return;
|
|
161
|
+
if (initPromise) return initPromise;
|
|
162
|
+
initPromise = (async () => {
|
|
163
|
+
const TreeSitter = require("web-tree-sitter");
|
|
164
|
+
await TreeSitter.init();
|
|
165
|
+
const wasmPath = require.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
|
|
166
|
+
tsxLanguage = await TreeSitter.Language.load(wasmPath);
|
|
167
|
+
parserInstance = new TreeSitter();
|
|
168
|
+
parserInstance.setLanguage(tsxLanguage);
|
|
169
|
+
initialized = true;
|
|
170
|
+
})();
|
|
171
|
+
return initPromise;
|
|
172
|
+
}
|
|
173
|
+
function ensureInit() {
|
|
174
|
+
if (!initialized || !tsxLanguage || !parserInstance) {
|
|
175
|
+
throw new Error("Tree-sitter not initialized. Call initTreeSitter() first.");
|
|
162
176
|
}
|
|
163
|
-
return tsModule;
|
|
164
177
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
178
|
+
function getQuery(name) {
|
|
179
|
+
ensureInit();
|
|
180
|
+
const cached = queryCache.get(name);
|
|
181
|
+
if (cached) return cached;
|
|
182
|
+
const scmPath = (0, import_node_path3.join)(queriesDir, `${name}.scm`);
|
|
183
|
+
const scm = (0, import_node_fs3.readFileSync)(scmPath, "utf-8");
|
|
184
|
+
const query = tsxLanguage.query(scm);
|
|
185
|
+
queryCache.set(name, query);
|
|
186
|
+
return query;
|
|
187
|
+
}
|
|
188
|
+
function parseSource(absPath) {
|
|
189
|
+
ensureInit();
|
|
167
190
|
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
return parserInstance.parse(content);
|
|
192
|
+
}
|
|
193
|
+
function setExtractorConfig(config) {
|
|
194
|
+
extraDbIdentifiers = config.dbIdentifiers ?? [];
|
|
195
|
+
extraMutationMethods = config.mutationMethods ?? [];
|
|
196
|
+
}
|
|
197
|
+
function getMutationMethods() {
|
|
198
|
+
return /* @__PURE__ */ new Set([...PRISMA_MUTATION_METHODS_BUILTIN, ...extraMutationMethods]);
|
|
199
|
+
}
|
|
200
|
+
function getFallbackDbIdentifiers() {
|
|
201
|
+
return /* @__PURE__ */ new Set([...DB_IDENTIFIERS_FALLBACK, ...extraDbIdentifiers]);
|
|
202
|
+
}
|
|
203
|
+
function looksLikeUrl(s) {
|
|
204
|
+
return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
|
|
205
|
+
}
|
|
206
|
+
function templateStartsWithSlash(text) {
|
|
207
|
+
const content = text.slice(1);
|
|
208
|
+
return content.startsWith("/");
|
|
209
|
+
}
|
|
210
|
+
function captureMap(match) {
|
|
211
|
+
const map = {};
|
|
212
|
+
for (const c of match.captures) {
|
|
213
|
+
map[c.name] = c.node.text;
|
|
214
|
+
}
|
|
215
|
+
return map;
|
|
216
|
+
}
|
|
217
|
+
function childrenOfType(node, type) {
|
|
218
|
+
return node.children.filter((n) => n.type === type);
|
|
219
|
+
}
|
|
220
|
+
function childOfType(node, type) {
|
|
221
|
+
return node.children.find((n) => n.type === type);
|
|
222
|
+
}
|
|
223
|
+
function parseFileTS(absPath) {
|
|
224
|
+
const tree = parseSource(absPath);
|
|
225
|
+
const root = tree.rootNode;
|
|
226
|
+
const imports = [];
|
|
227
|
+
const importStatements = childrenOfType(root, "import_statement");
|
|
228
|
+
for (const stmt of importStatements) {
|
|
229
|
+
const sourceNode = childOfType(stmt, "string");
|
|
230
|
+
const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
|
|
231
|
+
if (!frag) continue;
|
|
232
|
+
const specifier = frag.text;
|
|
233
|
+
const hasTypeKeyword = stmt.children.some(
|
|
234
|
+
(n) => n.type === "type" && n.text === "type"
|
|
235
|
+
);
|
|
236
|
+
const clause = childOfType(stmt, "import_clause");
|
|
237
|
+
if (!clause) {
|
|
238
|
+
imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const names = [];
|
|
242
|
+
const typeNames = /* @__PURE__ */ new Set();
|
|
243
|
+
const defaultId = childOfType(clause, "identifier");
|
|
244
|
+
if (defaultId) names.push(defaultId.text);
|
|
245
|
+
const nsImport = childOfType(clause, "namespace_import");
|
|
246
|
+
if (nsImport) {
|
|
247
|
+
const nsId = childOfType(nsImport, "identifier");
|
|
248
|
+
if (nsId) names.push(nsId.text);
|
|
249
|
+
}
|
|
250
|
+
const namedImports = childOfType(clause, "named_imports");
|
|
251
|
+
if (namedImports) {
|
|
252
|
+
for (const spec of childrenOfType(namedImports, "import_specifier")) {
|
|
253
|
+
const identifiers = childrenOfType(spec, "identifier");
|
|
254
|
+
const importedName = identifiers.length > 1 ? identifiers[identifiers.length - 1].text : identifiers[0]?.text;
|
|
255
|
+
if (importedName) {
|
|
256
|
+
names.push(importedName);
|
|
257
|
+
const specIsType = spec.children.some(
|
|
258
|
+
(n) => n.type === "type" && n.text === "type"
|
|
259
|
+
);
|
|
260
|
+
if (specIsType) typeNames.add(importedName);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (names.length > 0 || hasTypeKeyword) {
|
|
265
|
+
imports.push({ names, specifier, isTypeOnly: hasTypeKeyword, typeNames });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const importQuery = getQuery("imports");
|
|
269
|
+
const importMatches = importQuery.matches(root);
|
|
270
|
+
for (const m of importMatches) {
|
|
271
|
+
const caps = captureMap(m);
|
|
272
|
+
if (caps["import.dynamic"]) {
|
|
273
|
+
imports.push({
|
|
274
|
+
names: [],
|
|
275
|
+
specifier: caps["import.dynamic"],
|
|
276
|
+
isTypeOnly: false,
|
|
277
|
+
typeNames: /* @__PURE__ */ new Set()
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
178
281
|
const exportsSet = /* @__PURE__ */ new Set();
|
|
179
282
|
const exportsOrdered = [];
|
|
180
283
|
let defaultName = null;
|
|
181
284
|
let firstValueExport = null;
|
|
182
285
|
let firstTypeExport = null;
|
|
183
|
-
const imports = [];
|
|
184
|
-
const reExports = [];
|
|
185
|
-
const jsxElements = /* @__PURE__ */ new Set();
|
|
186
|
-
const navigations = [];
|
|
187
|
-
const fetchCalls = [];
|
|
188
|
-
const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
|
|
189
286
|
function addExport(name2, kind) {
|
|
190
287
|
if (!exportsSet.has(name2)) {
|
|
191
288
|
exportsSet.add(name2);
|
|
@@ -195,298 +292,354 @@ function parseFile(absPath) {
|
|
|
195
292
|
else if (kind === "value" && !firstValueExport) firstValueExport = name2;
|
|
196
293
|
else if (kind === "type" && !firstTypeExport) firstTypeExport = name2;
|
|
197
294
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
295
|
+
const exportQuery = getQuery("exports");
|
|
296
|
+
const exportMatches = exportQuery.matches(root);
|
|
297
|
+
for (const m of exportMatches) {
|
|
298
|
+
const caps = captureMap(m);
|
|
299
|
+
if (caps["export.default.func"]) addExport(caps["export.default.func"], "default");
|
|
300
|
+
else if (caps["export.default.class"]) addExport(caps["export.default.class"], "default");
|
|
301
|
+
else if (caps["export.default.value"]) addExport(caps["export.default.value"], "default");
|
|
302
|
+
else if (caps["export.named.func"]) addExport(caps["export.named.func"], "value");
|
|
303
|
+
else if (caps["export.named.class"]) addExport(caps["export.named.class"], "value");
|
|
304
|
+
else if (caps["export.named.const"]) addExport(caps["export.named.const"], "value");
|
|
305
|
+
else if (caps["export.named.enum"]) addExport(caps["export.named.enum"], "value");
|
|
306
|
+
else if (caps["export.named.type"]) addExport(caps["export.named.type"], "type");
|
|
307
|
+
else if (caps["export.named.interface"]) addExport(caps["export.named.interface"], "type");
|
|
308
|
+
else if (caps["export.bare.name"]) addExport(caps["export.bare.name"], "value");
|
|
309
|
+
if (caps["reexport.name"]) addExport(caps["reexport.name"], "value");
|
|
310
|
+
}
|
|
311
|
+
for (const stmt of childrenOfType(root, "export_statement")) {
|
|
312
|
+
const hasDefault = stmt.children.some((n) => n.text === "default");
|
|
313
|
+
if (hasDefault && !defaultName) {
|
|
314
|
+
defaultName = "default";
|
|
315
|
+
}
|
|
201
316
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
317
|
+
const reExports = [];
|
|
318
|
+
for (const m of exportMatches) {
|
|
319
|
+
const caps = captureMap(m);
|
|
320
|
+
if (caps["reexport.name"] && caps["reexport.source"]) {
|
|
321
|
+
reExports.push({ name: caps["reexport.name"], from: caps["reexport.source"] });
|
|
205
322
|
}
|
|
206
|
-
if (
|
|
207
|
-
|
|
323
|
+
if (caps["reexport.wildcard.source"]) {
|
|
324
|
+
reExports.push({ name: "*", from: caps["reexport.wildcard.source"], isWildcard: true });
|
|
208
325
|
}
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
function looksLikeUrl(s) {
|
|
212
|
-
return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
|
|
213
326
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
327
|
+
const jsxElements = /* @__PURE__ */ new Set();
|
|
328
|
+
const jsxQuery = getQuery("jsx-elements");
|
|
329
|
+
const jsxCaptures = jsxQuery.captures(root);
|
|
330
|
+
for (const c of jsxCaptures) {
|
|
331
|
+
if (c.name === "jsx.tag") {
|
|
332
|
+
if (/^[A-Z]/.test(c.node.text)) {
|
|
333
|
+
jsxElements.add(c.node.text);
|
|
334
|
+
}
|
|
335
|
+
} else if (c.name === "jsx.member_tag") {
|
|
336
|
+
const rootName = c.node.text.split(".")[0];
|
|
337
|
+
if (rootName && /^[A-Z]/.test(rootName)) {
|
|
338
|
+
jsxElements.add(rootName);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
217
341
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
342
|
+
const navigations = [];
|
|
343
|
+
const navQuery = getQuery("navigations");
|
|
344
|
+
const navMatches = navQuery.matches(root);
|
|
345
|
+
for (const m of navMatches) {
|
|
346
|
+
const caps = captureMap(m);
|
|
347
|
+
if (caps["nav.target.literal"] && caps["nav.method"]) {
|
|
348
|
+
navigations.push({
|
|
349
|
+
kind: caps["nav.method"] === "push" ? "router-push" : "router-replace",
|
|
350
|
+
target: caps["nav.target.literal"],
|
|
351
|
+
isTemplate: false
|
|
352
|
+
});
|
|
222
353
|
}
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
354
|
+
if (caps["nav.target.template"] && caps["nav.method.template"]) {
|
|
355
|
+
navigations.push({
|
|
356
|
+
kind: caps["nav.method.template"] === "push" ? "router-push" : "router-replace",
|
|
357
|
+
target: caps["nav.target.template"],
|
|
358
|
+
isTemplate: true
|
|
359
|
+
});
|
|
226
360
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
361
|
+
const linkLiteral = caps["nav.link.literal"] || caps["nav.link.literal.self"];
|
|
362
|
+
if (linkLiteral) {
|
|
363
|
+
navigations.push({ kind: "link-href", target: linkLiteral, isTemplate: false });
|
|
364
|
+
}
|
|
365
|
+
const linkTemplate = caps["nav.link.template"] || caps["nav.link.template.self"];
|
|
366
|
+
if (linkTemplate) {
|
|
367
|
+
navigations.push({ kind: "link-href", target: linkTemplate, isTemplate: true });
|
|
368
|
+
}
|
|
369
|
+
if (caps["nav.window.literal"]) {
|
|
370
|
+
navigations.push({ kind: "window-location", target: caps["nav.window.literal"], isTemplate: false });
|
|
371
|
+
}
|
|
372
|
+
if (caps["nav.window.assign.target"]) {
|
|
373
|
+
navigations.push({ kind: "window-location", target: caps["nav.window.assign.target"], isTemplate: false });
|
|
235
374
|
}
|
|
236
|
-
return null;
|
|
237
375
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
376
|
+
const fetchCalls = [];
|
|
377
|
+
const fetchQuery = getQuery("fetch-calls");
|
|
378
|
+
const fetchMatches = fetchQuery.matches(root);
|
|
379
|
+
for (const m of fetchMatches) {
|
|
380
|
+
const caps = captureMap(m);
|
|
381
|
+
if (caps["fetch.url.literal"] && looksLikeUrl(caps["fetch.url.literal"])) {
|
|
382
|
+
fetchCalls.push({ url: caps["fetch.url.literal"], isTemplate: false, kind: "fetch" });
|
|
383
|
+
}
|
|
384
|
+
if (caps["fetch.url.template"] && templateStartsWithSlash(caps["fetch.url.template"])) {
|
|
385
|
+
fetchCalls.push({ url: caps["fetch.url.template"], isTemplate: true, kind: "fetch" });
|
|
386
|
+
}
|
|
387
|
+
const clientUrl = caps["fetch.client.url.literal"] || caps["fetch.await.url.literal"];
|
|
388
|
+
const clientMethod = caps["fetch.method"] || caps["fetch.await.method"];
|
|
389
|
+
if (clientUrl && clientMethod && looksLikeUrl(clientUrl)) {
|
|
390
|
+
fetchCalls.push({
|
|
391
|
+
method: clientMethod.toUpperCase(),
|
|
392
|
+
url: clientUrl,
|
|
393
|
+
isTemplate: false,
|
|
394
|
+
kind: "client-method"
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const clientUrlTpl = caps["fetch.client.url.template"] || caps["fetch.await.url.template"];
|
|
398
|
+
const clientMethodTpl = caps["fetch.method.template"] || caps["fetch.await.method.template"];
|
|
399
|
+
if (clientUrlTpl && clientMethodTpl && templateStartsWithSlash(clientUrlTpl)) {
|
|
400
|
+
fetchCalls.push({
|
|
401
|
+
method: clientMethodTpl.toUpperCase(),
|
|
402
|
+
url: clientUrlTpl,
|
|
403
|
+
isTemplate: true,
|
|
404
|
+
kind: "client-method"
|
|
405
|
+
});
|
|
265
406
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
407
|
+
}
|
|
408
|
+
const name = defaultName ?? firstValueExport ?? firstTypeExport ?? "";
|
|
409
|
+
return { name, exports: exportsOrdered, imports, reExports, jsxElements, navigations, fetchCalls };
|
|
410
|
+
}
|
|
411
|
+
function extractDbCallsTS(absPath) {
|
|
412
|
+
const tree = parseSource(absPath);
|
|
413
|
+
const root = tree.rootNode;
|
|
414
|
+
const dbQuery = getQuery("db-calls");
|
|
415
|
+
const matches = dbQuery.matches(root);
|
|
416
|
+
const dbIdentifiers = /* @__PURE__ */ new Set();
|
|
417
|
+
for (const stmt of childrenOfType(root, "import_statement")) {
|
|
418
|
+
const sourceNode = childOfType(stmt, "string");
|
|
419
|
+
const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
|
|
420
|
+
if (!frag) continue;
|
|
421
|
+
const spec = frag.text;
|
|
422
|
+
if (spec.includes("prisma") || spec.includes("/db") || spec === "@prisma/client") {
|
|
423
|
+
const clause = childOfType(stmt, "import_clause");
|
|
424
|
+
if (clause) {
|
|
425
|
+
const defaultId = childOfType(clause, "identifier");
|
|
426
|
+
if (defaultId) dbIdentifiers.add(defaultId.text);
|
|
427
|
+
const named = childOfType(clause, "named_imports");
|
|
428
|
+
if (named) {
|
|
429
|
+
for (const specNode of childrenOfType(named, "import_specifier")) {
|
|
430
|
+
const ids = childrenOfType(specNode, "identifier");
|
|
431
|
+
const importedName = ids[ids.length - 1];
|
|
432
|
+
if (importedName) dbIdentifiers.add(importedName.text);
|
|
274
433
|
}
|
|
275
434
|
}
|
|
276
|
-
} else if (!node.exportClause && fromSpec) {
|
|
277
|
-
reExports.push({ name: "*", from: fromSpec, isWildcard: true });
|
|
278
435
|
}
|
|
279
436
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
437
|
+
}
|
|
438
|
+
if (dbIdentifiers.size === 0) {
|
|
439
|
+
for (const id of getFallbackDbIdentifiers()) dbIdentifiers.add(id);
|
|
440
|
+
} else {
|
|
441
|
+
for (const id of extraDbIdentifiers) dbIdentifiers.add(id);
|
|
442
|
+
}
|
|
443
|
+
const calls = [];
|
|
444
|
+
const seen = /* @__PURE__ */ new Set();
|
|
445
|
+
for (const m of matches) {
|
|
446
|
+
const caps = captureMap(m);
|
|
447
|
+
const identifier = caps["db.identifier"];
|
|
448
|
+
const model = caps["db.model"];
|
|
449
|
+
const method = caps["db.method"];
|
|
450
|
+
if (!identifier || !model || !method) continue;
|
|
451
|
+
if (!dbIdentifiers.has(identifier)) continue;
|
|
452
|
+
const key = `${model}.${method}`;
|
|
453
|
+
if (seen.has(key)) continue;
|
|
454
|
+
seen.add(key);
|
|
455
|
+
calls.push({ model, method, isMutation: getMutationMethods().has(method) });
|
|
456
|
+
}
|
|
457
|
+
return calls;
|
|
458
|
+
}
|
|
459
|
+
function extractAuthWrappersTS(absPath) {
|
|
460
|
+
const tree = parseSource(absPath);
|
|
461
|
+
const root = tree.rootNode;
|
|
462
|
+
const wrapperQuery = getQuery("wrappers");
|
|
463
|
+
const matches = wrapperQuery.matches(root);
|
|
464
|
+
const wrappers = /* @__PURE__ */ new Set();
|
|
465
|
+
for (const m of matches) {
|
|
466
|
+
const caps = captureMap(m);
|
|
467
|
+
if (caps["wrapper.fn_name"]) {
|
|
468
|
+
wrappers.add(caps["wrapper.fn_name"]);
|
|
290
469
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
470
|
+
}
|
|
471
|
+
return wrappers;
|
|
472
|
+
}
|
|
473
|
+
function trunc(s, max = 120) {
|
|
474
|
+
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
475
|
+
}
|
|
476
|
+
function extractDeep(absPath) {
|
|
477
|
+
const tree = parseSource(absPath);
|
|
478
|
+
const root = tree.rootNode;
|
|
479
|
+
const elements = [];
|
|
480
|
+
const elQuery = getQuery("deep/jsx-semantic");
|
|
481
|
+
const elMatches = elQuery.matches(root);
|
|
482
|
+
const elementMap = /* @__PURE__ */ new Map();
|
|
483
|
+
for (const m of elMatches) {
|
|
484
|
+
const tag = m.captures.find((c) => c.name === "el.tag")?.node;
|
|
485
|
+
if (!tag || !/^[A-Z]/.test(tag.text)) continue;
|
|
486
|
+
const elNode = m.captures.find((c) => c.name === "el.self" || c.name === "el.open")?.node;
|
|
487
|
+
const key = elNode ? `${elNode.startPosition.row}:${elNode.startPosition.column}` : `${tag.startPosition.row}:${tag.startPosition.column}`;
|
|
488
|
+
if (!elementMap.has(key)) {
|
|
489
|
+
elementMap.set(key, { tag: tag.text, props: {}, nodeKey: key });
|
|
297
490
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
491
|
+
const entry = elementMap.get(key);
|
|
492
|
+
const propName = m.captures.find((c) => c.name === "el.prop.name")?.node.text;
|
|
493
|
+
const propVal = m.captures.find((c) => c.name === "el.prop.value.str")?.node.text;
|
|
494
|
+
const propExpr = m.captures.find((c) => c.name === "el.prop.value.expr")?.node.text;
|
|
495
|
+
if (propName) {
|
|
496
|
+
entry.props[propName] = propVal ?? (propExpr ? trunc(propExpr, 60) : "true");
|
|
301
497
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
498
|
+
}
|
|
499
|
+
for (const m of elMatches) {
|
|
500
|
+
const textTag = m.captures.find((c) => c.name === "el.text.tag")?.node;
|
|
501
|
+
const textContent = m.captures.find((c) => c.name === "el.text.content")?.node;
|
|
502
|
+
if (textTag && textContent && /^[A-Z]/.test(textTag.text)) {
|
|
503
|
+
const key = `${textTag.startPosition.row}:${textTag.startPosition.column}`;
|
|
504
|
+
if (!elementMap.has(key)) {
|
|
505
|
+
elementMap.set(key, { tag: textTag.text, props: {}, nodeKey: key });
|
|
307
506
|
}
|
|
507
|
+
const entry = elementMap.get(key);
|
|
508
|
+
const text = textContent.text.trim();
|
|
509
|
+
if (text) entry.props["_text"] = text;
|
|
308
510
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (
|
|
314
|
-
|
|
511
|
+
}
|
|
512
|
+
for (const entry of elementMap.values()) {
|
|
513
|
+
const el = { tag: entry.tag, props: entry.props };
|
|
514
|
+
const hasExpr = Object.values(entry.props).some((v) => v.includes("{") || v.includes("("));
|
|
515
|
+
if (hasExpr) el.dynamic = true;
|
|
516
|
+
if (entry.props["_text"]) {
|
|
517
|
+
el.text = entry.props["_text"];
|
|
518
|
+
delete el.props["_text"];
|
|
315
519
|
}
|
|
316
|
-
|
|
317
|
-
|
|
520
|
+
elements.push(el);
|
|
521
|
+
}
|
|
522
|
+
const stateVars = [];
|
|
523
|
+
const hookQuery = getQuery("deep/state-hooks");
|
|
524
|
+
const hookMatches = hookQuery.matches(root);
|
|
525
|
+
for (const m of hookMatches) {
|
|
526
|
+
const caps = captureMap(m);
|
|
527
|
+
if (caps["hook.var"] && caps["hook.setter"] && caps["hook.fn"]) {
|
|
528
|
+
stateVars.push({
|
|
529
|
+
name: caps["hook.var"],
|
|
530
|
+
setter: caps["hook.setter"],
|
|
531
|
+
hook: caps["hook.fn"],
|
|
532
|
+
init: trunc(caps["hook.init"] ?? "", 80)
|
|
533
|
+
});
|
|
318
534
|
}
|
|
319
|
-
if (
|
|
320
|
-
|
|
535
|
+
if (caps["reducer.var"] && caps["reducer.dispatch"]) {
|
|
536
|
+
stateVars.push({
|
|
537
|
+
name: caps["reducer.var"],
|
|
538
|
+
setter: caps["reducer.dispatch"],
|
|
539
|
+
hook: "useReducer",
|
|
540
|
+
init: trunc(caps["reducer.init"] ?? "", 80)
|
|
541
|
+
});
|
|
321
542
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
543
|
+
}
|
|
544
|
+
const conditions = [];
|
|
545
|
+
const condQuery = getQuery("deep/conditions");
|
|
546
|
+
const condMatches = condQuery.matches(root);
|
|
547
|
+
for (const m of condMatches) {
|
|
548
|
+
const caps = captureMap(m);
|
|
549
|
+
if (caps["cond.test"]) {
|
|
550
|
+
conditions.push({
|
|
551
|
+
kind: "if",
|
|
552
|
+
test: trunc(caps["cond.test"], 100),
|
|
553
|
+
consequence: caps["cond.consequence"] ? trunc(caps["cond.consequence"], 80) : void 0
|
|
554
|
+
});
|
|
333
555
|
}
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const parsed = extractTargetFromExpression(arg);
|
|
340
|
-
if (parsed) {
|
|
341
|
-
navigations.push({
|
|
342
|
-
kind: expr.name.text === "push" ? "router-push" : "router-replace",
|
|
343
|
-
target: parsed.target,
|
|
344
|
-
isTemplate: parsed.isTemplate
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
556
|
+
if (caps["ternary.test"]) {
|
|
557
|
+
conditions.push({
|
|
558
|
+
kind: "ternary",
|
|
559
|
+
test: trunc(caps["ternary.test"], 100)
|
|
560
|
+
});
|
|
349
561
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
|
|
365
|
-
const methodName = expr.name.text;
|
|
366
|
-
if (HTTP_METHODS.has(methodName)) {
|
|
367
|
-
const extracted = extractUrlFromFetchArg(firstArg);
|
|
368
|
-
if (extracted) {
|
|
369
|
-
fetchCalls.push({
|
|
370
|
-
method: methodName.toUpperCase(),
|
|
371
|
-
url: extracted.url,
|
|
372
|
-
isTemplate: extracted.isTemplate,
|
|
373
|
-
...extracted.isConcat ? { isConcat: true } : {},
|
|
374
|
-
kind: "client-method"
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
562
|
+
}
|
|
563
|
+
const variables = [];
|
|
564
|
+
const varQuery = getQuery("deep/variables");
|
|
565
|
+
const varMatches = varQuery.matches(root);
|
|
566
|
+
for (const m of varMatches) {
|
|
567
|
+
const caps = captureMap(m);
|
|
568
|
+
const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
|
|
569
|
+
const kind = declNode?.children.find((n) => n.type === "const" || n.type === "let" || n.type === "var")?.type ?? "const";
|
|
570
|
+
if (caps["var.name"] && caps["var.init"]) {
|
|
571
|
+
variables.push({
|
|
572
|
+
name: caps["var.name"],
|
|
573
|
+
kind,
|
|
574
|
+
init: trunc(caps["var.init"], 100)
|
|
575
|
+
});
|
|
379
576
|
}
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (ts.isStringLiteral(init)) {
|
|
387
|
-
navigations.push({ kind: "link-href", target: init.text, isTemplate: false });
|
|
388
|
-
} else if (ts.isJsxExpression(init) && init.expression) {
|
|
389
|
-
const parsed = extractTargetFromExpression(init.expression);
|
|
390
|
-
if (parsed) {
|
|
391
|
-
navigations.push({ kind: "link-href", target: parsed.target, isTemplate: parsed.isTemplate });
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
577
|
+
if (caps["var.destructured.obj"]) {
|
|
578
|
+
variables.push({
|
|
579
|
+
name: trunc(caps["var.destructured.obj"], 60),
|
|
580
|
+
kind,
|
|
581
|
+
init: trunc(caps["var.destructured.init"], 100)
|
|
582
|
+
});
|
|
397
583
|
}
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
}
|
|
584
|
+
if (caps["var.array.pattern"]) {
|
|
585
|
+
variables.push({
|
|
586
|
+
name: trunc(caps["var.array.pattern"], 60),
|
|
587
|
+
kind,
|
|
588
|
+
init: trunc(caps["var.array.init"], 100)
|
|
589
|
+
});
|
|
406
590
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
591
|
+
}
|
|
592
|
+
const responses = [];
|
|
593
|
+
const respQuery = getQuery("deep/responses");
|
|
594
|
+
const respMatches = respQuery.matches(root);
|
|
595
|
+
const explicitBodies = /* @__PURE__ */ new Set();
|
|
596
|
+
for (const m of respMatches) {
|
|
597
|
+
const caps = captureMap(m);
|
|
598
|
+
if (caps["resp.status"] && caps["resp.body"]) {
|
|
599
|
+
explicitBodies.add(trunc(caps["resp.body"], 80));
|
|
600
|
+
responses.push({
|
|
601
|
+
status: caps["resp.status"],
|
|
602
|
+
body: trunc(caps["resp.body"], 80)
|
|
603
|
+
});
|
|
418
604
|
}
|
|
419
|
-
ts.forEachChild(node, visit);
|
|
420
605
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
reExports,
|
|
428
|
-
jsxElements,
|
|
429
|
-
navigations,
|
|
430
|
-
fetchCalls
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
function extractDbCalls(absPath) {
|
|
434
|
-
const ts = getTs();
|
|
435
|
-
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
436
|
-
const ext = (0, import_node_path3.extname)(absPath);
|
|
437
|
-
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
438
|
-
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
439
|
-
const calls = [];
|
|
440
|
-
const seen = /* @__PURE__ */ new Set();
|
|
441
|
-
function visit(node) {
|
|
442
|
-
if (ts.isCallExpression(node)) {
|
|
443
|
-
const expr = node.expression;
|
|
444
|
-
if (ts.isPropertyAccessExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "db") {
|
|
445
|
-
const model = expr.expression.name.text;
|
|
446
|
-
const method = expr.name.text;
|
|
447
|
-
const key = `${model}.${method}`;
|
|
448
|
-
if (!seen.has(key)) {
|
|
449
|
-
seen.add(key);
|
|
450
|
-
calls.push({
|
|
451
|
-
model,
|
|
452
|
-
method,
|
|
453
|
-
isMutation: MUTATION_METHODS.has(method)
|
|
454
|
-
});
|
|
455
|
-
}
|
|
606
|
+
for (const m of respMatches) {
|
|
607
|
+
const caps = captureMap(m);
|
|
608
|
+
if (caps["resp.body.default"] && !caps["resp.status"]) {
|
|
609
|
+
const bodyText = trunc(caps["resp.body.default"], 80);
|
|
610
|
+
if (!explicitBodies.has(bodyText)) {
|
|
611
|
+
responses.push({ status: "200", body: bodyText });
|
|
456
612
|
}
|
|
457
613
|
}
|
|
458
|
-
ts.forEachChild(node, visit);
|
|
459
614
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const AUTH_WRAPPERS = /* @__PURE__ */ new Set(["withAuth", "withPermission", "withRole", "requireAuth"]);
|
|
471
|
-
function visit(node) {
|
|
472
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
473
|
-
if (AUTH_WRAPPERS.has(node.expression.text)) {
|
|
474
|
-
wrappers.add(node.expression.text);
|
|
475
|
-
}
|
|
615
|
+
const params = [];
|
|
616
|
+
const paramQuery = getQuery("deep/request-params");
|
|
617
|
+
const paramMatches = paramQuery.matches(root);
|
|
618
|
+
for (const m of paramMatches) {
|
|
619
|
+
const caps = captureMap(m);
|
|
620
|
+
if (caps["param.name"]) {
|
|
621
|
+
params.push({ name: caps["param.name"], source: "body-field" });
|
|
622
|
+
}
|
|
623
|
+
if (caps["param.body"]) {
|
|
624
|
+
params.push({ name: caps["param.body"], source: "body" });
|
|
476
625
|
}
|
|
477
|
-
ts.forEachChild(node, visit);
|
|
478
626
|
}
|
|
479
|
-
|
|
480
|
-
return wrappers;
|
|
627
|
+
return { elements, stateVars, conditions, variables, responses, params };
|
|
481
628
|
}
|
|
482
|
-
var import_node_fs3, import_node_path3,
|
|
483
|
-
var
|
|
484
|
-
"src/server/graph/core/
|
|
629
|
+
var import_node_fs3, import_node_path3, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
|
|
630
|
+
var init_ts_extractor = __esm({
|
|
631
|
+
"src/server/graph/core/ts-extractor.ts"() {
|
|
485
632
|
"use strict";
|
|
486
633
|
import_node_fs3 = require("node:fs");
|
|
487
634
|
import_node_path3 = require("node:path");
|
|
488
|
-
|
|
489
|
-
|
|
635
|
+
initialized = false;
|
|
636
|
+
queriesDir = (() => {
|
|
637
|
+
const srcPath = (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "..", "queries");
|
|
638
|
+
if (require("fs").existsSync(srcPath)) return srcPath;
|
|
639
|
+
return (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "graph", "queries");
|
|
640
|
+
})();
|
|
641
|
+
queryCache = /* @__PURE__ */ new Map();
|
|
642
|
+
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
490
643
|
"create",
|
|
491
644
|
"createMany",
|
|
492
645
|
"createManyAndReturn",
|
|
@@ -496,7 +649,10 @@ var init_ast_helpers = __esm({
|
|
|
496
649
|
"upsert",
|
|
497
650
|
"delete",
|
|
498
651
|
"deleteMany"
|
|
499
|
-
]
|
|
652
|
+
];
|
|
653
|
+
DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
|
|
654
|
+
extraDbIdentifiers = [];
|
|
655
|
+
extraMutationMethods = [];
|
|
500
656
|
}
|
|
501
657
|
});
|
|
502
658
|
|
|
@@ -822,7 +978,7 @@ function generate(rootDir) {
|
|
|
822
978
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
823
979
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
824
980
|
for (const absPath of allDiscovered) {
|
|
825
|
-
parsedByPath.set(absPath,
|
|
981
|
+
parsedByPath.set(absPath, parseFileTS(absPath));
|
|
826
982
|
}
|
|
827
983
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
828
984
|
const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
|
|
@@ -836,7 +992,18 @@ function generate(rootDir) {
|
|
|
836
992
|
const parsed = parsedByPath.get(absPath);
|
|
837
993
|
const name = parsed.name || nameFromFilename(absPath);
|
|
838
994
|
const route = extractRoute(id);
|
|
839
|
-
|
|
995
|
+
const deep = extractDeep(absPath);
|
|
996
|
+
nodes.push({
|
|
997
|
+
id,
|
|
998
|
+
type,
|
|
999
|
+
name,
|
|
1000
|
+
route,
|
|
1001
|
+
exports: parsed.exports,
|
|
1002
|
+
elements: deep.elements,
|
|
1003
|
+
stateVars: deep.stateVars,
|
|
1004
|
+
conditions: deep.conditions,
|
|
1005
|
+
variables: deep.variables
|
|
1006
|
+
});
|
|
840
1007
|
nodeIdSet.add(id);
|
|
841
1008
|
nodeTypeMap.set(id, type);
|
|
842
1009
|
if (route) routeToNodeId.set(route, id);
|
|
@@ -895,7 +1062,7 @@ function generate(rootDir) {
|
|
|
895
1062
|
if (externalScanned.has(normalized)) continue;
|
|
896
1063
|
let parsed;
|
|
897
1064
|
try {
|
|
898
|
-
parsed =
|
|
1065
|
+
parsed = parseFileTS(absPath);
|
|
899
1066
|
} catch {
|
|
900
1067
|
continue;
|
|
901
1068
|
}
|
|
@@ -1015,7 +1182,7 @@ var init_react_nextjs = __esm({
|
|
|
1015
1182
|
"use strict";
|
|
1016
1183
|
import_node_fs4 = require("node:fs");
|
|
1017
1184
|
import_node_path4 = require("node:path");
|
|
1018
|
-
|
|
1185
|
+
init_ts_extractor();
|
|
1019
1186
|
RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
1020
1187
|
reactNextjsParser = {
|
|
1021
1188
|
id: "react-nextjs",
|
|
@@ -1065,12 +1232,12 @@ function generate2(rootDir) {
|
|
|
1065
1232
|
let endpointsWithAuth = 0;
|
|
1066
1233
|
let endpointsWithDbAccess = 0;
|
|
1067
1234
|
for (const absPath of routeFiles) {
|
|
1068
|
-
const parsed =
|
|
1069
|
-
const dbCalls =
|
|
1070
|
-
const authWrappers =
|
|
1235
|
+
const parsed = parseFileTS(absPath);
|
|
1236
|
+
const dbCalls = extractDbCallsTS(absPath);
|
|
1237
|
+
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1071
1238
|
const methods = [];
|
|
1072
1239
|
for (const exp of parsed.exports) {
|
|
1073
|
-
if (
|
|
1240
|
+
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1074
1241
|
}
|
|
1075
1242
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
1076
1243
|
const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
@@ -1089,6 +1256,7 @@ function generate2(rootDir) {
|
|
|
1089
1256
|
authUsage[w] = (authUsage[w] ?? 0) + 1;
|
|
1090
1257
|
}
|
|
1091
1258
|
if (authStrategy.length > 0) endpointsWithAuth++;
|
|
1259
|
+
const deep = extractDeep(absPath);
|
|
1092
1260
|
nodes.push({
|
|
1093
1261
|
id: relPath,
|
|
1094
1262
|
type: "endpoint",
|
|
@@ -1100,7 +1268,12 @@ function generate2(rootDir) {
|
|
|
1100
1268
|
mutates,
|
|
1101
1269
|
auth: authStrategy.length > 0 ? authStrategy : ["public"],
|
|
1102
1270
|
db_models: [...new Set(dbCalls.map((c) => c.model))],
|
|
1103
|
-
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))]
|
|
1271
|
+
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
|
|
1272
|
+
// Deep extraction fields
|
|
1273
|
+
conditions: deep.conditions,
|
|
1274
|
+
variables: deep.variables,
|
|
1275
|
+
responses: deep.responses,
|
|
1276
|
+
params: deep.params
|
|
1104
1277
|
});
|
|
1105
1278
|
const seenModels = /* @__PURE__ */ new Set();
|
|
1106
1279
|
for (const call of dbCalls) {
|
|
@@ -1140,7 +1313,7 @@ function generate2(rootDir) {
|
|
|
1140
1313
|
flagged_edges: [],
|
|
1141
1314
|
patterns: {
|
|
1142
1315
|
total_endpoints: nodes.length,
|
|
1143
|
-
methods_breakdown: [...
|
|
1316
|
+
methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
|
|
1144
1317
|
acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
|
|
1145
1318
|
return acc;
|
|
1146
1319
|
}, {}),
|
|
@@ -1150,14 +1323,14 @@ function generate2(rootDir) {
|
|
|
1150
1323
|
}
|
|
1151
1324
|
};
|
|
1152
1325
|
}
|
|
1153
|
-
var import_node_fs5, import_node_path5,
|
|
1326
|
+
var import_node_fs5, import_node_path5, HTTP_METHODS, nextjsRoutesParser;
|
|
1154
1327
|
var init_nextjs_routes = __esm({
|
|
1155
1328
|
"src/server/graph/parsers/api/nextjs-routes.ts"() {
|
|
1156
1329
|
"use strict";
|
|
1157
1330
|
import_node_fs5 = require("node:fs");
|
|
1158
1331
|
import_node_path5 = require("node:path");
|
|
1159
|
-
|
|
1160
|
-
|
|
1332
|
+
init_ts_extractor();
|
|
1333
|
+
HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1161
1334
|
nextjsRoutesParser = {
|
|
1162
1335
|
id: "nextjs-routes",
|
|
1163
1336
|
layer: "api",
|
|
@@ -2058,8 +2231,9 @@ function matchParts(pat, pi, id, ii) {
|
|
|
2058
2231
|
while (pi < pat.length && pat[pi] === "**") pi++;
|
|
2059
2232
|
return pi === pat.length && ii === id.length;
|
|
2060
2233
|
}
|
|
2061
|
-
function detectConventionDirs(rootDir) {
|
|
2234
|
+
function detectConventionDirs(rootDir, extraConventionDirs = []) {
|
|
2062
2235
|
const result = /* @__PURE__ */ new Map();
|
|
2236
|
+
const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
|
|
2063
2237
|
const searchDirs = [
|
|
2064
2238
|
rootDir,
|
|
2065
2239
|
(0, import_node_path11.join)(rootDir, "src"),
|
|
@@ -2067,7 +2241,7 @@ function detectConventionDirs(rootDir) {
|
|
|
2067
2241
|
(0, import_node_path11.join)(rootDir, "lib")
|
|
2068
2242
|
];
|
|
2069
2243
|
for (const base of searchDirs) {
|
|
2070
|
-
for (const convention of
|
|
2244
|
+
for (const convention of conventionDirs) {
|
|
2071
2245
|
const dir = (0, import_node_path11.join)(base, convention);
|
|
2072
2246
|
if (!(0, import_node_fs10.existsSync)(dir)) continue;
|
|
2073
2247
|
try {
|
|
@@ -2120,9 +2294,13 @@ function isTrivialGroup(name, extraTrivial) {
|
|
|
2120
2294
|
function normalizeGroupName(name) {
|
|
2121
2295
|
return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
|
|
2122
2296
|
}
|
|
2123
|
-
function extractModuleFromPath(id, extraTrivial) {
|
|
2297
|
+
function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
|
|
2124
2298
|
const segments = id.split("/");
|
|
2125
2299
|
const routeGroups = extractRouteGroups(id);
|
|
2300
|
+
const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
|
|
2301
|
+
if (extraSkipSegments) {
|
|
2302
|
+
for (const s of extraSkipSegments) skipSegments.add(s);
|
|
2303
|
+
}
|
|
2126
2304
|
const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
|
|
2127
2305
|
if (moduleGroups.length > 0) {
|
|
2128
2306
|
return moduleGroups[moduleGroups.length - 1];
|
|
@@ -2133,7 +2311,7 @@ function extractModuleFromPath(id, extraTrivial) {
|
|
|
2133
2311
|
if (isRouteGroup(seg)) continue;
|
|
2134
2312
|
if (isDynamicSegment(seg)) continue;
|
|
2135
2313
|
if (isDomainDir(seg)) continue;
|
|
2136
|
-
if (
|
|
2314
|
+
if (skipSegments.has(seg)) continue;
|
|
2137
2315
|
meaningful.push(seg);
|
|
2138
2316
|
}
|
|
2139
2317
|
if (meaningful.length > 0) {
|
|
@@ -2141,14 +2319,83 @@ function extractModuleFromPath(id, extraTrivial) {
|
|
|
2141
2319
|
}
|
|
2142
2320
|
return "root";
|
|
2143
2321
|
}
|
|
2144
|
-
var import_node_fs10, import_node_path11,
|
|
2322
|
+
var import_node_fs10, import_node_path11, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
|
|
2145
2323
|
var init_module_tagger = __esm({
|
|
2146
2324
|
"src/server/graph/taggers/module-tagger.ts"() {
|
|
2147
2325
|
"use strict";
|
|
2148
2326
|
import_node_fs10 = require("node:fs");
|
|
2149
2327
|
import_node_path11 = require("node:path");
|
|
2150
|
-
|
|
2151
|
-
|
|
2328
|
+
CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
|
|
2329
|
+
GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
|
|
2330
|
+
// JS/TS
|
|
2331
|
+
"components",
|
|
2332
|
+
"hooks",
|
|
2333
|
+
"pages",
|
|
2334
|
+
"views",
|
|
2335
|
+
"screens",
|
|
2336
|
+
"layouts",
|
|
2337
|
+
"utils",
|
|
2338
|
+
"helpers",
|
|
2339
|
+
"lib",
|
|
2340
|
+
"libs",
|
|
2341
|
+
"services",
|
|
2342
|
+
"api",
|
|
2343
|
+
"apis",
|
|
2344
|
+
"stores",
|
|
2345
|
+
"state",
|
|
2346
|
+
"store",
|
|
2347
|
+
"context",
|
|
2348
|
+
"contexts",
|
|
2349
|
+
"providers",
|
|
2350
|
+
"types",
|
|
2351
|
+
"interfaces",
|
|
2352
|
+
"models",
|
|
2353
|
+
"schemas",
|
|
2354
|
+
"constants",
|
|
2355
|
+
"config",
|
|
2356
|
+
"configs",
|
|
2357
|
+
"assets",
|
|
2358
|
+
"styles",
|
|
2359
|
+
"public",
|
|
2360
|
+
"middleware",
|
|
2361
|
+
"middlewares",
|
|
2362
|
+
"routes",
|
|
2363
|
+
"router",
|
|
2364
|
+
"tests",
|
|
2365
|
+
"test",
|
|
2366
|
+
"__tests__",
|
|
2367
|
+
"spec",
|
|
2368
|
+
"specs",
|
|
2369
|
+
// Go
|
|
2370
|
+
"cmd",
|
|
2371
|
+
"pkg",
|
|
2372
|
+
"internal",
|
|
2373
|
+
// Python
|
|
2374
|
+
"management",
|
|
2375
|
+
"migrations",
|
|
2376
|
+
"templatetags",
|
|
2377
|
+
"templates",
|
|
2378
|
+
// Java
|
|
2379
|
+
"controller",
|
|
2380
|
+
"controllers",
|
|
2381
|
+
"service",
|
|
2382
|
+
"repository",
|
|
2383
|
+
"repositories",
|
|
2384
|
+
"entity",
|
|
2385
|
+
"entities",
|
|
2386
|
+
"dto",
|
|
2387
|
+
"dtos",
|
|
2388
|
+
// General
|
|
2389
|
+
"shared",
|
|
2390
|
+
"common",
|
|
2391
|
+
"core",
|
|
2392
|
+
"base",
|
|
2393
|
+
"app",
|
|
2394
|
+
// Next.js specific
|
|
2395
|
+
"client",
|
|
2396
|
+
"server"
|
|
2397
|
+
]);
|
|
2398
|
+
SKIP_SEGMENTS_BUILTIN = /* @__PURE__ */ new Set([
|
|
2152
2399
|
"src",
|
|
2153
2400
|
"app",
|
|
2154
2401
|
"client",
|
|
@@ -2188,12 +2435,10 @@ var init_module_tagger = __esm({
|
|
|
2188
2435
|
layers: null,
|
|
2189
2436
|
// applies to all layers
|
|
2190
2437
|
tag(nodes, layer, rootDir) {
|
|
2191
|
-
if (cachedRootDir !== rootDir) {
|
|
2192
|
-
cachedConventionDirs = detectConventionDirs(rootDir);
|
|
2193
|
-
cachedRootDir = rootDir;
|
|
2194
|
-
}
|
|
2195
2438
|
let configRules = [];
|
|
2196
2439
|
let extraTrivial;
|
|
2440
|
+
let extraSkipSegments;
|
|
2441
|
+
let extraConventionDirs = [];
|
|
2197
2442
|
try {
|
|
2198
2443
|
const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
|
|
2199
2444
|
const config = loadConfig2(rootDir);
|
|
@@ -2202,8 +2447,21 @@ var init_module_tagger = __esm({
|
|
|
2202
2447
|
if (trivialFromConfig?.length) {
|
|
2203
2448
|
extraTrivial = new Set(trivialFromConfig);
|
|
2204
2449
|
}
|
|
2450
|
+
const skipFromConfig = config.taggers?.module?.skipSegments;
|
|
2451
|
+
if (skipFromConfig?.length) {
|
|
2452
|
+
extraSkipSegments = new Set(skipFromConfig);
|
|
2453
|
+
}
|
|
2454
|
+
extraConventionDirs = config.taggers?.module?.conventionDirs ?? [];
|
|
2455
|
+
const roleNamesFromConfig = config.taggers?.module?.genericRoleNames;
|
|
2456
|
+
if (roleNamesFromConfig?.length) {
|
|
2457
|
+
for (const name of roleNamesFromConfig) GENERIC_ROLE_NAMES_BUILTIN.add(name);
|
|
2458
|
+
}
|
|
2205
2459
|
} catch {
|
|
2206
2460
|
}
|
|
2461
|
+
if (cachedRootDir !== rootDir) {
|
|
2462
|
+
cachedConventionDirs = detectConventionDirs(rootDir, extraConventionDirs);
|
|
2463
|
+
cachedRootDir = rootDir;
|
|
2464
|
+
}
|
|
2207
2465
|
const result = /* @__PURE__ */ new Map();
|
|
2208
2466
|
for (const node of nodes) {
|
|
2209
2467
|
const id = node.id;
|
|
@@ -2229,7 +2487,7 @@ var init_module_tagger = __esm({
|
|
|
2229
2487
|
}
|
|
2230
2488
|
}
|
|
2231
2489
|
if (matched) continue;
|
|
2232
|
-
const module2 = extractModuleFromPath(id, extraTrivial);
|
|
2490
|
+
const module2 = extractModuleFromPath(id, extraTrivial, extraSkipSegments);
|
|
2233
2491
|
result.set(id, module2);
|
|
2234
2492
|
}
|
|
2235
2493
|
return result;
|
|
@@ -2476,7 +2734,13 @@ function readAllGraphs(rootDir) {
|
|
|
2476
2734
|
}
|
|
2477
2735
|
return result;
|
|
2478
2736
|
}
|
|
2479
|
-
function generateGraph(rootDir, layer) {
|
|
2737
|
+
async function generateGraph(rootDir, layer) {
|
|
2738
|
+
await initTreeSitter();
|
|
2739
|
+
const config = loadConfig(rootDir);
|
|
2740
|
+
setExtractorConfig({
|
|
2741
|
+
dbIdentifiers: config.parsers?.patterns?.dbIdentifiers,
|
|
2742
|
+
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
2743
|
+
});
|
|
2480
2744
|
const dir = graphsDir(rootDir);
|
|
2481
2745
|
(0, import_node_fs12.mkdirSync)(dir, { recursive: true });
|
|
2482
2746
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
@@ -2498,6 +2762,7 @@ var init_graph = __esm({
|
|
|
2498
2762
|
init_config();
|
|
2499
2763
|
init_tagger_registry();
|
|
2500
2764
|
init_tag_store();
|
|
2765
|
+
init_ts_extractor();
|
|
2501
2766
|
init_tag_store();
|
|
2502
2767
|
GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
2503
2768
|
LAYERS = ["ui", "api", "db"];
|
|
@@ -2533,10 +2798,10 @@ function findProjectRoot(startDir) {
|
|
|
2533
2798
|
}
|
|
2534
2799
|
return startDir;
|
|
2535
2800
|
}
|
|
2536
|
-
function buildMergedGraph(projectRoot) {
|
|
2801
|
+
async function buildMergedGraph(projectRoot) {
|
|
2537
2802
|
let graphs = readAllGraphs(projectRoot);
|
|
2538
2803
|
if (!graphs.ui && !graphs.api && !graphs.db) {
|
|
2539
|
-
generateGraph(projectRoot);
|
|
2804
|
+
await generateGraph(projectRoot);
|
|
2540
2805
|
graphs = readAllGraphs(projectRoot);
|
|
2541
2806
|
}
|
|
2542
2807
|
const nodes = [];
|
|
@@ -2646,13 +2911,18 @@ async function startChartServer(opts = {}) {
|
|
|
2646
2911
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
2647
2912
|
if (req.method === "GET" && url2.pathname === "/api/project-graph") {
|
|
2648
2913
|
const regenerate = url2.searchParams.get("regenerate") === "1";
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2914
|
+
(async () => {
|
|
2915
|
+
if (regenerate) await generateGraph(projectRoot);
|
|
2916
|
+
const merged = await buildMergedGraph(projectRoot);
|
|
2917
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2918
|
+
res.end(JSON.stringify({
|
|
2919
|
+
...merged,
|
|
2920
|
+
debug: { cwd, projectRoot }
|
|
2921
|
+
}));
|
|
2922
|
+
})().catch((e) => {
|
|
2923
|
+
res.writeHead(500);
|
|
2924
|
+
res.end(String(e));
|
|
2925
|
+
});
|
|
2656
2926
|
return;
|
|
2657
2927
|
}
|
|
2658
2928
|
if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
|
|
@@ -2662,8 +2932,8 @@ async function startChartServer(opts = {}) {
|
|
|
2662
2932
|
return;
|
|
2663
2933
|
}
|
|
2664
2934
|
if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
|
|
2665
|
-
|
|
2666
|
-
generateGraph(projectRoot);
|
|
2935
|
+
(async () => {
|
|
2936
|
+
await generateGraph(projectRoot);
|
|
2667
2937
|
const graphs = readAllGraphs(projectRoot);
|
|
2668
2938
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2669
2939
|
res.end(JSON.stringify({
|
|
@@ -2672,10 +2942,10 @@ async function startChartServer(opts = {}) {
|
|
|
2672
2942
|
api: graphs.api ?? null,
|
|
2673
2943
|
db: graphs.db ?? null
|
|
2674
2944
|
}));
|
|
2675
|
-
}
|
|
2945
|
+
})().catch((err2) => {
|
|
2676
2946
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2677
2947
|
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
2678
|
-
}
|
|
2948
|
+
});
|
|
2679
2949
|
return;
|
|
2680
2950
|
}
|
|
2681
2951
|
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
@@ -2704,7 +2974,7 @@ async function startChartServer(opts = {}) {
|
|
|
2704
2974
|
return;
|
|
2705
2975
|
}
|
|
2706
2976
|
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
2707
|
-
const
|
|
2977
|
+
const config2 = loadConfig(projectRoot);
|
|
2708
2978
|
const detection = [
|
|
2709
2979
|
{ id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
|
|
2710
2980
|
{ id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
|
|
@@ -2716,7 +2986,7 @@ async function startChartServer(opts = {}) {
|
|
|
2716
2986
|
{ id: "url-literal-scanner", label: "/api/... URL literals" }
|
|
2717
2987
|
];
|
|
2718
2988
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2719
|
-
res.end(JSON.stringify({ config, detection, crosslayerParsers }));
|
|
2989
|
+
res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
|
|
2720
2990
|
return;
|
|
2721
2991
|
}
|
|
2722
2992
|
if (req.method === "POST" && url2.pathname === "/api/parser-config") {
|
|
@@ -2739,14 +3009,14 @@ async function startChartServer(opts = {}) {
|
|
|
2739
3009
|
return;
|
|
2740
3010
|
}
|
|
2741
3011
|
if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
|
|
2742
|
-
const
|
|
3012
|
+
const config2 = loadConfig(projectRoot);
|
|
2743
3013
|
const builtinTaggers = [
|
|
2744
|
-
{ id: "module", tagKey: "module", trackUntagged:
|
|
2745
|
-
{ id: "screen", tagKey: "screen", trackUntagged:
|
|
3014
|
+
{ id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
|
|
3015
|
+
{ id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
|
|
2746
3016
|
];
|
|
2747
|
-
const disabled =
|
|
2748
|
-
const customTaggers =
|
|
2749
|
-
const moduleRules =
|
|
3017
|
+
const disabled = config2.taggers?.disabled ?? [];
|
|
3018
|
+
const customTaggers = config2.taggers?.custom ?? [];
|
|
3019
|
+
const moduleRules = config2.taggers?.module?.rules ?? [];
|
|
2750
3020
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2751
3021
|
res.end(JSON.stringify({ builtinTaggers, disabled, customTaggers, moduleRules }));
|
|
2752
3022
|
return;
|
|
@@ -2759,10 +3029,10 @@ async function startChartServer(opts = {}) {
|
|
|
2759
3029
|
req.on("end", () => {
|
|
2760
3030
|
try {
|
|
2761
3031
|
const taggerConfig = JSON.parse(body);
|
|
2762
|
-
const
|
|
2763
|
-
|
|
3032
|
+
const config2 = loadConfig(projectRoot);
|
|
3033
|
+
config2.taggers = taggerConfig;
|
|
2764
3034
|
const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
|
|
2765
|
-
import_node_fs13.default.writeFileSync(configPath, JSON.stringify(
|
|
3035
|
+
import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
2766
3036
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2767
3037
|
res.end(JSON.stringify({ ok: true }));
|
|
2768
3038
|
} catch (err2) {
|
|
@@ -2834,7 +3104,8 @@ async function startChartServer(opts = {}) {
|
|
|
2834
3104
|
res.end(JSON.stringify({ error: String(err2) }));
|
|
2835
3105
|
}
|
|
2836
3106
|
});
|
|
2837
|
-
const
|
|
3107
|
+
const config = loadConfig(projectRoot);
|
|
3108
|
+
const startPort = opts.port ?? config.port ?? randomPort();
|
|
2838
3109
|
const port = await bindWithFallback(server, startPort);
|
|
2839
3110
|
const url = `http://localhost:${port}`;
|
|
2840
3111
|
writeLock({
|
|
@@ -2940,7 +3211,7 @@ function toCompactNode(n) {
|
|
|
2940
3211
|
if (n.columns != null) out.c = n.columns;
|
|
2941
3212
|
if (tags != null) out.tg = tags;
|
|
2942
3213
|
for (const k of Object.keys(n)) {
|
|
2943
|
-
if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
|
|
3214
|
+
if (!COMPACT_NODE_KNOWN_KEYS.has(k) && !DEEP_FIELDS.has(k) && n[k] != null) out[k] = n[k];
|
|
2944
3215
|
}
|
|
2945
3216
|
return out;
|
|
2946
3217
|
}
|
|
@@ -3040,13 +3311,13 @@ function okJson(data) {
|
|
|
3040
3311
|
function err(text) {
|
|
3041
3312
|
return { content: [{ type: "text", text }], isError: true };
|
|
3042
3313
|
}
|
|
3043
|
-
function handleGenerateGraph(args) {
|
|
3314
|
+
async function handleGenerateGraph(args) {
|
|
3044
3315
|
const rootDir = process.cwd();
|
|
3045
3316
|
const layer = args.layer;
|
|
3046
3317
|
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
3047
3318
|
return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
|
|
3048
3319
|
}
|
|
3049
|
-
const results = generateGraph(rootDir, layer);
|
|
3320
|
+
const results = await generateGraph(rootDir, layer);
|
|
3050
3321
|
if (results.length === 0) {
|
|
3051
3322
|
return err(
|
|
3052
3323
|
layer ? `No parser detected for the "${layer}" layer in this project.` : "No parsers detected for this project. Check that the project has the expected structure."
|
|
@@ -3079,6 +3350,8 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
3079
3350
|
const layerIsDb = args.layer === "db";
|
|
3080
3351
|
const minimal = args.minimal ?? layerIsDb;
|
|
3081
3352
|
const includeEdges = args.include_edges;
|
|
3353
|
+
const offset = args.offset ?? 0;
|
|
3354
|
+
const limit = args.limit;
|
|
3082
3355
|
const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
|
|
3083
3356
|
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
3084
3357
|
return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
|
|
@@ -3150,18 +3423,29 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
3150
3423
|
hint: "No nodes matched. Check spelling, or call read_graph without filter to see the summary and available types/modules."
|
|
3151
3424
|
};
|
|
3152
3425
|
}
|
|
3426
|
+
const totalMatched = matched.length;
|
|
3427
|
+
const paginatedNodes = limit != null ? matched.slice(offset, offset + limit) : matched.slice(offset);
|
|
3428
|
+
const hasMore = offset + paginatedNodes.length < totalMatched;
|
|
3153
3429
|
const wantEdges = includeEdges ?? false;
|
|
3430
|
+
const returnedIds = new Set(paginatedNodes.map((n) => n.id));
|
|
3431
|
+
const returnedEdges = graph.edges.filter((e) => returnedIds.has(e.source) && returnedIds.has(e.target));
|
|
3154
3432
|
const result = {
|
|
3155
3433
|
layer,
|
|
3156
3434
|
filter: { search, type, module: module_ },
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3435
|
+
total: totalMatched,
|
|
3436
|
+
returned: paginatedNodes.length,
|
|
3437
|
+
offset,
|
|
3438
|
+
has_more: hasMore,
|
|
3439
|
+
edge_count: returnedEdges.length,
|
|
3440
|
+
nodes: minimal ? toMinimal(paginatedNodes) : paginatedNodes
|
|
3160
3441
|
};
|
|
3442
|
+
if (hasMore) {
|
|
3443
|
+
result.next_offset = offset + paginatedNodes.length;
|
|
3444
|
+
}
|
|
3161
3445
|
if (wantEdges) {
|
|
3162
|
-
result.edges =
|
|
3163
|
-
} else {
|
|
3164
|
-
result.edges_hint = `${
|
|
3446
|
+
result.edges = returnedEdges;
|
|
3447
|
+
} else if (returnedEdges.length > 0) {
|
|
3448
|
+
result.edges_hint = `${returnedEdges.length} edges between matched nodes omitted. Pass include_edges:true to retrieve them (only do this when you actually need edge data).`;
|
|
3165
3449
|
}
|
|
3166
3450
|
return result;
|
|
3167
3451
|
}
|
|
@@ -3229,6 +3513,48 @@ function nodeToFilePath(rootDir, layer, nodeId) {
|
|
|
3229
3513
|
if (layer === "db") return (0, import_node_path16.join)(rootDir, "prisma", "schema.prisma");
|
|
3230
3514
|
return null;
|
|
3231
3515
|
}
|
|
3516
|
+
function handleInspectNode(args) {
|
|
3517
|
+
const rootDir = process.cwd();
|
|
3518
|
+
const layer = args.layer;
|
|
3519
|
+
const nodeId = args.node_id;
|
|
3520
|
+
const search = args.search;
|
|
3521
|
+
const fields = args.fields;
|
|
3522
|
+
if (!layer) return err("layer is required.");
|
|
3523
|
+
if (!nodeId && !search) return err("Either node_id or search is required.");
|
|
3524
|
+
const graph = readGraph(rootDir, layer);
|
|
3525
|
+
if (!graph) return err(`No graph found for layer "${layer}". Run generate_graph first.`);
|
|
3526
|
+
let matched;
|
|
3527
|
+
if (nodeId) {
|
|
3528
|
+
const node = graph.nodes.find((n) => n.id === nodeId);
|
|
3529
|
+
if (!node) return err(`Node "${nodeId}" not found in ${layer} layer.`);
|
|
3530
|
+
matched = [node];
|
|
3531
|
+
} else {
|
|
3532
|
+
const searchLower = search.toLowerCase();
|
|
3533
|
+
matched = graph.nodes.filter(
|
|
3534
|
+
(n) => n.id.toLowerCase().includes(searchLower) || n.name.toLowerCase().includes(searchLower) || n.route?.toLowerCase().includes(searchLower)
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
if (matched.length === 0) return err(`No nodes matching "${search}" in ${layer} layer.`);
|
|
3538
|
+
if (matched.length > 5) {
|
|
3539
|
+
return err(`${matched.length} nodes match "${search}". Narrow your search (max 5 for inspect_node).`);
|
|
3540
|
+
}
|
|
3541
|
+
const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params"];
|
|
3542
|
+
const requestedFields = fields ?? allDeepFields;
|
|
3543
|
+
const results = matched.map((node) => {
|
|
3544
|
+
const deep = { id: node.id, name: node.name, type: node.type };
|
|
3545
|
+
for (const field of requestedFields) {
|
|
3546
|
+
if (allDeepFields.includes(field) && node[field] != null) {
|
|
3547
|
+
deep[field] = node[field];
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
return deep;
|
|
3551
|
+
});
|
|
3552
|
+
return okJson({
|
|
3553
|
+
layer,
|
|
3554
|
+
matched: results.length,
|
|
3555
|
+
nodes: results
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3232
3558
|
function handleGrepNodes(args) {
|
|
3233
3559
|
const rootDir = process.cwd();
|
|
3234
3560
|
const pattern = args.pattern;
|
|
@@ -3491,7 +3817,7 @@ function respond(id, result) {
|
|
|
3491
3817
|
function respondError(id, code, message) {
|
|
3492
3818
|
send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
3493
3819
|
}
|
|
3494
|
-
function handleMessage(msg) {
|
|
3820
|
+
async function handleMessage(msg) {
|
|
3495
3821
|
const method = msg.method;
|
|
3496
3822
|
const id = msg.id;
|
|
3497
3823
|
if (method === "initialize") {
|
|
@@ -3514,7 +3840,7 @@ function handleMessage(msg) {
|
|
|
3514
3840
|
const toolName = params.name;
|
|
3515
3841
|
const args = params.arguments ?? {};
|
|
3516
3842
|
if (toolName === "generate_graph") {
|
|
3517
|
-
respond(id ?? null, handleGenerateGraph(args));
|
|
3843
|
+
respond(id ?? null, await handleGenerateGraph(args));
|
|
3518
3844
|
return;
|
|
3519
3845
|
}
|
|
3520
3846
|
if (toolName === "read_graph") {
|
|
@@ -3525,6 +3851,10 @@ function handleMessage(msg) {
|
|
|
3525
3851
|
respond(id ?? null, handleGrepNodes(args));
|
|
3526
3852
|
return;
|
|
3527
3853
|
}
|
|
3854
|
+
if (toolName === "inspect_node") {
|
|
3855
|
+
respond(id ?? null, handleInspectNode(args));
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3528
3858
|
if (toolName === "chart_server_status") {
|
|
3529
3859
|
respond(id ?? null, handleChartServerStatus());
|
|
3530
3860
|
return;
|
|
@@ -3584,7 +3914,7 @@ function startGraphMcpServer() {
|
|
|
3584
3914
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
3585
3915
|
`);
|
|
3586
3916
|
}
|
|
3587
|
-
var import_node_fs14, import_node_path16, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
|
|
3917
|
+
var import_node_fs14, import_node_path16, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, DEEP_FIELDS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
|
|
3588
3918
|
var init_graph_mcp = __esm({
|
|
3589
3919
|
"src/server/graph-mcp.ts"() {
|
|
3590
3920
|
"use strict";
|
|
@@ -3619,7 +3949,7 @@ var init_graph_mcp = __esm({
|
|
|
3619
3949
|
},
|
|
3620
3950
|
{
|
|
3621
3951
|
name: "read_graph",
|
|
3622
|
-
description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
|
|
3952
|
+
description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nPAGINATION (filter queries):\n- Use `offset` and `limit` to paginate through large result sets.\n- Response includes: `total` (matched), `returned` (in this page), `has_more`, `next_offset`.\n- If `has_more: true`, call again with `offset: next_offset` to get the next page.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
|
|
3623
3953
|
inputSchema: {
|
|
3624
3954
|
type: "object",
|
|
3625
3955
|
properties: {
|
|
@@ -3664,6 +3994,14 @@ var init_graph_mcp = __esm({
|
|
|
3664
3994
|
type: "boolean",
|
|
3665
3995
|
description: "Include the edge list in the response. Default TRUE for neighborhood queries (node_id), FALSE for filter queries. Filter responses always include edge_count. Only set true on filter queries when you actually need edge data."
|
|
3666
3996
|
},
|
|
3997
|
+
offset: {
|
|
3998
|
+
type: "number",
|
|
3999
|
+
description: "Skip first N matched nodes (pagination). Default 0. Use next_offset from a previous response to get the next page."
|
|
4000
|
+
},
|
|
4001
|
+
limit: {
|
|
4002
|
+
type: "number",
|
|
4003
|
+
description: "Max nodes to return. Default: all matched nodes. Use with offset for pagination."
|
|
4004
|
+
},
|
|
3667
4005
|
queries: {
|
|
3668
4006
|
type: "array",
|
|
3669
4007
|
description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema. When set, top-level params are ignored. Subject to an aggregate size budget \u2014 later queries may return a skipped stub.",
|
|
@@ -3732,6 +4070,40 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
3732
4070
|
required: ["layer", "pattern"]
|
|
3733
4071
|
}
|
|
3734
4072
|
},
|
|
4073
|
+
{
|
|
4074
|
+
name: "inspect_node",
|
|
4075
|
+
description: `Get deep AST data for specific graph nodes \u2014 what's INSIDE a component or endpoint. Returns elements (JSX), state hooks, conditions, variables, responses, and request params.
|
|
4076
|
+
|
|
4077
|
+
USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?"
|
|
4078
|
+
|
|
4079
|
+
DO NOT USE FOR: structural queries (use read_graph), content search (use grep_nodes).
|
|
4080
|
+
|
|
4081
|
+
Returns deep fields only \u2014 not structural metadata (use read_graph for that).`,
|
|
4082
|
+
inputSchema: {
|
|
4083
|
+
type: "object",
|
|
4084
|
+
properties: {
|
|
4085
|
+
layer: {
|
|
4086
|
+
type: "string",
|
|
4087
|
+
enum: ["ui", "api", "db"],
|
|
4088
|
+
description: "Graph layer (required)."
|
|
4089
|
+
},
|
|
4090
|
+
node_id: {
|
|
4091
|
+
type: "string",
|
|
4092
|
+
description: "Node ID to inspect. Use read_graph to find node IDs first."
|
|
4093
|
+
},
|
|
4094
|
+
search: {
|
|
4095
|
+
type: "string",
|
|
4096
|
+
description: "Substring match on node id/name/route. Returns deep data for all matching nodes (max 5)."
|
|
4097
|
+
},
|
|
4098
|
+
fields: {
|
|
4099
|
+
type: "array",
|
|
4100
|
+
items: { type: "string" },
|
|
4101
|
+
description: "Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params. Omit for all."
|
|
4102
|
+
}
|
|
4103
|
+
},
|
|
4104
|
+
required: ["layer"]
|
|
4105
|
+
}
|
|
4106
|
+
},
|
|
3735
4107
|
{
|
|
3736
4108
|
name: "chart_server_status",
|
|
3737
4109
|
description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
|
|
@@ -3843,6 +4215,14 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
3843
4215
|
"columns",
|
|
3844
4216
|
"tags"
|
|
3845
4217
|
]);
|
|
4218
|
+
DEEP_FIELDS = /* @__PURE__ */ new Set([
|
|
4219
|
+
"elements",
|
|
4220
|
+
"stateVars",
|
|
4221
|
+
"conditions",
|
|
4222
|
+
"variables",
|
|
4223
|
+
"responses",
|
|
4224
|
+
"params"
|
|
4225
|
+
]);
|
|
3846
4226
|
EST_CHARS_PER_NODE_FULL = {
|
|
3847
4227
|
ui: 300,
|
|
3848
4228
|
api: 300,
|