@viberails/scanner 0.1.0 → 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/dist/index.js CHANGED
@@ -1,3 +1,153 @@
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 testRunner = packages.find((p) => p.stack.testRunner)?.stack.testRunner;
44
+ return {
45
+ language,
46
+ packageManager,
47
+ framework,
48
+ libraries: [...libraryMap.values()],
49
+ styling,
50
+ backend,
51
+ linter,
52
+ testRunner
53
+ };
54
+ }
55
+ function aggregateStructures(packages) {
56
+ if (packages.length === 1) return packages[0].structure;
57
+ const srcDir = packages.some((p) => p.structure.srcDir) ? "src" : void 0;
58
+ const directories = packages.flatMap(
59
+ (pkg) => pkg.structure.directories.map((dir) => ({
60
+ ...dir,
61
+ path: pkg.relativePath ? `${pkg.relativePath}/${dir.path}` : dir.path
62
+ }))
63
+ );
64
+ const testPatterns = packages.map((p) => p.structure.testPattern).filter((t) => t !== void 0);
65
+ let testPattern;
66
+ if (testPatterns.length > 0) {
67
+ const counts = /* @__PURE__ */ new Map();
68
+ for (const tp of testPatterns) {
69
+ const existing = counts.get(tp.value);
70
+ if (existing) {
71
+ existing.count++;
72
+ } else {
73
+ counts.set(tp.value, { count: 1, pattern: tp });
74
+ }
75
+ }
76
+ let best = { count: 0, pattern: testPatterns[0] };
77
+ for (const entry of counts.values()) {
78
+ if (entry.count > best.count) best = entry;
79
+ }
80
+ testPattern = best.pattern;
81
+ }
82
+ return { srcDir, directories, testPattern };
83
+ }
84
+ function aggregateConventions(packages) {
85
+ if (packages.length === 1) return packages[0].conventions;
86
+ const allKeys = /* @__PURE__ */ new Set();
87
+ for (const pkg of packages) {
88
+ for (const key of Object.keys(pkg.conventions)) {
89
+ allKeys.add(key);
90
+ }
91
+ }
92
+ const result = {};
93
+ for (const key of allKeys) {
94
+ const entries = packages.map((p) => p.conventions[key]).filter((c) => c !== void 0);
95
+ if (entries.length < packages.length / 2) continue;
96
+ const valueCounts = /* @__PURE__ */ new Map();
97
+ for (const entry of entries) {
98
+ const existing = valueCounts.get(entry.value);
99
+ if (existing) {
100
+ existing.count++;
101
+ existing.totalConsistency += entry.consistency;
102
+ existing.totalSamples += entry.sampleSize;
103
+ } else {
104
+ valueCounts.set(entry.value, {
105
+ count: 1,
106
+ totalConsistency: entry.consistency,
107
+ totalSamples: entry.sampleSize
108
+ });
109
+ }
110
+ }
111
+ let majorityValue = "";
112
+ let majorityData = { count: 0, totalConsistency: 0, totalSamples: 0 };
113
+ for (const [value, data] of valueCounts) {
114
+ if (data.count > majorityData.count) {
115
+ majorityValue = value;
116
+ majorityData = data;
117
+ }
118
+ }
119
+ const agreement = majorityData.count / entries.length;
120
+ const avgConsistency = majorityData.totalConsistency / majorityData.count;
121
+ const consistency = Math.round(avgConsistency * agreement);
122
+ result[key] = {
123
+ value: majorityValue,
124
+ confidence: confidenceFromConsistency(consistency),
125
+ sampleSize: majorityData.totalSamples,
126
+ consistency
127
+ };
128
+ }
129
+ return result;
130
+ }
131
+ function aggregateStatistics(packages) {
132
+ if (packages.length === 1) return packages[0].statistics;
133
+ const totalFiles = packages.reduce((sum, p) => sum + p.statistics.totalFiles, 0);
134
+ const totalLines = packages.reduce((sum, p) => sum + p.statistics.totalLines, 0);
135
+ const averageFileLines = totalFiles > 0 ? Math.round(totalLines / totalFiles) : 0;
136
+ const largestFiles = packages.flatMap(
137
+ (pkg) => pkg.statistics.largestFiles.map((f) => ({
138
+ path: pkg.relativePath ? `${pkg.relativePath}/${f.path}` : f.path,
139
+ lines: f.lines
140
+ }))
141
+ ).sort((a, b) => b.lines - a.lines).slice(0, 5);
142
+ const filesByExtension = {};
143
+ for (const pkg of packages) {
144
+ for (const [ext, count] of Object.entries(pkg.statistics.filesByExtension)) {
145
+ filesByExtension[ext] = (filesByExtension[ext] ?? 0) + count;
146
+ }
147
+ }
148
+ return { totalFiles, totalLines, averageFileLines, largestFiles, filesByExtension };
149
+ }
150
+
1
151
  // src/compute-statistics.ts
