@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.cjs CHANGED
@@ -21,6 +21,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  VERSION: () => VERSION,
24
+ aggregateConventions: () => aggregateConventions,
25
+ aggregateStacks: () => aggregateStacks,
26
+ aggregateStatistics: () => aggregateStatistics,
27
+ aggregateStructures: () => aggregateStructures,
24
28
  computeStatistics: () => computeStatistics,
25
29
  detectConventions: () => detectConventions,
26
30
  detectStack: () => detectStack,
@@ -29,10 +33,161 @@ __export(index_exports, {
29
33
  extractMajorVersion: () => extractMajorVersion,
30
34
  readPackageJson: () => readPackageJson,
31
35
  scan: () => scan,
36
+ scanPackage: () => scanPackage,
32
37
  walkDirectory: () => walkDirectory
33
38
  });
34
39
  module.exports = __toCommonJS(index_exports);
35
40
 
41
+ // src/aggregate.ts
42
+ var import_types = require("@viberails/types");
43
+ var FRAMEWORK_PRIORITY = [
44
+ "nextjs",
45
+ "sveltekit",
46
+ "astro",
47
+ "expo",
48
+ "react-native",
49
+ "svelte",
50
+ "vue",
51
+ "react"
52
+ ];
53
+ function aggregateStacks(packages) {
54
+ if (packages.length === 1) return packages[0].stack;
55
+ const language = packages.some((p) => p.stack.language.name === "typescript") ? packages.find((p) => p.stack.language.name === "typescript").stack.language : packages[0].stack.language;
56
+ const packageManager = packages[0].stack.packageManager;
57
+ const frameworkPackages = packages.filter((p) => p.stack.framework);
58
+ let framework;
59
+ if (frameworkPackages.length > 0) {
60
+ frameworkPackages.sort((a, b) => {
61
+ const aIdx = FRAMEWORK_PRIORITY.indexOf(a.stack.framework.name);
62
+ const bIdx = FRAMEWORK_PRIORITY.indexOf(b.stack.framework.name);
63
+ return (aIdx === -1 ? Infinity : aIdx) - (bIdx === -1 ? Infinity : bIdx);
64
+ });
65
+ framework = frameworkPackages[0].stack.framework;
66
+ }
67
+ const libraryMap = /* @__PURE__ */ new Map();
68
+ for (const pkg of packages) {
69
+ if (pkg.stack.framework && pkg.stack.framework.name !== framework?.name) {
70
+ libraryMap.set(pkg.stack.framework.name, pkg.stack.framework);
71
+ }
72
+ for (const lib of pkg.stack.libraries) {
73
+ if (!libraryMap.has(lib.name)) {
74
+ libraryMap.set(lib.name, lib);
75
+ }
76
+ }
77
+ }
78
+ const styling = packages.find((p) => p.stack.styling)?.stack.styling;
79
+ const backend = packages.find((p) => p.stack.backend)?.stack.backend;
80
+ const linter = packages.find((p) => p.stack.linter)?.stack.linter;
81
+ const formatter = packages.find((p) => p.stack.formatter)?.stack.formatter;
82
+ const testRunner = packages.find((p) => p.stack.testRunner)?.stack.testRunner;
83
+ return {
84
+ language,
85
+ packageManager,
86
+ framework,
87
+ libraries: [...libraryMap.values()],
88
+ styling,
89
+ backend,
90
+ linter,
91
+ formatter,
92
+ testRunner
93
+ };
94
+ }
95
+ function aggregateStructures(packages) {
96
+ if (packages.length === 1) return packages[0].structure;
97
+ const srcDir = packages.some((p) => p.structure.srcDir) ? "src" : void 0;
98
+ const directories = packages.flatMap(
99
+ (pkg) => pkg.structure.directories.map((dir) => ({
100
+ ...dir,
101
+ path: pkg.relativePath ? `${pkg.relativePath}/${dir.path}` : dir.path
102
+ }))
103
+ );
104
+ const testPatterns = packages.map((p) => p.structure.testPattern).filter((t) => t !== void 0);
105
+ let testPattern;
106
+ if (testPatterns.length > 0) {
107
+ const counts = /* @__PURE__ */ new Map();
108
+ for (const tp of testPatterns) {
109
+ const existing = counts.get(tp.value);
110
+ if (existing) {
111
+ existing.count++;
112
+ } else {
113
+ counts.set(tp.value, { count: 1, pattern: tp });
114
+ }
115
+ }
116
+ let best = { count: 0, pattern: testPatterns[0] };
117
+ for (const entry of counts.values()) {
118
+ if (entry.count > best.count) best = entry;
119
+ }
120
+ testPattern = best.pattern;
121
+ }
122
+ return { srcDir, directories, testPattern };
123
+ }
124
+ function aggregateConventions(packages) {
125
+ if (packages.length === 1) return packages[0].conventions;
126
+ const allKeys = /* @__PURE__ */ new Set();
127
+ for (const pkg of packages) {
128
+ for (const key of Object.keys(pkg.conventions)) {
129
+ allKeys.add(key);
130
+ }
131
+ }
132
+ const result = {};
133
+ for (const key of allKeys) {
134
+ const entries = packages.map((p) => p.conventions[key]).filter((c) => c !== void 0);
135
+ if (entries.length < packages.length / 2) continue;
136
+ const valueCounts = /* @__PURE__ */ new Map();
137
+ for (const entry of entries) {
138
+ const existing = valueCounts.get(entry.value);
139
+ if (existing) {
140
+ existing.count++;
141
+ existing.totalConsistency += entry.consistency;
142
+ existing.totalSamples += entry.sampleSize;
143
+ } else {
144
+ valueCounts.set(entry.value, {
145
+ count: 1,
146
+ totalConsistency: entry.consistency,
147
+ totalSamples: entry.sampleSize
148
+ });
149
+ }
150
+ }
151
+ let majorityValue = "";
152
+ let majorityData = { count: 0, totalConsistency: 0, totalSamples: 0 };
153
+ for (const [value, data] of valueCounts) {
154
+ if (data.count > majorityData.count) {
155
+ majorityValue = value;
156
+ majorityData = data;
157
+ }
158
+ }
159
+ const agreement = majorityData.count / entries.length;
160
+ const avgConsistency = majorityData.totalConsistency / majorityData.count;
161
+ const consistency = Math.round(avgConsistency * agreement);
162
+ result[key] = {
163
+ value: majorityValue,
164
+ confidence: (0, import_types.confidenceFromConsistency)(consistency),
165
+ sampleSize: majorityData.totalSamples,
166
+ consistency
167
+ };
168
+ }
169
+ return result;
170
+ }
171
+ function aggregateStatistics(packages) {
172
+ if (packages.length === 1) return packages[0].statistics;
173
+ const totalFiles = packages.reduce((sum, p) => sum + p.statistics.totalFiles, 0);
174
+ const totalLines = packages.reduce((sum, p) => sum + p.statistics.totalLines, 0);
175
+ const averageFileLines = totalFiles > 0 ? Math.round(totalLines / totalFiles) : 0;
176
+ const largestFiles = packages.flatMap(
177
+ (pkg) => pkg.statistics.largestFiles.map((f) => ({
178
+ path: pkg.relativePath ? `${pkg.relativePath}/${f.path}` : f.path,
179
+ lines: f.lines
180
+ }))
181
+ ).sort((a, b) => b.lines - a.lines).slice(0, 5);
182
+ const filesByExtension = {};
183
+ for (const pkg of packages) {
184
+ for (const [ext, count] of Object.entries(pkg.statistics.filesByExtension)) {
185
+ filesByExtension[ext] = (filesByExtension[ext] ?? 0) + count;
186
+ }
187
+ }
188
+ return { totalFiles, totalLines, averageFileLines, largestFiles, filesByExtension };
189
+ }
190
+
36
191
  // src/compute-statistics.ts
