@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,683 @@
1
+ import type { FileSource } from './fileSource';
2
+ import type {
3
+ AnalyzerWarning,
4
+ DependencyEdge,
5
+ FactConfidence,
6
+ ImportResolutionKind,
7
+ ModuleNode,
8
+ ParsedFile,
9
+ ParsedFileSummary,
10
+ RawImport,
11
+ ScanProgressCallback,
12
+ } from './types';
13
+ import { createParserRegistry, isParseFailure } from './parsers';
14
+ import type { Framework } from './detect';
15
+ import { applyAlias, type PathAlias, type Resolver } from './resolve';
16
+ import { classifyKind, isInfra } from './classify';
17
+ import { classifyModuleRuntime } from './rsc';
18
+ import type { ArchoraConfig } from '../config/frontScopeConfig';
19
+
20
+ export interface BuildGraphInput {
21
+ source: FileSource;
22
+ files: string[];
23
+ resolver: Resolver;
24
+ aliases?: PathAlias[];
25
+ config?: ArchoraConfig;
26
+ framework?: Framework;
27
+ onProgress?: ScanProgressCallback;
28
+ }
29
+
30
+ export interface BuildGraphResult {
31
+ modules: ModuleNode[];
32
+ edges: DependencyEdge[];
33
+ parserFacts: ParsedFileSummary[];
34
+ warnings: AnalyzerWarning[];
35
+ }
36
+
37
+ const CHUNK_SIZE = 100;
38
+ const MAX_FILE_BYTES = 1_000_000;
39
+ const yieldToEventLoop = (): Promise<void> =>
40
+ new Promise((resolve) => {
41
+ setTimeout(resolve, 0);
42
+ });
43
+
44
+ export async function buildGraph(input: BuildGraphInput): Promise<BuildGraphResult> {
45
+ const { source, files, resolver, aliases = [], config, framework, onProgress } = input;
46
+ const registry = createParserRegistry({
47
+ ...(config?.dynamicLoaders ? { dynamicLoaders: config.dynamicLoaders } : {}),
48
+ ...(framework ? { framework } : {}),
49
+ });
50
+
51
+ const parsed: ParsedFile[] = [];
52
+ const warnings: AnalyzerWarning[] = [];
53
+
54
+ onProgress?.({ phase: 'parse', current: 0, total: files.length });
55
+ for (let i = 0; i < files.length; i += CHUNK_SIZE) {
56
+ const chunk = files.slice(i, i + CHUNK_SIZE);
57
+ const results = await Promise.all(
58
+ chunk.map(async (relPath): Promise<ParsedFile | null> => {
59
+ try {
60
+ const content = await source.read(relPath);
61
+ if (content.length > MAX_FILE_BYTES) {
62
+ warnings.push({
63
+ code: 'parse-failed',
64
+ message: `File exceeds ${MAX_FILE_BYTES} bytes, skipped`,
65
+ file: relPath,
66
+ });
67
+ return null;
68
+ }
69
+ const result = registry.parse({ relPath, content });
70
+ if (isParseFailure(result)) {
71
+ warnings.push({
72
+ code: 'unsupported-extension',
73
+ message: 'Unsupported extension',
74
+ file: relPath,
75
+ });
76
+ return null;
77
+ }
78
+ return result;
79
+ } catch (err) {
80
+ warnings.push({
81
+ code: 'parse-failed',
82
+ message: 'Parse failed',
83
+ file: relPath,
84
+ detail: err instanceof Error ? err.message : String(err),
85
+ });
86
+ return null;
87
+ }
88
+ }),
89
+ );
90
+ for (const r of results) if (r) parsed.push(r);
91
+ onProgress?.({
92
+ phase: 'parse',
93
+ current: Math.min(i + CHUNK_SIZE, files.length),
94
+ total: files.length,
95
+ });
96
+ await yieldToEventLoop();
97
+ }
98
+
99
+ const modules: ModuleNode[] = parsed.map((p) => ({
100
+ id: p.relPath,
101
+ absPath: p.relPath,
102
+ kind: classifyKind(p, p.relPath),
103
+ language: p.language,
104
+ loc: p.loc,
105
+ exports: p.exports,
106
+ isInfra: isInfra(p.relPath),
107
+ runtime: classifyModuleRuntime({
108
+ relPath: p.relPath,
109
+ framework: input.framework ?? 'unknown',
110
+ ...(p.directives ? { directives: p.directives } : {}),
111
+ }),
112
+ }));
113
+ const parserFacts = parsed.map((p) => toParsedFileSummary(p, input.framework ?? 'unknown'));
114
+
115
+ const moduleIds = new Set(modules.map((m) => m.id));
116
+ const edges: DependencyEdge[] = [];
117
+
118
+ const filesByDir = indexFilesByDir(files);
119
+
120
+ for (const file of parsed) {
121
+ const negativeGlobPatterns = file.imports
122
+ .filter((imp) => imp.pattern === 'glob' && imp.specifier.startsWith('!'))
123
+ .map((imp) => imp.specifier.slice(1));
124
+ for (const imp of file.imports) {
125
+ if (isAssetSpecifier(imp.specifier)) continue;
126
+ // glob/prefix specifiers may start with a bare name ('src/**') -
127
+ // skip the external check for them
128
+ if (
129
+ imp.pattern !== 'glob' &&
130
+ imp.pattern !== 'prefix' &&
131
+ isExternal(imp.specifier) &&
132
+ resolver.hasLocalCandidate?.(imp.specifier) !== true
133
+ ) {
134
+ continue;
135
+ }
136
+
137
+ if (imp.pattern === 'glob') {
138
+ if (imp.specifier.startsWith('!')) continue;
139
+ if (isAssetGlobSpecifier(imp.specifier)) continue;
140
+ const fromDir = posixDirnameLocal(file.relPath);
141
+ const matched = expandGlob(
142
+ imp.specifier,
143
+ fromDir,
144
+ files,
145
+ moduleIds,
146
+ negativeGlobPatterns,
147
+ aliases,
148
+ );
149
+ if (matched.length === 0) {
150
+ warnings.push({
151
+ code: 'resolve-failed',
152
+ message: `import.meta.glob("${imp.specifier}") matched no files`,
153
+ file: file.relPath,
154
+ });
155
+ continue;
156
+ }
157
+ for (const target of matched) {
158
+ if (target === file.relPath) continue;
159
+ edges.push({
160
+ from: file.relPath,
161
+ to: target,
162
+ kind: imp.kind,
163
+ specifier: imp.specifier,
164
+ resolved: true,
165
+ ...edgeMeta(imp, 'glob', 'low', true),
166
+ });
167
+ }
168
+ continue;
169
+ }
170
+
171
+ if (imp.pattern === 'prefix') {
172
+ // template-literal dynamic import like `import('./mfes/' + x)`:
173
+ // connect to every file under the static prefix dir
174
+ const baseDir = resolvePrefixDir(imp.specifier, file.relPath);
175
+ if (baseDir === null) {
176
+ warnings.push({
177
+ code: 'resolve-failed',
178
+ message: `Cannot resolve dynamic prefix "${imp.specifier}"`,
179
+ file: file.relPath,
180
+ });
181
+ continue;
182
+ }
183
+ const matched = expandPrefix(baseDir, filesByDir, moduleIds);
184
+ if (matched.length === 0) {
185
+ warnings.push({
186
+ code: 'resolve-failed',
187
+ message: `Dynamic prefix "${imp.specifier}" matched no files`,
188
+ file: file.relPath,
189
+ });
190
+ continue;
191
+ }
192
+ for (const target of matched) {
193
+ if (target === file.relPath) continue;
194
+ edges.push({
195
+ from: file.relPath,
196
+ to: target,
197
+ kind: imp.kind,
198
+ specifier: imp.specifier,
199
+ resolved: true,
200
+ ...edgeMeta(imp, 'prefix', 'medium', true),
201
+ });
202
+ }
203
+ continue;
204
+ }
205
+
206
+ const resolved = await resolver.resolve(imp.specifier, file.relPath);
207
+ if (resolved && moduleIds.has(resolved)) {
208
+ if (resolved === file.relPath) {
209
+ warnings.push({
210
+ code: 'self-import',
211
+ message: 'Module imports itself',
212
+ file: file.relPath,
213
+ });
214
+ }
215
+ edges.push({
216
+ from: file.relPath,
217
+ to: resolved,
218
+ kind: imp.kind,
219
+ specifier: imp.specifier,
220
+ resolved: true,
221
+ ...edgeMeta(imp, imp.kind === 'dynamic' ? 'dynamic-literal' : 'literal', 'high', false),
222
+ });
223
+ } else {
224
+ warnings.push({
225
+ code: 'resolve-failed',
226
+ message: `Cannot resolve "${imp.specifier}"`,
227
+ file: file.relPath,
228
+ });
229
+ }
230
+ }
231
+ }
232
+
233
+ // unplugin-vue-components: PascalCase template tags -> src/components/**/*.vue
234
+ // (vite) or components/**/*.vue (nuxt). runs after static imports for dedupe
235
+ const componentRegistry = buildComponentRegistry(files, framework);
236
+ const existingEdges = new Set(edges.map((e) => `${e.from}\u0001${e.to}`));
237
+ for (const file of parsed) {
238
+ if (!file.templateRefs || file.templateRefs.length === 0) continue;
239
+ for (const name of file.templateRefs) {
240
+ const target = componentRegistry.get(name);
241
+ if (!target || target === file.relPath) continue;
242
+ const key = `${file.relPath}\u0001${target}`;
243
+ if (existingEdges.has(key)) continue;
244
+ existingEdges.add(key);
245
+ edges.push({
246
+ from: file.relPath,
247
+ to: target,
248
+ kind: 'auto-import',
249
+ specifier: name,
250
+ resolved: true,
251
+ confidence: 'medium',
252
+ resolutionKind: 'framework-auto',
253
+ approximate: true,
254
+ });
255
+ }
256
+ }
257
+
258
+ return { modules, edges, parserFacts, warnings };
259
+ }
260
+
261
+ function edgeMeta(
262
+ imp: RawImport,
263
+ resolutionKind: ImportResolutionKind,
264
+ confidence: FactConfidence,
265
+ approximate: boolean,
266
+ ): Pick<DependencyEdge, 'confidence' | 'resolutionKind' | 'approximate'> {
267
+ return {
268
+ confidence: imp.confidence ?? confidence,
269
+ resolutionKind: imp.resolutionKind ?? resolutionKind,
270
+ approximate: imp.approximate ?? approximate,
271
+ };
272
+ }
273
+
274
+ function toParsedFileSummary(file: ParsedFile, framework: string): ParsedFileSummary {
275
+ const imports = file.imports.map((imp) => {
276
+ const asset = isAssetSpecifier(imp.specifier);
277
+ const patternKind =
278
+ imp.pattern === 'glob'
279
+ ? 'glob'
280
+ : imp.pattern === 'prefix'
281
+ ? 'prefix'
282
+ : asset
283
+ ? 'asset'
284
+ : imp.kind === 'dynamic'
285
+ ? 'dynamic-literal'
286
+ : 'literal';
287
+ return {
288
+ specifier: imp.specifier,
289
+ kind: imp.kind,
290
+ resolutionKind: imp.resolutionKind ?? patternKind,
291
+ confidence:
292
+ imp.confidence ??
293
+ (imp.pattern === 'glob' ? 'low' : imp.pattern === 'prefix' ? 'medium' : 'high'),
294
+ approximate: imp.approximate ?? (imp.pattern === 'glob' || imp.pattern === 'prefix'),
295
+ ...(asset ? { isAsset: true } : {}),
296
+ ...(imp.pattern === 'glob' ? { globEager: imp.globEager ?? false } : {}),
297
+ ...(imp.globImport ? { globImport: imp.globImport } : {}),
298
+ ...(imp.pattern === 'glob' && imp.specifier.startsWith('!') ? { negative: true } : {}),
299
+ };
300
+ });
301
+ const assetFacts = file.imports
302
+ .filter((imp) => isAssetSpecifier(imp.specifier))
303
+ .map((imp) => ({
304
+ specifier: imp.specifier,
305
+ assetKind: assetKindOf(imp.specifier),
306
+ confidence: 'high' as const,
307
+ }));
308
+ const runtime = file.directives?.includes('use server')
309
+ ? 'server'
310
+ : file.directives?.includes('use client')
311
+ ? 'client'
312
+ : 'shared';
313
+ const runtimeSource =
314
+ file.directives && file.directives.length > 0
315
+ ? 'directive'
316
+ : /(^|\/)(server|app\/api)\//u.test(file.relPath) || /\+server\.[jt]s$/u.test(file.relPath)
317
+ ? 'framework-convention'
318
+ : 'default';
319
+ return {
320
+ relPath: file.relPath,
321
+ language: file.language,
322
+ loc: file.loc,
323
+ imports,
324
+ exports: file.exports.map((name) => ({ name, kind: exportKindOf(name), confidence: 'medium' })),
325
+ runtimeFacts: [
326
+ {
327
+ runtime,
328
+ source: runtimeSource,
329
+ confidence: runtimeSource === 'default' ? 'low' : 'high',
330
+ },
331
+ ],
332
+ frameworkFacts: [
333
+ ...(file.templateRefs ?? []).map((value) => ({
334
+ kind: 'vue-template-ref' as const,
335
+ value,
336
+ confidence: 'medium' as const,
337
+ })),
338
+ ...frameworkFactsFromPath(file.relPath, framework),
339
+ ],
340
+ routeFacts: routeFactsFromPath(file.relPath, framework),
341
+ stateFacts: file.hasDefineStore
342
+ ? [{ kind: 'pinia-store' as const, confidence: 'high' as const }]
343
+ : [],
344
+ assetFacts,
345
+ limitations: limitationsFor(file),
346
+ };
347
+ }
348
+
349
+ // vite default: src/components/**, nuxt: components/**. first match wins on basename collision.
350
+ function buildComponentRegistry(
351
+ files: string[],
352
+ framework: Framework | undefined,
353
+ ): Map<string, string> {
354
+ const registry = new Map<string, string>();
355
+ const re =
356
+ framework === 'nuxt' ? /^components\/|(^|\/)src\/components\//u : /(^|\/)src\/components\//u;
357
+ for (const f of files) {
358
+ if (!re.test(f)) continue;
359
+ if (!f.toLowerCase().endsWith('.vue')) continue;
360
+ const slash = f.lastIndexOf('/');
361
+ const base = (slash === -1 ? f : f.slice(slash + 1)).slice(0, -'.vue'.length);
362
+ if (base.length === 0) continue;
363
+ if (!registry.has(base)) registry.set(base, f);
364
+ }
365
+ return registry;
366
+ }
367
+
368
+ function indexFilesByDir(files: string[]): Map<string, string[]> {
369
+ const map = new Map<string, string[]>();
370
+ for (const f of files) {
371
+ const dir = posixDirname(f);
372
+ let bucket = map.get(dir);
373
+ if (!bucket) {
374
+ bucket = [];
375
+ map.set(dir, bucket);
376
+ }
377
+ bucket.push(f);
378
+ }
379
+ return map;
380
+ }
381
+
382
+ // returns null if the prefix would expand to project root or outside the tree
383
+ function resolvePrefixDir(prefix: string, fromRel: string): string | null {
384
+ if (prefix.startsWith('/')) return null;
385
+ const trailingSlash = prefix.endsWith('/');
386
+ const fromDir = posixDirname(fromRel);
387
+ const isRelative = prefix.startsWith('./') || prefix.startsWith('../');
388
+ const joined = isRelative ? posixJoin(fromDir, prefix) : prefix;
389
+ const base = trailingSlash
390
+ ? joined
391
+ : (() => {
392
+ const idx = joined.lastIndexOf('/');
393
+ return idx === -1 ? '' : joined.slice(0, idx);
394
+ })();
395
+ if (base === '' || !base.includes('/')) return null;
396
+ return base;
397
+ }
398
+
399
+ const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.vue', '.svelte']);
400
+
401
+ function globToRegex(glob: string): RegExp {
402
+ let re = '';
403
+ for (let i = 0; i < glob.length; i++) {
404
+ const c = glob[i]!;
405
+ if (c === '*') {
406
+ const next = glob[i + 1];
407
+ if (next === '*') {
408
+ if (glob[i + 2] === '/') {
409
+ re += '(?:.*/)?';
410
+ i += 2;
411
+ } else {
412
+ re += '.*';
413
+ i += 1;
414
+ }
415
+ } else {
416
+ re += '[^/]*';
417
+ }
418
+ } else if (c === '?') {
419
+ re += '[^/]';
420
+ } else if (/[.+^$(){}|\\[\]]/u.test(c)) {
421
+ re += `\\${c}`;
422
+ } else {
423
+ re += c;
424
+ }
425
+ }
426
+ return new RegExp(`^${re}$`);
427
+ }
428
+
429
+ function expandGlob(
430
+ pattern: string,
431
+ fromDir: string,
432
+ files: string[],
433
+ moduleIds: Set<string>,
434
+ negativePatterns: readonly string[] = [],
435
+ aliases: readonly PathAlias[] = [],
436
+ ): string[] {
437
+ const patterns = expandAliasedGlobPattern(pattern, fromDir, aliases);
438
+ const res = patterns.map(globToRegex);
439
+ const negativeRes = negativePatterns
440
+ .flatMap((p) => expandAliasedGlobPattern(p, fromDir, aliases))
441
+ .map(globToRegex);
442
+ const out: string[] = [];
443
+ for (const f of files) {
444
+ if (!moduleIds.has(f)) continue;
445
+ if (res.some((re) => re.test(f))) out.push(f);
446
+ }
447
+ return negativeRes.length === 0 ? out : out.filter((f) => !negativeRes.some((re) => re.test(f)));
448
+ }
449
+
450
+ function expandAliasedGlobPattern(
451
+ pattern: string,
452
+ fromDir: string,
453
+ aliases: readonly PathAlias[],
454
+ ): string[] {
455
+ const anchored = anchorGlobPattern(pattern, fromDir);
456
+ const aliased = applyAlias(anchored, [...aliases]);
457
+ return aliased.length > 0 ? aliased : [anchored];
458
+ }
459
+
460
+ function anchorGlobPattern(pattern: string, fromDir: string): string {
461
+ // vite globs are relative to the file -- anchor to project root
462
+ if (pattern.startsWith('/')) return pattern.slice(1);
463
+ if (pattern.startsWith('./') || pattern.startsWith('../')) return posixJoin(fromDir, pattern);
464
+ return pattern;
465
+ }
466
+
467
+ function posixDirnameLocal(p: string): string {
468
+ return posixDirname(p);
469
+ }
470
+
471
+ function expandPrefix(
472
+ baseDir: string,
473
+ filesByDir: Map<string, string[]>,
474
+ moduleIds: Set<string>,
475
+ ): string[] {
476
+ const out: string[] = [];
477
+ const prefix = baseDir === '' ? '' : `${baseDir}/`;
478
+ for (const [dir, bucket] of filesByDir) {
479
+ if (dir !== baseDir && !dir.startsWith(prefix)) continue;
480
+ for (const f of bucket) {
481
+ const ext = f.slice(f.lastIndexOf('.')).toLowerCase();
482
+ if (!SOURCE_EXTS.has(ext)) continue;
483
+ if (!moduleIds.has(f)) continue;
484
+ out.push(f);
485
+ }
486
+ }
487
+ return out;
488
+ }
489
+
490
+ function posixDirname(p: string): string {
491
+ const i = p.lastIndexOf('/');
492
+ return i === -1 ? '' : p.slice(0, i);
493
+ }
494
+
495
+ function posixJoin(a: string, b: string): string {
496
+ const segments = `${a}/${b}`.split('/');
497
+ const stack: string[] = [];
498
+ for (const s of segments) {
499
+ if (s === '' || s === '.') continue;
500
+ if (s === '..') {
501
+ if (stack.length > 0 && stack[stack.length - 1] !== '..') stack.pop();
502
+ else stack.push('..');
503
+ continue;
504
+ }
505
+ stack.push(s);
506
+ }
507
+ return stack.join('/');
508
+ }
509
+
510
+ function isExternal(spec: string): boolean {
511
+ if (spec.startsWith('.') || spec.startsWith('/')) return false;
512
+ if (spec.startsWith('@/') || spec.startsWith('~/')) return false;
513
+ return /^[a-z@]/u.test(spec) && !spec.includes('://');
514
+ }
515
+
516
+ const ASSET_EXTS = new Set([
517
+ '.css',
518
+ '.scss',
519
+ '.sass',
520
+ '.less',
521
+ '.module.css',
522
+ '.module.scss',
523
+ '.json',
524
+ '.html',
525
+ '.svg',
526
+ '.png',
527
+ '.jpg',
528
+ '.jpeg',
529
+ '.gif',
530
+ '.webp',
531
+ '.avif',
532
+ '.ico',
533
+ '.woff',
534
+ '.woff2',
535
+ '.ttf',
536
+ '.otf',
537
+ ]);
538
+
539
+ function isAssetGlobSpecifier(specifier: string): boolean {
540
+ return ASSET_EXTS.has(extensionFromPattern(specifier));
541
+ }
542
+
543
+ function extensionFromPattern(specifier: string): string {
544
+ const clean = (specifier.split('?')[0] ?? specifier).toLowerCase();
545
+ const match = clean.match(/\.([a-z0-9]+)(?:$|[!*,{}[\]/])/u);
546
+ return match ? `.${match[1]}` : '';
547
+ }
548
+
549
+ function isAssetSpecifier(specifier: string): boolean {
550
+ const clean = specifier.split('?')[0] ?? specifier;
551
+ const lower = clean.toLowerCase();
552
+ for (const ext of ASSET_EXTS) {
553
+ if (lower.endsWith(ext)) return true;
554
+ }
555
+ return false;
556
+ }
557
+
558
+ function assetKindOf(specifier: string): 'style' | 'json' | 'image' | 'font' | 'other' {
559
+ const clean = (specifier.split('?')[0] ?? specifier).toLowerCase();
560
+ if (/\.(css|scss|sass|less)$/u.test(clean)) return 'style';
561
+ if (clean.endsWith('.json')) return 'json';
562
+ if (/\.(svg|png|jpg|jpeg|gif|webp|avif|ico)$/u.test(clean)) return 'image';
563
+ if (/\.(woff2?|ttf|otf)$/u.test(clean)) return 'font';
564
+ return 'other';
565
+ }
566
+
567
+ function exportKindOf(name: string): 'value' | 'type' | 'default' | 'namespace' | 'named' {
568
+ if (name === 'default') return 'default';
569
+ if (name === '*') return 'namespace';
570
+ return 'named';
571
+ }
572
+
573
+ function frameworkFactsFromPath(
574
+ relPath: string,
575
+ framework: string,
576
+ ): {
577
+ kind:
578
+ | 'react-lazy'
579
+ | 'next-dynamic'
580
+ | 'nuxt-auto-component'
581
+ | 'nuxt-auto-composable'
582
+ | 'sveltekit-route';
583
+ value: string;
584
+ confidence: FactConfidence;
585
+ }[] {
586
+ const facts: {
587
+ kind:
588
+ | 'react-lazy'
589
+ | 'next-dynamic'
590
+ | 'nuxt-auto-component'
591
+ | 'nuxt-auto-composable'
592
+ | 'sveltekit-route';
593
+ value: string;
594
+ confidence: FactConfidence;
595
+ }[] = [];
596
+ if (
597
+ framework === 'svelte' &&
598
+ /(^|\/)\+(page|layout|server)\.svelte$|(^|\/)\+server\.[jt]s$/u.test(relPath)
599
+ ) {
600
+ facts.push({ kind: 'sveltekit-route', value: relPath, confidence: 'medium' });
601
+ }
602
+ if (framework === 'nuxt' && /(^|\/)components\/.+\.vue$/u.test(relPath)) {
603
+ facts.push({ kind: 'nuxt-auto-component', value: relPath, confidence: 'medium' });
604
+ }
605
+ if (framework === 'nuxt' && /(^|\/)composables\/.+\.[cm]?[jt]s$/u.test(relPath)) {
606
+ facts.push({ kind: 'nuxt-auto-composable', value: relPath, confidence: 'medium' });
607
+ }
608
+ return facts;
609
+ }
610
+
611
+ function routeFactsFromPath(
612
+ relPath: string,
613
+ framework: string,
614
+ ): {
615
+ routeKind: 'page' | 'layout' | 'api' | 'middleware' | 'server-route';
616
+ confidence: FactConfidence;
617
+ }[] {
618
+ const facts: {
619
+ routeKind: 'page' | 'layout' | 'api' | 'middleware' | 'server-route';
620
+ confidence: FactConfidence;
621
+ }[] = [];
622
+ if (framework === 'next') {
623
+ if (/(^|\/)app(?:\/.*)?\/page\.[jt]sx?$/u.test(relPath))
624
+ facts.push({ routeKind: 'page', confidence: 'medium' });
625
+ if (/(^|\/)app(?:\/.*)?\/layout\.[jt]sx?$/u.test(relPath))
626
+ facts.push({ routeKind: 'layout', confidence: 'medium' });
627
+ if (/(^|\/)app\/api\/.*\/route\.[jt]s$/u.test(relPath))
628
+ facts.push({ routeKind: 'api', confidence: 'medium' });
629
+ if (/(^|\/)pages\/api\/.*\.[jt]sx?$/u.test(relPath))
630
+ facts.push({ routeKind: 'api', confidence: 'medium' });
631
+ if (/(^|\/)pages\/(?!api\/).*\.[jt]sx?$/u.test(relPath))
632
+ facts.push({ routeKind: 'page', confidence: 'medium' });
633
+ if (/(^|\/)middleware\.[jt]s$/u.test(relPath))
634
+ facts.push({ routeKind: 'middleware', confidence: 'medium' });
635
+ }
636
+ if (framework === 'nuxt') {
637
+ if (/(^|\/)pages\/.*\.vue$/u.test(relPath))
638
+ facts.push({ routeKind: 'page', confidence: 'medium' });
639
+ if (/(^|\/)layouts\/.*\.vue$/u.test(relPath))
640
+ facts.push({ routeKind: 'layout', confidence: 'medium' });
641
+ if (/(^|\/)server\/api\/.*\.[cm]?[jt]s$/u.test(relPath))
642
+ facts.push({ routeKind: 'api', confidence: 'medium' });
643
+ if (/(^|\/)middleware\/.*\.[cm]?[jt]s$/u.test(relPath))
644
+ facts.push({ routeKind: 'middleware', confidence: 'medium' });
645
+ }
646
+ if (framework === 'svelte') {
647
+ if (/(^|\/)\+page\.svelte$/u.test(relPath))
648
+ facts.push({ routeKind: 'page', confidence: 'medium' });
649
+ if (/(^|\/)\+page\.[cm]?[jt]s$/u.test(relPath))
650
+ facts.push({ routeKind: 'page', confidence: 'medium' });
651
+ if (/(^|\/)\+layout\.svelte$/u.test(relPath))
652
+ facts.push({ routeKind: 'layout', confidence: 'medium' });
653
+ if (/(^|\/)\+layout\.[cm]?[jt]s$/u.test(relPath))
654
+ facts.push({ routeKind: 'layout', confidence: 'medium' });
655
+ if (/(^|\/)\+page\.server\.[cm]?[jt]s$/u.test(relPath))
656
+ facts.push({ routeKind: 'server-route', confidence: 'medium' });
657
+ if (/(^|\/)\+server\.[jt]s$/u.test(relPath))
658
+ facts.push({ routeKind: 'server-route', confidence: 'medium' });
659
+ }
660
+ if (framework === 'react') {
661
+ if (/(^|\/)src\/routes\/__root\.[jt]sx?$/u.test(relPath))
662
+ facts.push({ routeKind: 'layout', confidence: 'low' });
663
+ if (/(^|\/)src\/routes\/(?!__root\.)[^/]+\.[jt]sx?$/u.test(relPath))
664
+ facts.push({ routeKind: 'page', confidence: 'low' });
665
+ }
666
+ return facts;
667
+ }
668
+
669
+ function limitationsFor(file: ParsedFile): string[] {
670
+ const out: string[] = [];
671
+ if (file.imports.some((imp) => imp.pattern === 'glob')) {
672
+ out.push(
673
+ 'import.meta.glob edges are expanded statically and marked low-confidence approximate',
674
+ );
675
+ }
676
+ if (file.imports.some((imp) => imp.pattern === 'prefix')) {
677
+ out.push('dynamic import prefix edges are approximate');
678
+ }
679
+ if (file.exports.length > 0) {
680
+ out.push('export facts are name-only; type/value provenance is not fully resolved yet');
681
+ }
682
+ return out;
683
+ }