@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,156 @@
1
+ import type { Cycle, DependencyEdge, ModuleId, ModuleMetrics, ModuleNode } from './types';
2
+
3
+ export interface ComputeMetricsInput {
4
+ modules: ModuleNode[];
5
+ edges: DependencyEdge[];
6
+ cycles: Cycle[];
7
+ entries: ModuleId[];
8
+ }
9
+
10
+ export function computeMetrics(input: ComputeMetricsInput): Record<ModuleId, ModuleMetrics> {
11
+ const { modules, edges, cycles, entries } = input;
12
+
13
+ const graphEdges = edges.filter((e) => e.kind !== 'type-only');
14
+ const ids = modules.map((m) => m.id);
15
+
16
+ const fanIn = new Map<ModuleId, number>();
17
+ const fanOut = new Map<ModuleId, number>();
18
+ for (const id of ids) {
19
+ fanIn.set(id, 0);
20
+ fanOut.set(id, 0);
21
+ }
22
+ for (const e of graphEdges) {
23
+ fanOut.set(e.from, (fanOut.get(e.from) ?? 0) + 1);
24
+ fanIn.set(e.to, (fanIn.get(e.to) ?? 0) + 1);
25
+ }
26
+
27
+ const inCycle = new Set<ModuleId>();
28
+ for (const c of cycles) {
29
+ if (c.length > 1 || c.severity === 'direct') {
30
+ for (const id of c.modules) inCycle.add(id);
31
+ }
32
+ }
33
+
34
+ const depths = computeDepths(ids, graphEdges, cycles, entries);
35
+
36
+ const result: Record<ModuleId, ModuleMetrics> = {};
37
+
38
+ let maxCoupling = 0;
39
+ const rawCoupling = new Map<ModuleId, number>();
40
+ for (const id of ids) {
41
+ const fi = fanIn.get(id) ?? 0;
42
+ const fo = fanOut.get(id) ?? 0;
43
+ const c = Math.log2(fi + 1) * Math.log2(fo + 1);
44
+ rawCoupling.set(id, c);
45
+ if (c > maxCoupling) maxCoupling = c;
46
+ }
47
+
48
+ let maxLoc = 0;
49
+ for (const m of modules) if (m.loc > maxLoc) maxLoc = m.loc;
50
+
51
+ for (const m of modules) {
52
+ const id = m.id;
53
+ const fi = fanIn.get(id) ?? 0;
54
+ const fo = fanOut.get(id) ?? 0;
55
+ const sum = fi + fo;
56
+ const instability = sum === 0 ? 0 : fo / sum;
57
+ const cycle = inCycle.has(id);
58
+ const coupling = maxCoupling === 0 ? 0 : (rawCoupling.get(id) ?? 0) / maxCoupling;
59
+ const sizeFactor = maxLoc === 0 ? 1 : 1 + Math.log2(1 + m.loc) / Math.log2(1 + maxLoc);
60
+ const hotness = coupling * (cycle ? 1.5 : 1) * sizeFactor;
61
+ result[id] = {
62
+ fanIn: fi,
63
+ fanOut: fo,
64
+ instability: round(instability),
65
+ depth: depths.get(id) ?? 0,
66
+ inCycle: cycle,
67
+ couplingScore: round(coupling),
68
+ hotnessScore: round(hotness),
69
+ };
70
+ }
71
+
72
+ return result;
73
+ }
74
+
75
+ function computeDepths(
76
+ ids: ModuleId[],
77
+ edges: DependencyEdge[],
78
+ cycles: Cycle[],
79
+ entries: ModuleId[],
80
+ ): Map<ModuleId, number> {
81
+ const compOf = new Map<ModuleId, number>();
82
+ let nextComp = 0;
83
+ for (const c of cycles) {
84
+ if (c.length > 1) {
85
+ for (const id of c.modules) compOf.set(id, nextComp);
86
+ nextComp++;
87
+ }
88
+ }
89
+ for (const id of ids) {
90
+ if (!compOf.has(id)) {
91
+ compOf.set(id, nextComp++);
92
+ }
93
+ }
94
+
95
+ const compEdges = new Map<number, Set<number>>();
96
+ for (let i = 0; i < nextComp; i++) compEdges.set(i, new Set());
97
+ for (const e of edges) {
98
+ const a = compOf.get(e.from);
99
+ const b = compOf.get(e.to);
100
+ if (a === undefined || b === undefined || a === b) continue;
101
+ compEdges.get(a)?.add(b);
102
+ }
103
+
104
+ const indeg = new Map<number, number>();
105
+ for (let i = 0; i < nextComp; i++) indeg.set(i, 0);
106
+ for (const [, outs] of compEdges) for (const o of outs) indeg.set(o, (indeg.get(o) ?? 0) + 1);
107
+
108
+ const order: number[] = [];
109
+ const queue: number[] = [];
110
+ for (const [c, d] of indeg) if (d === 0) queue.push(c);
111
+ while (queue.length) {
112
+ const c = queue.shift();
113
+ if (c === undefined) break;
114
+ order.push(c);
115
+ for (const o of compEdges.get(c) ?? []) {
116
+ const d = (indeg.get(o) ?? 0) - 1;
117
+ indeg.set(o, d);
118
+ if (d === 0) queue.push(o);
119
+ }
120
+ }
121
+
122
+ const dist = new Map<number, number>();
123
+ const entryComps = new Set<number>();
124
+ for (const id of entries) {
125
+ const c = compOf.get(id);
126
+ if (c !== undefined) entryComps.add(c);
127
+ }
128
+ if (entryComps.size === 0) {
129
+ for (let i = 0; i < nextComp; i++) if ((indeg.get(i) ?? 0) === 0) entryComps.add(i);
130
+ }
131
+ for (const c of entryComps) dist.set(c, 0);
132
+
133
+ for (const c of order) {
134
+ if (!dist.has(c)) continue;
135
+ const d = dist.get(c) ?? 0;
136
+ for (const o of compEdges.get(c) ?? []) {
137
+ const cur = dist.get(o);
138
+ if (cur === undefined || cur < d + 1) dist.set(o, d + 1);
139
+ }
140
+ }
141
+
142
+ const depthByModule = new Map<ModuleId, number>();
143
+ for (const id of ids) {
144
+ const c = compOf.get(id);
145
+ if (c === undefined) {
146
+ depthByModule.set(id, 0);
147
+ continue;
148
+ }
149
+ depthByModule.set(id, dist.get(c) ?? 0);
150
+ }
151
+ return depthByModule;
152
+ }
153
+
154
+ function round(x: number): number {
155
+ return Math.round(x * 1000) / 1000;
156
+ }
@@ -0,0 +1,62 @@
1
+ import type { ModuleLanguage, ParsedFile } from '../types';
2
+ import type { Framework } from '../detect';
3
+ import type { DynamicLoaderConfig } from '../../config/frontScopeConfig';
4
+ import { createTsParser, type TsParser } from './tsParser';
5
+ import { createVueParser, type VueParser } from './vueParser';
6
+ import { createReactParser, type ReactParser } from './reactParser';
7
+ import { createSvelteParser, type SvelteParser } from './svelteParser';
8
+
9
+ export interface ParserRegistryOptions {
10
+ framework?: Framework;
11
+ dynamicLoaders?: DynamicLoaderConfig[];
12
+ }
13
+
14
+ export interface ParseFailure {
15
+ reason: 'unsupported-extension';
16
+ }
17
+
18
+ export interface ParserRegistry {
19
+ parse(input: { relPath: string; content: string }): ParsedFile | ParseFailure;
20
+ }
21
+
22
+ const TS_LANG_BY_EXT: Record<string, ModuleLanguage> = {
23
+ ts: 'ts',
24
+ tsx: 'ts',
25
+ js: 'js',
26
+ jsx: 'js',
27
+ mjs: 'js',
28
+ cjs: 'js',
29
+ };
30
+
31
+ export function createParserRegistry(options: ParserRegistryOptions = {}): ParserRegistry {
32
+ const tsParser: TsParser = createTsParser({
33
+ ...(options.dynamicLoaders ? { dynamicLoaders: options.dynamicLoaders } : {}),
34
+ });
35
+ const vueParser: VueParser = createVueParser(tsParser);
36
+ const reactParser: ReactParser = createReactParser(tsParser);
37
+ const svelteParser: SvelteParser = createSvelteParser(tsParser);
38
+ const isReactProject = options.framework === 'react' || options.framework === 'next';
39
+
40
+ return {
41
+ parse({ relPath, content }) {
42
+ const ext = extOf(relPath);
43
+ if (ext === 'vue') return vueParser.parse({ relPath, content });
44
+ if (ext === 'svelte') return svelteParser.parse({ relPath, content });
45
+ const lang = TS_LANG_BY_EXT[ext];
46
+ if (lang) {
47
+ if (isReactProject) return reactParser.parse({ relPath, content, language: lang });
48
+ return tsParser.parse({ relPath, content, language: lang });
49
+ }
50
+ return { reason: 'unsupported-extension' };
51
+ },
52
+ };
53
+ }
54
+
55
+ export function isParseFailure(result: ParsedFile | ParseFailure): result is ParseFailure {
56
+ return 'reason' in result;
57
+ }
58
+
59
+ function extOf(relPath: string): string {
60
+ const i = relPath.lastIndexOf('.');
61
+ return i === -1 ? '' : relPath.slice(i + 1).toLowerCase();
62
+ }
@@ -0,0 +1,24 @@
1
+ import type { ParsedFile } from '../types';
2
+ import type { TsParser, TsParseInput } from './tsParser';
3
+
4
+ export interface ReactParseInput {
5
+ relPath: string;
6
+ content: string;
7
+ language: TsParseInput['language'];
8
+ }
9
+
10
+ export interface ReactParser {
11
+ parse(input: ReactParseInput): ParsedFile;
12
+ }
13
+
14
+ export function createReactParser(tsParser: TsParser): ReactParser {
15
+ return {
16
+ parse(input) {
17
+ return tsParser.parse({
18
+ relPath: input.relPath,
19
+ content: input.content,
20
+ language: input.language,
21
+ });
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,46 @@
1
+ import type { ParsedFile } from '../types';
2
+ import type { TsParser } from './tsParser';
3
+
4
+ export interface SvelteParseInput {
5
+ relPath: string;
6
+ content: string;
7
+ }
8
+
9
+ export interface SvelteParser {
10
+ parse(input: SvelteParseInput): ParsedFile;
11
+ }
12
+
13
+ const SCRIPT_BLOCK_RE = /<\s*script\b[^>]*>([\s\S]*?)<\/\s*script\s*>/giu;
14
+
15
+ export function createSvelteParser(tsParser: TsParser): SvelteParser {
16
+ return {
17
+ parse(input) {
18
+ const code = extractScripts(input.content);
19
+ const parsed = tsParser.parse({
20
+ relPath: input.relPath,
21
+ content: code,
22
+ language: 'ts',
23
+ });
24
+ return {
25
+ ...parsed,
26
+ loc: countLines(input.content),
27
+ language: 'svelte',
28
+ };
29
+ },
30
+ };
31
+ }
32
+
33
+ function extractScripts(content: string): string {
34
+ const blocks: string[] = [];
35
+ for (const m of content.matchAll(SCRIPT_BLOCK_RE)) {
36
+ if (m[1]) blocks.push(m[1]);
37
+ }
38
+ return blocks.join('\n');
39
+ }
40
+
41
+ function countLines(content: string): number {
42
+ if (content.length === 0) return 0;
43
+ let n = 1;
44
+ for (let i = 0; i < content.length; i++) if (content[i] === '\n') n++;
45
+ return n;
46
+ }
@@ -0,0 +1,364 @@
1
+ import ts from 'typescript';
2
+ import type { ModuleLanguage, ParsedFile, RawImport } from '../types';
3
+ import type { DynamicLoaderConfig } from '../../config/frontScopeConfig';
4
+
5
+ export interface TsParseInput {
6
+ relPath: string;
7
+ content: string;
8
+ language: ModuleLanguage;
9
+ }
10
+
11
+ export interface TsParser {
12
+ parse(input: TsParseInput): ParsedFile;
13
+ }
14
+
15
+ export interface TsParserOptions {
16
+ dynamicLoaders?: DynamicLoaderConfig[];
17
+ }
18
+
19
+ const SCRIPT_KIND_BY_EXT: Record<string, ts.ScriptKind> = {
20
+ ts: ts.ScriptKind.TS,
21
+ tsx: ts.ScriptKind.TSX,
22
+ js: ts.ScriptKind.JS,
23
+ jsx: ts.ScriptKind.JSX,
24
+ mjs: ts.ScriptKind.JS,
25
+ cjs: ts.ScriptKind.JS,
26
+ };
27
+
28
+ export function createTsParser(options: TsParserOptions = {}): TsParser {
29
+ const loaders = options.dynamicLoaders ?? [];
30
+ return {
31
+ parse(input: TsParseInput): ParsedFile {
32
+ const sf = ts.createSourceFile(
33
+ input.relPath,
34
+ input.content,
35
+ ts.ScriptTarget.Latest,
36
+ false,
37
+ scriptKindFor(input.relPath, input.language),
38
+ );
39
+
40
+ const imports: RawImport[] = [];
41
+ const exports = new Set<string>();
42
+ let hasDefineStore = false;
43
+
44
+ visit(
45
+ sf,
46
+ sf,
47
+ imports,
48
+ exports,
49
+ () => {
50
+ hasDefineStore = true;
51
+ },
52
+ loaders,
53
+ );
54
+
55
+ const directives = readDirectivePrologue(sf);
56
+
57
+ return {
58
+ relPath: input.relPath,
59
+ language: input.language,
60
+ loc: countLines(input.content),
61
+ imports,
62
+ exports: [...exports].sort(),
63
+ hasDefineStore: hasDefineStore || /\bdefineStore\s*\(/.test(input.content),
64
+ ...(directives.length > 0 ? { directives } : {}),
65
+ };
66
+ },
67
+ };
68
+ }
69
+
70
+ function scriptKindFor(relPath: string, language: ModuleLanguage): ts.ScriptKind {
71
+ if (language === 'vue') return ts.ScriptKind.TS;
72
+ const ext = relPath.slice(relPath.lastIndexOf('.') + 1).toLowerCase();
73
+ return SCRIPT_KIND_BY_EXT[ext] ?? ts.ScriptKind.TS;
74
+ }
75
+
76
+ function visit(
77
+ node: ts.Node,
78
+ sf: ts.SourceFile,
79
+ imports: RawImport[],
80
+ exports: Set<string>,
81
+ markDefineStore: () => void,
82
+ loaders: DynamicLoaderConfig[],
83
+ ): void {
84
+ if (ts.isImportDeclaration(node)) {
85
+ const spec = literalText(node.moduleSpecifier);
86
+ if (spec !== null) {
87
+ imports.push({ specifier: spec, kind: classifyImport(node) });
88
+ }
89
+ return;
90
+ }
91
+
92
+ if (ts.isExportDeclaration(node)) {
93
+ const spec = node.moduleSpecifier ? literalText(node.moduleSpecifier) : null;
94
+ if (spec !== null) {
95
+ imports.push({ specifier: spec, kind: node.isTypeOnly ? 'type-only' : 'static' });
96
+ }
97
+ if (node.exportClause && ts.isNamedExports(node.exportClause)) {
98
+ for (const el of node.exportClause.elements) exports.add(el.name.text);
99
+ } else if (node.exportClause && ts.isNamespaceExport(node.exportClause)) {
100
+ exports.add(node.exportClause.name.text);
101
+ }
102
+ return;
103
+ }
104
+
105
+ if (
106
+ ts.isCallExpression(node) &&
107
+ node.expression.kind === ts.SyntaxKind.ImportKeyword &&
108
+ node.arguments.length > 0
109
+ ) {
110
+ const arg = node.arguments[0];
111
+ if (arg) {
112
+ const spec = literalText(arg);
113
+ if (spec !== null) {
114
+ imports.push({ specifier: spec, kind: 'dynamic' });
115
+ } else {
116
+ // template-literal import() - keep the static prefix
117
+ // TODO: handle the dynamic tail too (e.g. via a pattern marker), so
118
+ // `./views/${name}.vue` resolves against actual files in ./views.
119
+ const prefix = staticPrefix(arg);
120
+ if (prefix !== null && prefix.length > 0) {
121
+ imports.push({ specifier: prefix, kind: 'dynamic', pattern: 'prefix' });
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ if (
128
+ ts.isCallExpression(node) &&
129
+ ts.isIdentifier(node.expression) &&
130
+ node.expression.text === 'defineStore'
131
+ ) {
132
+ markDefineStore();
133
+ }
134
+
135
+ // CommonJS require() -> static dep
136
+ if (
137
+ ts.isCallExpression(node) &&
138
+ ts.isIdentifier(node.expression) &&
139
+ node.expression.text === 'require' &&
140
+ node.arguments.length > 0
141
+ ) {
142
+ const arg = node.arguments[0];
143
+ if (arg) {
144
+ const spec = literalText(arg);
145
+ if (spec !== null) imports.push({ specifier: spec, kind: 'static' });
146
+ }
147
+ }
148
+
149
+ // user-defined dynamic loaders from .archora.json (match by ident name only)
150
+ if (
151
+ loaders.length > 0 &&
152
+ ts.isCallExpression(node) &&
153
+ ts.isIdentifier(node.expression) &&
154
+ node.arguments.length > 0
155
+ ) {
156
+ const fnName = node.expression.text;
157
+ for (const loader of loaders) {
158
+ if (loader.name !== fnName) continue;
159
+ const idx = loader.argIndex ?? 0;
160
+ const arg = node.arguments[idx];
161
+ if (!arg) continue;
162
+ const literal = literalText(arg);
163
+ if (literal === null) continue;
164
+ const resolved = loader.resolveAs.replace(/\{0\}/gu, literal);
165
+ imports.push({ specifier: resolved, kind: 'dynamic' });
166
+ }
167
+ }
168
+
169
+ // import.meta.glob / globEager - accepts a string or array; both eager and lazy modes count
170
+ if (ts.isCallExpression(node) && isImportMetaGlob(node.expression) && node.arguments.length > 0) {
171
+ const first = node.arguments[0];
172
+ if (first) {
173
+ const patterns = collectGlobPatterns(first);
174
+ const globOptions = collectGlobOptions(
175
+ node.arguments[1],
176
+ isImportMetaGlobEager(node.expression),
177
+ );
178
+ for (const p of patterns) {
179
+ imports.push({
180
+ specifier: p,
181
+ kind: 'dynamic',
182
+ pattern: 'glob',
183
+ confidence: globOptions.eager ? 'medium' : 'low',
184
+ approximate: true,
185
+ globEager: globOptions.eager,
186
+ ...(globOptions.importName ? { globImport: globOptions.importName } : {}),
187
+ });
188
+ }
189
+ }
190
+ }
191
+
192
+ if (ts.isVariableStatement(node) && hasExportModifier(node)) {
193
+ for (const decl of node.declarationList.declarations) collectBindingNames(decl.name, exports);
194
+ } else if (
195
+ (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) &&
196
+ hasExportModifier(node)
197
+ ) {
198
+ if (node.name) exports.add(node.name.text);
199
+ else if (hasDefaultModifier(node)) exports.add('default');
200
+ } else if (
201
+ (ts.isInterfaceDeclaration(node) ||
202
+ ts.isTypeAliasDeclaration(node) ||
203
+ ts.isEnumDeclaration(node)) &&
204
+ hasExportModifier(node)
205
+ ) {
206
+ exports.add(node.name.text);
207
+ } else if (ts.isExportAssignment(node)) {
208
+ exports.add('default');
209
+ }
210
+
211
+ ts.forEachChild(node, (child) => visit(child, sf, imports, exports, markDefineStore, loaders));
212
+ }
213
+
214
+ function literalText(node: ts.Node): string | null {
215
+ if (ts.isStringLiteralLike(node)) return node.text;
216
+ return null;
217
+ }
218
+
219
+ // matches `import.meta.glob`, `import.meta.globEager`, and bracket variants
220
+ function isImportMetaGlob(expr: ts.Expression): boolean {
221
+ const name = importMetaPropertyName(expr);
222
+ return name === 'glob' || name === 'globEager';
223
+ }
224
+
225
+ function isImportMetaGlobEager(expr: ts.Expression): boolean {
226
+ return importMetaPropertyName(expr) === 'globEager';
227
+ }
228
+
229
+ function importMetaPropertyName(expr: ts.Expression): string | null {
230
+ if (!ts.isPropertyAccessExpression(expr) && !ts.isElementAccessExpression(expr)) return null;
231
+ const obj = expr.expression;
232
+ if (!ts.isMetaProperty(obj)) return null;
233
+ if (obj.keywordToken !== ts.SyntaxKind.ImportKeyword) return null;
234
+ if (obj.name.text !== 'meta') return null;
235
+ if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
236
+ return literalText(expr.argumentExpression);
237
+ }
238
+
239
+ function collectGlobPatterns(node: ts.Node): string[] {
240
+ const out: string[] = [];
241
+ if (ts.isStringLiteralLike(node)) {
242
+ out.push(node.text);
243
+ } else if (ts.isArrayLiteralExpression(node)) {
244
+ for (const el of node.elements) {
245
+ const t = literalText(el);
246
+ if (t !== null) out.push(t);
247
+ }
248
+ }
249
+ return out;
250
+ }
251
+
252
+ function collectGlobOptions(
253
+ node: ts.Node | undefined,
254
+ legacyEager = false,
255
+ ): { eager: boolean; importName?: string } {
256
+ if (!node || !ts.isObjectLiteralExpression(node)) return { eager: legacyEager };
257
+ let eager = legacyEager;
258
+ let importName: string | undefined;
259
+ for (const prop of node.properties) {
260
+ if (!ts.isPropertyAssignment(prop)) continue;
261
+ const key = propertyNameText(prop.name);
262
+ if (key === 'eager' && prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
263
+ eager = true;
264
+ } else if (key === 'import') {
265
+ const literal = literalText(prop.initializer);
266
+ if (literal !== null) importName = literal;
267
+ }
268
+ }
269
+ return importName ? { eager, importName } : { eager };
270
+ }
271
+
272
+ function propertyNameText(name: ts.PropertyName): string | null {
273
+ if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text;
274
+ return null;
275
+ }
276
+
277
+ // extracts the static prefix from `./mfes/${name}` or `'./mfes/' + name`.
278
+ // buildGraph uses it for prefix expansion of MFE-style loaders.
279
+ function staticPrefix(node: ts.Node): string | null {
280
+ if (ts.isTemplateExpression(node)) {
281
+ return node.head.text;
282
+ }
283
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
284
+ const left = staticPrefix(node.left);
285
+ if (left !== null) return left;
286
+ return null;
287
+ }
288
+ if (ts.isStringLiteralLike(node)) {
289
+ return node.text;
290
+ }
291
+ return null;
292
+ }
293
+
294
+ function classifyImport(node: ts.ImportDeclaration): RawImport['kind'] {
295
+ if (node.importClause?.isTypeOnly) return 'type-only';
296
+ const clause = node.importClause;
297
+ if (!clause) return 'side-effect';
298
+ const namedBindings = clause.namedBindings;
299
+ if (
300
+ namedBindings &&
301
+ ts.isNamedImports(namedBindings) &&
302
+ !clause.name &&
303
+ namedBindings.elements.length > 0 &&
304
+ namedBindings.elements.every((el) => el.isTypeOnly)
305
+ ) {
306
+ return 'type-only';
307
+ }
308
+ return 'static';
309
+ }
310
+
311
+ function hasExportModifier(node: ts.Node): boolean {
312
+ return ts.canHaveModifiers(node)
313
+ ? !!ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
314
+ : false;
315
+ }
316
+
317
+ function hasDefaultModifier(node: ts.Node): boolean {
318
+ return ts.canHaveModifiers(node)
319
+ ? !!ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)
320
+ : false;
321
+ }
322
+
323
+ function collectBindingNames(name: ts.BindingName, out: Set<string>): void {
324
+ if (ts.isIdentifier(name)) {
325
+ out.add(name.text);
326
+ return;
327
+ }
328
+ if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
329
+ for (const el of name.elements) {
330
+ if (ts.isBindingElement(el)) collectBindingNames(el.name, out);
331
+ }
332
+ }
333
+ }
334
+
335
+ // Directive prologue: a sequence of plain string-literal expression statements
336
+ // at the top of the file (and the top of each function body, but for our
337
+ // purposes the top of the file is enough). Stops at the first non-directive.
338
+ function readDirectivePrologue(sf: ts.SourceFile): ('use server' | 'use client')[] {
339
+ const out: ('use server' | 'use client')[] = [];
340
+ for (const stmt of sf.statements) {
341
+ if (
342
+ ts.isExpressionStatement(stmt) &&
343
+ ts.isStringLiteralLike(stmt.expression) &&
344
+ !stmt.expression.getText(sf).startsWith('`')
345
+ ) {
346
+ const text = stmt.expression.text;
347
+ if (text === 'use server' || text === 'use client') {
348
+ out.push(text);
349
+ continue;
350
+ }
351
+ // Other prologue strings (e.g. 'use strict') are tolerated.
352
+ continue;
353
+ }
354
+ break;
355
+ }
356
+ return out;
357
+ }
358
+
359
+ function countLines(content: string): number {
360
+ if (content.length === 0) return 0;
361
+ let n = 1;
362
+ for (let i = 0; i < content.length; i++) if (content[i] === '\n') n++;
363
+ return n;
364
+ }