@viberails/scanner 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,155 @@
1
+ // src/aggregate.ts
2
+ import {
3
+ confidenceFromConsistency
4
+ } from "@viberails/types";
5
+ var FRAMEWORK_PRIORITY = [
6
+ "nextjs",
7
+ "sveltekit",
8
+ "astro",
9
+ "expo",
10
+ "react-native",
11
+ "svelte",
12
+ "vue",
13
+ "react"
14
+ ];
15
+ function aggregateStacks(packages) {
16
+ if (packages.length === 1) return packages[0].stack;
17
+ const language = packages.some((p) => p.stack.language.name === "typescript") ? packages.find((p) => p.stack.language.name === "typescript").stack.language : packages[0].stack.language;
18
+ const packageManager = packages[0].stack.packageManager;
19
+ const frameworkPackages = packages.filter((p) => p.stack.framework);
20
+ let framework;
21
+ if (frameworkPackages.length > 0) {
22
+ frameworkPackages.sort((a, b) => {
23
+ const aIdx = FRAMEWORK_PRIORITY.indexOf(a.stack.framework.name);
24
+ const bIdx = FRAMEWORK_PRIORITY.indexOf(b.stack.framework.name);
25
+ return (aIdx === -1 ? Infinity : aIdx) - (bIdx === -1 ? Infinity : bIdx);
26
+ });
27
+ framework = frameworkPackages[0].stack.framework;
28
+ }
29
+ const libraryMap = /* @__PURE__ */ new Map();
30
+ for (const pkg of packages) {
31
+ if (pkg.stack.framework && pkg.stack.framework.name !== framework?.name) {
32
+ libraryMap.set(pkg.stack.framework.name, pkg.stack.framework);
33
+ }
34
+ for (const lib of pkg.stack.libraries) {
35
+ if (!libraryMap.has(lib.name)) {
36
+ libraryMap.set(lib.name, lib);
37
+ }
38
+ }
39
+ }
40
+ const styling = packages.find((p) => p.stack.styling)?.stack.styling;
41
+ const backend = packages.find((p) => p.stack.backend)?.stack.backend;
42
+ const linter = packages.find((p) => p.stack.linter)?.stack.linter;
43
+ const formatter = packages.find((p) => p.stack.formatter)?.stack.formatter;
44
+ const testRunner = packages.find((p) => p.stack.testRunner)?.stack.testRunner;
45
+ return {
46
+ language,
47
+ packageManager,
48
+ framework,
49
+ libraries: [...libraryMap.values()],
50
+ styling,
51
+ backend,
52
+ linter,
53
+ formatter,
54
+ testRunner
55
+ };
56
+ }
57
+ function aggregateStructures(packages) {
58
+ if (packages.length === 1) return packages[0].structure;
59
+ const srcDir = packages.some((p) => p.structure.srcDir) ? "src" : void 0;
60
+ const directories = packages.flatMap(
61
+ (pkg) => pkg.structure.directories.map((dir) => ({
62
+ ...dir,
63
+ path: pkg.relativePath ? `${pkg.relativePath}/${dir.path}` : dir.path
64
+ }))
65
+ );
66
+ const testPatterns = packages.map((p) => p.structure.testPattern).filter((t) => t !== void 0);
67
+ let testPattern;
68
+ if (testPatterns.length > 0) {
69
+ const counts = /* @__PURE__ */ new Map();
70
+ for (const tp of testPatterns) {
71
+ const existing = counts.get(tp.value);
72
+ if (existing) {
73
+ existing.count++;
74
+ } else {
75
+ counts.set(tp.value, { count: 1, pattern: tp });
76
+ }
77
+ }
78
+ let best = { count: 0, pattern: testPatterns[0] };
79
+ for (const entry of counts.values()) {
80
+ if (entry.count > best.count) best = entry;
81
+ }
82
+ testPattern = best.pattern;
83
+ }
84
+ return { srcDir, directories, testPattern };
85
+ }
86
+ function aggregateConventions(packages) {
87
+ if (packages.length === 1) return packages[0].conventions;
88
+ const allKeys = /* @__PURE__ */ new Set();
89
+ for (const pkg of packages) {
90
+ for (const key of Object.keys(pkg.conventions)) {
91
+ allKeys.add(key);
92
+ }
93
+ }
94
+ const result = {};
95
+ for (const key of allKeys) {
96
+ const entries = packages.map((p) => p.conventions[key]).filter((c) => c !== void 0);
97
+ if (entries.length < packages.length / 2) continue;
98
+ const valueCounts = /* @__PURE__ */ new Map();
99
+ for (const entry of entries) {
100
+ const existing = valueCounts.get(entry.value);
101
+ if (existing) {
102
+ existing.count++;
103
+ existing.totalConsistency += entry.consistency;
104
+ existing.totalSamples += entry.sampleSize;
105
+ } else {
106
+ valueCounts.set(entry.value, {
107
+ count: 1,
108
+ totalConsistency: entry.consistency,
109
+ totalSamples: entry.sampleSize
110
+ });
111
+ }
112
+ }
113
+ let majorityValue = "";
114
+ let majorityData = { count: 0, totalConsistency: 0, totalSamples: 0 };
115
+ for (const [value, data] of valueCounts) {
116
+ if (data.count > majorityData.count) {
117
+ majorityValue = value;
118
+ majorityData = data;
119
+ }
120
+ }
121
+ const agreement = majorityData.count / entries.length;
122
+ const avgConsistency = majorityData.totalConsistency / majorityData.count;
123
+ const consistency = Math.round(avgConsistency * agreement);
124
+ result[key] = {
125
+ value: majorityValue,
126
+ confidence: confidenceFromConsistency(consistency),
127
+ sampleSize: majorityData.totalSamples,
128
+ consistency
129
+ };
130
+ }
131
+ return result;
132
+ }
133
+ function aggregateStatistics(packages) {
134
+ if (packages.length === 1) return packages[0].statistics;
135
+ const totalFiles = packages.reduce((sum, p) => sum + p.statistics.totalFiles, 0);
136
+ const totalLines = packages.reduce((sum, p) => sum + p.statistics.totalLines, 0);
137
+ const averageFileLines = totalFiles > 0 ? Math.round(totalLines / totalFiles) : 0;
138
+ const largestFiles = packages.flatMap(
139
+ (pkg) => pkg.statistics.largestFiles.map((f) => ({
140
+ path: pkg.relativePath ? `${pkg.relativePath}/${f.path}` : f.path,
141
+ lines: f.lines
142
+ }))
143
+ ).sort((a, b) => b.lines - a.lines).slice(0, 5);
144
+ const filesByExtension = {};
145
+ for (const pkg of packages) {
146
+ for (const [ext, count] of Object.entries(pkg.statistics.filesByExtension)) {
147
+ filesByExtension[ext] = (filesByExtension[ext] ?? 0) + count;
148
+ }
149
+ }
150
+ return { totalFiles, totalLines, averageFileLines, largestFiles, filesByExtension };
151
+ }
152
+
1
153
  // src/compute-statistics.ts
