@archora/core 1.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.
Files changed (112) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +62 -0
  3. package/package.json +36 -0
  4. package/src/README.md +4 -0
  5. package/src/analyzer/__tests__/__snapshots__/referenceSnapshot.test.ts.snap +145 -0
  6. package/src/analyzer/__tests__/_paths.ts +8 -0
  7. package/src/analyzer/__tests__/analyze.test.ts +522 -0
  8. package/src/analyzer/__tests__/archDebt.test.ts +111 -0
  9. package/src/analyzer/__tests__/asyncLifecycleRisk.test.ts +122 -0
  10. package/src/analyzer/__tests__/browserFsAccessFileSource.test.ts +97 -0
  11. package/src/analyzer/__tests__/bundle.test.ts +191 -0
  12. package/src/analyzer/__tests__/classify.test.ts +99 -0
  13. package/src/analyzer/__tests__/contracts.test.ts +372 -0
  14. package/src/analyzer/__tests__/crossSourceConsistency.test.ts +317 -0
  15. package/src/analyzer/__tests__/cyclePatterns.test.ts +132 -0
  16. package/src/analyzer/__tests__/cycles.test.ts +74 -0
  17. package/src/analyzer/__tests__/detect.test.ts +62 -0
  18. package/src/analyzer/__tests__/discover.test.ts +68 -0
  19. package/src/analyzer/__tests__/displayId.test.ts +30 -0
  20. package/src/analyzer/__tests__/feedbackArcSet.test.ts +168 -0
  21. package/src/analyzer/__tests__/inMemoryFileSource.test.ts +34 -0
  22. package/src/analyzer/__tests__/incremental.test.ts +154 -0
  23. package/src/analyzer/__tests__/layers.test.ts +87 -0
  24. package/src/analyzer/__tests__/layersOverrides.test.ts +120 -0
  25. package/src/analyzer/__tests__/memoryRisk.test.ts +132 -0
  26. package/src/analyzer/__tests__/metrics.test.ts +59 -0
  27. package/src/analyzer/__tests__/parserRegistry.test.ts +54 -0
  28. package/src/analyzer/__tests__/parsers.test.ts +187 -0
  29. package/src/analyzer/__tests__/reactParser.test.ts +93 -0
  30. package/src/analyzer/__tests__/recommendations.test.ts +171 -0
  31. package/src/analyzer/__tests__/referenceSnapshot.test.ts +63 -0
  32. package/src/analyzer/__tests__/resolve.test.ts +294 -0
  33. package/src/analyzer/__tests__/rsc.test.ts +130 -0
  34. package/src/analyzer/__tests__/signals.test.ts +316 -0
  35. package/src/analyzer/__tests__/suggestContracts.test.ts +108 -0
  36. package/src/analyzer/__tests__/svelteParser.test.ts +108 -0
  37. package/src/analyzer/__tests__/typeOnlyCandidates.test.ts +163 -0
  38. package/src/analyzer/__tests__/vueAutoImport.test.ts +177 -0
  39. package/src/analyzer/archDebt.ts +68 -0
  40. package/src/analyzer/asyncLifecycleRisk.ts +234 -0
  41. package/src/analyzer/buildGraph.ts +683 -0
  42. package/src/analyzer/bundle/analyzeBundle.ts +147 -0
  43. package/src/analyzer/bundle/index.ts +12 -0
  44. package/src/analyzer/bundle/parseStats.ts +152 -0
  45. package/src/analyzer/bundle/types.ts +85 -0
  46. package/src/analyzer/classify.ts +54 -0
  47. package/src/analyzer/contracts.ts +265 -0
  48. package/src/analyzer/cyclePatterns.ts +138 -0
  49. package/src/analyzer/cycles.ts +98 -0
  50. package/src/analyzer/detect.ts +34 -0
  51. package/src/analyzer/discover.ts +131 -0
  52. package/src/analyzer/displayId.ts +21 -0
  53. package/src/analyzer/entryPoints.ts +136 -0
  54. package/src/analyzer/feedbackArcSet.ts +332 -0
  55. package/src/analyzer/fileSource.ts +8 -0
  56. package/src/analyzer/hotZones.ts +17 -0
  57. package/src/analyzer/incremental.ts +455 -0
  58. package/src/analyzer/index.ts +444 -0
  59. package/src/analyzer/layers.ts +183 -0
  60. package/src/analyzer/loadAliases.ts +288 -0
  61. package/src/analyzer/memoryRisk.ts +345 -0
  62. package/src/analyzer/metrics.ts +156 -0
  63. package/src/analyzer/parsers/index.ts +62 -0
  64. package/src/analyzer/parsers/reactParser.ts +24 -0
  65. package/src/analyzer/parsers/svelteParser.ts +46 -0
  66. package/src/analyzer/parsers/tsParser.ts +364 -0
  67. package/src/analyzer/parsers/vueParser.ts +109 -0
  68. package/src/analyzer/recommendations.ts +432 -0
  69. package/src/analyzer/resolve.ts +315 -0
  70. package/src/analyzer/rsc.ts +120 -0
  71. package/src/analyzer/signals.ts +684 -0
  72. package/src/analyzer/sources/browserFsAccessFileSource.ts +132 -0
  73. package/src/analyzer/sources/inMemoryFileSource.ts +24 -0
  74. package/src/analyzer/sources/nodeFsFileSource.ts +93 -0
  75. package/src/analyzer/sources/tauriFileSource.ts +68 -0
  76. package/src/analyzer/suggestContracts.ts +214 -0
  77. package/src/analyzer/typeOnlyCandidates.ts +233 -0
  78. package/src/analyzer/types.ts +537 -0
  79. package/src/cache/__tests__/cache.test.ts +316 -0
  80. package/src/cache/index.ts +432 -0
  81. package/src/codegen/__tests__/applyTypeOnlyFix.integration.test.ts +62 -0
  82. package/src/codegen/__tests__/applyTypeOnlyFix.test.ts +176 -0
  83. package/src/codegen/__tests__/configSnippets.test.ts +230 -0
  84. package/src/codegen/applyTypeOnlyFix.ts +344 -0
  85. package/src/codegen/configSnippets.ts +172 -0
  86. package/src/codegen/initConfig.ts +223 -0
  87. package/src/config/__tests__/frontScopeConfig.test.ts +187 -0
  88. package/src/config/frontScopeConfig.ts +830 -0
  89. package/src/diff/__tests__/diffScans.test.ts +103 -0
  90. package/src/diff/diffScans.ts +61 -0
  91. package/src/diff/index.ts +2 -0
  92. package/src/diff/types.ts +39 -0
  93. package/src/git/__tests__/computeChurn.test.ts +113 -0
  94. package/src/git/__tests__/computeTemporalCoupling.test.ts +125 -0
  95. package/src/git/__tests__/parseGitLog.test.ts +120 -0
  96. package/src/git/computeChurn.ts +111 -0
  97. package/src/git/computeTemporalCoupling.ts +114 -0
  98. package/src/git/index.ts +24 -0
  99. package/src/git/parseGitLog.ts +124 -0
  100. package/src/git/readGitHistory.ts +130 -0
  101. package/src/git/types.ts +119 -0
  102. package/src/index.ts +137 -0
  103. package/src/report/__tests__/buildFixPlan.test.ts +357 -0
  104. package/src/report/__tests__/buildJsonReport.test.ts +34 -0
  105. package/src/report/buildFixPlan.ts +481 -0
  106. package/src/report/buildJsonReport.ts +27 -0
  107. package/src/search/__tests__/parseQuery.test.ts +67 -0
  108. package/src/search/__tests__/search.test.ts +172 -0
  109. package/src/search/index.ts +281 -0
  110. package/src/search/parseQuery.ts +75 -0
  111. package/src/views/__tests__/analyzerViews.test.ts +558 -0
  112. package/src/views/analyzerViews.ts +1294 -0
