@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.
- package/LICENSE +201 -0
- package/README.md +62 -0
- package/package.json +36 -0
- package/src/README.md +4 -0
- package/src/analyzer/__tests__/__snapshots__/referenceSnapshot.test.ts.snap +145 -0
- package/src/analyzer/__tests__/_paths.ts +8 -0
- package/src/analyzer/__tests__/analyze.test.ts +522 -0
- package/src/analyzer/__tests__/archDebt.test.ts +111 -0
- package/src/analyzer/__tests__/asyncLifecycleRisk.test.ts +122 -0
- package/src/analyzer/__tests__/browserFsAccessFileSource.test.ts +97 -0
- package/src/analyzer/__tests__/bundle.test.ts +191 -0
- package/src/analyzer/__tests__/classify.test.ts +99 -0
- package/src/analyzer/__tests__/contracts.test.ts +372 -0
- package/src/analyzer/__tests__/crossSourceConsistency.test.ts +317 -0
- package/src/analyzer/__tests__/cyclePatterns.test.ts +132 -0
- package/src/analyzer/__tests__/cycles.test.ts +74 -0
- package/src/analyzer/__tests__/detect.test.ts +62 -0
- package/src/analyzer/__tests__/discover.test.ts +68 -0
- package/src/analyzer/__tests__/displayId.test.ts +30 -0
- package/src/analyzer/__tests__/feedbackArcSet.test.ts +168 -0
- package/src/analyzer/__tests__/inMemoryFileSource.test.ts +34 -0
- package/src/analyzer/__tests__/incremental.test.ts +154 -0
- package/src/analyzer/__tests__/layers.test.ts +87 -0
- package/src/analyzer/__tests__/layersOverrides.test.ts +120 -0
- package/src/analyzer/__tests__/memoryRisk.test.ts +132 -0
- package/src/analyzer/__tests__/metrics.test.ts +59 -0
- package/src/analyzer/__tests__/parserRegistry.test.ts +54 -0
- package/src/analyzer/__tests__/parsers.test.ts +187 -0
- package/src/analyzer/__tests__/reactParser.test.ts +93 -0
- package/src/analyzer/__tests__/recommendations.test.ts +171 -0
- package/src/analyzer/__tests__/referenceSnapshot.test.ts +63 -0
- package/src/analyzer/__tests__/resolve.test.ts +294 -0
- package/src/analyzer/__tests__/rsc.test.ts +130 -0
- package/src/analyzer/__tests__/signals.test.ts +316 -0
- package/src/analyzer/__tests__/suggestContracts.test.ts +108 -0
- package/src/analyzer/__tests__/svelteParser.test.ts +108 -0
- package/src/analyzer/__tests__/typeOnlyCandidates.test.ts +163 -0
- package/src/analyzer/__tests__/vueAutoImport.test.ts +177 -0
- package/src/analyzer/archDebt.ts +68 -0
- package/src/analyzer/asyncLifecycleRisk.ts +234 -0
- package/src/analyzer/buildGraph.ts +683 -0
- package/src/analyzer/bundle/analyzeBundle.ts +147 -0
- package/src/analyzer/bundle/index.ts +12 -0
- package/src/analyzer/bundle/parseStats.ts +152 -0
- package/src/analyzer/bundle/types.ts +85 -0
- package/src/analyzer/classify.ts +54 -0
- package/src/analyzer/contracts.ts +265 -0
- package/src/analyzer/cyclePatterns.ts +138 -0
- package/src/analyzer/cycles.ts +98 -0
- package/src/analyzer/detect.ts +34 -0
- package/src/analyzer/discover.ts +131 -0
- package/src/analyzer/displayId.ts +21 -0
- package/src/analyzer/entryPoints.ts +136 -0
- package/src/analyzer/feedbackArcSet.ts +332 -0
- package/src/analyzer/fileSource.ts +8 -0
- package/src/analyzer/hotZones.ts +17 -0
- package/src/analyzer/incremental.ts +455 -0
- package/src/analyzer/index.ts +444 -0
- package/src/analyzer/layers.ts +183 -0
- package/src/analyzer/loadAliases.ts +288 -0
- package/src/analyzer/memoryRisk.ts +345 -0
- package/src/analyzer/metrics.ts +156 -0
- package/src/analyzer/parsers/index.ts +62 -0
- package/src/analyzer/parsers/reactParser.ts +24 -0
- package/src/analyzer/parsers/svelteParser.ts +46 -0
- package/src/analyzer/parsers/tsParser.ts +364 -0
- package/src/analyzer/parsers/vueParser.ts +109 -0
- package/src/analyzer/recommendations.ts +432 -0
- package/src/analyzer/resolve.ts +315 -0
- package/src/analyzer/rsc.ts +120 -0
- package/src/analyzer/signals.ts +684 -0
- package/src/analyzer/sources/browserFsAccessFileSource.ts +132 -0
- package/src/analyzer/sources/inMemoryFileSource.ts +24 -0
- package/src/analyzer/sources/nodeFsFileSource.ts +93 -0
- package/src/analyzer/sources/tauriFileSource.ts +68 -0
- package/src/analyzer/suggestContracts.ts +214 -0
- package/src/analyzer/typeOnlyCandidates.ts +233 -0
- package/src/analyzer/types.ts +537 -0
- package/src/cache/__tests__/cache.test.ts +316 -0
- package/src/cache/index.ts +432 -0
- package/src/codegen/__tests__/applyTypeOnlyFix.integration.test.ts +62 -0
- package/src/codegen/__tests__/applyTypeOnlyFix.test.ts +176 -0
- package/src/codegen/__tests__/configSnippets.test.ts +230 -0
- package/src/codegen/applyTypeOnlyFix.ts +344 -0
- package/src/codegen/configSnippets.ts +172 -0
- package/src/codegen/initConfig.ts +223 -0
- package/src/config/__tests__/frontScopeConfig.test.ts +187 -0
- package/src/config/frontScopeConfig.ts +830 -0
- package/src/diff/__tests__/diffScans.test.ts +103 -0
- package/src/diff/diffScans.ts +61 -0
- package/src/diff/index.ts +2 -0
- package/src/diff/types.ts +39 -0
- package/src/git/__tests__/computeChurn.test.ts +113 -0
- package/src/git/__tests__/computeTemporalCoupling.test.ts +125 -0
- package/src/git/__tests__/parseGitLog.test.ts +120 -0
- package/src/git/computeChurn.ts +111 -0
- package/src/git/computeTemporalCoupling.ts +114 -0
- package/src/git/index.ts +24 -0
- package/src/git/parseGitLog.ts +124 -0
- package/src/git/readGitHistory.ts +130 -0
- package/src/git/types.ts +119 -0
- package/src/index.ts +137 -0
- package/src/report/__tests__/buildFixPlan.test.ts +357 -0
- package/src/report/__tests__/buildJsonReport.test.ts +34 -0
- package/src/report/buildFixPlan.ts +481 -0
- package/src/report/buildJsonReport.ts +27 -0
- package/src/search/__tests__/parseQuery.test.ts +67 -0
- package/src/search/__tests__/search.test.ts +172 -0
- package/src/search/index.ts +281 -0
- package/src/search/parseQuery.ts +75 -0
- package/src/views/__tests__/analyzerViews.test.ts +558 -0
- 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
|
+
}
|