2
154
  import { readdir as readdir2, readFile } from "fs/promises";
3
155
  import { extname, join as join2 } from "path";
@@ -16,7 +168,12 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
16
168
  "coverage",
17
169
  ".turbo",
18
170
  ".cache",
19
- ".output"
171
+ ".output",
172
+ ".expo",
173
+ "android",
174
+ "ios",
175
+ "Pods",
176
+ ".gradle"
20
177
  ]);
21
178
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
22
179
  ".ts",
@@ -43,7 +200,9 @@ async function walkDirectory(projectPath, maxDepth = 4) {
43
200
  return results;
44
201
  }
45
202
  while (queue.length > 0) {
46
- const { absolutePath, depth } = queue.shift();
203
+ const item = queue.shift();
204
+ if (!item) break;
205
+ const { absolutePath, depth } = item;
47
206
  const sourceFileNames = [];
48
207
  try {
49
208
  const entries = await readdir(absolutePath, { withFileTypes: true });
@@ -167,7 +326,7 @@ async function computeStatistics(projectPath, dirs) {
167
326
  // src/detect-conventions.ts
168
327
  import { readFile as readFile2 } from "fs/promises";
169
328
  import { join as join3 } from "path";
170
- import { confidenceFromConsistency } from "@viberails/types";
329
+ import { confidenceFromConsistency as confidenceFromConsistency2 } from "@viberails/types";
171
330
 
172
331
  // src/utils/classify-filename.ts
173
332
  var PATTERNS = [
@@ -224,7 +383,7 @@ function detectFileNaming(dirs) {
224
383
  if (sorted.length === 0) return void 0;
225
384
  const [dominantConvention, dominantCount] = sorted[0];
226
385
  const consistency = Math.round(dominantCount / total * 100);
227
- const confidence = confidenceFromConsistency(consistency);
386
+ const confidence = confidenceFromConsistency2(consistency);
228
387
  if (confidence === "low") return void 0;
229
388
  return {
230
389
  value: dominantConvention,
@@ -250,7 +409,7 @@ function detectComponentNaming(dirs, structure) {
250
409
  const dominantValue = pascalCount >= tsxFiles.length / 2 ? "PascalCase" : "camelCase";
251
410
  return {
252
411
  value: dominantValue,
253
- confidence: confidenceFromConsistency(consistency),
412
+ confidence: confidenceFromConsistency2(consistency),
254
413
  sampleSize: tsxFiles.length,
255
414
  consistency
256
415
  };
@@ -286,7 +445,7 @@ function detectHookNaming(dirs, structure) {
286
445
  const consistency = Math.round(dominantCount / total * 100);
287
446
  return {
288
447
  value: isDominantKebab ? "use-*" : "useXxx",
289
- confidence: confidenceFromConsistency(consistency),
448
+ confidence: confidenceFromConsistency2(consistency),
290
449
  sampleSize: total,
291
450
  consistency
292
451
  };
@@ -300,7 +459,7 @@ async function detectImportAlias(projectPath) {
300
459
  const aliases = Object.keys(paths);
301
460
  if (aliases.length === 0) return void 0;
302
461
  return {
303
- value: aliases[0],
462
+ value: aliases.join(","),
304
463
  confidence: "high",
305
464
  sampleSize: aliases.length,
306
465
  consistency: 100
@@ -341,6 +500,8 @@ async function fileExists(filePath) {
341
500
  }
342
501
  var FRAMEWORK_MAPPINGS = [
343
502
  { dep: "next", name: "nextjs" },
503
+ { dep: "expo", name: "expo" },
504
+ { dep: "react-native", name: "react-native", excludeDep: "expo" },
344
505
  { dep: "@sveltejs/kit", name: "sveltekit" },
345
506
  { dep: "svelte", name: "svelte" },
346
507
  { dep: "astro", name: "astro" },
@@ -374,9 +535,10 @@ var LOCK_FILE_MAP = [
374
535
  { file: "bun.lockb", name: "bun" },
375
536
  { file: "package-lock.json", name: "npm" }
376
537
  ];
377
- async function detectStack(projectPath) {
538
+ async function detectStack(projectPath, additionalDeps) {
378
539
  const pkg = await readPackageJson(projectPath);
379
540
  const allDeps = {
541
+ ...additionalDeps,
380
542
  ...pkg?.dependencies,
381
543
  ...pkg?.devDependencies
382
544
  };
@@ -386,6 +548,7 @@ async function detectStack(projectPath) {
386
548
  const backend = detectFirst(allDeps, BACKEND_MAPPINGS);
387
549
  const packageManager = await detectPackageManager(projectPath);
388
550
  const linter = detectLinter(allDeps);
551
+ const formatter = detectFormatter(allDeps);
389
552
  const testRunner = detectTestRunner(allDeps);
390
553
  const libraries = detectLibraries(allDeps);
391
554
  return {
@@ -395,6 +558,7 @@ async function detectStack(projectPath) {
395
558
  ...backend && { backend },
396
559
  packageManager,
397
560
  ...linter && { linter },
561
+ ...formatter && { formatter },
398
562
  ...testRunner && { testRunner },
399
563
  libraries
400
564
  };
@@ -453,6 +617,18 @@ function detectLinter(allDeps) {
453
617
  }
454
618
  return void 0;
455
619
  }
620
+ function detectFormatter(allDeps) {
621
+ if ("prettier" in allDeps) {
622
+ return { name: "prettier", version: extractMajorVersion(allDeps.prettier) };
623
+ }
624
+ if ("@biomejs/biome" in allDeps) {
625
+ return {
626
+ name: "biome",
627
+ version: extractMajorVersion(allDeps["@biomejs/biome"])
628
+ };
629
+ }
630
+ return void 0;
631
+ }
456
632
  function detectTestRunner(allDeps) {
457
633
  if ("vitest" in allDeps) {
458
634
  return { name: "vitest", version: extractMajorVersion(allDeps.vitest) };
@@ -477,7 +653,7 @@ function detectLibraries(allDeps) {
477
653
  }
478
654
 
479
655
  // src/detect-structure.ts
480
- import { confidenceFromConsistency as confidenceFromConsistency2 } from "@viberails/types";
656
+ import { confidenceFromConsistency as confidenceFromConsistency3 } from "@viberails/types";
481
657
 
482
658
  // src/utils/classify-directory.ts
483
659
  var ROLE_PATTERNS = [
@@ -518,7 +694,9 @@ function classifyDirectory(dir) {
518
694
  function matchByName(relativePath) {
519
695
  for (const { role, pathPatterns } of ROLE_PATTERNS) {
520
696
  for (const pattern of pathPatterns) {
521
- if (relativePath === pattern) return role;
697
+ if (relativePath === pattern || relativePath.endsWith(`/${pattern}`)) {
698
+ return role;
699
+ }
522
700
  }
523
701
  }
524
702
  return null;
@@ -529,7 +707,7 @@ function inferFromContent(dir) {
529
707
  const name = f.split(".")[0];
530
708
  return name.startsWith("use-") || /^use[A-Z]/.test(name);
531
709
  });
532
- if (hookFiles.length > 0 && hookFiles.length / sourceFileCount >= 0.5) {
710
+ if (hookFiles.length >= 2 && hookFiles.length / sourceFileCount >= 0.5) {
533
711
  return {
534
712
  path: dir.relativePath,
535
713
  role: "hooks",
@@ -538,7 +716,7 @@ function inferFromContent(dir) {
538
716
  };
539
717
  }
540
718
  const testFiles = sourceFileNames.filter((f) => f.includes(".test.") || f.includes(".spec."));
541
- if (testFiles.length > 0 && testFiles.length / sourceFileCount >= 0.5) {
719
+ if (testFiles.length >= 2 && testFiles.length / sourceFileCount >= 0.5) {
542
720
  return {
543
721
  path: dir.relativePath,
544
722
  role: "tests",
@@ -583,7 +761,7 @@ function detectTestPattern(dirs) {
583
761
  const sep = isDotTest ? "test" : "spec";
584
762
  return {
585
763
  value: `*.${sep}${topExt}`,
586
- confidence: confidenceFromConsistency2(consistency),
764
+ confidence: confidenceFromConsistency3(consistency),
587
765
  sampleSize: testFiles.length,
588
766
  consistency
589
767
  };
@@ -685,18 +863,33 @@ async function resolvePackages(projectRoot, dirs) {
685
863
 
686
864
  // src/scan.ts
687
865
  import { stat } from "fs/promises";
866
+ import { basename, resolve as resolve2 } from "path";
867
+
868
+ // src/scan-package.ts
688
869
  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)));
870
+ async function scanPackage(packagePath, name, relativePath, rootDeps) {
871
+ const root = resolve(packagePath);
872
+ const dirs = await walkDirectory(root, 4);
873
+ const [stack, structure, statistics] = await Promise.all([
874
+ detectStack(root, rootDeps),
875
+ detectStructure(root, dirs),
876
+ computeStatistics(root, dirs)
877
+ ]);
878
+ const conventions = await detectConventions(root, structure, dirs);
879
+ return {
880
+ name,
881
+ root,
882
+ relativePath,
883
+ stack,
884
+ structure,
885
+ conventions,
886
+ statistics
887
+ };
697
888
  }
889
+
890
+ // src/scan.ts
698
891
  async function scan(projectPath, _options) {
699
- const root = resolve(projectPath);
892
+ const root = resolve2(projectPath);
700
893
  try {
701
894
  const st = await stat(root);
702
895
  if (!st.isDirectory()) {
@@ -708,22 +901,36 @@ async function scan(projectPath, _options) {
708
901
  }
709
902
  throw new Error(`Project path does not exist: ${root}`);
710
903
  }
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
904
  const workspace = await detectWorkspace(root);
905
+ if (workspace && workspace.packages.length > 0) {
906
+ const rootPkg2 = await readPackageJson(root);
907
+ const rootDeps = {
908
+ ...rootPkg2?.dependencies,
909
+ ...rootPkg2?.devDependencies
910
+ };
911
+ const packages = await Promise.all(
912
+ workspace.packages.map((wp) => scanPackage(wp.path, wp.name, wp.relativePath, rootDeps))
913
+ );
914
+ return {
915
+ root,
916
+ stack: aggregateStacks(packages),
917
+ structure: aggregateStructures(packages),
918
+ conventions: aggregateConventions(packages),
919
+ statistics: aggregateStatistics(packages),
920
+ workspace,
921
+ packages
922
+ };
923
+ }
924
+ const rootPkg = await readPackageJson(root);
925
+ const name = rootPkg?.name ?? basename(root);
926
+ const pkg = await scanPackage(root, name, "");
720
927
  return {
721
928
  root,
722
- stack,
723
- structure,
724
- conventions,
725
- statistics,
726
- workspace
929
+ stack: pkg.stack,
930
+ structure: pkg.structure,
931
+ conventions: pkg.conventions,
932
+ statistics: pkg.statistics,
933
+ packages: [pkg]
727
934
  };
728
935
  }
729
936
 
@@ -731,6 +938,10 @@ async function scan(projectPath, _options) {
731
938
  var VERSION = "0.1.0";
732
939
  export {
733
940
  VERSION,
941
+ aggregateConventions,
942
+ aggregateStacks,
943
+ aggregateStatistics,
944
+ aggregateStructures,
734
945
  computeStatistics,
735
946
  detectConventions,
736
947
  detectStack,
@@ -739,6 +950,7 @@ export {
739
950
  extractMajorVersion,
740
951
  readPackageJson,
741
952
  scan,
953
+ scanPackage,
742
954
  walkDirectory
743
955
  };
744
956
  //# sourceMappingURL=index.js.map