@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,233 @@
1
+ import ts from 'typescript';
2
+ import type { FileSource } from './fileSource';
3
+ import type { ModuleId, ModuleLanguage, ModuleNode } from './types';
4
+
5
+ /**
6
+ * A feedback edge whose imported binding(s) are referenced only in type
7
+ * positions in the source file. Rewriting the import as `import type {...}`
8
+ * (or marking individual specifiers with `type`) drops the edge from cycle
9
+ * detection without any architectural change - the cheapest possible fix.
10
+ */
11
+ export interface TypeOnlyCandidate {
12
+ from: ModuleId;
13
+ to: ModuleId;
14
+ specifier: string;
15
+ /** Names from the import that can safely be moved to `import type`. */
16
+ bindings: string[];
17
+ }
18
+
19
+ interface FeedbackEdge {
20
+ from: ModuleId;
21
+ to: ModuleId;
22
+ specifier: string;
23
+ }
24
+
25
+ /**
26
+ * For each feedback edge, parse the source file and walk usages of the
27
+ * imported bindings. Emit a candidate iff every usage is in a type position
28
+ * (and the import isn't already type-only).
29
+ *
30
+ * Vue/Svelte SFCs are read raw and the script blocks are stitched together;
31
+ * we don't need symbol resolution, just AST positions, so a one-shot
32
+ * `ts.createSourceFile` per file is enough.
33
+ */
34
+ export async function findTypeOnlyCandidates(args: {
35
+ edges: FeedbackEdge[];
36
+ source: FileSource;
37
+ modules: ModuleNode[];
38
+ }): Promise<TypeOnlyCandidate[]> {
39
+ const { edges, source, modules } = args;
40
+ if (edges.length === 0) return [];
41
+
42
+ const langById = new Map(modules.map((m) => [m.id, m.language]));
43
+ const byFrom = new Map<ModuleId, FeedbackEdge[]>();
44
+ for (const e of edges) {
45
+ if (!byFrom.has(e.from)) byFrom.set(e.from, []);
46
+ byFrom.get(e.from)!.push(e);
47
+ }
48
+
49
+ const candidates: TypeOnlyCandidate[] = [];
50
+ for (const [from, fromEdges] of byFrom) {
51
+ const lang = langById.get(from);
52
+ if (!lang) continue;
53
+ let content: string;
54
+ try {
55
+ content = await source.read(from);
56
+ } catch {
57
+ continue;
58
+ }
59
+ const script = lang === 'vue' || lang === 'svelte' ? extractScripts(content) : content;
60
+ // setParentNodes=true: isInTypePosition walks node.parent up to a type node
61
+ const sf = ts.createSourceFile(
62
+ from,
63
+ script,
64
+ ts.ScriptTarget.Latest,
65
+ true,
66
+ scriptKindFor(from, lang),
67
+ );
68
+
69
+ for (const edge of fromEdges) {
70
+ const usage = inspectImport(sf, edge.specifier);
71
+ if (usage && usage.allTypes && usage.bindings.length > 0) {
72
+ candidates.push({
73
+ from: edge.from,
74
+ to: edge.to,
75
+ specifier: edge.specifier,
76
+ bindings: usage.bindings,
77
+ });
78
+ }
79
+ }
80
+ }
81
+ return candidates;
82
+ }
83
+
84
+ const KIND_BY_EXT: Record<string, ts.ScriptKind> = {
85
+ ts: ts.ScriptKind.TS,
86
+ tsx: ts.ScriptKind.TSX,
87
+ js: ts.ScriptKind.JS,
88
+ jsx: ts.ScriptKind.JSX,
89
+ mjs: ts.ScriptKind.JS,
90
+ cjs: ts.ScriptKind.JS,
91
+ };
92
+
93
+ function scriptKindFor(relPath: ModuleId, lang: ModuleLanguage): ts.ScriptKind {
94
+ if (lang === 'vue' || lang === 'svelte') return ts.ScriptKind.TS;
95
+ const ext = relPath.slice(relPath.lastIndexOf('.') + 1).toLowerCase();
96
+ return KIND_BY_EXT[ext] ?? ts.ScriptKind.TS;
97
+ }
98
+
99
+ interface ImportUsage {
100
+ bindings: string[];
101
+ allTypes: boolean;
102
+ }
103
+
104
+ function inspectImport(sf: ts.SourceFile, specifier: string): ImportUsage | null {
105
+ // collect import declarations matching the specifier
106
+ let importDecl: ts.ImportDeclaration | null = null;
107
+ for (const stmt of sf.statements) {
108
+ if (!ts.isImportDeclaration(stmt)) continue;
109
+ const lit = stmt.moduleSpecifier;
110
+ if (!ts.isStringLiteralLike(lit)) continue;
111
+ if (lit.text !== specifier) continue;
112
+ // already type-only at the declaration level - no further fix possible
113
+ if (stmt.importClause?.isTypeOnly) return null;
114
+ importDecl = stmt;
115
+ break;
116
+ }
117
+ if (!importDecl || !importDecl.importClause) return null;
118
+
119
+ // collect bindings to check; skip any that are declared `type` already
120
+ const names: string[] = [];
121
+ const clause = importDecl.importClause;
122
+ if (clause.name) names.push(clause.name.text);
123
+ if (clause.namedBindings) {
124
+ if (ts.isNamespaceImport(clause.namedBindings)) {
125
+ names.push(clause.namedBindings.name.text);
126
+ } else if (ts.isNamedImports(clause.namedBindings)) {
127
+ for (const el of clause.namedBindings.elements) {
128
+ if (el.isTypeOnly) continue;
129
+ names.push(el.name.text);
130
+ }
131
+ }
132
+ }
133
+ if (names.length === 0) return null;
134
+
135
+ const nameSet = new Set(names);
136
+ const seen = new Set<string>();
137
+ let allTypes = true;
138
+ let anyUsed = false;
139
+
140
+ const visit = (node: ts.Node): void => {
141
+ if (!allTypes) return;
142
+ if (ts.isIdentifier(node) && nameSet.has(node.text) && !isImportSpecifierNameNode(node)) {
143
+ anyUsed = true;
144
+ seen.add(node.text);
145
+ if (!isInTypePosition(node)) {
146
+ allTypes = false;
147
+ return;
148
+ }
149
+ }
150
+ ts.forEachChild(node, visit);
151
+ };
152
+ visit(sf);
153
+
154
+ if (!anyUsed) return null;
155
+ // all referenced bindings are types; report only the bindings actually used
156
+ return { bindings: [...seen].sort(), allTypes };
157
+ }
158
+
159
+ // the identifier inside `import { X }` is itself a name node, not a usage
160
+ function isImportSpecifierNameNode(id: ts.Identifier): boolean {
161
+ const n: ts.Node | undefined = id.parent;
162
+ if (!n) return false;
163
+ if (ts.isImportSpecifier(n) && n.name === id) return true;
164
+ if (ts.isImportClause(n) && n.name === id) return true;
165
+ if (ts.isNamespaceImport(n) && n.name === id) return true;
166
+ return false;
167
+ }
168
+
169
+ function isInTypePosition(id: ts.Identifier): boolean {
170
+ let n: ts.Node | undefined = id.parent;
171
+ while (n) {
172
+ if (ts.isHeritageClause(n)) {
173
+ // class `extends Base` needs Base as a runtime constructor; only
174
+ // `interface extends T` and `class implements T` are type-only.
175
+ if (n.token === ts.SyntaxKind.ImplementsKeyword) return true;
176
+ if (n.parent && ts.isInterfaceDeclaration(n.parent)) return true;
177
+ return false;
178
+ }
179
+ if (
180
+ ts.isTypeReferenceNode(n) ||
181
+ ts.isTypeAliasDeclaration(n) ||
182
+ ts.isInterfaceDeclaration(n) ||
183
+ ts.isTypeQueryNode(n) ||
184
+ ts.isImportTypeNode(n) ||
185
+ ts.isTypePredicateNode(n) ||
186
+ ts.isTypeOperatorNode(n) ||
187
+ ts.isIndexedAccessTypeNode(n) ||
188
+ ts.isMappedTypeNode(n) ||
189
+ ts.isConditionalTypeNode(n) ||
190
+ ts.isUnionTypeNode(n) ||
191
+ ts.isIntersectionTypeNode(n) ||
192
+ ts.isParenthesizedTypeNode(n) ||
193
+ ts.isLiteralTypeNode(n) ||
194
+ ts.isTupleTypeNode(n) ||
195
+ ts.isArrayTypeNode(n) ||
196
+ ts.isTypeLiteralNode(n) ||
197
+ ts.isFunctionTypeNode(n) ||
198
+ ts.isConstructorTypeNode(n)
199
+ ) {
200
+ // typeQuery (`typeof X`) needs X to be a value at runtime, so it's NOT
201
+ // safe to move to `import type` - bail out.
202
+ if (ts.isTypeQueryNode(n)) return false;
203
+ return true;
204
+ }
205
+ // satisfies / as expressions: the right-hand side is a type
206
+ if ((ts.isAsExpression(n) || ts.isSatisfiesExpression(n)) && isAncestorTypeBranch(n, id)) {
207
+ return true;
208
+ }
209
+ n = n.parent;
210
+ }
211
+ return false;
212
+ }
213
+
214
+ function isAncestorTypeBranch(
215
+ n: ts.AsExpression | ts.SatisfiesExpression,
216
+ id: ts.Identifier,
217
+ ): boolean {
218
+ // walk up from id; if we cross n.expression we're on the value side
219
+ let cur: ts.Node = id;
220
+ while (cur && cur !== n) {
221
+ if (cur === n.expression) return false;
222
+ cur = cur.parent;
223
+ }
224
+ return true;
225
+ }
226
+
227
+ const SCRIPT_RE = /<\s*script\b[^>]*>([\s\S]*?)<\/\s*script\s*>/giu;
228
+
229
+ function extractScripts(sfc: string): string {
230
+ const out: string[] = [];
231
+ for (const m of sfc.matchAll(SCRIPT_RE)) out.push(m[1] ?? '');
232
+ return out.join('\n');
233
+ }