@viberails/scanner 0.1.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 +21 -0
- package/dist/index.cjs +780 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +126 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.js +744 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
// src/compute-statistics.ts
|
|
2
|
+
import { readdir as readdir2, readFile } from "fs/promises";
|
|
3
|
+
import { extname, join as join2 } from "path";
|
|
4
|
+
|
|
5
|
+
// src/utils/walk-directory.ts
|
|
6
|
+
import { readdir } from "fs/promises";
|
|
7
|
+
import { join, relative } from "path";
|
|
8
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
9
|
+
"node_modules",
|
|
10
|
+
".git",
|
|
11
|
+
"dist",
|
|
12
|
+
"build",
|
|
13
|
+
".next",
|
|
14
|
+
".nuxt",
|
|
15
|
+
".viberails",
|
|
16
|
+
"coverage",
|
|
17
|
+
".turbo",
|
|
18
|
+
".cache",
|
|
19
|
+
".output"
|
|
20
|
+
]);
|
|
21
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
22
|
+
".ts",
|
|
23
|
+
".tsx",
|
|
24
|
+
".js",
|
|
25
|
+
".jsx",
|
|
26
|
+
".mjs",
|
|
27
|
+
".cjs",
|
|
28
|
+
".vue",
|
|
29
|
+
".svelte",
|
|
30
|
+
".astro"
|
|
31
|
+
]);
|
|
32
|
+
async function walkDirectory(projectPath, maxDepth = 4) {
|
|
33
|
+
const results = [];
|
|
34
|
+
const queue = [];
|
|
35
|
+
try {
|
|
36
|
+
const rootEntries = await readdir(projectPath, { withFileTypes: true });
|
|
37
|
+
for (const entry of rootEntries) {
|
|
38
|
+
if (entry.isDirectory() && !entry.isSymbolicLink() && !IGNORED_DIRS.has(entry.name)) {
|
|
39
|
+
queue.push({ absolutePath: join(projectPath, entry.name), depth: 1 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
while (queue.length > 0) {
|
|
46
|
+
const { absolutePath, depth } = queue.shift();
|
|
47
|
+
const sourceFileNames = [];
|
|
48
|
+
try {
|
|
49
|
+
const entries = await readdir(absolutePath, { withFileTypes: true });
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (entry.isSymbolicLink()) continue;
|
|
52
|
+
if (entry.isDirectory() && depth < maxDepth && !IGNORED_DIRS.has(entry.name)) {
|
|
53
|
+
queue.push({ absolutePath: join(absolutePath, entry.name), depth: depth + 1 });
|
|
54
|
+
} else if (entry.isFile()) {
|
|
55
|
+
const dotIndex = entry.name.lastIndexOf(".");
|
|
56
|
+
if (dotIndex > 0) {
|
|
57
|
+
const ext = entry.name.substring(dotIndex);
|
|
58
|
+
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
59
|
+
sourceFileNames.push(entry.name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const rel = relative(projectPath, absolutePath).split("\\").join("/");
|
|
68
|
+
results.push({
|
|
69
|
+
relativePath: rel,
|
|
70
|
+
absolutePath,
|
|
71
|
+
sourceFileCount: sourceFileNames.length,
|
|
72
|
+
sourceFileNames,
|
|
73
|
+
depth
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/compute-statistics.ts
|
|
80
|
+
async function countLines(filePath) {
|
|
81
|
+
try {
|
|
82
|
+
const content = await readFile(filePath, "utf-8");
|
|
83
|
+
if (content.length === 0) return 0;
|
|
84
|
+
let count = 0;
|
|
85
|
+
for (let i = 0; i < content.length; i++) {
|
|
86
|
+
if (content.charCodeAt(i) === 10) count++;
|
|
87
|
+
}
|
|
88
|
+
if (content.charCodeAt(content.length - 1) !== 10) count++;
|
|
89
|
+
return count;
|
|
90
|
+
} catch {
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function getRootSourceFiles(projectPath) {
|
|
95
|
+
try {
|
|
96
|
+
const entries = await readdir2(projectPath, { withFileTypes: true });
|
|
97
|
+
const sourceFiles = [];
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (entry.isFile()) {
|
|
100
|
+
const ext = extname(entry.name);
|
|
101
|
+
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
102
|
+
sourceFiles.push(entry.name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return sourceFiles;
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function computeStatistics(projectPath, dirs) {
|
|
112
|
+
const directories = dirs ?? await walkDirectory(projectPath);
|
|
113
|
+
const rootFiles = await getRootSourceFiles(projectPath);
|
|
114
|
+
const filesToProcess = [];
|
|
115
|
+
for (const name of rootFiles) {
|
|
116
|
+
filesToProcess.push({
|
|
117
|
+
relativePath: name,
|
|
118
|
+
absolutePath: join2(projectPath, name),
|
|
119
|
+
ext: extname(name)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
for (const dir of directories) {
|
|
123
|
+
for (const name of dir.sourceFileNames) {
|
|
124
|
+
filesToProcess.push({
|
|
125
|
+
relativePath: `${dir.relativePath}/${name}`,
|
|
126
|
+
absolutePath: join2(dir.absolutePath, name),
|
|
127
|
+
ext: extname(name)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const totalFiles = filesToProcess.length;
|
|
132
|
+
if (totalFiles === 0) {
|
|
133
|
+
return {
|
|
134
|
+
totalFiles: 0,
|
|
135
|
+
totalLines: 0,
|
|
136
|
+
averageFileLines: 0,
|
|
137
|
+
largestFiles: [],
|
|
138
|
+
filesByExtension: {}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const lineResults = await Promise.all(
|
|
142
|
+
filesToProcess.map(async (file) => ({
|
|
143
|
+
path: file.relativePath,
|
|
144
|
+
lines: await countLines(file.absolutePath),
|
|
145
|
+
ext: file.ext
|
|
146
|
+
}))
|
|
147
|
+
);
|
|
148
|
+
let totalLines = 0;
|
|
149
|
+
const filesByExtension = {};
|
|
150
|
+
const allFiles = [];
|
|
151
|
+
for (const result of lineResults) {
|
|
152
|
+
totalLines += result.lines;
|
|
153
|
+
filesByExtension[result.ext] = (filesByExtension[result.ext] ?? 0) + 1;
|
|
154
|
+
allFiles.push({ path: result.path, lines: result.lines });
|
|
155
|
+
}
|
|
156
|
+
allFiles.sort((a, b) => b.lines - a.lines);
|
|
157
|
+
const largestFiles = allFiles.slice(0, 5);
|
|
158
|
+
return {
|
|
159
|
+
totalFiles,
|
|
160
|
+
totalLines,
|
|
161
|
+
averageFileLines: Math.round(totalLines / totalFiles),
|
|
162
|
+
largestFiles,
|
|
163
|
+
filesByExtension
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/detect-conventions.ts
|
|
168
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
169
|
+
import { join as join3 } from "path";
|
|
170
|
+
import { confidenceFromConsistency } from "@viberails/types";
|
|
171
|
+
|
|
172
|
+
// src/utils/classify-filename.ts
|
|
173
|
+
var PATTERNS = [
|
|
174
|
+
{ convention: "PascalCase", regex: /^[A-Z][a-zA-Z0-9]*$/ },
|
|
175
|
+
{ convention: "camelCase", regex: /^[a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*$/ },
|
|
176
|
+
{ convention: "kebab-case", regex: /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/ },
|
|
177
|
+
{ convention: "snake_case", regex: /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/ }
|
|
178
|
+
];
|
|
179
|
+
function classifyFilename(filename) {
|
|
180
|
+
for (const { convention, regex } of PATTERNS) {
|
|
181
|
+
if (regex.test(filename)) return convention;
|
|
182
|
+
}
|
|
183
|
+
return "unknown";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/detect-conventions.ts
|
|
187
|
+
async function detectConventions(projectPath, structure, dirs) {
|
|
188
|
+
if (!dirs) {
|
|
189
|
+
dirs = await walkDirectory(projectPath, 4);
|
|
190
|
+
}
|
|
191
|
+
const result = {};
|
|
192
|
+
const fileNaming = detectFileNaming(dirs);
|
|
193
|
+
if (fileNaming) result.fileNaming = fileNaming;
|
|
194
|
+
const componentNaming = detectComponentNaming(dirs, structure);
|
|
195
|
+
if (componentNaming) result.componentNaming = componentNaming;
|
|
196
|
+
const hookNaming = detectHookNaming(dirs, structure);
|
|
197
|
+
if (hookNaming) result.hookNaming = hookNaming;
|
|
198
|
+
const importAlias = await detectImportAlias(projectPath);
|
|
199
|
+
if (importAlias) result.importAlias = importAlias;
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
function stripExtension(filename) {
|
|
203
|
+
const firstDot = filename.indexOf(".");
|
|
204
|
+
return firstDot > 0 ? filename.substring(0, firstDot) : filename;
|
|
205
|
+
}
|
|
206
|
+
function detectFileNaming(dirs) {
|
|
207
|
+
const conventionCounts = /* @__PURE__ */ new Map();
|
|
208
|
+
let total = 0;
|
|
209
|
+
for (const dir of dirs) {
|
|
210
|
+
if (dir.sourceFileCount < 3) continue;
|
|
211
|
+
for (const filename of dir.sourceFileNames) {
|
|
212
|
+
if (filename.includes(".test.") || filename.includes(".spec.")) continue;
|
|
213
|
+
const bare = stripExtension(filename);
|
|
214
|
+
if (bare === "index") continue;
|
|
215
|
+
const convention = classifyFilename(bare);
|
|
216
|
+
total++;
|
|
217
|
+
if (convention !== "unknown") {
|
|
218
|
+
conventionCounts.set(convention, (conventionCounts.get(convention) ?? 0) + 1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (total < 3) return void 0;
|
|
223
|
+
const sorted = [...conventionCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
224
|
+
if (sorted.length === 0) return void 0;
|
|
225
|
+
const [dominantConvention, dominantCount] = sorted[0];
|
|
226
|
+
const consistency = Math.round(dominantCount / total * 100);
|
|
227
|
+
const confidence = confidenceFromConsistency(consistency);
|
|
228
|
+
if (confidence === "low") return void 0;
|
|
229
|
+
return {
|
|
230
|
+
value: dominantConvention,
|
|
231
|
+
confidence,
|
|
232
|
+
sampleSize: total,
|
|
233
|
+
consistency
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function detectComponentNaming(dirs, structure) {
|
|
237
|
+
const componentPaths = new Set(
|
|
238
|
+
structure.directories.filter((d) => d.role === "components").map((d) => d.path)
|
|
239
|
+
);
|
|
240
|
+
const tsxFiles = [];
|
|
241
|
+
for (const dir of dirs) {
|
|
242
|
+
if (!componentPaths.has(dir.relativePath)) continue;
|
|
243
|
+
for (const f of dir.sourceFileNames) {
|
|
244
|
+
if (f.endsWith(".tsx")) tsxFiles.push(f);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (tsxFiles.length < 3) return void 0;
|
|
248
|
+
const pascalCount = tsxFiles.filter((f) => /^[A-Z]/.test(stripExtension(f))).length;
|
|
249
|
+
const consistency = Math.round(pascalCount / tsxFiles.length * 100);
|
|
250
|
+
const dominantValue = pascalCount >= tsxFiles.length / 2 ? "PascalCase" : "camelCase";
|
|
251
|
+
return {
|
|
252
|
+
value: dominantValue,
|
|
253
|
+
confidence: confidenceFromConsistency(consistency),
|
|
254
|
+
sampleSize: tsxFiles.length,
|
|
255
|
+
consistency
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function detectHookNaming(dirs, structure) {
|
|
259
|
+
const hookPaths = new Set(
|
|
260
|
+
structure.directories.filter((d) => d.role === "hooks").map((d) => d.path)
|
|
261
|
+
);
|
|
262
|
+
const hookFiles = [];
|
|
263
|
+
for (const dir of dirs) {
|
|
264
|
+
if (!hookPaths.has(dir.relativePath)) continue;
|
|
265
|
+
for (const f of dir.sourceFileNames) {
|
|
266
|
+
const bare = stripExtension(f);
|
|
267
|
+
if (bare.startsWith("use-") || /^use[A-Z]/.test(bare)) {
|
|
268
|
+
hookFiles.push(f);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (hookFiles.length < 3) return void 0;
|
|
273
|
+
let kebabCount = 0;
|
|
274
|
+
let camelCount = 0;
|
|
275
|
+
for (const filename of hookFiles) {
|
|
276
|
+
const bare = stripExtension(filename);
|
|
277
|
+
if (bare.startsWith("use-")) {
|
|
278
|
+
kebabCount++;
|
|
279
|
+
} else if (/^use[A-Z]/.test(bare)) {
|
|
280
|
+
camelCount++;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const total = hookFiles.length;
|
|
284
|
+
const isDominantKebab = kebabCount >= camelCount;
|
|
285
|
+
const dominantCount = isDominantKebab ? kebabCount : camelCount;
|
|
286
|
+
const consistency = Math.round(dominantCount / total * 100);
|
|
287
|
+
return {
|
|
288
|
+
value: isDominantKebab ? "use-*" : "useXxx",
|
|
289
|
+
confidence: confidenceFromConsistency(consistency),
|
|
290
|
+
sampleSize: total,
|
|
291
|
+
consistency
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
async function detectImportAlias(projectPath) {
|
|
295
|
+
try {
|
|
296
|
+
const raw = await readFile2(join3(projectPath, "tsconfig.json"), "utf-8");
|
|
297
|
+
const tsconfig = JSON.parse(raw);
|
|
298
|
+
const paths = tsconfig.compilerOptions?.paths;
|
|
299
|
+
if (!paths) return void 0;
|
|
300
|
+
const aliases = Object.keys(paths);
|
|
301
|
+
if (aliases.length === 0) return void 0;
|
|
302
|
+
return {
|
|
303
|
+
value: aliases[0],
|
|
304
|
+
confidence: "high",
|
|
305
|
+
sampleSize: aliases.length,
|
|
306
|
+
consistency: 100
|
|
307
|
+
};
|
|
308
|
+
} catch {
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/detect-stack.ts
|
|
314
|
+
import { access } from "fs/promises";
|
|
315
|
+
import { join as join5 } from "path";
|
|
316
|
+
|
|
317
|
+
// src/utils/read-package-json.ts
|
|
318
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
319
|
+
import { join as join4 } from "path";
|
|
320
|
+
async function readPackageJson(projectPath) {
|
|
321
|
+
try {
|
|
322
|
+
const raw = await readFile3(join4(projectPath, "package.json"), "utf-8");
|
|
323
|
+
return JSON.parse(raw);
|
|
324
|
+
} catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/detect-stack.ts
|
|
330
|
+
function extractMajorVersion(range) {
|
|
331
|
+
const match = range.match(/(\d+)/);
|
|
332
|
+
return match?.[1];
|
|
333
|
+
}
|
|
334
|
+
async function fileExists(filePath) {
|
|
335
|
+
try {
|
|
336
|
+
await access(filePath);
|
|
337
|
+
return true;
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
var FRAMEWORK_MAPPINGS = [
|
|
343
|
+
{ dep: "next", name: "nextjs" },
|
|
344
|
+
{ dep: "@sveltejs/kit", name: "sveltekit" },
|
|
345
|
+
{ dep: "svelte", name: "svelte" },
|
|
346
|
+
{ dep: "astro", name: "astro" },
|
|
347
|
+
{ dep: "vue", name: "vue" },
|
|
348
|
+
{ dep: "react", name: "react", excludeDep: "next" }
|
|
349
|
+
];
|
|
350
|
+
var BACKEND_MAPPINGS = [
|
|
351
|
+
{ dep: "express", name: "express" },
|
|
352
|
+
{ dep: "fastify", name: "fastify" },
|
|
353
|
+
{ dep: "hono", name: "hono" },
|
|
354
|
+
{ dep: "@supabase/supabase-js", name: "supabase" },
|
|
355
|
+
{ dep: "firebase", name: "firebase" },
|
|
356
|
+
{ dep: "@prisma/client", name: "prisma" },
|
|
357
|
+
{ dep: "prisma", name: "prisma" },
|
|
358
|
+
{ dep: "drizzle-orm", name: "drizzle" }
|
|
359
|
+
];
|
|
360
|
+
var STYLING_MAPPINGS = [
|
|
361
|
+
{ dep: "tailwindcss", name: "tailwindcss" },
|
|
362
|
+
{ dep: "styled-components", name: "styled-components" },
|
|
363
|
+
{ dep: "@emotion/react", name: "emotion" },
|
|
364
|
+
{ dep: "sass", name: "sass" }
|
|
365
|
+
];
|
|
366
|
+
var LIBRARY_MAPPINGS = [
|
|
367
|
+
{ deps: ["zod"], name: "zod" },
|
|
368
|
+
{ deps: ["@trpc/server", "@trpc/client"], name: "trpc" },
|
|
369
|
+
{ deps: ["@tanstack/react-query"], name: "react-query" }
|
|
370
|
+
];
|
|
371
|
+
var LOCK_FILE_MAP = [
|
|
372
|
+
{ file: "pnpm-lock.yaml", name: "pnpm" },
|
|
373
|
+
{ file: "yarn.lock", name: "yarn" },
|
|
374
|
+
{ file: "bun.lockb", name: "bun" },
|
|
375
|
+
{ file: "package-lock.json", name: "npm" }
|
|
376
|
+
];
|
|
377
|
+
async function detectStack(projectPath) {
|
|
378
|
+
const pkg = await readPackageJson(projectPath);
|
|
379
|
+
const allDeps = {
|
|
380
|
+
...pkg?.dependencies,
|
|
381
|
+
...pkg?.devDependencies
|
|
382
|
+
};
|
|
383
|
+
const framework = detectFramework(allDeps);
|
|
384
|
+
const language = await detectLanguage(projectPath, allDeps);
|
|
385
|
+
const styling = detectFirst(allDeps, STYLING_MAPPINGS);
|
|
386
|
+
const backend = detectFirst(allDeps, BACKEND_MAPPINGS);
|
|
387
|
+
const packageManager = await detectPackageManager(projectPath);
|
|
388
|
+
const linter = detectLinter(allDeps);
|
|
389
|
+
const testRunner = detectTestRunner(allDeps);
|
|
390
|
+
const libraries = detectLibraries(allDeps);
|
|
391
|
+
return {
|
|
392
|
+
...framework && { framework },
|
|
393
|
+
language,
|
|
394
|
+
...styling && { styling },
|
|
395
|
+
...backend && { backend },
|
|
396
|
+
packageManager,
|
|
397
|
+
...linter && { linter },
|
|
398
|
+
...testRunner && { testRunner },
|
|
399
|
+
libraries
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function detectFramework(allDeps) {
|
|
403
|
+
for (const mapping of FRAMEWORK_MAPPINGS) {
|
|
404
|
+
if (!(mapping.dep in allDeps)) continue;
|
|
405
|
+
if (mapping.excludeDep && mapping.excludeDep in allDeps) continue;
|
|
406
|
+
return {
|
|
407
|
+
name: mapping.name,
|
|
408
|
+
version: extractMajorVersion(allDeps[mapping.dep])
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return void 0;
|
|
412
|
+
}
|
|
413
|
+
async function detectLanguage(projectPath, allDeps) {
|
|
414
|
+
if ("typescript" in allDeps) {
|
|
415
|
+
return {
|
|
416
|
+
name: "typescript",
|
|
417
|
+
version: extractMajorVersion(allDeps.typescript)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (await fileExists(join5(projectPath, "tsconfig.json"))) {
|
|
421
|
+
return { name: "typescript" };
|
|
422
|
+
}
|
|
423
|
+
return { name: "javascript" };
|
|
424
|
+
}
|
|
425
|
+
function detectFirst(allDeps, mappings) {
|
|
426
|
+
for (const mapping of mappings) {
|
|
427
|
+
if (mapping.dep in allDeps) {
|
|
428
|
+
return {
|
|
429
|
+
name: mapping.name,
|
|
430
|
+
version: extractMajorVersion(allDeps[mapping.dep])
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return void 0;
|
|
435
|
+
}
|
|
436
|
+
async function detectPackageManager(projectPath) {
|
|
437
|
+
for (const entry of LOCK_FILE_MAP) {
|
|
438
|
+
if (await fileExists(join5(projectPath, entry.file))) {
|
|
439
|
+
return { name: entry.name };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { name: "npm" };
|
|
443
|
+
}
|
|
444
|
+
function detectLinter(allDeps) {
|
|
445
|
+
if ("eslint" in allDeps) {
|
|
446
|
+
return { name: "eslint", version: extractMajorVersion(allDeps.eslint) };
|
|
447
|
+
}
|
|
448
|
+
if ("@biomejs/biome" in allDeps) {
|
|
449
|
+
return {
|
|
450
|
+
name: "biome",
|
|
451
|
+
version: extractMajorVersion(allDeps["@biomejs/biome"])
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
return void 0;
|
|
455
|
+
}
|
|
456
|
+
function detectTestRunner(allDeps) {
|
|
457
|
+
if ("vitest" in allDeps) {
|
|
458
|
+
return { name: "vitest", version: extractMajorVersion(allDeps.vitest) };
|
|
459
|
+
}
|
|
460
|
+
if ("jest" in allDeps) {
|
|
461
|
+
return { name: "jest", version: extractMajorVersion(allDeps.jest) };
|
|
462
|
+
}
|
|
463
|
+
return void 0;
|
|
464
|
+
}
|
|
465
|
+
function detectLibraries(allDeps) {
|
|
466
|
+
const libs = [];
|
|
467
|
+
for (const mapping of LIBRARY_MAPPINGS) {
|
|
468
|
+
const found = mapping.deps.find((dep) => dep in allDeps);
|
|
469
|
+
if (found) {
|
|
470
|
+
libs.push({
|
|
471
|
+
name: mapping.name,
|
|
472
|
+
version: extractMajorVersion(allDeps[found])
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return libs;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/detect-structure.ts
|
|
480
|
+
import { confidenceFromConsistency as confidenceFromConsistency2 } from "@viberails/types";
|
|
481
|
+
|
|
482
|
+
// src/utils/classify-directory.ts
|
|
483
|
+
var ROLE_PATTERNS = [
|
|
484
|
+
{ role: "pages", pathPatterns: ["src/app", "src/pages", "app", "pages"] },
|
|
485
|
+
{ role: "components", pathPatterns: ["src/components", "components"] },
|
|
486
|
+
{ role: "hooks", pathPatterns: ["src/hooks", "hooks"] },
|
|
487
|
+
{
|
|
488
|
+
role: "utils",
|
|
489
|
+
pathPatterns: ["src/lib", "src/utils", "src/helpers", "lib", "utils", "helpers"]
|
|
490
|
+
},
|
|
491
|
+
{ role: "types", pathPatterns: ["src/types", "types", "src/@types", "@types"] },
|
|
492
|
+
{ role: "tests", pathPatterns: ["__tests__", "tests", "test", "src/__tests__", "src/tests"] },
|
|
493
|
+
{ role: "styles", pathPatterns: ["src/styles", "styles", "src/css", "css"] },
|
|
494
|
+
{ role: "api", pathPatterns: ["src/api", "api", "src/app/api", "app/api"] },
|
|
495
|
+
{ role: "config", pathPatterns: ["config", "src/config"] }
|
|
496
|
+
];
|
|
497
|
+
function classifyDirectory(dir) {
|
|
498
|
+
const nameMatch = matchByName(dir.relativePath);
|
|
499
|
+
if (nameMatch) {
|
|
500
|
+
const confidence = dir.sourceFileCount > 0 ? "high" : "low";
|
|
501
|
+
return {
|
|
502
|
+
path: dir.relativePath,
|
|
503
|
+
role: nameMatch,
|
|
504
|
+
fileCount: dir.sourceFileCount,
|
|
505
|
+
confidence
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
if (dir.sourceFileCount === 0) return null;
|
|
509
|
+
const contentRole = inferFromContent(dir);
|
|
510
|
+
if (contentRole) return contentRole;
|
|
511
|
+
return {
|
|
512
|
+
path: dir.relativePath,
|
|
513
|
+
role: "unknown",
|
|
514
|
+
fileCount: dir.sourceFileCount,
|
|
515
|
+
confidence: "low"
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function matchByName(relativePath) {
|
|
519
|
+
for (const { role, pathPatterns } of ROLE_PATTERNS) {
|
|
520
|
+
for (const pattern of pathPatterns) {
|
|
521
|
+
if (relativePath === pattern) return role;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
function inferFromContent(dir) {
|
|
527
|
+
const { sourceFileNames, sourceFileCount } = dir;
|
|
528
|
+
const hookFiles = sourceFileNames.filter((f) => {
|
|
529
|
+
const name = f.split(".")[0];
|
|
530
|
+
return name.startsWith("use-") || /^use[A-Z]/.test(name);
|
|
531
|
+
});
|
|
532
|
+
if (hookFiles.length > 0 && hookFiles.length / sourceFileCount >= 0.5) {
|
|
533
|
+
return {
|
|
534
|
+
path: dir.relativePath,
|
|
535
|
+
role: "hooks",
|
|
536
|
+
fileCount: sourceFileCount,
|
|
537
|
+
confidence: hookFiles.length / sourceFileCount >= 0.9 ? "high" : "medium"
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const testFiles = sourceFileNames.filter((f) => f.includes(".test.") || f.includes(".spec."));
|
|
541
|
+
if (testFiles.length > 0 && testFiles.length / sourceFileCount >= 0.5) {
|
|
542
|
+
return {
|
|
543
|
+
path: dir.relativePath,
|
|
544
|
+
role: "tests",
|
|
545
|
+
fileCount: sourceFileCount,
|
|
546
|
+
confidence: testFiles.length / sourceFileCount >= 0.9 ? "high" : "medium"
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/detect-structure.ts
|
|
553
|
+
async function detectStructure(projectPath, dirs) {
|
|
554
|
+
if (!dirs) {
|
|
555
|
+
dirs = await walkDirectory(projectPath, 4);
|
|
556
|
+
}
|
|
557
|
+
const hasSrcDir = dirs.some((d) => d.relativePath === "src" || d.relativePath.startsWith("src/"));
|
|
558
|
+
const srcDir = hasSrcDir ? "src" : void 0;
|
|
559
|
+
const directories = dirs.map((d) => classifyDirectory(d)).filter((d) => d !== null);
|
|
560
|
+
const testPattern = detectTestPattern(dirs);
|
|
561
|
+
return {
|
|
562
|
+
...srcDir !== void 0 && { srcDir },
|
|
563
|
+
directories,
|
|
564
|
+
...testPattern !== void 0 && { testPattern }
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function detectTestPattern(dirs) {
|
|
568
|
+
const allFiles = dirs.flatMap((d) => d.sourceFileNames);
|
|
569
|
+
const testFiles = allFiles.filter((f) => f.includes(".test.") || f.includes(".spec."));
|
|
570
|
+
if (testFiles.length < 3) return void 0;
|
|
571
|
+
const dotTestCount = testFiles.filter((f) => f.includes(".test.")).length;
|
|
572
|
+
const dotSpecCount = testFiles.filter((f) => f.includes(".spec.")).length;
|
|
573
|
+
const isDotTest = dotTestCount >= dotSpecCount;
|
|
574
|
+
const dominantSep = isDotTest ? ".test." : ".spec.";
|
|
575
|
+
const dominantCount = isDotTest ? dotTestCount : dotSpecCount;
|
|
576
|
+
const consistency = Math.round(dominantCount / testFiles.length * 100);
|
|
577
|
+
const extCounts = /* @__PURE__ */ new Map();
|
|
578
|
+
for (const f of testFiles.filter((f2) => f2.includes(dominantSep))) {
|
|
579
|
+
const ext = f.substring(f.lastIndexOf("."));
|
|
580
|
+
extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
|
|
581
|
+
}
|
|
582
|
+
const topExt = [...extCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? ".ts";
|
|
583
|
+
const sep = isDotTest ? "test" : "spec";
|
|
584
|
+
return {
|
|
585
|
+
value: `*.${sep}${topExt}`,
|
|
586
|
+
confidence: confidenceFromConsistency2(consistency),
|
|
587
|
+
sampleSize: testFiles.length,
|
|
588
|
+
consistency
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/detect-workspace.ts
|
|
593
|
+
import { readdir as readdir3, readFile as readFile4 } from "fs/promises";
|
|
594
|
+
import { join as join6, relative as relative2 } from "path";
|
|
595
|
+
async function detectWorkspace(projectRoot) {
|
|
596
|
+
const patterns = await readWorkspacePatterns(projectRoot);
|
|
597
|
+
if (!patterns || patterns.length === 0) return void 0;
|
|
598
|
+
const packageDirs = await resolvePatterns(projectRoot, patterns);
|
|
599
|
+
const packages = await resolvePackages(projectRoot, packageDirs);
|
|
600
|
+
if (packages.length === 0) return void 0;
|
|
601
|
+
const packageNames = new Set(packages.map((p) => p.name));
|
|
602
|
+
for (const pkg of packages) {
|
|
603
|
+
pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
|
|
604
|
+
}
|
|
605
|
+
return { patterns, packages };
|
|
606
|
+
}
|
|
607
|
+
async function readWorkspacePatterns(projectRoot) {
|
|
608
|
+
try {
|
|
609
|
+
const yaml = await readFile4(join6(projectRoot, "pnpm-workspace.yaml"), "utf-8");
|
|
610
|
+
return parsePnpmWorkspaceYaml(yaml);
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
const pkg = await readPackageJson(projectRoot);
|
|
614
|
+
if (!pkg) return void 0;
|
|
615
|
+
const raw = pkg;
|
|
616
|
+
const workspaces = raw.workspaces;
|
|
617
|
+
if (Array.isArray(workspaces)) {
|
|
618
|
+
return workspaces.filter((w) => typeof w === "string");
|
|
619
|
+
}
|
|
620
|
+
if (workspaces && typeof workspaces === "object" && "packages" in workspaces) {
|
|
621
|
+
const nested = workspaces.packages;
|
|
622
|
+
if (Array.isArray(nested)) {
|
|
623
|
+
return nested.filter((w) => typeof w === "string");
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return void 0;
|
|
627
|
+
}
|
|
628
|
+
function parsePnpmWorkspaceYaml(content) {
|
|
629
|
+
const patterns = [];
|
|
630
|
+
let inPackages = false;
|
|
631
|
+
for (const line of content.split("\n")) {
|
|
632
|
+
const trimmed = line.trim();
|
|
633
|
+
if (trimmed === "packages:") {
|
|
634
|
+
inPackages = true;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (inPackages && trimmed.length > 0 && !trimmed.startsWith("-")) {
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
if (inPackages && trimmed.startsWith("-")) {
|
|
641
|
+
const value = trimmed.slice(1).trim().replace(/^['"]|['"]$/g, "");
|
|
642
|
+
if (value) patterns.push(value);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return patterns;
|
|
646
|
+
}
|
|
647
|
+
async function resolvePatterns(projectRoot, patterns) {
|
|
648
|
+
const dirs = [];
|
|
649
|
+
for (const pattern of patterns) {
|
|
650
|
+
if (pattern.endsWith("/*")) {
|
|
651
|
+
const parent = join6(projectRoot, pattern.slice(0, -2));
|
|
652
|
+
try {
|
|
653
|
+
const entries = await readdir3(parent, { withFileTypes: true });
|
|
654
|
+
for (const entry of entries) {
|
|
655
|
+
if (entry.isDirectory()) {
|
|
656
|
+
dirs.push(join6(parent, entry.name));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
dirs.push(join6(projectRoot, pattern));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return dirs;
|
|
666
|
+
}
|
|
667
|
+
async function resolvePackages(projectRoot, dirs) {
|
|
668
|
+
const packages = [];
|
|
669
|
+
for (const dir of dirs) {
|
|
670
|
+
const pkg = await readPackageJson(dir);
|
|
671
|
+
if (!pkg?.name) continue;
|
|
672
|
+
const allDeps = [
|
|
673
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
674
|
+
...Object.keys(pkg.devDependencies ?? {})
|
|
675
|
+
];
|
|
676
|
+
packages.push({
|
|
677
|
+
name: pkg.name,
|
|
678
|
+
path: dir,
|
|
679
|
+
relativePath: relative2(projectRoot, dir),
|
|
680
|
+
internalDeps: allDeps
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return packages;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/scan.ts
|
|
687
|
+
import { stat } from "fs/promises";
|
|
688
|
+
import { resolve } from "path";
|
|
689
|
+
var FIXTURE_PATTERNS = [
|
|
690
|
+
/^tests\/fixtures(\/|$)/,
|
|
691
|
+
/^test\/fixtures(\/|$)/,
|
|
692
|
+
/^__tests__\/fixtures(\/|$)/,
|
|
693
|
+
/^fixtures(\/|$)/
|
|
694
|
+
];
|
|
695
|
+
function filterFixtureDirs(dirs) {
|
|
696
|
+
return dirs.filter((d) => !FIXTURE_PATTERNS.some((pattern) => pattern.test(d.relativePath)));
|
|
697
|
+
}
|
|
698
|
+
async function scan(projectPath, _options) {
|
|
699
|
+
const root = resolve(projectPath);
|
|
700
|
+
try {
|
|
701
|
+
const st = await stat(root);
|
|
702
|
+
if (!st.isDirectory()) {
|
|
703
|
+
throw new Error(`Project path is not a directory: ${root}`);
|
|
704
|
+
}
|
|
705
|
+
} catch (err) {
|
|
706
|
+
if (err instanceof Error && err.message.startsWith("Project path is not")) {
|
|
707
|
+
throw err;
|
|
708
|
+
}
|
|
709
|
+
throw new Error(`Project path does not exist: ${root}`);
|
|
710
|
+
}
|
|
711
|
+
const allDirs = await walkDirectory(root, 4);
|
|
712
|
+
const dirs = filterFixtureDirs(allDirs);
|
|
713
|
+
const [stack, structure, statistics] = await Promise.all([
|
|
714
|
+
detectStack(root),
|
|
715
|
+
detectStructure(root, dirs),
|
|
716
|
+
computeStatistics(root, dirs)
|
|
717
|
+
]);
|
|
718
|
+
const conventions = await detectConventions(root, structure, dirs);
|
|
719
|
+
const workspace = await detectWorkspace(root);
|
|
720
|
+
return {
|
|
721
|
+
root,
|
|
722
|
+
stack,
|
|
723
|
+
structure,
|
|
724
|
+
conventions,
|
|
725
|
+
statistics,
|
|
726
|
+
workspace
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/index.ts
|
|
731
|
+
var VERSION = "0.1.0";
|
|
732
|
+
export {
|
|
733
|
+
VERSION,
|
|
734
|
+
computeStatistics,
|
|
735
|
+
detectConventions,
|
|
736
|
+
detectStack,
|
|
737
|
+
detectStructure,
|
|
738
|
+
detectWorkspace,
|
|
739
|
+
extractMajorVersion,
|
|
740
|
+
readPackageJson,
|
|
741
|
+
scan,
|
|
742
|
+
walkDirectory
|
|
743
|
+
};
|
|
744
|
+
//# sourceMappingURL=index.js.map
|