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,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,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,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
|
+
});
|