2
152
  import { readdir as readdir2, readFile } from "fs/promises";
3
153
  import { extname, join as join2 } from "path";
@@ -167,7 +317,7 @@ async function computeStatistics(projectPath, dirs) {
167
317
  // src/detect-conventions.ts
168
318
  import { readFile as readFile2 } from "fs/promises";
169
319
  import { join as join3 } from "path";
170
- import { confidenceFromConsistency } from "@viberails/types";
320
+ import { confidenceFromConsistency as confidenceFromConsistency2 } from "@viberails/types";
171
321
 
172
322
  // src/utils/classify-filename.ts
173
323
  var PATTERNS = [
@@ -224,7 +374,7 @@ function detectFileNaming(dirs) {
224
374
  if (sorted.length === 0) return void 0;
225
375
  const [dominantConvention, dominantCount] = sorted[0];
226
376
  const consistency = Math.round(dominantCount / total * 100);
227
- const confidence = confidenceFromConsistency(consistency);
377
+ const confidence = confidenceFromConsistency2(consistency);
228
378
  if (confidence === "low") return void 0;
229
379
  return {
230
380
  value: dominantConvention,
@@ -250,7 +400,7 @@ function detectComponentNaming(dirs, structure) {
250
400
  const dominantValue = pascalCount >= tsxFiles.length / 2 ? "PascalCase" : "camelCase";
251
401
  return {
252
402
  value: dominantValue,
253
- confidence: confidenceFromConsistency(consistency),
403
+ confidence: confidenceFromConsistency2(consistency),
254
404
  sampleSize: tsxFiles.length,
255
405
  consistency
256
406
  };
@@ -286,7 +436,7 @@ function detectHookNaming(dirs, structure) {
286
436
  const consistency = Math.round(dominantCount / total * 100);
287
437
  return {
288
438
  value: isDominantKebab ? "use-*" : "useXxx",
289
- confidence: confidenceFromConsistency(consistency),
439
+ confidence: confidenceFromConsistency2(consistency),
290
440
  sampleSize: total,
291
441
  consistency
292
442
  };
@@ -300,7 +450,7 @@ async function detectImportAlias(projectPath) {
300
450
  const aliases = Object.keys(paths);
301
451
  if (aliases.length === 0) return void 0;
302
452
  return {
303
- value: aliases[0],
453
+ value: aliases.join(","),
304
454
  confidence: "high",
305
455
  sampleSize: aliases.length,
306
456
  consistency: 100
@@ -341,6 +491,8 @@ async function fileExists(filePath) {
341
491
  }
342
492
  var FRAMEWORK_MAPPINGS = [
343
493
  { dep: "next", name: "nextjs" },
494
+ { dep: "expo", name: "expo" },
495
+ { dep: "react-native", name: "react-native", excludeDep: "expo" },
344
496
  { dep: "@sveltejs/kit", name: "sveltekit" },
345
497
  { dep: "svelte", name: "svelte" },
346
498
  { dep: "astro", name: "astro" },
@@ -374,9 +526,10 @@ var LOCK_FILE_MAP = [
374
526
  { file: "bun.lockb", name: "bun" },
375
527
  { file: "package-lock.json", name: "npm" }
376
528
  ];
377
- async function detectStack(projectPath) {
529
+ async function detectStack(projectPath, additionalDeps) {
378
530
  const pkg = await readPackageJson(projectPath);
379
531
  const allDeps = {
532
+ ...additionalDeps,
380
533
  ...pkg?.dependencies,
381
534
  ...pkg?.devDependencies
382
535
  };
@@ -477,7 +630,7 @@ function detectLibraries(allDeps) {
477
630
  }
478
631
 
479
632
  // src/detect-structure.ts
480
- import { confidenceFromConsistency as confidenceFromConsistency2 } from "@viberails/types";
633
+ import { confidenceFromConsistency as confidenceFromConsistency3 } from "@viberails/types";
481
634
 
482
635
  // src/utils/classify-directory.ts
483
636
  var ROLE_PATTERNS = [
@@ -518,7 +671,9 @@ function classifyDirectory(dir) {
518
671
  function matchByName(relativePath) {
519
672
  for (const { role, pathPatterns } of ROLE_PATTERNS) {
520
673
  for (const pattern of pathPatterns) {
521
- if (relativePath === pattern) return role;
674
+ if (relativePath === pattern || relativePath.endsWith(`/${pattern}`)) {
675
+ return role;
676
+ }
522
677
  }
523
678
  }
524
679
  return null;
@@ -529,7 +684,7 @@ function inferFromContent(dir) {
529
684
  const name = f.split(".")[0];
530
685
  return name.startsWith("use-") || /^use[A-Z]/.test(name);
531
686
  });
532
- if (hookFiles.length > 0 && hookFiles.length / sourceFileCount >= 0.5) {
687
+ if (hookFiles.length >= 2 && hookFiles.length / sourceFileCount >= 0.5) {
533
688
  return {
534
689
  path: dir.relativePath,
535
690
  role: "hooks",
@@ -538,7 +693,7 @@ function inferFromContent(dir) {
538
693
  };
539
694
  }
540
695
  const testFiles = sourceFileNames.filter((f) => f.includes(".test.") || f.includes(".spec."));
541
- if (testFiles.length > 0 && testFiles.length / sourceFileCount >= 0.5) {
696
+ if (testFiles.length >= 2 && testFiles.length / sourceFileCount >= 0.5) {
542
697
  return {
543
698
  path: dir.relativePath,
544
699
  role: "tests",
@@ -583,7 +738,7 @@ function detectTestPattern(dirs) {
583
738
  const sep = isDotTest ? "test" : "spec";
584
739
  return {
585
740
  value: `*.${sep}${topExt}`,
586
- confidence: confidenceFromConsistency2(consistency),
741
+ confidence: confidenceFromConsistency3(consistency),
587
742
  sampleSize: testFiles.length,
588
743
  consistency
589
744
  };
@@ -683,20 +838,33 @@ async function resolvePackages(projectRoot, dirs) {
683
838
  return packages;
684
839
  }
685
840
 
686
- // src/scan.ts
687
- import { stat } from "fs/promises";
841
+ // src/scan-package.ts
688
842
  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)));
843
+ async function scanPackage(packagePath, name, relativePath, rootDeps) {
844
+ const root = resolve(packagePath);
845
+ const dirs = await walkDirectory(root, 4);
846
+ const [stack, structure, statistics] = await Promise.all([
847
+ detectStack(root, rootDeps),
848
+ detectStructure(root, dirs),
849
+ computeStatistics(root, dirs)
850
+ ]);
851
+ const conventions = await detectConventions(root, structure, dirs);
852
+ return {
853
+ name,
854
+ root,
855
+ relativePath,
856
+ stack,
857
+ structure,
858
+ conventions,
859
+ statistics
860
+ };
697
861
  }
862
+
863
+ // src/scan.ts
864
+ import { stat } from "fs/promises";
865
+ import { basename, resolve as resolve2 } from "path";
698
866
  async function scan(projectPath, _options) {
699
- const root = resolve(projectPath);
867
+ const root = resolve2(projectPath);
700
868
  try {
701
869
  const st = await stat(root);
702
870
  if (!st.isDirectory()) {
@@ -708,22 +876,36 @@ async function scan(projectPath, _options) {
708
876
  }
709
877
  throw new Error(`Project path does not exist: ${root}`);
710
878
  }
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
879
  const workspace = await detectWorkspace(root);
880
+ if (workspace && workspace.packages.length > 0) {
881
+ const rootPkg2 = await readPackageJson(root);
882
+ const rootDeps = {
883
+ ...rootPkg2?.dependencies,
884
+ ...rootPkg2?.devDependencies
885
+ };
886
+ const packages = await Promise.all(
887
+ workspace.packages.map((wp) => scanPackage(wp.path, wp.name, wp.relativePath, rootDeps))
888
+ );
889
+ return {
890
+ root,
891
+ stack: aggregateStacks(packages),
892
+ structure: aggregateStructures(packages),
893
+ conventions: aggregateConventions(packages),
894
+ statistics: aggregateStatistics(packages),
895
+ workspace,
896
+ packages
897
+ };
898
+ }
899
+ const rootPkg = await readPackageJson(root);
900
+ const name = rootPkg?.name ?? basename(root);
901
+ const pkg = await scanPackage(root, name, "");
720
902
  return {
721
903
  root,
722
- stack,
723
- structure,
724
- conventions,
725
- statistics,
726
- workspace
904
+ stack: pkg.stack,
905
+ structure: pkg.structure,
906
+ conventions: pkg.conventions,
907
+ statistics: pkg.statistics,
908
+ packages: [pkg]
727
909
  };
728
910
  }
729
911
 
@@ -731,6 +913,10 @@ async function scan(projectPath, _options) {
731
913
  var VERSION = "0.1.0";
732
914
  export {
733
915
  VERSION,
916
+ aggregateConventions,
917
+ aggregateStacks,
918
+ aggregateStatistics,
919
+ aggregateStructures,
734
920
  computeStatistics,
735
921
  detectConventions,
736
922
  detectStack,
@@ -739,6 +925,7 @@ export {
739
925
  extractMajorVersion,
740
926
  readPackageJson,
741
927
  scan,
928
+ scanPackage,
742
929
  walkDirectory
743
930
  };
744
931
  //# sourceMappingURL=index.js.map