@@ -0,0 +1,147 @@
1
+ // Map a parsed bundler stats payload onto the analyzer's module graph and
2
+ // derive `bundle-bloat` issues from the result.
3
+ //
4
+ // Mapping strategy: chunk modules carry a `normalizedPath`; we look it up
5
+ // against the set of `ModuleNode.id` values produced by the analyzer. Misses
6
+ // are kept on the chunk for size accounting but don't surface as recs (we'd
7
+ // flag them as "external" otherwise, which is noise on every chunk).
8
+
9
+ import type { ModuleId, ModuleNode } from '../types';
10
+ import {
11
+ DEFAULT_BUNDLE_THRESHOLDS,
12
+ type BundleBloat,
13
+ type BundleChunk,
14
+ type BundleChunkModule,
15
+ type BundleReport,
16
+ type BundleThresholds,
17
+ type ParsedBundleStats,
18
+ } from './types';
19
+ import { displayShortId } from '../displayId';
20
+
21
+ export interface AnalyzeBundleInput {
22
+ modules: ModuleNode[];
23
+ stats: ParsedBundleStats;
24
+ thresholds?: Partial<BundleThresholds>;
25
+ }
26
+
27
+ export function analyzeBundle(input: AnalyzeBundleInput): BundleReport {
28
+ const thresholds: BundleThresholds = {
29
+ ...DEFAULT_BUNDLE_THRESHOLDS,
30
+ ...(input.thresholds ?? {}),
31
+ };
32
+ const moduleIds = new Set(input.modules.map((m) => m.id));
33
+
34
+ const chunks: BundleChunk[] = input.stats.chunks.map((c) => ({
35
+ ...c,
36
+ modules: c.modules.map((m) => attachModuleId(m, moduleIds)),
37
+ }));
38
+
39
+ const moduleToChunks: Record<ModuleId, string[]> = {};
40
+ for (const c of chunks) {
41
+ for (const m of c.modules) {
42
+ if (!m.moduleId) continue;
43
+ const arr = moduleToChunks[m.moduleId] ?? [];
44
+ if (!arr.includes(c.id)) arr.push(c.id);
45
+ moduleToChunks[m.moduleId] = arr;
46
+ }
47
+ }
48
+
49
+ const totalSize = chunks.reduce((acc, c) => acc + c.size, 0);
50
+ const bloat = detectBloat(chunks, moduleToChunks, thresholds);
51
+
52
+ return {
53
+ format: input.stats.format,
54
+ totalSize,
55
+ chunks,
56
+ moduleToChunks,
57
+ bloat,
58
+ };
59
+ }
60
+
61
+ function attachModuleId(m: BundleChunkModule, ids: Set<ModuleId>): BundleChunkModule {
62
+ // Direct hit first (cheap path; covers most webpack/vite output).
63
+ if (ids.has(m.normalizedPath)) {
64
+ return { ...m, moduleId: m.normalizedPath };
65
+ }
66
+ // Some bundlers emit paths without the leading `src/` segment; try both
67
+ // directions before giving up.
68
+ const stripped = m.normalizedPath.replace(/^src\//u, '');
69
+ if (ids.has(stripped)) return { ...m, moduleId: stripped };
70
+ const prefixed = `src/${m.normalizedPath}`;
71
+ if (ids.has(prefixed)) return { ...m, moduleId: prefixed };
72
+ return m;
73
+ }
74
+
75
+ function detectBloat(
76
+ chunks: BundleChunk[],
77
+ moduleToChunks: Record<ModuleId, string[]>,
78
+ thresholds: BundleThresholds,
79
+ ): BundleBloat[] {
80
+ const out: BundleBloat[] = [];
81
+ let serial = 0;
82
+
83
+ // 1. duplicate modules across chunks
84
+ for (const [moduleId, chunkIds] of Object.entries(moduleToChunks)) {
85
+ if (chunkIds.length < thresholds.duplicateMinChunks) continue;
86
+ const totalSize = chunks
87
+ .filter((c) => chunkIds.includes(c.id))
88
+ .reduce((acc, c) => {
89
+ const m = c.modules.find((x) => x.moduleId === moduleId);
90
+ return acc + (m?.size ?? 0);
91
+ }, 0);
92
+ out.push({
93
+ id: `bundle:dup:${serial++}:${moduleId}`,
94
+ kind: 'duplicate',
95
+ severity: chunkIds.length >= 4 ? 'high' : chunkIds.length >= 3 ? 'medium' : 'low',
96
+ message: `Module "${displayShortId(moduleId)}" is duplicated across ${chunkIds.length} chunks`,
97
+ modules: [moduleId],
98
+ chunks: chunkIds,
99
+ detail: { sizeBytes: totalSize, chunkCount: chunkIds.length },
100
+ });
101
+ }
102
+
103
+ // 2. heavy chunks
104
+ for (const c of chunks) {
105
+ if (c.size < thresholds.heavyChunkBytes) continue;
106
+ const ratio = c.size / thresholds.heavyChunkBytes;
107
+ out.push({
108
+ id: `bundle:heavy:${serial++}:${c.id}`,
109
+ kind: 'heavy-chunk',
110
+ severity: ratio >= 4 ? 'high' : ratio >= 2 ? 'medium' : 'low',
111
+ message: `Chunk "${c.name}" is ${formatBytes(c.size)} (limit ${formatBytes(thresholds.heavyChunkBytes)})`,
112
+ modules: c.modules.flatMap((m) => (m.moduleId ? [m.moduleId] : [])),
113
+ chunks: [c.id],
114
+ detail: { sizeBytes: c.size },
115
+ });
116
+ }
117
+
118
+ // 3. solo-hot module: a single internal module dominates its chunk
119
+ for (const c of chunks) {
120
+ if (c.size === 0 || c.modules.length === 0) continue;
121
+ const internal = c.modules.filter((m) => m.moduleId);
122
+ if (internal.length === 0) continue;
123
+ const top = [...internal].sort((a, b) => b.size - a.size)[0]!;
124
+ const share = top.size / c.size;
125
+ if (share < thresholds.soloHotShare) continue;
126
+ // Only flag when chunk is meaningfully large; a 12 KB chunk dominated by
127
+ // a single 11 KB module is just a normal small chunk.
128
+ if (c.size < thresholds.heavyChunkBytes / 2) continue;
129
+ out.push({
130
+ id: `bundle:solo:${serial++}:${c.id}`,
131
+ kind: 'solo-hot',
132
+ severity: c.size >= thresholds.heavyChunkBytes ? 'high' : 'medium',
133
+ message: `Module "${displayShortId(top.moduleId!)}" takes ${Math.round(share * 100)}% of chunk "${c.name}"`,
134
+ modules: [top.moduleId!],
135
+ chunks: [c.id],
136
+ detail: { sizeBytes: top.size, sharePercent: Math.round(share * 100) },
137
+ });
138
+ }
139
+
140
+ return out;
141
+ }
142
+
143
+ function formatBytes(bytes: number): string {
144
+ if (bytes < 1024) return `${bytes} B`;
145
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
146
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
147
+ }
@@ -0,0 +1,12 @@
1
+ export { parseBundleStats, type ParseStatsOptions } from './parseStats';
2
+ export { analyzeBundle, type AnalyzeBundleInput } from './analyzeBundle';
3
+ export {
4
+ DEFAULT_BUNDLE_THRESHOLDS,
5
+ type BundleBloat,
6
+ type BundleBloatKind,
7
+ type BundleChunk,
8
+ type BundleChunkModule,
9
+ type BundleReport,
10
+ type BundleThresholds,
11
+ type ParsedBundleStats,
12
+ } from './types';
@@ -0,0 +1,152 @@
1
+ // Parse two bundler stats formats into a single normalized shape:
2
+ //
3
+ // 1. Webpack `stats.json` (or `compilation.toJson()` output): top-level
4
+ // `{ chunks: [{ id, names, files, size, modules: [{ name, size }] }] }`.
5
+ // 2. `rollup-plugin-visualizer` JSON template: `{ tree: { name, children } }`
6
+ // where the first level under `tree.children` is per-chunk and leaves
7
+ // have a `value` (gzip size) / `size` (raw size) field. Nested groups
8
+ // represent directories.
9
+ //
10
+ // Both are also produced by Vite (visualizer plugin) and Webpack respectively,
11
+ // so users on Vite/Rollup/Webpack pipelines can feed us any of them.
12
+ //
13
+ // Path normalization: we strip query strings, leading `./`, and resolve
14
+ // against the project root if the path is absolute. The result is a forward-
15
+ // slash relative path that matches `ModuleNode.id` produced by the analyzer.
16
+
17
+ import type { BundleChunk, BundleChunkModule, ParsedBundleStats } from './types';
18
+
19
+ export interface ParseStatsOptions {
20
+ /** Project root path; used to relativize absolute module paths. */
21
+ rootPath: string;
22
+ }
23
+
24
+ export function parseBundleStats(raw: unknown, options: ParseStatsOptions): ParsedBundleStats {
25
+ if (!raw || typeof raw !== 'object') {
26
+ return { format: 'unknown', chunks: [] };
27
+ }
28
+ const r = raw as Record<string, unknown>;
29
+ if (Array.isArray(r['chunks'])) {
30
+ return parseWebpack(r['chunks'] as unknown[], options);
31
+ }
32
+ if (r['tree'] && typeof r['tree'] === 'object') {
33
+ return parseRollupVisualizer(r['tree'] as Record<string, unknown>, options);
34
+ }
35
+ return { format: 'unknown', chunks: [] };
36
+ }
37
+
38
+ // --- webpack ---------------------------------------------------------------
39
+
40
+ function parseWebpack(rawChunks: unknown[], options: ParseStatsOptions): ParsedBundleStats {
41
+ const chunks: BundleChunk[] = [];
42
+ for (const c of rawChunks) {
43
+ if (!c || typeof c !== 'object') continue;
44
+ const cr = c as Record<string, unknown>;
45
+ const id = stringOrNumber(cr['id']);
46
+ if (id === null) continue;
47
+ const names = Array.isArray(cr['names'])
48
+ ? (cr['names'] as unknown[]).filter((x): x is string => typeof x === 'string')
49
+ : [];
50
+ const files = Array.isArray(cr['files'])
51
+ ? (cr['files'] as unknown[]).filter((x): x is string => typeof x === 'string')
52
+ : [];
53
+ const name = files[0] ?? names[0] ?? id;
54
+ const size = typeof cr['size'] === 'number' ? cr['size'] : 0;
55
+ const modules: BundleChunkModule[] = [];
56
+ if (Array.isArray(cr['modules'])) {
57
+ for (const m of cr['modules'] as unknown[]) {
58
+ if (!m || typeof m !== 'object') continue;
59
+ const mr = m as Record<string, unknown>;
60
+ const path = typeof mr['name'] === 'string' ? mr['name'] : null;
61
+ if (!path) continue;
62
+ const moduleSize = typeof mr['size'] === 'number' ? mr['size'] : 0;
63
+ modules.push(buildChunkModule(path, moduleSize, options));
64
+ }
65
+ }
66
+ chunks.push({ id, name, size, modules });
67
+ }
68
+ return { format: 'webpack', chunks };
69
+ }
70
+
71
+ // --- rollup-plugin-visualizer ---------------------------------------------
72
+
73
+ interface VisualizerNode {
74
+ name?: unknown;
75
+ children?: unknown;
76
+ value?: unknown;
77
+ size?: unknown;
78
+ }
79
+
80
+ function parseRollupVisualizer(
81
+ tree: Record<string, unknown>,
82
+ options: ParseStatsOptions,
83
+ ): ParsedBundleStats {
84
+ const top = (tree['children'] ?? []) as unknown[];
85
+ if (!Array.isArray(top)) return { format: 'rollup-visualizer', chunks: [] };
86
+ const chunks: BundleChunk[] = [];
87
+ for (const node of top) {
88
+ if (!node || typeof node !== 'object') continue;
89
+ const n = node as VisualizerNode;
90
+ const name = typeof n.name === 'string' ? n.name : '<chunk>';
91
+ const leaves: { path: string; size: number }[] = [];
92
+ collectVisualizerLeaves(n, '', leaves);
93
+ if (leaves.length === 0) continue;
94
+ const modules = leaves.map((l) => buildChunkModule(l.path, l.size, options));
95
+ const size = leaves.reduce((acc, l) => acc + l.size, 0);
96
+ chunks.push({ id: name, name, size, modules });
97
+ }
98
+ return { format: 'rollup-visualizer', chunks };
99
+ }
100
+
101
+ function collectVisualizerLeaves(
102
+ node: VisualizerNode,
103
+ prefix: string,
104
+ out: { path: string; size: number }[],
105
+ ): void {
106
+ const name = typeof node.name === 'string' ? node.name : '';
107
+ const here = prefix && name ? `${prefix}/${name}` : prefix || name;
108
+ if (Array.isArray(node.children) && node.children.length > 0) {
109
+ for (const child of node.children as unknown[]) {
110
+ if (child && typeof child === 'object')
111
+ collectVisualizerLeaves(child as VisualizerNode, here, out);
112
+ }
113
+ return;
114
+ }
115
+ // Leaf: prefer `size` (raw bytes), fall back to `value`.
116
+ const size =
117
+ typeof node.size === 'number' ? node.size : typeof node.value === 'number' ? node.value : 0;
118
+ if (here) out.push({ path: here, size });
119
+ }
120
+
121
+ // --- shared helpers --------------------------------------------------------
122
+
123
+ function buildChunkModule(
124
+ path: string,
125
+ size: number,
126
+ options: ParseStatsOptions,
127
+ ): BundleChunkModule {
128
+ const normalizedPath = normalizePath(path, options.rootPath);
129
+ return { rawPath: path, normalizedPath, size };
130
+ }
131
+
132
+ function normalizePath(input: string, rootPath: string): string {
133
+ // Strip query strings and webpack-style loader prefixes (e.g. `!!`).
134
+ let p = input.replace(/\?.*$/u, '');
135
+ const bang = p.lastIndexOf('!');
136
+ if (bang !== -1) p = p.slice(bang + 1);
137
+ p = p.replace(/\\/gu, '/');
138
+ if (p.startsWith('./')) p = p.slice(2);
139
+
140
+ const rootNorm = rootPath.replace(/\\/gu, '/').replace(/\/+$/u, '');
141
+ if (rootNorm && p.startsWith(`${rootNorm}/`)) p = p.slice(rootNorm.length + 1);
142
+
143
+ // Drop pnpm-style virtual prefixes that visualizer sometimes emits.
144
+ p = p.replace(/^\/+/u, '');
145
+ return p;
146
+ }
147
+
148
+ function stringOrNumber(v: unknown): string | null {
149
+ if (typeof v === 'string') return v;
150
+ if (typeof v === 'number') return String(v);
151
+ return null;
152
+ }
@@ -0,0 +1,85 @@
1
+ // Bundle-aware analysis.
2
+ //
3
+ // `BundleReport` is computed when the user supplies a parsed bundler stats
4
+ // file (`AnalyzeOptions.bundleStats`). We never read disk here - the CLI is
5
+ // responsible for loading the file and handing us a parsed object.
6
+
7
+ import type { ModuleId } from '../types';
8
+
9
+ /** Normalized chunk after parsing whatever stats format we got. */
10
+ export interface BundleChunk {
11
+ /** Stable identifier from the source stats (chunk id, output filename, etc.). */
12
+ id: string;
13
+ /** Display name (output filename when available). */
14
+ name: string;
15
+ /** Total chunk size in bytes. */
16
+ size: number;
17
+ /** Modules attached to this chunk, after path normalization. */
18
+ modules: BundleChunkModule[];
19
+ }
20
+
21
+ export interface BundleChunkModule {
22
+ /** Module id matching `ModuleNode.id` when resolution succeeds. */
23
+ moduleId?: ModuleId;
24
+ /** Path as it appears in the stats file (relative or absolute). */
25
+ rawPath: string;
26
+ /** Path normalized relative to project root, forward slashes. */
27
+ normalizedPath: string;
28
+ /** Module size inside this chunk (bytes). */
29
+ size: number;
30
+ }
31
+
32
+ export type BundleBloatKind = 'duplicate' | 'heavy-chunk' | 'solo-hot';
33
+
34
+ export interface BundleBloat {
35
+ /** Stable id for grouping/diffing. */
36
+ id: string;
37
+ kind: BundleBloatKind;
38
+ severity: 'high' | 'medium' | 'low';
39
+ /** One-line headline (rendered verbatim by CLI). */
40
+ message: string;
41
+ /** Module(s) implicated. */
42
+ modules: ModuleId[];
43
+ /** Chunk(s) involved (chunk.id values). */
44
+ chunks: string[];
45
+ /** Optional metric details for tooltips/CI output. */
46
+ detail?: {
47
+ sizeBytes?: number;
48
+ chunkCount?: number;
49
+ sharePercent?: number;
50
+ };
51
+ }
52
+
53
+ export interface BundleReport {
54
+ /** Source format we recognized. */
55
+ format: 'webpack' | 'rollup-visualizer' | 'unknown';
56
+ /** Total uncompressed size summed across chunks. */
57
+ totalSize: number;
58
+ chunks: BundleChunk[];
59
+ /** Map module id -> list of chunk ids it landed in. */
60
+ moduleToChunks: Record<ModuleId, string[]>;
61
+ /** Detected bundle-bloat issues (drives recommendations + UI). */
62
+ bloat: BundleBloat[];
63
+ }
64
+
65
+ /** User-tunable thresholds. Wired through `.archora.json -> bundle`. */
66
+ export interface BundleThresholds {
67
+ /** Chunk size in bytes that triggers `heavy-chunk`. */
68
+ heavyChunkBytes: number;
69
+ /** Module appearing in >= N chunks triggers `duplicate`. */
70
+ duplicateMinChunks: number;
71
+ /** Module taking >= share of a heavy chunk triggers `solo-hot` (0..1). */
72
+ soloHotShare: number;
73
+ }
74
+
75
+ export const DEFAULT_BUNDLE_THRESHOLDS: BundleThresholds = {
76
+ heavyChunkBytes: 500_000,
77
+ duplicateMinChunks: 2,
78
+ soloHotShare: 0.8,
79
+ };
80
+
81
+ /** Parsed-but-not-yet-analyzed stats. Produced by the format parsers. */
82
+ export interface ParsedBundleStats {
83
+ format: BundleReport['format'];
84
+ chunks: BundleChunk[];
85
+ }
@@ -0,0 +1,54 @@
1
+ import type { ModuleKind, ParsedFile } from './types';
2
+
3
+ // `.config.[ext]` is the catch-all that covers rollup/webpack/astro/svelte/
4
+ // playwright/cypress/jest/babel/tsup/etc. without listing each one. The
5
+ // explicit entries above it stay because they match flat names (`vite.config`)
6
+ // or non-`.config` conventions (`.eslintrc.cjs`).
7
+ const INFRA_PATTERNS = [
8
+ /\.d\.ts$/u,
9
+ /(^|\/)vite\.config\.[cm]?[jt]sx?$/u,
10
+ /(^|\/)vitest\.config\.[cm]?[jt]sx?$/u,
11
+ /(^|\/)eslint\.[a-z]+\.[cm]?[jt]sx?$/u,
12
+ /(^|\/)\.eslintrc\.[a-z]+$/u,
13
+ /(^|\/)postcss\.config\.[cm]?[jt]sx?$/u,
14
+ /(^|\/)tailwind\.config\.[cm]?[jt]sx?$/u,
15
+ /\.config\.[cm]?[jt]sx?$/u,
16
+ ];
17
+
18
+ export function isInfra(relPath: string): boolean {
19
+ return INFRA_PATTERNS.some((p) => p.test(relPath));
20
+ }
21
+
22
+ // runtime-integration files (loaders/plugins/registries/...). kept in graph
23
+ // but skipped by recommendations (they look like fanIn=0 leaves otherwise).
24
+ const INTEGRATION_NAME_RE =
25
+ /(^|\/)([A-Za-z0-9]+)?(?:Loader|Plugin|Registry|Provider|Bootstrap)(?:\.[a-z]+)?(?:\.[jt]sx?|\.vue|\.svelte)$/u;
26
+
27
+ export function classifyKind(parsed: ParsedFile, relPath: string): ModuleKind {
28
+ if (/(^|\/)(__tests__|tests?|spec|e2e)(\/|\.)/u.test(relPath)) return 'test';
29
+ if (/\.(test|spec)\.[cm]?[jt]sx?$/u.test(relPath)) return 'test';
30
+ if (/\.config\.[cm]?[jt]sx?$/u.test(relPath) || /(^|\/)config(\/|\.)/u.test(relPath)) {
31
+ return 'config';
32
+ }
33
+ if (/(^|\/)(styles?|theme|tokens)\//u.test(relPath) || /\.(css|scss|sass)$/u.test(relPath)) {
34
+ return 'style';
35
+ }
36
+ if (INTEGRATION_NAME_RE.test(relPath)) return 'integration';
37
+ if (parsed.language === 'vue' || parsed.language === 'svelte') return 'component';
38
+ if (parsed.hasDefineStore) return 'store';
39
+ if (/(^|\/)(api|openapi|graphql)(\/|\.)/u.test(relPath)) return 'api';
40
+ if (/(^|\/)(services?|clients?|repositories)(\/|\.)/u.test(relPath)) return 'service';
41
+ if (/(^|\/)(model|state)(\/|\.)/u.test(relPath)) return 'model';
42
+ if (/(^|\/)(schemas?|dto|types)(\/|\.)/u.test(relPath)) return 'schema';
43
+ if (
44
+ /(^|\/)composables\//u.test(relPath) ||
45
+ /(^|\/)use[A-Z][A-Za-z0-9]*\.[jt]sx?$/u.test(relPath)
46
+ ) {
47
+ return 'composable';
48
+ }
49
+ if (parsed.exports.some((n) => /^use[A-Z]/u.test(n))) return 'composable';
50
+ if (/(^|\/)router(\/|\.)/u.test(relPath)) return 'route';
51
+ if (/(^|\/)(utils|lib|helpers)(\/|\.)/u.test(relPath)) return 'util';
52
+ if (/(^|\/)(main|index|entry)\.[jt]sx?$/u.test(relPath)) return 'entry';
53
+ return 'module';
54
+ }