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.
Files changed (136) hide show
  1. package/LICENSE +674 -0
  2. package/dist/clustering/index.d.ts +27 -0
  3. package/dist/clustering/index.js +149 -0
  4. package/dist/config/index.d.ts +10 -0
  5. package/dist/config/index.js +78 -0
  6. package/dist/config/providers/file.d.ts +19 -0
  7. package/dist/config/providers/file.js +215 -0
  8. package/dist/config/providers/request.d.ts +2 -0
  9. package/dist/config/providers/request.js +72 -0
  10. package/dist/config/types.d.ts +46 -0
  11. package/dist/config/types.js +81 -0
  12. package/dist/config/writer.d.ts +29 -0
  13. package/dist/config/writer.js +103 -0
  14. package/dist/filesystem/appRouter.d.ts +2 -0
  15. package/dist/filesystem/appRouter.js +126 -0
  16. package/dist/filesystem/backendRoutes.d.ts +2 -0
  17. package/dist/filesystem/backendRoutes.js +161 -0
  18. package/dist/filesystem/index.d.ts +2 -0
  19. package/dist/filesystem/index.js +28 -0
  20. package/dist/filesystem/index.test.d.ts +1 -0
  21. package/dist/filesystem/index.test.js +178 -0
  22. package/dist/filesystem/pagesRouter.d.ts +2 -0
  23. package/dist/filesystem/pagesRouter.js +109 -0
  24. package/dist/fingerprint/detectors.d.ts +8 -0
  25. package/dist/fingerprint/detectors.js +174 -0
  26. package/dist/fingerprint/index.d.ts +2 -0
  27. package/dist/fingerprint/index.js +41 -0
  28. package/dist/fingerprint/index.test.d.ts +1 -0
  29. package/dist/fingerprint/index.test.js +148 -0
  30. package/dist/graph/buildLookup.d.ts +10 -0
  31. package/dist/graph/buildLookup.js +32 -0
  32. package/dist/graph/edges/callEdges.d.ts +7 -0
  33. package/dist/graph/edges/callEdges.js +145 -0
  34. package/dist/graph/edges/eventEdges.d.ts +7 -0
  35. package/dist/graph/edges/eventEdges.js +203 -0
  36. package/dist/graph/edges/guardEdges.d.ts +3 -0
  37. package/dist/graph/edges/guardEdges.js +232 -0
  38. package/dist/graph/edges/hookEdges.d.ts +3 -0
  39. package/dist/graph/edges/hookEdges.js +54 -0
  40. package/dist/graph/edges/importEdges.d.ts +8 -0
  41. package/dist/graph/edges/importEdges.js +224 -0
  42. package/dist/graph/edges/propEdges.d.ts +3 -0
  43. package/dist/graph/edges/propEdges.js +142 -0
  44. package/dist/graph/edges/routeEdge.d.ts +3 -0
  45. package/dist/graph/edges/routeEdge.js +124 -0
  46. package/dist/graph/edges/stateEdges.d.ts +3 -0
  47. package/dist/graph/edges/stateEdges.js +206 -0
  48. package/dist/graph/edges/testEdges.d.ts +3 -0
  49. package/dist/graph/edges/testEdges.js +143 -0
  50. package/dist/graph/edges/utils.d.ts +2 -0
  51. package/dist/graph/edges/utils.js +25 -0
  52. package/dist/graph/index.d.ts +6 -0
  53. package/dist/graph/index.js +65 -0
  54. package/dist/graph/index.test.d.ts +1 -0
  55. package/dist/graph/index.test.js +542 -0
  56. package/dist/graph/thirdPartyLibs.d.ts +8 -0
  57. package/dist/graph/thirdPartyLibs.js +162 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +15 -0
  60. package/dist/jobs/index.d.ts +5 -0
  61. package/dist/jobs/index.js +11 -0
  62. package/dist/jobs/queue/interface.d.ts +13 -0
  63. package/dist/jobs/queue/interface.js +1 -0
  64. package/dist/jobs/queue/memory.d.ts +24 -0
  65. package/dist/jobs/queue/memory.js +291 -0
  66. package/dist/jobs/runner.d.ts +3 -0
  67. package/dist/jobs/runner.js +136 -0
  68. package/dist/jobs/types.d.ts +112 -0
  69. package/dist/jobs/types.js +33 -0
  70. package/dist/parser/directives.d.ts +4 -0
  71. package/dist/parser/directives.js +31 -0
  72. package/dist/parser/extractors/components.d.ts +5 -0
  73. package/dist/parser/extractors/components.js +240 -0
  74. package/dist/parser/extractors/functions.d.ts +4 -0
  75. package/dist/parser/extractors/functions.js +240 -0
  76. package/dist/parser/extractors/hooks.d.ts +4 -0
  77. package/dist/parser/extractors/hooks.js +128 -0
  78. package/dist/parser/extractors/stores.d.ts +3 -0
  79. package/dist/parser/extractors/stores.js +181 -0
  80. package/dist/parser/index.d.ts +14 -0
  81. package/dist/parser/index.js +168 -0
  82. package/dist/parser/index.test.d.ts +1 -0
  83. package/dist/parser/index.test.js +319 -0
  84. package/dist/parser/typeUtils.d.ts +9 -0
  85. package/dist/parser/typeUtils.js +46 -0
  86. package/dist/pipeline/index.d.ts +50 -0
  87. package/dist/pipeline/index.js +249 -0
  88. package/dist/scoring/connectionCounter.d.ts +28 -0
  89. package/dist/scoring/connectionCounter.js +134 -0
  90. package/dist/scoring/fileScorer.d.ts +2 -0
  91. package/dist/scoring/fileScorer.js +44 -0
  92. package/dist/scoring/index.d.ts +22 -0
  93. package/dist/scoring/index.js +130 -0
  94. package/dist/scoring/index.test.d.ts +1 -0
  95. package/dist/scoring/index.test.js +453 -0
  96. package/dist/scoring/nodeScorer.d.ts +3 -0
  97. package/dist/scoring/nodeScorer.js +108 -0
  98. package/dist/scoring/noiseFilter.d.ts +18 -0
  99. package/dist/scoring/noiseFilter.js +92 -0
  100. package/dist/storage/fileStorage.d.ts +117 -0
  101. package/dist/storage/fileStorage.js +616 -0
  102. package/dist/storage/index.d.ts +4 -0
  103. package/dist/storage/index.js +2 -0
  104. package/dist/storage/interface.d.ts +27 -0
  105. package/dist/storage/interface.js +1 -0
  106. package/dist/summarizer/checkpoint.d.ts +15 -0
  107. package/dist/summarizer/checkpoint.js +110 -0
  108. package/dist/summarizer/index.d.ts +2 -0
  109. package/dist/summarizer/index.js +281 -0
  110. package/dist/summarizer/mapreduce.d.ts +4 -0
  111. package/dist/summarizer/mapreduce.js +87 -0
  112. package/dist/summarizer/prompts.d.ts +22 -0
  113. package/dist/summarizer/prompts.js +205 -0
  114. package/dist/summarizer/providers/anthropic.d.ts +9 -0
  115. package/dist/summarizer/providers/anthropic.js +78 -0
  116. package/dist/summarizer/providers/gemini.d.ts +9 -0
  117. package/dist/summarizer/providers/gemini.js +79 -0
  118. package/dist/summarizer/providers/index.d.ts +3 -0
  119. package/dist/summarizer/providers/index.js +43 -0
  120. package/dist/summarizer/providers/ollama.d.ts +9 -0
  121. package/dist/summarizer/providers/ollama.js +23 -0
  122. package/dist/summarizer/providers/openRouter.d.ts +9 -0
  123. package/dist/summarizer/providers/openRouter.js +19 -0
  124. package/dist/summarizer/providers/openai.d.ts +9 -0
  125. package/dist/summarizer/providers/openai.js +72 -0
  126. package/dist/summarizer/providers/types.d.ts +32 -0
  127. package/dist/summarizer/providers/types.js +1 -0
  128. package/dist/summarizer/retry.d.ts +7 -0
  129. package/dist/summarizer/retry.js +51 -0
  130. package/dist/summarizer/topological.d.ts +3 -0
  131. package/dist/summarizer/topological.js +105 -0
  132. package/dist/summarizer/types.d.ts +57 -0
  133. package/dist/summarizer/types.js +17 -0
  134. package/dist/types.d.ts +78 -0
  135. package/dist/types.js +1 -0
  136. package/package.json +48 -0