37
192
  var import_promises2 = require("fs/promises");
38
193
  var import_node_path2 = require("path");
@@ -51,7 +206,12 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
51
206
  "coverage",
52
207
  ".turbo",
53
208
  ".cache",
54
- ".output"
209
+ ".output",
210
+ ".expo",
211
+ "android",
212
+ "ios",
213
+ "Pods",
214
+ ".gradle"
55
215
  ]);
56
216
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
57
217
  ".ts",
@@ -78,7 +238,9 @@ async function walkDirectory(projectPath, maxDepth = 4) {
78
238
  return results;
79
239
  }
80
240
  while (queue.length > 0) {
81
- const { absolutePath, depth } = queue.shift();
241
+ const item = queue.shift();
242
+ if (!item) break;
243
+ const { absolutePath, depth } = item;
82
244
  const sourceFileNames = [];
83
245
  try {
84
246
  const entries = await (0, import_promises.readdir)(absolutePath, { withFileTypes: true });
@@ -202,7 +364,7 @@ async function computeStatistics(projectPath, dirs) {
202
364
  // src/detect-conventions.ts
203
365
  var import_promises3 = require("fs/promises");
204
366
  var import_node_path3 = require("path");
205
- var import_types = require("@viberails/types");
367
+ var import_types2 = require("@viberails/types");
206
368
 
207
369
  // src/utils/classify-filename.ts
208
370
  var PATTERNS = [
@@ -259,7 +421,7 @@ function detectFileNaming(dirs) {
259
421
  if (sorted.length === 0) return void 0;
260
422
  const [dominantConvention, dominantCount] = sorted[0];
261
423
  const consistency = Math.round(dominantCount / total * 100);
262
- const confidence = (0, import_types.confidenceFromConsistency)(consistency);
424
+ const confidence = (0, import_types2.confidenceFromConsistency)(consistency);
263
425
  if (confidence === "low") return void 0;
264
426
  return {
265
427
  value: dominantConvention,
@@ -285,7 +447,7 @@ function detectComponentNaming(dirs, structure) {
285
447
  const dominantValue = pascalCount >= tsxFiles.length / 2 ? "PascalCase" : "camelCase";
286
448
  return {
287
449
  value: dominantValue,
288
- confidence: (0, import_types.confidenceFromConsistency)(consistency),
450
+ confidence: (0, import_types2.confidenceFromConsistency)(consistency),
289
451
  sampleSize: tsxFiles.length,
290
452
  consistency
291
453
  };
@@ -321,7 +483,7 @@ function detectHookNaming(dirs, structure) {
321
483
  const consistency = Math.round(dominantCount / total * 100);
322
484
  return {
323
485
  value: isDominantKebab ? "use-*" : "useXxx",
324
- confidence: (0, import_types.confidenceFromConsistency)(consistency),
486
+ confidence: (0, import_types2.confidenceFromConsistency)(consistency),
325
487
  sampleSize: total,
326
488
  consistency
327
489
  };
@@ -335,7 +497,7 @@ async function detectImportAlias(projectPath) {
335
497
  const aliases = Object.keys(paths);
336
498
  if (aliases.length === 0) return void 0;
337
499
  return {
338
- value: aliases[0],
500
+ value: aliases.join(","),
339
501
  confidence: "high",
340
502
  sampleSize: aliases.length,
341
503
  consistency: 100
@@ -376,6 +538,8 @@ async function fileExists(filePath) {
376
538
  }
377
539
  var FRAMEWORK_MAPPINGS = [
378
540
  { dep: "next", name: "nextjs" },
541
+ { dep: "expo", name: "expo" },
542
+ { dep: "react-native", name: "react-native", excludeDep: "expo" },
379
543
  { dep: "@sveltejs/kit", name: "sveltekit" },
380
544
  { dep: "svelte", name: "svelte" },
381
545
  { dep: "astro", name: "astro" },
@@ -409,9 +573,10 @@ var LOCK_FILE_MAP = [
409
573
  { file: "bun.lockb", name: "bun" },
410
574
  { file: "package-lock.json", name: "npm" }
411
575
  ];
412
- async function detectStack(projectPath) {
576
+ async function detectStack(projectPath, additionalDeps) {
413
577
  const pkg = await readPackageJson(projectPath);
414
578
  const allDeps = {
579
+ ...additionalDeps,
415
580
  ...pkg?.dependencies,
416
581
  ...pkg?.devDependencies
417
582
  };
@@ -421,6 +586,7 @@ async function detectStack(projectPath) {
421
586
  const backend = detectFirst(allDeps, BACKEND_MAPPINGS);
422
587
  const packageManager = await detectPackageManager(projectPath);
423
588
  const linter = detectLinter(allDeps);
589
+ const formatter = detectFormatter(allDeps);
424
590
  const testRunner = detectTestRunner(allDeps);
425
591
  const libraries = detectLibraries(allDeps);
426
592
  return {
@@ -430,6 +596,7 @@ async function detectStack(projectPath) {
430
596
  ...backend && { backend },
431
597
  packageManager,
432
598
  ...linter && { linter },
599
+ ...formatter && { formatter },
433
600
  ...testRunner && { testRunner },
434
601
  libraries
435
602
  };
@@ -488,6 +655,18 @@ function detectLinter(allDeps) {
488
655
  }
489
656
  return void 0;
490
657
  }
658
+ function detectFormatter(allDeps) {
659
+ if ("prettier" in allDeps) {
660
+ return { name: "prettier", version: extractMajorVersion(allDeps.prettier) };
661
+ }
662
+ if ("@biomejs/biome" in allDeps) {
663
+ return {
664
+ name: "biome",
665
+ version: extractMajorVersion(allDeps["@biomejs/biome"])
666
+ };
667
+ }
668
+ return void 0;
669
+ }
491
670
  function detectTestRunner(allDeps) {
492
671
  if ("vitest" in allDeps) {
493
672
  return { name: "vitest", version: extractMajorVersion(allDeps.vitest) };
@@ -512,7 +691,7 @@ function detectLibraries(allDeps) {
512
691
  }
513
692
 
514
693
  // src/detect-structure.ts
515
- var import_types2 = require("@viberails/types");
694
+ var import_types3 = require("@viberails/types");
516
695
 
517
696
  // src/utils/classify-directory.ts
518
697
  var ROLE_PATTERNS = [
@@ -553,7 +732,9 @@ function classifyDirectory(dir) {
553
732
  function matchByName(relativePath) {
554
733
  for (const { role, pathPatterns } of ROLE_PATTERNS) {
555
734
  for (const pattern of pathPatterns) {
556
- if (relativePath === pattern) return role;
735
+ if (relativePath === pattern || relativePath.endsWith(`/${pattern}`)) {
736
+ return role;
737
+ }
557
738
  }
558
739
  }
559
740
  return null;
@@ -564,7 +745,7 @@ function inferFromContent(dir) {
564
745
  const name = f.split(".")[0];
565
746
  return name.startsWith("use-") || /^use[A-Z]/.test(name);
566
747
  });
567
- if (hookFiles.length > 0 && hookFiles.length / sourceFileCount >= 0.5) {
748
+ if (hookFiles.length >= 2 && hookFiles.length / sourceFileCount >= 0.5) {
568
749
  return {
569
750
  path: dir.relativePath,
570
751
  role: "hooks",
@@ -573,7 +754,7 @@ function inferFromContent(dir) {
573
754
  };
574
755
  }
575
756
  const testFiles = sourceFileNames.filter((f) => f.includes(".test.") || f.includes(".spec."));
576
- if (testFiles.length > 0 && testFiles.length / sourceFileCount >= 0.5) {
757
+ if (testFiles.length >= 2 && testFiles.length / sourceFileCount >= 0.5) {
577
758
  return {
578
759
  path: dir.relativePath,
579
760
  role: "tests",
@@ -618,7 +799,7 @@ function detectTestPattern(dirs) {
618
799
  const sep = isDotTest ? "test" : "spec";
619
800
  return {
620
801
  value: `*.${sep}${topExt}`,
621
- confidence: (0, import_types2.confidenceFromConsistency)(consistency),
802
+ confidence: (0, import_types3.confidenceFromConsistency)(consistency),
622
803
  sampleSize: testFiles.length,
623
804
  consistency
624
805
  };
@@ -720,18 +901,33 @@ async function resolvePackages(projectRoot, dirs) {
720
901
 
721
902
  // src/scan.ts
722
903
  var import_promises7 = require("fs/promises");
904
+ var import_node_path8 = require("path");
905
+
906
+ // src/scan-package.ts
723
907
  var import_node_path7 = require("path");
724
- var FIXTURE_PATTERNS = [
725
- /^tests\/fixtures(\/|$)/,
726
- /^test\/fixtures(\/|$)/,
727
- /^__tests__\/fixtures(\/|$)/,
728
- /^fixtures(\/|$)/
729
- ];
730
- function filterFixtureDirs(dirs) {
731
- return dirs.filter((d) => !FIXTURE_PATTERNS.some((pattern) => pattern.test(d.relativePath)));
908
+ async function scanPackage(packagePath, name, relativePath, rootDeps) {
909
+ const root = (0, import_node_path7.resolve)(packagePath);
910
+ const dirs = await walkDirectory(root, 4);
911
+ const [stack, structure, statistics] = await Promise.all([
912
+ detectStack(root, rootDeps),
913
+ detectStructure(root, dirs),
914
+ computeStatistics(root, dirs)
915
+ ]);
916
+ const conventions = await detectConventions(root, structure, dirs);
917
+ return {
918
+ name,
919
+ root,
920
+ relativePath,
921
+ stack,
922
+ structure,
923
+ conventions,
924
+ statistics
925
+ };
732
926
  }
927
+
928
+ // src/scan.ts
733
929
  async function scan(projectPath, _options) {
734
- const root = (0, import_node_path7.resolve)(projectPath);
930
+ const root = (0, import_node_path8.resolve)(projectPath);
735
931
  try {
736
932
  const st = await (0, import_promises7.stat)(root);
737
933
  if (!st.isDirectory()) {
@@ -743,22 +939,36 @@ async function scan(projectPath, _options) {
743
939
  }
744
940
  throw new Error(`Project path does not exist: ${root}`);
745
941
  }
746
- const allDirs = await walkDirectory(root, 4);
747
- const dirs = filterFixtureDirs(allDirs);
748
- const [stack, structure, statistics] = await Promise.all([
749
- detectStack(root),
750
- detectStructure(root, dirs),
751
- computeStatistics(root, dirs)
752
- ]);
753
- const conventions = await detectConventions(root, structure, dirs);
754
942
  const workspace = await detectWorkspace(root);
943
+ if (workspace && workspace.packages.length > 0) {
944
+ const rootPkg2 = await readPackageJson(root);
945
+ const rootDeps = {
946
+ ...rootPkg2?.dependencies,
947
+ ...rootPkg2?.devDependencies
948
+ };
949
+ const packages = await Promise.all(
950
+ workspace.packages.map((wp) => scanPackage(wp.path, wp.name, wp.relativePath, rootDeps))
951
+ );
952
+ return {
953
+ root,
954
+ stack: aggregateStacks(packages),
955
+ structure: aggregateStructures(packages),
956
+ conventions: aggregateConventions(packages),
957
+ statistics: aggregateStatistics(packages),
958
+ workspace,
959
+ packages
960
+ };
961
+ }
962
+ const rootPkg = await readPackageJson(root);
963
+ const name = rootPkg?.name ?? (0, import_node_path8.basename)(root);
964
+ const pkg = await scanPackage(root, name, "");
755
965
  return {
756
966
  root,
757
- stack,
758
- structure,
759
- conventions,
760
- statistics,
761
- workspace
967
+ stack: pkg.stack,
968
+ structure: pkg.structure,
969
+ conventions: pkg.conventions,
970
+ statistics: pkg.statistics,
971
+ packages: [pkg]
762
972
  };
763
973
  }
764
974
 
@@ -767,6 +977,10 @@ var VERSION = "0.1.0";
767
977
  // Annotate the CommonJS export names for ESM import in node:
768
978
  0 && (module.exports = {
769
979
  VERSION,
980
+ aggregateConventions,
981
+ aggregateStacks,
982
+ aggregateStatistics,
983
+ aggregateStructures,
770
984
  computeStatistics,
771
985
  detectConventions,
772
986
  detectStack,
@@ -775,6 +989,7 @@ var VERSION = "0.1.0";
775
989
  extractMajorVersion,
776
990
  readPackageJson,
777
991
  scan,
992
+ scanPackage,
778
993
  walkDirectory
779
994
  });
780
995
  //# sourceMappingURL=index.cjs.map