devlensio 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/dist/clustering/index.d.ts +27 -0
- package/dist/clustering/index.js +149 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +78 -0
- package/dist/config/providers/file.d.ts +19 -0
- package/dist/config/providers/file.js +215 -0
- package/dist/config/providers/request.d.ts +2 -0
- package/dist/config/providers/request.js +72 -0
- package/dist/config/types.d.ts +46 -0
- package/dist/config/types.js +81 -0
- package/dist/config/writer.d.ts +29 -0
- package/dist/config/writer.js +103 -0
- package/dist/filesystem/appRouter.d.ts +2 -0
- package/dist/filesystem/appRouter.js +126 -0
- package/dist/filesystem/backendRoutes.d.ts +2 -0
- package/dist/filesystem/backendRoutes.js +161 -0
- package/dist/filesystem/index.d.ts +2 -0
- package/dist/filesystem/index.js +28 -0
- package/dist/filesystem/index.test.d.ts +1 -0
- package/dist/filesystem/index.test.js +178 -0
- package/dist/filesystem/pagesRouter.d.ts +2 -0
- package/dist/filesystem/pagesRouter.js +109 -0
- package/dist/fingerprint/detectors.d.ts +8 -0
- package/dist/fingerprint/detectors.js +174 -0
- package/dist/fingerprint/index.d.ts +2 -0
- package/dist/fingerprint/index.js +41 -0
- package/dist/fingerprint/index.test.d.ts +1 -0
- package/dist/fingerprint/index.test.js +148 -0
- package/dist/graph/buildLookup.d.ts +10 -0
- package/dist/graph/buildLookup.js +32 -0
- package/dist/graph/edges/callEdges.d.ts +7 -0
- package/dist/graph/edges/callEdges.js +145 -0
- package/dist/graph/edges/eventEdges.d.ts +7 -0
- package/dist/graph/edges/eventEdges.js +203 -0
- package/dist/graph/edges/guardEdges.d.ts +3 -0
- package/dist/graph/edges/guardEdges.js +232 -0
- package/dist/graph/edges/hookEdges.d.ts +3 -0
- package/dist/graph/edges/hookEdges.js +54 -0
- package/dist/graph/edges/importEdges.d.ts +8 -0
- package/dist/graph/edges/importEdges.js +224 -0
- package/dist/graph/edges/propEdges.d.ts +3 -0
- package/dist/graph/edges/propEdges.js +142 -0
- package/dist/graph/edges/routeEdge.d.ts +3 -0
- package/dist/graph/edges/routeEdge.js +124 -0
- package/dist/graph/edges/stateEdges.d.ts +3 -0
- package/dist/graph/edges/stateEdges.js +206 -0
- package/dist/graph/edges/testEdges.d.ts +3 -0
- package/dist/graph/edges/testEdges.js +143 -0
- package/dist/graph/edges/utils.d.ts +2 -0
- package/dist/graph/edges/utils.js +25 -0
- package/dist/graph/index.d.ts +6 -0
- package/dist/graph/index.js +65 -0
- package/dist/graph/index.test.d.ts +1 -0
- package/dist/graph/index.test.js +542 -0
- package/dist/graph/thirdPartyLibs.d.ts +8 -0
- package/dist/graph/thirdPartyLibs.js +162 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/jobs/index.d.ts +5 -0
- package/dist/jobs/index.js +11 -0
- package/dist/jobs/queue/interface.d.ts +13 -0
- package/dist/jobs/queue/interface.js +1 -0
- package/dist/jobs/queue/memory.d.ts +24 -0
- package/dist/jobs/queue/memory.js +291 -0
- package/dist/jobs/runner.d.ts +3 -0
- package/dist/jobs/runner.js +136 -0
- package/dist/jobs/types.d.ts +112 -0
- package/dist/jobs/types.js +33 -0
- package/dist/parser/directives.d.ts +4 -0
- package/dist/parser/directives.js +31 -0
- package/dist/parser/extractors/components.d.ts +5 -0
- package/dist/parser/extractors/components.js +240 -0
- package/dist/parser/extractors/functions.d.ts +4 -0
- package/dist/parser/extractors/functions.js +240 -0
- package/dist/parser/extractors/hooks.d.ts +4 -0
- package/dist/parser/extractors/hooks.js +128 -0
- package/dist/parser/extractors/stores.d.ts +3 -0
- package/dist/parser/extractors/stores.js +181 -0
- package/dist/parser/index.d.ts +14 -0
- package/dist/parser/index.js +168 -0
- package/dist/parser/index.test.d.ts +1 -0
- package/dist/parser/index.test.js +319 -0
- package/dist/parser/typeUtils.d.ts +9 -0
- package/dist/parser/typeUtils.js +46 -0
- package/dist/pipeline/index.d.ts +50 -0
- package/dist/pipeline/index.js +249 -0
- package/dist/scoring/connectionCounter.d.ts +28 -0
- package/dist/scoring/connectionCounter.js +134 -0
- package/dist/scoring/fileScorer.d.ts +2 -0
- package/dist/scoring/fileScorer.js +44 -0
- package/dist/scoring/index.d.ts +22 -0
- package/dist/scoring/index.js +130 -0
- package/dist/scoring/index.test.d.ts +1 -0
- package/dist/scoring/index.test.js +453 -0
- package/dist/scoring/nodeScorer.d.ts +3 -0
- package/dist/scoring/nodeScorer.js +108 -0
- package/dist/scoring/noiseFilter.d.ts +18 -0
- package/dist/scoring/noiseFilter.js +92 -0
- package/dist/storage/fileStorage.d.ts +117 -0
- package/dist/storage/fileStorage.js +616 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/interface.d.ts +27 -0
- package/dist/storage/interface.js +1 -0
- package/dist/summarizer/checkpoint.d.ts +15 -0
- package/dist/summarizer/checkpoint.js +110 -0
- package/dist/summarizer/index.d.ts +2 -0
- package/dist/summarizer/index.js +281 -0
- package/dist/summarizer/mapreduce.d.ts +4 -0
- package/dist/summarizer/mapreduce.js +87 -0
- package/dist/summarizer/prompts.d.ts +22 -0
- package/dist/summarizer/prompts.js +205 -0
- package/dist/summarizer/providers/anthropic.d.ts +9 -0
- package/dist/summarizer/providers/anthropic.js +78 -0
- package/dist/summarizer/providers/gemini.d.ts +9 -0
- package/dist/summarizer/providers/gemini.js +79 -0
- package/dist/summarizer/providers/index.d.ts +3 -0
- package/dist/summarizer/providers/index.js +43 -0
- package/dist/summarizer/providers/ollama.d.ts +9 -0
- package/dist/summarizer/providers/ollama.js +23 -0
- package/dist/summarizer/providers/openRouter.d.ts +9 -0
- package/dist/summarizer/providers/openRouter.js +19 -0
- package/dist/summarizer/providers/openai.d.ts +9 -0
- package/dist/summarizer/providers/openai.js +72 -0
- package/dist/summarizer/providers/types.d.ts +32 -0
- package/dist/summarizer/providers/types.js +1 -0
- package/dist/summarizer/retry.d.ts +7 -0
- package/dist/summarizer/retry.js +51 -0
- package/dist/summarizer/topological.d.ts +3 -0
- package/dist/summarizer/topological.js +105 -0
- package/dist/summarizer/types.d.ts +57 -0
- package/dist/summarizer/types.js +17 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Note: apiKey is intentionally absent from all defaults.
|
|
2
|
+
// If a user reaches the defaults with no key configured anywhere,
|
|
3
|
+
// the system fails clearly at the LLM call — never silently sends an empty key.
|
|
4
|
+
export const OLLAMA_DEFAULTS = {
|
|
5
|
+
deploymentMode: "local",
|
|
6
|
+
summarization: {
|
|
7
|
+
provider: "ollama",
|
|
8
|
+
model: "qwen2.5-coder:3b", // code-aware, 3B params, runs on ~2GB RAM
|
|
9
|
+
baseUrl: "http://localhost:11434",
|
|
10
|
+
batchSize: 50,
|
|
11
|
+
},
|
|
12
|
+
embedding: {
|
|
13
|
+
provider: "ollama",
|
|
14
|
+
model: "nomic-embed-text", // best local embedding model, 768 dims
|
|
15
|
+
baseUrl: "http://localhost:11434",
|
|
16
|
+
},
|
|
17
|
+
// neo4j absent — file-only mode is the safe default
|
|
18
|
+
};
|
|
19
|
+
export const ANTHROPIC_DEFAULTS = {
|
|
20
|
+
deploymentMode: "local",
|
|
21
|
+
summarization: {
|
|
22
|
+
provider: "anthropic",
|
|
23
|
+
model: "claude-haiku-4-5", // fastest Claude, cheapest, good code understanding
|
|
24
|
+
batchSize: 50,
|
|
25
|
+
// apiKey intentionally absent — user must set in config.json
|
|
26
|
+
},
|
|
27
|
+
embedding: {
|
|
28
|
+
provider: "openai",
|
|
29
|
+
model: "text-embedding-3-small", // most common, cheapest OpenAI embedding
|
|
30
|
+
// apiKey intentionally absent
|
|
31
|
+
},
|
|
32
|
+
// neo4j absent
|
|
33
|
+
};
|
|
34
|
+
// ─── Request Header Names ─────────────────────────────────────────────────────
|
|
35
|
+
//
|
|
36
|
+
// Exact header names the cloud backend sends to this Bun backend.
|
|
37
|
+
// Defined as constants so they are never mistyped across files.
|
|
38
|
+
//
|
|
39
|
+
// The server layer MUST call sanitizeHeaders() before logging any request.
|
|
40
|
+
// Headers marked "NEVER LOG" must never appear in any log output.
|
|
41
|
+
export const CONFIG_HEADERS = {
|
|
42
|
+
// LLM provider for summarization
|
|
43
|
+
PROVIDER: "x-llm-provider", // e.g. "anthropic"
|
|
44
|
+
MODEL: "x-llm-model", // e.g. "claude-haiku-4-5"
|
|
45
|
+
API_KEY: "x-llm-key",
|
|
46
|
+
BASE_URL: "x-llm-base-url", // for Ollama: "http://localhost:11434"
|
|
47
|
+
BATCH_SIZE: "x-batch-size", // e.g. "30"
|
|
48
|
+
EMBED_PROVIDER: "x-embed-provider",
|
|
49
|
+
EMBED_MODEL: "x-embed-model",
|
|
50
|
+
EMBED_KEY: "x-embed-key",
|
|
51
|
+
EMBED_BASE_URL: "x-embed-base-url", // for Ollama embedding
|
|
52
|
+
NEO4J_URL: "x-neo4j-url",
|
|
53
|
+
NEO4J_USER: "x-neo4j-user",
|
|
54
|
+
NEO4J_PASSWORD: "x-neo4j-password",
|
|
55
|
+
NEO4J_STORECODE: "false",
|
|
56
|
+
};
|
|
57
|
+
// ─── Sensitive Headers Set ────────────────────────────────────────────────────
|
|
58
|
+
//
|
|
59
|
+
// Used by sanitizeHeaders() below.
|
|
60
|
+
// Add any new secret header here the moment it is added to CONFIG_HEADERS.
|
|
61
|
+
const SENSITIVE_HEADERS = new Set([
|
|
62
|
+
CONFIG_HEADERS.API_KEY,
|
|
63
|
+
CONFIG_HEADERS.EMBED_KEY,
|
|
64
|
+
CONFIG_HEADERS.NEO4J_PASSWORD,
|
|
65
|
+
]);
|
|
66
|
+
// ─── sanitizeHeaders ──────────────────────────────────────────────────────────
|
|
67
|
+
//
|
|
68
|
+
// Call this before logging ANY request headers anywhere in the codebase.
|
|
69
|
+
// Replaces secret values with "[REDACTED]" so API keys never appear in logs.
|
|
70
|
+
//
|
|
71
|
+
// Usage:
|
|
72
|
+
// console.log("Incoming headers:", sanitizeHeaders(Object.fromEntries(req.headers)));
|
|
73
|
+
export function sanitizeHeaders(headers) {
|
|
74
|
+
const sanitized = {};
|
|
75
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
76
|
+
sanitized[key] = SENSITIVE_HEADERS.has(key.toLowerCase())
|
|
77
|
+
? "[REDACTED]"
|
|
78
|
+
: value;
|
|
79
|
+
}
|
|
80
|
+
return sanitized;
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DevLensConfig } from "./types.js";
|
|
2
|
+
type DeepPartial<T> = {
|
|
3
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
4
|
+
};
|
|
5
|
+
type PartialConfig = DeepPartial<DevLensConfig>;
|
|
6
|
+
export interface SafeConfig {
|
|
7
|
+
deploymentMode: DevLensConfig["deploymentMode"];
|
|
8
|
+
summarization: {
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
batchSize: number;
|
|
13
|
+
apiKeyHint?: string;
|
|
14
|
+
};
|
|
15
|
+
embedding: {
|
|
16
|
+
provider: string;
|
|
17
|
+
model: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
apiKeyHint?: string;
|
|
20
|
+
};
|
|
21
|
+
neo4j?: {
|
|
22
|
+
url: string;
|
|
23
|
+
username: string;
|
|
24
|
+
storeRawCode: boolean;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export declare function writeConfig(partial: PartialConfig): void;
|
|
28
|
+
export declare function maskConfig(config: DevLensConfig): SafeConfig;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//! The concept of local config file should only exist in open Source, and in case of deployment the apis for writing this cofig file should never be exposed.
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { CONFIG_FILE, CONFIG_DIR } from "./providers/file.js";
|
|
4
|
+
function readRawFile() {
|
|
5
|
+
if (!fs.existsSync(CONFIG_FILE))
|
|
6
|
+
return {};
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
console.warn(`DevLens: config file is malformed, resetting to empty.`);
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// atomicWrite
|
|
16
|
+
// Writes to a temp file first, then renames to the real path.
|
|
17
|
+
// If the server crashes mid-write, the old config survives intact.
|
|
18
|
+
function atomicWrite(filePath, content) {
|
|
19
|
+
const tmp = `${filePath}.tmp`;
|
|
20
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
21
|
+
fs.renameSync(tmp, filePath);
|
|
22
|
+
}
|
|
23
|
+
// writeConfig ─
|
|
24
|
+
//
|
|
25
|
+
// Public — called by PATCH /api/config handler.
|
|
26
|
+
//
|
|
27
|
+
// Takes only what the user changed from the UI settings form.
|
|
28
|
+
// Merges it on top of whatever is currently in config.json.
|
|
29
|
+
// Writes the result back atomically.
|
|
30
|
+
//
|
|
31
|
+
// Does NOT merge with defaults or env vars — that is file.ts's job at read time.
|
|
32
|
+
// The file only ever contains what the user explicitly set.
|
|
33
|
+
export function writeConfig(partial) {
|
|
34
|
+
// Ensure ~/.devlens/ exists — creates it on first save
|
|
35
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
36
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
const existing = readRawFile();
|
|
39
|
+
const updated = {
|
|
40
|
+
...existing,
|
|
41
|
+
// Only merge blocks that the user actually touched
|
|
42
|
+
...(partial.deploymentMode && {
|
|
43
|
+
deploymentMode: partial.deploymentMode,
|
|
44
|
+
}),
|
|
45
|
+
...(partial.summarization && {
|
|
46
|
+
summarization: {
|
|
47
|
+
...existing.summarization,
|
|
48
|
+
...partial.summarization,
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
...(partial.embedding && {
|
|
52
|
+
embedding: {
|
|
53
|
+
...existing.embedding,
|
|
54
|
+
...partial.embedding,
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
// neo4j: if user sent it, merge. If user sent null explicitly, delete it.
|
|
58
|
+
// undefined means "don't touch it"
|
|
59
|
+
...(partial.neo4j !== undefined && {
|
|
60
|
+
neo4j: partial.neo4j === null
|
|
61
|
+
? undefined // user explicitly removed Neo4j config
|
|
62
|
+
: {
|
|
63
|
+
...existing.neo4j,
|
|
64
|
+
...partial.neo4j,
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
atomicWrite(CONFIG_FILE, JSON.stringify(updated, null, 2));
|
|
69
|
+
}
|
|
70
|
+
function maskKey(key) {
|
|
71
|
+
if (!key)
|
|
72
|
+
return undefined;
|
|
73
|
+
// Show last 3 characters only — enough to identify which key is set
|
|
74
|
+
return `...${key.slice(-3)}`;
|
|
75
|
+
}
|
|
76
|
+
// maskConfig
|
|
77
|
+
// Public — called by GET /api/config handler.
|
|
78
|
+
export function maskConfig(config) {
|
|
79
|
+
return {
|
|
80
|
+
deploymentMode: config.deploymentMode,
|
|
81
|
+
summarization: {
|
|
82
|
+
provider: config.summarization.provider,
|
|
83
|
+
model: config.summarization.model,
|
|
84
|
+
baseUrl: config.summarization.baseUrl,
|
|
85
|
+
batchSize: config.summarization.batchSize,
|
|
86
|
+
apiKeyHint: maskKey(config.summarization.apiKey),
|
|
87
|
+
},
|
|
88
|
+
embedding: {
|
|
89
|
+
provider: config.embedding.provider,
|
|
90
|
+
model: config.embedding.model,
|
|
91
|
+
baseUrl: config.embedding.baseUrl,
|
|
92
|
+
apiKeyHint: maskKey(config.embedding.apiKey),
|
|
93
|
+
},
|
|
94
|
+
// neo4j: return url, storeCode, and username only — password never sent to browser
|
|
95
|
+
...(config.neo4j && {
|
|
96
|
+
neo4j: {
|
|
97
|
+
url: config.neo4j.url,
|
|
98
|
+
username: config.neo4j.username,
|
|
99
|
+
storeRawCode: config.neo4j.storeRawCode,
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
// Converts a filesystem path to a URL path
|
|
4
|
+
// e.g. /users/[userId]/posts → /users/:userId/posts
|
|
5
|
+
function toUrlPath(relativePath) {
|
|
6
|
+
return ("/" +
|
|
7
|
+
relativePath
|
|
8
|
+
.split(path.sep)
|
|
9
|
+
.map((segment) => {
|
|
10
|
+
if (segment.startsWith("[...") && segment.endsWith("]")) {
|
|
11
|
+
// Catch-all: [...slug] → :slug*
|
|
12
|
+
const param = segment.slice(4, -1);
|
|
13
|
+
return `:${param}*`;
|
|
14
|
+
}
|
|
15
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
16
|
+
// Dynamic: [userId] → :userId
|
|
17
|
+
const param = segment.slice(1, -1);
|
|
18
|
+
return `:${param}`;
|
|
19
|
+
}
|
|
20
|
+
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
21
|
+
// Route group: (auth) → ignored in URL
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return segment;
|
|
25
|
+
})
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join("/"));
|
|
28
|
+
}
|
|
29
|
+
// Extracts param names from a url path
|
|
30
|
+
// e.g. /users/:userId/posts/:postId → ["userId", "postId"]
|
|
31
|
+
function extractParams(urlPath) {
|
|
32
|
+
const matches = urlPath.match(/:([a-zA-Z]+)\*?/g) || [];
|
|
33
|
+
return matches.map((m) => m.replace(":", "").replace("*", ""));
|
|
34
|
+
}
|
|
35
|
+
// Determines the RouteNodeType from a filename
|
|
36
|
+
function getRouteNodeType(fileName) {
|
|
37
|
+
if (fileName === "page.tsx" || fileName === "page.ts" || fileName === "page.jsx" || fileName === "page.js")
|
|
38
|
+
return "PAGE";
|
|
39
|
+
if (fileName === "layout.tsx" || fileName === "layout.ts" || fileName === "layout.jsx" || fileName === "layout.js")
|
|
40
|
+
return "LAYOUT";
|
|
41
|
+
if (fileName === "loading.tsx" || fileName === "loading.ts")
|
|
42
|
+
return "LOADING";
|
|
43
|
+
if (fileName === "error.tsx" || fileName === "error.ts")
|
|
44
|
+
return "ERROR";
|
|
45
|
+
if (fileName === "not-found.tsx" || fileName === "not-found.ts")
|
|
46
|
+
return "NOT_FOUND";
|
|
47
|
+
if (fileName === "route.ts" || fileName === "route.js")
|
|
48
|
+
return "API_ROUTE";
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
// Finds the closest layout file that wraps a given route
|
|
52
|
+
function findLayoutPath(filePath, appDir) {
|
|
53
|
+
let dir = path.dirname(filePath);
|
|
54
|
+
while (dir !== appDir && dir !== path.dirname(appDir)) {
|
|
55
|
+
const layout = ["layout.tsx", "layout.ts", "layout.jsx", "layout.js"]
|
|
56
|
+
.map((f) => path.join(dir, f))
|
|
57
|
+
.find((f) => fs.existsSync(f));
|
|
58
|
+
if (layout)
|
|
59
|
+
return layout;
|
|
60
|
+
dir = path.dirname(dir);
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
// Recursively walks the app directory and collects all route nodes
|
|
65
|
+
function walkAppDir(currentDir, appDir, nodes) {
|
|
66
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
walkAppDir(fullPath, appDir, nodes);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const nodeType = getRouteNodeType(entry.name);
|
|
74
|
+
if (!nodeType)
|
|
75
|
+
continue;
|
|
76
|
+
// Get path relative to app directory
|
|
77
|
+
const relativePath = path.relative(appDir, path.dirname(fullPath));
|
|
78
|
+
const urlPath = relativePath === "." ? "/" : toUrlPath(relativePath);
|
|
79
|
+
const params = extractParams(urlPath);
|
|
80
|
+
const isDynamic = params.length > 0;
|
|
81
|
+
const isCatchAll = urlPath.includes("*");
|
|
82
|
+
const isGroupRoute = path
|
|
83
|
+
.dirname(fullPath)
|
|
84
|
+
.split(path.sep)
|
|
85
|
+
.some((s) => s.startsWith("(") && s.endsWith(")"));
|
|
86
|
+
const layoutPath = nodeType === "PAGE"
|
|
87
|
+
? findLayoutPath(fullPath, appDir)
|
|
88
|
+
: undefined;
|
|
89
|
+
nodes.push({
|
|
90
|
+
type: nodeType,
|
|
91
|
+
urlPath,
|
|
92
|
+
filePath: fullPath,
|
|
93
|
+
isDynamic,
|
|
94
|
+
isCatchAll,
|
|
95
|
+
isGroupRoute,
|
|
96
|
+
layoutPath,
|
|
97
|
+
params: params.length > 0 ? params : undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function analyzeAppRouter(repoPath) {
|
|
102
|
+
let appDir = path.join(repoPath, "src/app");
|
|
103
|
+
if (!fs.existsSync(appDir)) {
|
|
104
|
+
appDir = path.join(repoPath, "app");
|
|
105
|
+
}
|
|
106
|
+
if (!fs.existsSync(appDir)) {
|
|
107
|
+
throw new Error(`No app directory found at: ${appDir}`);
|
|
108
|
+
}
|
|
109
|
+
// Check for middleware at root level
|
|
110
|
+
const nodes = [];
|
|
111
|
+
const middlewarePath = ["middleware.ts", "middleware.js"]
|
|
112
|
+
.map((f) => path.join(repoPath, f))
|
|
113
|
+
.find((f) => fs.existsSync(f));
|
|
114
|
+
if (middlewarePath) {
|
|
115
|
+
nodes.push({
|
|
116
|
+
type: "MIDDLEWARE",
|
|
117
|
+
urlPath: "*",
|
|
118
|
+
filePath: middlewarePath,
|
|
119
|
+
isDynamic: false,
|
|
120
|
+
isCatchAll: true,
|
|
121
|
+
isGroupRoute: false,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
walkAppDir(appDir, appDir, nodes);
|
|
125
|
+
return nodes;
|
|
126
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
const HTTP_METHODS = [
|
|
5
|
+
"get", "post", "put", "delete",
|
|
6
|
+
"patch", "options", "head",
|
|
7
|
+
];
|
|
8
|
+
const APP_INSTANCE_NAMES = [
|
|
9
|
+
"app", "router", "fastify",
|
|
10
|
+
"server", "api", "koa",
|
|
11
|
+
];
|
|
12
|
+
const IGNORE_DIRS = [
|
|
13
|
+
"node_modules", "dist", "build",
|
|
14
|
+
".next", "coverage", ".git",
|
|
15
|
+
];
|
|
16
|
+
function extractParams(urlPath) {
|
|
17
|
+
const matches = urlPath.match(/:([a-zA-Z0-9_]+)/g) || [];
|
|
18
|
+
return matches.map((m) => m.replace(":", ""));
|
|
19
|
+
}
|
|
20
|
+
function normalizeMethod(method) {
|
|
21
|
+
return method.toUpperCase();
|
|
22
|
+
}
|
|
23
|
+
// Detects backend framework from file import statements
|
|
24
|
+
function detectFileFramework(fileContent) {
|
|
25
|
+
if (fileContent.includes("from 'express'") ||
|
|
26
|
+
fileContent.includes('from "express"') ||
|
|
27
|
+
fileContent.includes("require('express')") ||
|
|
28
|
+
fileContent.includes('require("express")'))
|
|
29
|
+
return "express";
|
|
30
|
+
if (fileContent.includes("from 'fastify'") ||
|
|
31
|
+
fileContent.includes('from "fastify"') ||
|
|
32
|
+
fileContent.includes("require('fastify')") ||
|
|
33
|
+
fileContent.includes('require("fastify")'))
|
|
34
|
+
return "fastify";
|
|
35
|
+
if (fileContent.includes("from 'koa'") ||
|
|
36
|
+
fileContent.includes('from "koa"') ||
|
|
37
|
+
fileContent.includes("require('koa')") ||
|
|
38
|
+
fileContent.includes('require("koa")'))
|
|
39
|
+
return "koa";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function findBackendFiles(dir, files = []) {
|
|
43
|
+
let entries;
|
|
44
|
+
try {
|
|
45
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const fullPath = path.join(dir, entry.name);
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
if (IGNORE_DIRS.includes(entry.name))
|
|
54
|
+
continue;
|
|
55
|
+
findBackendFiles(fullPath, files);
|
|
56
|
+
}
|
|
57
|
+
else if (entry.isFile()) {
|
|
58
|
+
if (/\.(ts|js)$/.test(entry.name)) {
|
|
59
|
+
files.push(fullPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
export function analyzeBackendRoutes(repoPath) {
|
|
66
|
+
const nodes = [];
|
|
67
|
+
const project = new Project({
|
|
68
|
+
compilerOptions: {
|
|
69
|
+
allowJs: true,
|
|
70
|
+
checkJs: false,
|
|
71
|
+
strict: false,
|
|
72
|
+
},
|
|
73
|
+
skipAddingFilesFromTsConfig: true,
|
|
74
|
+
});
|
|
75
|
+
const files = findBackendFiles(repoPath);
|
|
76
|
+
// Only add files that actually import a backend framework
|
|
77
|
+
for (const filePath of files) {
|
|
78
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
79
|
+
if (detectFileFramework(content)) {
|
|
80
|
+
project.addSourceFileAtPath(filePath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const file of project.getSourceFiles()) {
|
|
84
|
+
const filePath = file.getFilePath();
|
|
85
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
86
|
+
const framework = detectFileFramework(content);
|
|
87
|
+
if (!framework)
|
|
88
|
+
continue;
|
|
89
|
+
const callExpressions = file.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
90
|
+
for (const call of callExpressions) {
|
|
91
|
+
try {
|
|
92
|
+
const expression = call.getExpression();
|
|
93
|
+
const expressionText = expression.getText();
|
|
94
|
+
const parts = expressionText.split(".");
|
|
95
|
+
if (parts.length < 2)
|
|
96
|
+
continue;
|
|
97
|
+
const methodName = parts[parts.length - 1].toLowerCase();
|
|
98
|
+
const objectName = parts[parts.length - 2].toLowerCase();
|
|
99
|
+
if (!HTTP_METHODS.includes(methodName))
|
|
100
|
+
continue;
|
|
101
|
+
const isKnownInstance = APP_INSTANCE_NAMES.includes(objectName);
|
|
102
|
+
const looksLikeRouter = objectName.includes("router") ||
|
|
103
|
+
objectName.includes("app") ||
|
|
104
|
+
objectName.includes("server") ||
|
|
105
|
+
objectName.includes("api");
|
|
106
|
+
if (!isKnownInstance && !looksLikeRouter)
|
|
107
|
+
continue;
|
|
108
|
+
const args = call.getArguments();
|
|
109
|
+
if (args.length === 0)
|
|
110
|
+
continue;
|
|
111
|
+
const firstArgText = args[0].getText();
|
|
112
|
+
// Must be a string literal
|
|
113
|
+
if (!firstArgText.startsWith("'") &&
|
|
114
|
+
!firstArgText.startsWith('"') &&
|
|
115
|
+
!firstArgText.startsWith("`"))
|
|
116
|
+
continue;
|
|
117
|
+
// Remove quotes correctly — handles single, double, and backtick
|
|
118
|
+
const urlPath = firstArgText.replace(/^['"`]|['"`]$/g, "");
|
|
119
|
+
if (!urlPath.startsWith("/"))
|
|
120
|
+
continue;
|
|
121
|
+
// Extract handler name if last argument is a simple identifier
|
|
122
|
+
let handlerName;
|
|
123
|
+
let inlineHandler;
|
|
124
|
+
if (args.length >= 2) {
|
|
125
|
+
const lastArg = args[args.length - 1];
|
|
126
|
+
const lastArgText = lastArg.getText();
|
|
127
|
+
if (!lastArgText.includes("=>") &&
|
|
128
|
+
!lastArgText.includes("function") &&
|
|
129
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(lastArgText)) {
|
|
130
|
+
handlerName = lastArgText;
|
|
131
|
+
}
|
|
132
|
+
else if (lastArgText.includes("=>") || lastArgText.includes("function")) {
|
|
133
|
+
//extract the inline handler
|
|
134
|
+
inlineHandler = {
|
|
135
|
+
rawCode: lastArgText,
|
|
136
|
+
startLine: lastArg.getStartLineNumber(),
|
|
137
|
+
endLine: lastArg.getEndLineNumber(),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
;
|
|
141
|
+
}
|
|
142
|
+
const params = extractParams(urlPath);
|
|
143
|
+
nodes.push({
|
|
144
|
+
type: "BACKEND_ROUTE",
|
|
145
|
+
urlPath,
|
|
146
|
+
filePath,
|
|
147
|
+
httpMethod: normalizeMethod(methodName),
|
|
148
|
+
handlerName,
|
|
149
|
+
inlineHandler,
|
|
150
|
+
framework,
|
|
151
|
+
isDynamic: params.length > 0,
|
|
152
|
+
params: params.length > 0 ? params : undefined,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return nodes;
|
|
161
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { analyzeAppRouter } from "./appRouter.js";
|
|
3
|
+
import { analyzePagesRouter } from "./pagesRouter.js";
|
|
4
|
+
import { analyzeBackendRoutes } from "./backendRoutes.js";
|
|
5
|
+
export function analyzeFilesystem(repoPath, fingerprint) {
|
|
6
|
+
// Handle backend frameworks first
|
|
7
|
+
if (["express", "fastify", "koa"].includes(fingerprint.framework)) {
|
|
8
|
+
return analyzeBackendRoutes(repoPath);
|
|
9
|
+
}
|
|
10
|
+
// Handle Next.js frontend
|
|
11
|
+
if (fingerprint.framework !== "nextjs") {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
switch (fingerprint.router) {
|
|
15
|
+
case "app":
|
|
16
|
+
return analyzeAppRouter(repoPath);
|
|
17
|
+
case "pages":
|
|
18
|
+
return analyzePagesRouter(repoPath);
|
|
19
|
+
case "app+pages":
|
|
20
|
+
// Analyze both and merge results
|
|
21
|
+
const appRoutes = analyzeAppRouter(repoPath);
|
|
22
|
+
const pagesRoutes = analyzePagesRouter(repoPath);
|
|
23
|
+
return [...appRoutes, ...pagesRoutes];
|
|
24
|
+
default:
|
|
25
|
+
console.warn(`Next.js project detected but no app or pages folder found at: ${path.resolve(repoPath)}`);
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|