@@ -0,0 +1,178 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { analyzeFilesystem } from "./index.js";
5
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
6
+ function createFakeRepo(structure) {
7
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devlens-fs-test-"));
8
+ for (const filePath of structure) {
9
+ const fullPath = path.join(tmpDir, filePath);
10
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
11
+ fs.writeFileSync(fullPath, "// fake file");
12
+ }
13
+ return tmpDir;
14
+ }
15
+ function deleteFakeRepo(repoPath) {
16
+ fs.rmSync(repoPath, { recursive: true, force: true });
17
+ }
18
+ function makeFingerprint(framework, router) {
19
+ return {
20
+ language: "typescript",
21
+ projectType: "frontend",
22
+ framework,
23
+ router,
24
+ stateManagement: ["context-only"],
25
+ dataFetching: ["fetch"],
26
+ databases: [],
27
+ rawDependencies: {},
28
+ };
29
+ }
30
+ // ─── Tests ────────────────────────────────────────────────────────────────────
31
+ describe("analyzeFilesystem", () => {
32
+ // ─── Non Next.js projects ─────────────────────────────────────────────────
33
+ it("should return empty array for plain React projects", () => {
34
+ const repoPath = createFakeRepo(["src/App.tsx"]);
35
+ const fingerprint = makeFingerprint("react", "react-router");
36
+ const routes = analyzeFilesystem(repoPath, fingerprint);
37
+ expect(routes).toHaveLength(0);
38
+ deleteFakeRepo(repoPath);
39
+ });
40
+ it("should return empty array for unknown projects", () => {
41
+ const repoPath = createFakeRepo(["src/index.ts"]);
42
+ const fingerprint = makeFingerprint("unknown", "none");
43
+ const routes = analyzeFilesystem(repoPath, fingerprint);
44
+ expect(routes).toHaveLength(0);
45
+ deleteFakeRepo(repoPath);
46
+ });
47
+ // ─── App Router ───────────────────────────────────────────────────────────
48
+ it("should detect root page in app router", () => {
49
+ const repoPath = createFakeRepo(["src/app/page.tsx"]);
50
+ const fingerprint = makeFingerprint("nextjs", "app");
51
+ const routes = analyzeFilesystem(repoPath, fingerprint);
52
+ const page = routes.find((r) => r.type === "PAGE");
53
+ expect(page).toBeDefined();
54
+ expect(page?.urlPath).toBe("/");
55
+ deleteFakeRepo(repoPath);
56
+ });
57
+ it("should detect nested page in app router", () => {
58
+ const repoPath = createFakeRepo(["src/app/dashboard/page.tsx"]);
59
+ const fingerprint = makeFingerprint("nextjs", "app");
60
+ const routes = analyzeFilesystem(repoPath, fingerprint);
61
+ const page = routes.find((r) => r.type === "PAGE");
62
+ expect(page?.urlPath).toBe("/dashboard");
63
+ deleteFakeRepo(repoPath);
64
+ });
65
+ it("should detect dynamic route in app router", () => {
66
+ const repoPath = createFakeRepo(["src/app/users/[userId]/page.tsx"]);
67
+ const fingerprint = makeFingerprint("nextjs", "app");
68
+ const routes = analyzeFilesystem(repoPath, fingerprint);
69
+ const page = routes.find((r) => r.type === "PAGE");
70
+ expect(page?.isDynamic).toBe(true);
71
+ expect(page?.params).toContain("userId");
72
+ deleteFakeRepo(repoPath);
73
+ });
74
+ it("should detect catch all route in app router", () => {
75
+ const repoPath = createFakeRepo(["src/app/docs/[...slug]/page.tsx"]);
76
+ const fingerprint = makeFingerprint("nextjs", "app");
77
+ const routes = analyzeFilesystem(repoPath, fingerprint);
78
+ const page = routes.find((r) => r.type === "PAGE");
79
+ expect(page?.isCatchAll).toBe(true);
80
+ deleteFakeRepo(repoPath);
81
+ });
82
+ it("should detect route group and ignore it in url", () => {
83
+ const repoPath = createFakeRepo(["src/app/(auth)/login/page.tsx"]);
84
+ const fingerprint = makeFingerprint("nextjs", "app");
85
+ const routes = analyzeFilesystem(repoPath, fingerprint);
86
+ const page = routes.find((r) => r.type === "PAGE");
87
+ expect(page?.urlPath).toBe("/login");
88
+ expect(page?.isGroupRoute).toBe(true);
89
+ deleteFakeRepo(repoPath);
90
+ });
91
+ it("should detect layout in app router", () => {
92
+ const repoPath = createFakeRepo(["src/app/layout.tsx", "src/app/page.tsx"]);
93
+ const fingerprint = makeFingerprint("nextjs", "app");
94
+ const routes = analyzeFilesystem(repoPath, fingerprint);
95
+ const layout = routes.find((r) => r.type === "LAYOUT");
96
+ expect(layout).toBeDefined();
97
+ deleteFakeRepo(repoPath);
98
+ });
99
+ it("should detect api route in app router", () => {
100
+ const repoPath = createFakeRepo(["src/app/api/users/route.ts"]);
101
+ const fingerprint = makeFingerprint("nextjs", "app");
102
+ const routes = analyzeFilesystem(repoPath, fingerprint);
103
+ const api = routes.find((r) => r.type === "API_ROUTE");
104
+ expect(api).toBeDefined();
105
+ expect(api?.urlPath).toBe("/api/users");
106
+ deleteFakeRepo(repoPath);
107
+ });
108
+ it("should detect middleware in app router project", () => {
109
+ const repoPath = createFakeRepo([
110
+ "src/app/page.tsx",
111
+ "middleware.ts",
112
+ ]);
113
+ const fingerprint = makeFingerprint("nextjs", "app");
114
+ const routes = analyzeFilesystem(repoPath, fingerprint);
115
+ const middleware = routes.find((r) => r.type === "MIDDLEWARE");
116
+ expect(middleware).toBeDefined();
117
+ expect(middleware?.isCatchAll).toBe(true);
118
+ deleteFakeRepo(repoPath);
119
+ });
120
+ it("should detect layout path for a page", () => {
121
+ const repoPath = createFakeRepo([
122
+ "src/app/dashboard/layout.tsx",
123
+ "src/app/dashboard/page.tsx",
124
+ ]);
125
+ const fingerprint = makeFingerprint("nextjs", "app");
126
+ const routes = analyzeFilesystem(repoPath, fingerprint);
127
+ const page = routes.find((r) => r.type === "PAGE");
128
+ expect(page?.layoutPath).toBeDefined();
129
+ deleteFakeRepo(repoPath);
130
+ });
131
+ // ─── Pages Router ─────────────────────────────────────────────────────────
132
+ it("should detect root page in pages router", () => {
133
+ const repoPath = createFakeRepo(["src/pages/index.tsx"]);
134
+ const fingerprint = makeFingerprint("nextjs", "pages");
135
+ const routes = analyzeFilesystem(repoPath, fingerprint);
136
+ const page = routes.find((r) => r.type === "PAGE");
137
+ expect(page?.urlPath).toBe("/");
138
+ deleteFakeRepo(repoPath);
139
+ });
140
+ it("should detect nested page in pages router", () => {
141
+ const repoPath = createFakeRepo(["src/pages/dashboard/index.tsx"]);
142
+ const fingerprint = makeFingerprint("nextjs", "pages");
143
+ const routes = analyzeFilesystem(repoPath, fingerprint);
144
+ const page = routes.find((r) => r.type === "PAGE");
145
+ expect(page?.urlPath).toBe("/dashboard");
146
+ deleteFakeRepo(repoPath);
147
+ });
148
+ it("should detect dynamic route in pages router", () => {
149
+ const repoPath = createFakeRepo(["src/pages/users/[userId].tsx"]);
150
+ const fingerprint = makeFingerprint("nextjs", "pages");
151
+ const routes = analyzeFilesystem(repoPath, fingerprint);
152
+ const page = routes.find((r) => r.type === "PAGE");
153
+ expect(page?.isDynamic).toBe(true);
154
+ expect(page?.params).toContain("userId");
155
+ deleteFakeRepo(repoPath);
156
+ });
157
+ it("should detect api route in pages router", () => {
158
+ const repoPath = createFakeRepo(["src/pages/api/users.ts"]);
159
+ const fingerprint = makeFingerprint("nextjs", "pages");
160
+ const routes = analyzeFilesystem(repoPath, fingerprint);
161
+ const api = routes.find((r) => r.type === "API_ROUTE");
162
+ expect(api).toBeDefined();
163
+ expect(api?.urlPath).toBe("/api/users");
164
+ deleteFakeRepo(repoPath);
165
+ });
166
+ it("should skip special files in pages router", () => {
167
+ const repoPath = createFakeRepo([
168
+ "src/pages/_app.tsx",
169
+ "src/pages/_document.tsx",
170
+ "src/pages/index.tsx",
171
+ ]);
172
+ const fingerprint = makeFingerprint("nextjs", "pages");
173
+ const routes = analyzeFilesystem(repoPath, fingerprint);
174
+ // Only index.tsx should be detected, not _app or _document
175
+ expect(routes).toHaveLength(1);
176
+ deleteFakeRepo(repoPath);
177
+ });
178
+ });
@@ -0,0 +1,2 @@
1
+ import type { RouteNode } from "../types.js";
2
+ export declare function analyzePagesRouter(repoPath: string): RouteNode[];
@@ -0,0 +1,109 @@
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
+ if (relativePath === "index")
7
+ return "/";
8
+ return ("/" +
9
+ relativePath
10
+ .split(path.sep)
11
+ .map((segment) => {
12
+ // Remove file extension from last segment
13
+ segment = segment.replace(/\.(tsx|ts|jsx|js)$/, "");
14
+ if (segment === "index")
15
+ return null;
16
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
17
+ const param = segment.slice(4, -1);
18
+ return `:${param}*`;
19
+ }
20
+ if (segment.startsWith("[") && segment.endsWith("]")) {
21
+ const param = segment.slice(1, -1);
22
+ return `:${param}`;
23
+ }
24
+ return segment;
25
+ })
26
+ .filter(Boolean)
27
+ .join("/"));
28
+ }
29
+ // Extracts param names from a url path
30
+ function extractParams(urlPath) {
31
+ const matches = urlPath.match(/:([a-zA-Z]+)\*?/g) || [];
32
+ return matches.map((m) => m.replace(":", "").replace("*", ""));
33
+ }
34
+ // Checks if a file is a valid page file
35
+ function isPageFile(fileName) {
36
+ return /\.(tsx|ts|jsx|js)$/.test(fileName);
37
+ }
38
+ // Checks if a file is a special Next.js file to skip
39
+ function isSpecialFile(fileName) {
40
+ const special = ["_app", "_document", "_error", "_middleware"];
41
+ const nameWithoutExt = fileName.replace(/\.(tsx|ts|jsx|js)$/, "");
42
+ return special.includes(nameWithoutExt);
43
+ }
44
+ // Determines node type based on file location
45
+ function getRouteNodeType(relativePath, fileName) {
46
+ const nameWithoutExt = fileName.replace(/\.(tsx|ts|jsx|js)$/, "");
47
+ // Files inside pages/api are API routes
48
+ if (relativePath.startsWith("api" + path.sep) || relativePath === "api") {
49
+ return "API_ROUTE";
50
+ }
51
+ if (nameWithoutExt === "404")
52
+ return "NOT_FOUND";
53
+ return "PAGE";
54
+ }
55
+ function walkPagesDir(currentDir, pagesDir, nodes) {
56
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ const fullPath = path.join(currentDir, entry.name);
59
+ if (entry.isDirectory()) {
60
+ walkPagesDir(fullPath, pagesDir, nodes);
61
+ continue;
62
+ }
63
+ // Skip non page files
64
+ if (!isPageFile(entry.name))
65
+ continue;
66
+ // Skip special Next.js files like _app.tsx, _document.tsx
67
+ if (isSpecialFile(entry.name))
68
+ continue;
69
+ const relativePath = path.relative(pagesDir, fullPath);
70
+ const nodeType = getRouteNodeType(path.relative(pagesDir, currentDir), entry.name);
71
+ const urlPath = toUrlPath(relativePath);
72
+ const params = extractParams(urlPath);
73
+ nodes.push({
74
+ type: nodeType,
75
+ urlPath,
76
+ filePath: fullPath,
77
+ isDynamic: params.length > 0,
78
+ isCatchAll: urlPath.includes("*"),
79
+ isGroupRoute: false, // pages router has no route groups
80
+ params: params.length > 0 ? params : undefined,
81
+ });
82
+ }
83
+ }
84
+ export function analyzePagesRouter(repoPath) {
85
+ let pagesDir = path.join(repoPath, "src/pages");
86
+ if (!fs.existsSync(pagesDir)) {
87
+ pagesDir = path.join(repoPath, "pages");
88
+ }
89
+ if (!fs.existsSync(pagesDir)) {
90
+ throw new Error(`No pages directory found at: ${pagesDir}`);
91
+ }
92
+ const nodes = [];
93
+ // Check for middleware at root level
94
+ const middlewarePath = ["middleware.ts", "middleware.js"]
95
+ .map((f) => path.join(repoPath, f))
96
+ .find((f) => fs.existsSync(f));
97
+ if (middlewarePath) {
98
+ nodes.push({
99
+ type: "MIDDLEWARE",
100
+ urlPath: "*",
101
+ filePath: middlewarePath,
102
+ isDynamic: false,
103
+ isCatchAll: true,
104
+ isGroupRoute: false,
105
+ });
106
+ }
107
+ walkPagesDir(pagesDir, pagesDir, nodes);
108
+ return nodes;
109
+ }
@@ -0,0 +1,8 @@
1
+ import type { Language, Framework, RouterType, StateLibrary, DataFetchingLibrary, DatabaseLibrary, ProjectType } from "../types.js";
2
+ export declare function detectLanguage(repoPath: string): Language;
3
+ export declare function detectFramework(deps: Record<string, string>): Framework;
4
+ export declare function detectRouter(deps: Record<string, string>, framework: Framework, repoPath: string): RouterType;
5
+ export declare function detectStateManagement(deps: Record<string, string>): StateLibrary[];
6
+ export declare function detectDataFetching(deps: Record<string, string>): DataFetchingLibrary[];
7
+ export declare function detectDatabases(deps: Record<string, string>): DatabaseLibrary[];
8
+ export declare function detectProjectType(framework: Framework, deps: Record<string, string>, repoPath: string): ProjectType;
@@ -0,0 +1,174 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ // Framework category arrays — used instead of type casts
4
+ const FRONTEND_FRAMEWORKS = ["nextjs", "react"];
5
+ const BACKEND_FRAMEWORKS = ["express", "fastify", "koa"];
6
+ export function detectLanguage(repoPath) {
7
+ if (fs.existsSync(path.join(repoPath, "tsconfig.json")))
8
+ return "typescript";
9
+ return "javascript";
10
+ }
11
+ export function detectFramework(deps) {
12
+ if ("next" in deps)
13
+ return "nextjs";
14
+ if ("react" in deps)
15
+ return "react";
16
+ if ("express" in deps)
17
+ return "express";
18
+ if ("fastify" in deps)
19
+ return "fastify";
20
+ if ("koa" in deps)
21
+ return "koa";
22
+ return "unknown";
23
+ }
24
+ export function detectRouter(deps, framework, repoPath) {
25
+ // Router detection only applies to Next.js and React
26
+ // Backend frameworks handle routing via code not filesystem
27
+ if (framework === "nextjs") {
28
+ const hasAppDir = fs.existsSync(path.join(repoPath, "src/app")) || fs.existsSync(path.join(repoPath, "app"));
29
+ const hasPagesDir = fs.existsSync(path.join(repoPath, "src/pages")) || fs.existsSync(path.join(repoPath, "pages"));
30
+ if (hasAppDir && hasPagesDir)
31
+ return "app+pages";
32
+ if (hasAppDir)
33
+ return "app";
34
+ if (hasPagesDir)
35
+ return "pages";
36
+ }
37
+ if (framework === "react") {
38
+ if ("react-router-dom" in deps)
39
+ return "react-router";
40
+ }
41
+ return "none";
42
+ }
43
+ export function detectStateManagement(deps) {
44
+ const found = [];
45
+ if ("zustand" in deps)
46
+ found.push("zustand");
47
+ if ("redux" in deps || "@reduxjs/toolkit" in deps)
48
+ found.push("redux");
49
+ if ("recoil" in deps)
50
+ found.push("recoil");
51
+ if ("jotai" in deps)
52
+ found.push("jotai");
53
+ if (found.length === 0)
54
+ found.push("context-only");
55
+ return found;
56
+ }
57
+ export function detectDataFetching(deps) {
58
+ const found = [];
59
+ if ("@tanstack/react-query" in deps || "react-query" in deps)
60
+ found.push("react-query");
61
+ if ("swr" in deps)
62
+ found.push("swr");
63
+ if ("axios" in deps)
64
+ found.push("axios");
65
+ if (found.length === 0)
66
+ found.push("fetch");
67
+ return found;
68
+ }
69
+ export function detectDatabases(deps) {
70
+ const found = [];
71
+ if ("prisma" in deps || "@prisma/client" in deps)
72
+ found.push("prisma");
73
+ if ("drizzle-orm" in deps)
74
+ found.push("drizzle");
75
+ if ("mongoose" in deps || "mongodb" in deps)
76
+ found.push("mongodb");
77
+ if ("firebase" in deps || "firebase-admin" in deps)
78
+ found.push("firebase");
79
+ if ("@supabase/supabase-js" in deps)
80
+ found.push("supabase");
81
+ if ("@planetscale/database" in deps)
82
+ found.push("planetscale");
83
+ if ("pg" in deps || "postgres" in deps)
84
+ found.push("postgres");
85
+ if ("mysql2" in deps)
86
+ found.push("mysql");
87
+ if ("better-sqlite3" in deps)
88
+ found.push("sqlite");
89
+ return found;
90
+ }
91
+ export function detectProjectType(framework, deps, repoPath) {
92
+ const frontendDeps = [
93
+ "next", "react", "react-dom",
94
+ "@tanstack/react-query", "swr",
95
+ ];
96
+ const backendDeps = [
97
+ "express", "fastify", "koa",
98
+ "koa-router", "@fastify/router",
99
+ ];
100
+ const databaseDeps = [
101
+ "prisma", "@prisma/client", "drizzle-orm",
102
+ "mongoose", "mongodb", "pg", "mysql2",
103
+ "better-sqlite3", "firebase", "@supabase/supabase-js",
104
+ ];
105
+ const hasFrontendDeps = Object.keys(deps).some((d) => frontendDeps.includes(d));
106
+ const hasBackendDeps = Object.keys(deps).some((d) => backendDeps.includes(d));
107
+ const hasDatabaseDeps = Object.keys(deps).some((d) => databaseDeps.includes(d));
108
+ // Check directory structures
109
+ const hasFrontendStructure = fs.existsSync(path.join(repoPath, "src", "app")) ||
110
+ fs.existsSync(path.join(repoPath, "src", "pages")) ||
111
+ (fs.existsSync(path.join(repoPath, "src")) &&
112
+ fs.existsSync(path.join(repoPath, "src", "components")));
113
+ const hasBackendStructure = fs.existsSync(path.join(repoPath, "routes")) ||
114
+ fs.existsSync(path.join(repoPath, "server")) ||
115
+ fs.existsSync(path.join(repoPath, "controllers")) ||
116
+ fs.existsSync(path.join(repoPath, "services")) ||
117
+ fs.existsSync(path.join(repoPath, "middleware"));
118
+ fs.existsSync(path.join(repoPath, "src/routes")) ||
119
+ fs.existsSync(path.join(repoPath, "src/server")) ||
120
+ fs.existsSync(path.join(repoPath, "src/controllers")) ||
121
+ fs.existsSync(path.join(repoPath, "src/services")) ||
122
+ fs.existsSync(path.join(repoPath, "src/middleware"));
123
+ // Framework-based detection first — most reliable signal
124
+ if (FRONTEND_FRAMEWORKS.includes(framework)) {
125
+ if (framework === "nextjs") {
126
+ const hasApiRoutes = fs.existsSync(path.join(repoPath, "app", "api")) ||
127
+ fs.existsSync(path.join(repoPath, "pages", "api"));
128
+ if (hasApiRoutes || hasBackendDeps || hasDatabaseDeps) {
129
+ return "fullstack";
130
+ }
131
+ return "frontend";
132
+ }
133
+ // Plain React
134
+ if (hasBackendDeps || hasDatabaseDeps || hasBackendStructure) {
135
+ return "fullstack";
136
+ }
137
+ return "frontend";
138
+ }
139
+ if (BACKEND_FRAMEWORKS.includes(framework)) {
140
+ if (hasFrontendDeps || hasFrontendStructure) {
141
+ return "fullstack";
142
+ }
143
+ return "backend";
144
+ }
145
+ // Framework unknown — score based on signals
146
+ if (framework === "unknown") {
147
+ let frontendScore = 0;
148
+ let backendScore = 0;
149
+ if (hasFrontendDeps)
150
+ frontendScore += 2;
151
+ if (hasFrontendStructure)
152
+ frontendScore += 2;
153
+ if (fs.existsSync(path.join(repoPath, "public")))
154
+ frontendScore += 1;
155
+ if (fs.existsSync(path.join(repoPath, "index.html")))
156
+ frontendScore += 1;
157
+ if (hasBackendDeps)
158
+ backendScore += 2;
159
+ if (hasDatabaseDeps)
160
+ backendScore += 2;
161
+ if (hasBackendStructure)
162
+ backendScore += 2;
163
+ if (fs.existsSync(path.join(repoPath, "server.ts")) ||
164
+ fs.existsSync(path.join(repoPath, "server.js")))
165
+ backendScore += 1;
166
+ if (frontendScore > 0 && backendScore > 0)
167
+ return "fullstack";
168
+ if (frontendScore > backendScore)
169
+ return "frontend";
170
+ if (backendScore > frontendScore)
171
+ return "backend";
172
+ }
173
+ return "unknown";
174
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProjectFingerprint } from "../types.js";
2
+ export declare function analyzeFingerprint(repoPath: string): ProjectFingerprint;
@@ -0,0 +1,41 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { detectLanguage, detectFramework, detectRouter, detectStateManagement, detectDataFetching, detectDatabases, detectProjectType, } from "./detectors.js";
4
+ export function analyzeFingerprint(repoPath) {
5
+ if (!fs.existsSync(repoPath)) {
6
+ throw new Error(`Repo path does not exist: ${repoPath}`);
7
+ }
8
+ const packageJsonPath = path.join(repoPath, "package.json");
9
+ if (!fs.existsSync(packageJsonPath)) {
10
+ throw new Error(`No package.json found at: ${packageJsonPath}`);
11
+ }
12
+ const raw = fs.readFileSync(packageJsonPath, "utf-8");
13
+ const packageJson = JSON.parse(raw);
14
+ const deps = {
15
+ ...(packageJson.dependencies || {}),
16
+ ...(packageJson.devDependencies || {}),
17
+ };
18
+ const language = detectLanguage(repoPath);
19
+ const framework = detectFramework(deps);
20
+ const router = detectRouter(deps, framework, repoPath);
21
+ const projectType = detectProjectType(framework, deps, repoPath);
22
+ const databases = detectDatabases(deps);
23
+ // Backend projects have no React state or frontend data fetching
24
+ const isFrontendRelevant = projectType === "frontend" || projectType === "fullstack";
25
+ const stateManagement = isFrontendRelevant
26
+ ? detectStateManagement(deps)
27
+ : [];
28
+ const dataFetching = isFrontendRelevant
29
+ ? detectDataFetching(deps)
30
+ : [];
31
+ return {
32
+ language,
33
+ projectType,
34
+ framework,
35
+ router,
36
+ stateManagement,
37
+ dataFetching,
38
+ databases,
39
+ rawDependencies: deps,
40
+ };
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,148 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { analyzeFingerprint } from "./index.js";
5
+ function createFakeRepo(deps, extraFiles = []) {
6
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devlens-test-"));
7
+ const packageJson = {
8
+ name: "test-project",
9
+ version: "1.0.0",
10
+ dependencies: deps,
11
+ };
12
+ fs.writeFileSync(path.join(tmpDir, "package.json"), JSON.stringify(packageJson));
13
+ // Create any extra files/folders the test needs
14
+ for (const file of extraFiles) {
15
+ const fullPath = path.join(tmpDir, file);
16
+ fs.mkdirSync(fullPath, { recursive: true });
17
+ }
18
+ return tmpDir;
19
+ }
20
+ function deleteFakeRepo(repoPath) {
21
+ fs.rmSync(repoPath, { recursive: true, force: true });
22
+ }
23
+ describe("analyzeFingerprint", () => {
24
+ // ─── Framework Detection ───────────────────────────────────────────────────
25
+ it("should detect a Next.js project", () => {
26
+ const repoPath = createFakeRepo({ next: "14.0.0", react: "18.0.0" });
27
+ const fingerprint = analyzeFingerprint(repoPath);
28
+ expect(fingerprint.framework).toBe("nextjs");
29
+ deleteFakeRepo(repoPath);
30
+ });
31
+ it("should detect a plain React project", () => {
32
+ const repoPath = createFakeRepo({ react: "18.0.0" });
33
+ const fingerprint = analyzeFingerprint(repoPath);
34
+ expect(fingerprint.framework).toBe("react");
35
+ deleteFakeRepo(repoPath);
36
+ });
37
+ it("should return unknown for unrecognized project", () => {
38
+ const repoPath = createFakeRepo({ lodash: "4.17.21" });
39
+ const fingerprint = analyzeFingerprint(repoPath);
40
+ expect(fingerprint.framework).toBe("unknown");
41
+ deleteFakeRepo(repoPath);
42
+ });
43
+ // ─── Router Detection ──────────────────────────────────────────────────────
44
+ it("should detect Next.js app router", () => {
45
+ const repoPath = createFakeRepo({ next: "14.0.0" }, ["src/app"]);
46
+ const fingerprint = analyzeFingerprint(repoPath);
47
+ expect(fingerprint.router).toBe("app");
48
+ deleteFakeRepo(repoPath);
49
+ });
50
+ it("should detect Next.js pages router", () => {
51
+ const repoPath = createFakeRepo({ next: "14.0.0" }, ["src/pages"]);
52
+ const fingerprint = analyzeFingerprint(repoPath);
53
+ expect(fingerprint.router).toBe("pages");
54
+ deleteFakeRepo(repoPath);
55
+ });
56
+ it("should detect React Router in plain React project", () => {
57
+ const repoPath = createFakeRepo({ react: "18.0.0", "react-router-dom": "6.0.0" });
58
+ const fingerprint = analyzeFingerprint(repoPath);
59
+ expect(fingerprint.router).toBe("react-router");
60
+ deleteFakeRepo(repoPath);
61
+ });
62
+ it("should detect both routers during migration", () => {
63
+ const repoPath = createFakeRepo({ next: "14.0.0" }, ["src/app", "src/pages"]);
64
+ const fingerprint = analyzeFingerprint(repoPath);
65
+ expect(fingerprint.router).toBe("app+pages");
66
+ deleteFakeRepo(repoPath);
67
+ });
68
+ // ─── State Management Detection ───────────────────────────────────────────
69
+ it("should detect zustand", () => {
70
+ const repoPath = createFakeRepo({ react: "18.0.0", zustand: "4.0.0" });
71
+ const fingerprint = analyzeFingerprint(repoPath);
72
+ expect(fingerprint.stateManagement).toContain("zustand");
73
+ deleteFakeRepo(repoPath);
74
+ });
75
+ it("should detect redux", () => {
76
+ const repoPath = createFakeRepo({ react: "18.0.0", "@reduxjs/toolkit": "2.0.0" });
77
+ const fingerprint = analyzeFingerprint(repoPath);
78
+ expect(fingerprint.stateManagement).toContain("redux");
79
+ deleteFakeRepo(repoPath);
80
+ });
81
+ it("should fall back to context-only when no state library found", () => {
82
+ const repoPath = createFakeRepo({ react: "18.0.0" });
83
+ const fingerprint = analyzeFingerprint(repoPath);
84
+ expect(fingerprint.stateManagement).toContain("context-only");
85
+ deleteFakeRepo(repoPath);
86
+ });
87
+ // ─── Data Fetching Detection ───────────────────────────────────────────────
88
+ it("should detect axios", () => {
89
+ const repoPath = createFakeRepo({ react: "18.0.0", axios: "1.0.0" });
90
+ const fingerprint = analyzeFingerprint(repoPath);
91
+ expect(fingerprint.dataFetching).toContain("axios");
92
+ deleteFakeRepo(repoPath);
93
+ });
94
+ it("should detect react-query", () => {
95
+ const repoPath = createFakeRepo({ react: "18.0.0", "@tanstack/react-query": "5.0.0" });
96
+ const fingerprint = analyzeFingerprint(repoPath);
97
+ expect(fingerprint.dataFetching).toContain("react-query");
98
+ deleteFakeRepo(repoPath);
99
+ });
100
+ it("should fall back to fetch when no data fetching library found", () => {
101
+ const repoPath = createFakeRepo({ react: "18.0.0" });
102
+ const fingerprint = analyzeFingerprint(repoPath);
103
+ expect(fingerprint.dataFetching).toContain("fetch");
104
+ deleteFakeRepo(repoPath);
105
+ });
106
+ // ─── Database Detection ────────────────────────────────────────────────────
107
+ it("should detect prisma", () => {
108
+ const repoPath = createFakeRepo({ react: "18.0.0", prisma: "5.0.0" });
109
+ const fingerprint = analyzeFingerprint(repoPath);
110
+ expect(fingerprint.databases).toContain("prisma");
111
+ deleteFakeRepo(repoPath);
112
+ });
113
+ it("should detect supabase", () => {
114
+ const repoPath = createFakeRepo({ react: "18.0.0", "@supabase/supabase-js": "2.0.0" });
115
+ const fingerprint = analyzeFingerprint(repoPath);
116
+ expect(fingerprint.databases).toContain("supabase");
117
+ deleteFakeRepo(repoPath);
118
+ });
119
+ it("should return empty array when no database found", () => {
120
+ const repoPath = createFakeRepo({ react: "18.0.0" });
121
+ const fingerprint = analyzeFingerprint(repoPath);
122
+ expect(fingerprint.databases).toHaveLength(0);
123
+ deleteFakeRepo(repoPath);
124
+ });
125
+ // ─── Language Detection ────────────────────────────────────────────────────
126
+ it("should detect typescript when tsconfig.json exists", () => {
127
+ const repoPath = createFakeRepo({ react: "18.0.0" });
128
+ fs.writeFileSync(path.join(repoPath, "tsconfig.json"), "{}");
129
+ const fingerprint = analyzeFingerprint(repoPath);
130
+ expect(fingerprint.language).toBe("typescript");
131
+ deleteFakeRepo(repoPath);
132
+ });
133
+ it("should detect javascript when no tsconfig.json exists", () => {
134
+ const repoPath = createFakeRepo({ react: "18.0.0" });
135
+ const fingerprint = analyzeFingerprint(repoPath);
136
+ expect(fingerprint.language).toBe("javascript");
137
+ deleteFakeRepo(repoPath);
138
+ });
139
+ // ─── Error Handling ────────────────────────────────────────────────────────
140
+ it("should throw if repo path does not exist", () => {
141
+ expect(() => analyzeFingerprint("/fake/path/that/does/not/exist")).toThrow();
142
+ });
143
+ it("should throw if no package.json found", () => {
144
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devlens-test-"));
145
+ expect(() => analyzeFingerprint(tmpDir)).toThrow();
146
+ fs.rmSync(tmpDir, { recursive: true, force: true });
147
+ });
148
+ });