@juzser/blacksmith 0.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/.claude/agents/auditor.md +146 -0
- package/.claude/agents/coder.md +164 -0
- package/.claude/agents/grader.md +110 -0
- package/.claude/agents/merger.md +107 -0
- package/.claude/agents/planner.md +92 -0
- package/.claude/agents/researcher.md +105 -0
- package/.claude/agents/reviewer.md +171 -0
- package/.claude/agents/scribe.md +72 -0
- package/.claude/agents/security-reviewer.md +130 -0
- package/.claude/agents/spec-reviewer.md +215 -0
- package/.claude/agents/tester.md +121 -0
- package/.claude/agents/uiux.md +126 -0
- package/.claude/agents/verifier.md +103 -0
- package/.claude/agents/wave-runner.md +74 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/factory/orchestrator/dist/actors.js +46 -0
- package/factory/orchestrator/dist/agents-registry.js +308 -0
- package/factory/orchestrator/dist/args.js +84 -0
- package/factory/orchestrator/dist/artifacts.js +111 -0
- package/factory/orchestrator/dist/attribution.js +160 -0
- package/factory/orchestrator/dist/audit.js +812 -0
- package/factory/orchestrator/dist/autonomy.js +151 -0
- package/factory/orchestrator/dist/budgetAlarm.js +366 -0
- package/factory/orchestrator/dist/budgets.js +196 -0
- package/factory/orchestrator/dist/claims.js +633 -0
- package/factory/orchestrator/dist/cli.js +3185 -0
- package/factory/orchestrator/dist/commit.js +152 -0
- package/factory/orchestrator/dist/coverage.js +188 -0
- package/factory/orchestrator/dist/crossFinding.js +479 -0
- package/factory/orchestrator/dist/crosscheck.js +396 -0
- package/factory/orchestrator/dist/daemon.js +707 -0
- package/factory/orchestrator/dist/db/projector.js +1317 -0
- package/factory/orchestrator/dist/db/queries.js +1714 -0
- package/factory/orchestrator/dist/db/schema.js +305 -0
- package/factory/orchestrator/dist/delegation.js +400 -0
- package/factory/orchestrator/dist/diffstat.js +164 -0
- package/factory/orchestrator/dist/dispatchAudit.js +275 -0
- package/factory/orchestrator/dist/dotenv.js +72 -0
- package/factory/orchestrator/dist/effort.js +202 -0
- package/factory/orchestrator/dist/epic.js +1018 -0
- package/factory/orchestrator/dist/epicWidth.js +238 -0
- package/factory/orchestrator/dist/errorIssues.js +249 -0
- package/factory/orchestrator/dist/errors.js +11 -0
- package/factory/orchestrator/dist/escalation.js +500 -0
- package/factory/orchestrator/dist/events.js +1015 -0
- package/factory/orchestrator/dist/findingAge.js +91 -0
- package/factory/orchestrator/dist/findingContext.js +103 -0
- package/factory/orchestrator/dist/findings.js +1156 -0
- package/factory/orchestrator/dist/gate.js +911 -0
- package/factory/orchestrator/dist/gh.js +146 -0
- package/factory/orchestrator/dist/git.js +87 -0
- package/factory/orchestrator/dist/goalCheck.js +380 -0
- package/factory/orchestrator/dist/graph.js +61 -0
- package/factory/orchestrator/dist/hookDecision.js +106 -0
- package/factory/orchestrator/dist/immutability.js +171 -0
- package/factory/orchestrator/dist/impact.js +280 -0
- package/factory/orchestrator/dist/integration.js +141 -0
- package/factory/orchestrator/dist/judgePreflight.js +155 -0
- package/factory/orchestrator/dist/judges.js +310 -0
- package/factory/orchestrator/dist/lessonAudit.js +399 -0
- package/factory/orchestrator/dist/lessons.js +1329 -0
- package/factory/orchestrator/dist/mcp.js +555 -0
- package/factory/orchestrator/dist/paths.js +82 -0
- package/factory/orchestrator/dist/plan.js +502 -0
- package/factory/orchestrator/dist/planQuorum.js +421 -0
- package/factory/orchestrator/dist/policy.js +1308 -0
- package/factory/orchestrator/dist/policyHook.js +28 -0
- package/factory/orchestrator/dist/preconditions.js +47 -0
- package/factory/orchestrator/dist/projects.js +134 -0
- package/factory/orchestrator/dist/prompts.js +57 -0
- package/factory/orchestrator/dist/provenance.js +201 -0
- package/factory/orchestrator/dist/providers/api-transport.js +117 -0
- package/factory/orchestrator/dist/providers/cli-transport.js +208 -0
- package/factory/orchestrator/dist/providers/index.js +54 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +152 -0
- package/factory/orchestrator/dist/providers/types.js +49 -0
- package/factory/orchestrator/dist/queue.js +263 -0
- package/factory/orchestrator/dist/quorum.js +347 -0
- package/factory/orchestrator/dist/quorumEscalations.js +132 -0
- package/factory/orchestrator/dist/results.js +65 -0
- package/factory/orchestrator/dist/roadmap.js +437 -0
- package/factory/orchestrator/dist/runtime.js +86 -0
- package/factory/orchestrator/dist/sameMistakeKpi.js +243 -0
- package/factory/orchestrator/dist/sandbox.js +165 -0
- package/factory/orchestrator/dist/scaffold.js +373 -0
- package/factory/orchestrator/dist/scheduler.js +408 -0
- package/factory/orchestrator/dist/schemas.js +195 -0
- package/factory/orchestrator/dist/security.js +222 -0
- package/factory/orchestrator/dist/severity.js +293 -0
- package/factory/orchestrator/dist/spec.js +376 -0
- package/factory/orchestrator/dist/specChange.js +497 -0
- package/factory/orchestrator/dist/stack.js +324 -0
- package/factory/orchestrator/dist/symbols.js +966 -0
- package/factory/orchestrator/dist/taskEvents.js +399 -0
- package/factory/orchestrator/dist/taskId.js +126 -0
- package/factory/orchestrator/dist/taxonomy.js +113 -0
- package/factory/orchestrator/dist/testSelect.js +129 -0
- package/factory/orchestrator/dist/testerAudit.js +264 -0
- package/factory/orchestrator/dist/testgate.js +171 -0
- package/factory/orchestrator/dist/usage.js +874 -0
- package/factory/orchestrator/dist/waivers.js +228 -0
- package/factory/orchestrator/dist/waveBudget.js +223 -0
- package/factory/orchestrator/dist/waveConcurrency.js +250 -0
- package/factory/orchestrator/dist/waveNext.js +210 -0
- package/factory/orchestrator/dist/waveSchedule.js +146 -0
- package/factory/orchestrator/dist/worktree.js +207 -0
- package/factory/orchestrator/drizzle/0000_broken_rockslide.sql +183 -0
- package/factory/orchestrator/drizzle/0001_luxuriant_zaladane.sql +1 -0
- package/factory/orchestrator/drizzle/0002_yellow_wild_pack.sql +8 -0
- package/factory/orchestrator/drizzle/0003_damp_riptide.sql +12 -0
- package/factory/orchestrator/drizzle/0004_broad_lady_vermin.sql +1 -0
- package/factory/orchestrator/drizzle/0005_romantic_jocasta.sql +2 -0
- package/factory/orchestrator/drizzle/0006_bent_madelyne_pryor.sql +16 -0
- package/factory/orchestrator/drizzle/0007_overjoyed_goliath.sql +1 -0
- package/factory/orchestrator/drizzle/0008_smart_iron_patriot.sql +2 -0
- package/factory/orchestrator/drizzle/0009_agents_epic_id.sql +2 -0
- package/factory/orchestrator/drizzle/0010_milestone_kind.sql +1 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +83 -0
- package/factory/policies/budgets.yml +262 -0
- package/factory/policies/crosscheck.yml +308 -0
- package/factory/policies/delegation.yml +44 -0
- package/factory/policies/effort.yml +142 -0
- package/factory/policies/guardrails.yml +306 -0
- package/factory/policies/lessons.md +239 -0
- package/factory/policies/scheduler.yml +90 -0
- package/factory/policies/sensitive-paths.yml +148 -0
- package/factory/policies/severity.yml +83 -0
- package/factory/policies/stack.yml +60 -0
- package/factory/policies/taxonomy.yml +340 -0
- package/factory/policies/worktree.yml +95 -0
- package/factory/scaffold/base/.github/workflows/ci.yml +34 -0
- package/factory/scaffold/base/.gitignore.tmpl +11 -0
- package/factory/scaffold/base/AGENTS.md +33 -0
- package/factory/scaffold/base/README.md +25 -0
- package/factory/scaffold/base/biome.json.tmpl +30 -0
- package/factory/scaffold/base/package.json +25 -0
- package/factory/scaffold/base/pnpm-workspace.yaml.tmpl +16 -0
- package/factory/scaffold/base/src/index.ts +4 -0
- package/factory/scaffold/base/test/index.test.ts +8 -0
- package/factory/scaffold/base/tsconfig.json +20 -0
- package/factory/scaffold/base/tsconfig.test.json +9 -0
- package/factory/scaffold/base/vitest.config.ts +29 -0
- package/factory/scaffold/mcp/mcp.manifest.json +22 -0
- package/factory/scaffold/mcp/package.fragment.json +9 -0
- package/factory/scaffold/mcp/src/mcp/README.md +63 -0
- package/factory/scaffold/mcp/src/mcp/guard.ts +110 -0
- package/factory/scaffold/mcp/src/mcp/redact.ts +69 -0
- package/factory/scaffold/mcp/src/mcp/server.ts +55 -0
- package/factory/scaffold/mcp/src/mcp/tools/health.ts +60 -0
- package/factory/scaffold/mcp/test/mcp/guard.test.ts +108 -0
- package/factory/scaffold/mcp/test/mcp/redact.test.ts +61 -0
- package/factory/scaffold/mcp/test/mcp/surface.test.ts +100 -0
- package/factory/scaffold/ui/index.html +12 -0
- package/factory/scaffold/ui/package.fragment.json +13 -0
- package/factory/scaffold/ui/src/App.vue +10 -0
- package/factory/scaffold/ui/src/main.ts +5 -0
- package/factory/scaffold/ui/src/shims.d.ts +11 -0
- package/factory/scaffold/ui/vite.config.ts +20 -0
- package/factory/scaffold/ui-tailwind/package.fragment.json +6 -0
- package/factory/scaffold/ui-tailwind/vite.config.ts +26 -0
- package/factory/specs/schema/event.schema.json +82 -0
- package/factory/specs/schema/finding-evidence.schema.json +45 -0
- package/factory/specs/schema/finding.schema.json +115 -0
- package/factory/specs/schema/grader-verdict.schema.json +50 -0
- package/factory/specs/schema/judge-verdict.schema.json +19 -0
- package/factory/specs/schema/lesson.schema.json +85 -0
- package/factory/specs/schema/mcp-manifest.schema.json +230 -0
- package/factory/specs/schema/result.schema.json +81 -0
- package/factory/specs/schema/spec-change-request.schema.json +46 -0
- package/factory/specs/schema/task-spec.schema.json +134 -0
- package/package.json +98 -0
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compile-time module graph that file-level path claims cannot see.
|
|
3
|
+
*
|
|
4
|
+
* A path claim answers "did two tasks touch the same file". It cannot answer
|
|
5
|
+
* "did task A change a signature that task B's file imports", because that
|
|
6
|
+
* coupling does not live inside either file — it lives on the edge between
|
|
7
|
+
* them. This module reads those edges: which file imports which symbol from
|
|
8
|
+
* which file, and which file exports what.
|
|
9
|
+
*
|
|
10
|
+
* It ships its own scanner instead of calling a compiler, and that is a
|
|
11
|
+
* decision rather than an oversight. `typescript@7` is the native rewrite and
|
|
12
|
+
* exposes no compiler API: its package `exports` map offers `./unstable/sync`
|
|
13
|
+
* (a client that spawns a native language server) and `./unstable/ast`, both
|
|
14
|
+
* under the package's own word for "this will change". `ts.createSourceFile`
|
|
15
|
+
* is not there to call. Pinning the claim gate to that surface would trade a
|
|
16
|
+
* small scanner for a large moving dependency.
|
|
17
|
+
*
|
|
18
|
+
* The trade the scanner makes in return: it is a lexer, not a type checker, so
|
|
19
|
+
* some shapes are beyond it. Every one of them is *named* rather than quietly
|
|
20
|
+
* dropped — `unanalyzed`, `unresolved`, `unreadExports`, `opaqueImports`. A
|
|
21
|
+
* hole an operator can see is a different thing from a graph that claims to be
|
|
22
|
+
* complete and is not.
|
|
23
|
+
*/
|
|
24
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
25
|
+
import { dirname, extname, join } from 'node:path/posix';
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Lexical helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/**
|
|
30
|
+
* Punctuation after which a `/` opens a regex rather than dividing. Without
|
|
31
|
+
* this, `const r = /['"]/;` would open a fake string and swallow the rest of
|
|
32
|
+
* the file.
|
|
33
|
+
*/
|
|
34
|
+
const REGEX_ALLOWED_AFTER_PUNCT = new Set([
|
|
35
|
+
'(',
|
|
36
|
+
',',
|
|
37
|
+
'=',
|
|
38
|
+
':',
|
|
39
|
+
'[',
|
|
40
|
+
'!',
|
|
41
|
+
'&',
|
|
42
|
+
'|',
|
|
43
|
+
'?',
|
|
44
|
+
'{',
|
|
45
|
+
'}',
|
|
46
|
+
';',
|
|
47
|
+
'+',
|
|
48
|
+
'-',
|
|
49
|
+
'*',
|
|
50
|
+
'%',
|
|
51
|
+
'~',
|
|
52
|
+
'^',
|
|
53
|
+
'<',
|
|
54
|
+
'>',
|
|
55
|
+
]);
|
|
56
|
+
const REGEX_ALLOWED_AFTER_WORD = new Set([
|
|
57
|
+
'return',
|
|
58
|
+
'typeof',
|
|
59
|
+
'instanceof',
|
|
60
|
+
'in',
|
|
61
|
+
'of',
|
|
62
|
+
'new',
|
|
63
|
+
'delete',
|
|
64
|
+
'void',
|
|
65
|
+
'case',
|
|
66
|
+
'do',
|
|
67
|
+
'else',
|
|
68
|
+
'yield',
|
|
69
|
+
'await',
|
|
70
|
+
]);
|
|
71
|
+
const EXPORT_MODIFIERS = new Set(['declare', 'abstract', 'async']);
|
|
72
|
+
const NAMED_DECLARATION_KEYWORDS = new Set([
|
|
73
|
+
'function',
|
|
74
|
+
'class',
|
|
75
|
+
'interface',
|
|
76
|
+
'type',
|
|
77
|
+
'enum',
|
|
78
|
+
'namespace',
|
|
79
|
+
'module',
|
|
80
|
+
]);
|
|
81
|
+
function isIdentStart(ch) {
|
|
82
|
+
return /[A-Za-z_$]/.test(ch);
|
|
83
|
+
}
|
|
84
|
+
function isIdentPart(ch) {
|
|
85
|
+
return /[A-Za-z0-9_$]/.test(ch);
|
|
86
|
+
}
|
|
87
|
+
function readIdent(text, at) {
|
|
88
|
+
if (at >= text.length || !isIdentStart(text.charAt(at)))
|
|
89
|
+
return '';
|
|
90
|
+
let end = at;
|
|
91
|
+
while (end < text.length && isIdentPart(text.charAt(end)))
|
|
92
|
+
end += 1;
|
|
93
|
+
return text.slice(at, end);
|
|
94
|
+
}
|
|
95
|
+
function skipSpace(text, at) {
|
|
96
|
+
let i = at;
|
|
97
|
+
while (i < text.length && /\s/.test(text.charAt(i)))
|
|
98
|
+
i += 1;
|
|
99
|
+
return i;
|
|
100
|
+
}
|
|
101
|
+
function leadingWord(text) {
|
|
102
|
+
return readIdent(text.trimStart(), 0);
|
|
103
|
+
}
|
|
104
|
+
function startsRegex(prevToken) {
|
|
105
|
+
if (prevToken === '')
|
|
106
|
+
return true;
|
|
107
|
+
return REGEX_ALLOWED_AFTER_PUNCT.has(prevToken) || REGEX_ALLOWED_AFTER_WORD.has(prevToken);
|
|
108
|
+
}
|
|
109
|
+
function scanQuoted(source, start, quote) {
|
|
110
|
+
let i = start + 1;
|
|
111
|
+
while (i < source.length) {
|
|
112
|
+
const ch = source.charAt(i);
|
|
113
|
+
if (ch === '\\') {
|
|
114
|
+
i += 2;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (ch === quote)
|
|
118
|
+
return { end: i, closed: true };
|
|
119
|
+
if (ch === '\n')
|
|
120
|
+
return { end: i, closed: false };
|
|
121
|
+
i += 1;
|
|
122
|
+
}
|
|
123
|
+
return { end: source.length, closed: false };
|
|
124
|
+
}
|
|
125
|
+
/** Index of the closing `/`, or null when this `/` was division after all. */
|
|
126
|
+
function scanRegex(source, start) {
|
|
127
|
+
let i = start + 1;
|
|
128
|
+
let inClass = false;
|
|
129
|
+
while (i < source.length) {
|
|
130
|
+
const ch = source.charAt(i);
|
|
131
|
+
if (ch === '\\') {
|
|
132
|
+
i += 2;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch === '\n')
|
|
136
|
+
return null;
|
|
137
|
+
if (inClass) {
|
|
138
|
+
if (ch === ']')
|
|
139
|
+
inClass = false;
|
|
140
|
+
i += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (ch === '[') {
|
|
144
|
+
inClass = true;
|
|
145
|
+
i += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (ch === '/')
|
|
149
|
+
return i;
|
|
150
|
+
i += 1;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Blank out comment and literal interiors *in place*: same length, same line
|
|
156
|
+
* breaks, quote delimiters kept. That means every offset into the result still
|
|
157
|
+
* slices the original, and a masked specifier is recognisable as an empty
|
|
158
|
+
* quoted run.
|
|
159
|
+
*/
|
|
160
|
+
export function maskLiterals(source) {
|
|
161
|
+
const out = source.split('');
|
|
162
|
+
let unterminated = false;
|
|
163
|
+
const blank = (from, to) => {
|
|
164
|
+
for (let k = from; k < to && k < out.length; k += 1) {
|
|
165
|
+
if (out[k] !== '\n')
|
|
166
|
+
out[k] = ' ';
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
// Each open `${` remembers the brace depth it interrupted, so its matching
|
|
170
|
+
// `}` can be told apart from a plain block close.
|
|
171
|
+
const templateFrames = [];
|
|
172
|
+
let inTemplate = false;
|
|
173
|
+
let braceDepth = 0;
|
|
174
|
+
let prevToken = '';
|
|
175
|
+
let i = 0;
|
|
176
|
+
while (i < source.length) {
|
|
177
|
+
const ch = source.charAt(i);
|
|
178
|
+
if (inTemplate) {
|
|
179
|
+
if (ch === '\\') {
|
|
180
|
+
blank(i, i + 2);
|
|
181
|
+
i += 2;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (ch === '`') {
|
|
185
|
+
inTemplate = false;
|
|
186
|
+
prevToken = '`';
|
|
187
|
+
i += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (ch === '$' && source.charAt(i + 1) === '{') {
|
|
191
|
+
templateFrames.push(braceDepth);
|
|
192
|
+
inTemplate = false;
|
|
193
|
+
prevToken = '{';
|
|
194
|
+
i += 2;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
blank(i, i + 1);
|
|
198
|
+
i += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const next = source.charAt(i + 1);
|
|
202
|
+
if (ch === '/' && next === '/') {
|
|
203
|
+
let end = source.indexOf('\n', i);
|
|
204
|
+
if (end === -1)
|
|
205
|
+
end = source.length;
|
|
206
|
+
blank(i, end);
|
|
207
|
+
i = end;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (ch === '/' && next === '*') {
|
|
211
|
+
const end = source.indexOf('*/', i + 2);
|
|
212
|
+
if (end === -1) {
|
|
213
|
+
blank(i, source.length);
|
|
214
|
+
unterminated = true;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
blank(i, end + 2);
|
|
218
|
+
i = end + 2;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (ch === '"' || ch === "'") {
|
|
222
|
+
const quoted = scanQuoted(source, i, ch);
|
|
223
|
+
blank(i + 1, quoted.end);
|
|
224
|
+
if (!quoted.closed)
|
|
225
|
+
unterminated = true;
|
|
226
|
+
i = quoted.closed ? quoted.end + 1 : quoted.end;
|
|
227
|
+
prevToken = ch;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (ch === '`') {
|
|
231
|
+
inTemplate = true;
|
|
232
|
+
i += 1;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (ch === '/' && startsRegex(prevToken)) {
|
|
236
|
+
const close = scanRegex(source, i);
|
|
237
|
+
if (close !== null) {
|
|
238
|
+
blank(i + 1, close);
|
|
239
|
+
i = close + 1;
|
|
240
|
+
// A closed regex is a value, so the next `/` divides.
|
|
241
|
+
prevToken = 'regex';
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
// Fall through: this `/` divides.
|
|
245
|
+
}
|
|
246
|
+
if (ch === '{') {
|
|
247
|
+
braceDepth += 1;
|
|
248
|
+
prevToken = ch;
|
|
249
|
+
i += 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (ch === '}') {
|
|
253
|
+
const frame = templateFrames[templateFrames.length - 1];
|
|
254
|
+
if (frame !== undefined && braceDepth === frame) {
|
|
255
|
+
templateFrames.pop();
|
|
256
|
+
inTemplate = true;
|
|
257
|
+
i += 1;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
braceDepth -= 1;
|
|
261
|
+
prevToken = ch;
|
|
262
|
+
i += 1;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (isIdentStart(ch)) {
|
|
266
|
+
const word = readIdent(source, i);
|
|
267
|
+
prevToken = word;
|
|
268
|
+
i += word.length;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!/\s/.test(ch))
|
|
272
|
+
prevToken = ch;
|
|
273
|
+
i += 1;
|
|
274
|
+
}
|
|
275
|
+
if (inTemplate)
|
|
276
|
+
unterminated = true;
|
|
277
|
+
return { masked: out.join(''), unterminated };
|
|
278
|
+
}
|
|
279
|
+
/** Words that prove a `{` opens a declaration body, not a named import list. */
|
|
280
|
+
const DECLARATION_WORDS = new Set([
|
|
281
|
+
'function',
|
|
282
|
+
'class',
|
|
283
|
+
'interface',
|
|
284
|
+
'enum',
|
|
285
|
+
'namespace',
|
|
286
|
+
'module',
|
|
287
|
+
'const',
|
|
288
|
+
'let',
|
|
289
|
+
'var',
|
|
290
|
+
'declare',
|
|
291
|
+
'abstract',
|
|
292
|
+
'async',
|
|
293
|
+
'default',
|
|
294
|
+
]);
|
|
295
|
+
/**
|
|
296
|
+
* Where the clause's own named list opens, or -1. A named list is reachable
|
|
297
|
+
* from the keyword through nothing but names, commas, `*` and whitespace —
|
|
298
|
+
* `import d, { a } from` qualifies, `export function f() {` does not.
|
|
299
|
+
*/
|
|
300
|
+
function findListBrace(masked, at) {
|
|
301
|
+
let i = at;
|
|
302
|
+
while (i < masked.length) {
|
|
303
|
+
const ch = masked.charAt(i);
|
|
304
|
+
if (ch === '{')
|
|
305
|
+
return i;
|
|
306
|
+
if (ch === ',' || ch === '*' || /\s/.test(ch)) {
|
|
307
|
+
i += 1;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (isIdentStart(ch)) {
|
|
311
|
+
const word = readIdent(masked, i);
|
|
312
|
+
if (DECLARATION_WORDS.has(word))
|
|
313
|
+
return -1;
|
|
314
|
+
i += word.length;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
return -1;
|
|
318
|
+
}
|
|
319
|
+
return -1;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* A statement clause ends at the first depth-0 `;` or `{` — unless that `{` is
|
|
323
|
+
* the clause's own named import/export list, which is exactly what tells
|
|
324
|
+
* `import { a } from` apart from `export function f() {`. A depth-0 newline
|
|
325
|
+
* also ends it, unless the next token is `from`.
|
|
326
|
+
*/
|
|
327
|
+
function readClause(masked, start, keyword) {
|
|
328
|
+
let i = start + keyword.length;
|
|
329
|
+
const listBrace = findListBrace(masked, i);
|
|
330
|
+
let depth = 0;
|
|
331
|
+
let consumedList = false;
|
|
332
|
+
while (i < masked.length) {
|
|
333
|
+
const ch = masked.charAt(i);
|
|
334
|
+
if (ch === '(' || ch === '[') {
|
|
335
|
+
depth += 1;
|
|
336
|
+
i += 1;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (ch === ')' || ch === ']') {
|
|
340
|
+
depth -= 1;
|
|
341
|
+
i += 1;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (ch === '{') {
|
|
345
|
+
if (depth === 0 && !consumedList && i === listBrace) {
|
|
346
|
+
consumedList = true;
|
|
347
|
+
depth += 1;
|
|
348
|
+
i += 1;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (depth === 0)
|
|
352
|
+
return { textEnd: i, end: i, stop: '{' };
|
|
353
|
+
depth += 1;
|
|
354
|
+
i += 1;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (ch === '}') {
|
|
358
|
+
depth -= 1;
|
|
359
|
+
i += 1;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (ch === ';' && depth === 0)
|
|
363
|
+
return { textEnd: i, end: i + 1, stop: ';' };
|
|
364
|
+
if (ch === '\n' && depth === 0 && readIdent(masked, skipSpace(masked, i)) !== 'from') {
|
|
365
|
+
return { textEnd: i, end: i, stop: 'nl' };
|
|
366
|
+
}
|
|
367
|
+
i += 1;
|
|
368
|
+
}
|
|
369
|
+
return { textEnd: masked.length, end: masked.length, stop: 'eof' };
|
|
370
|
+
}
|
|
371
|
+
function splitTopLevel(text, sep) {
|
|
372
|
+
const parts = [];
|
|
373
|
+
let depth = 0;
|
|
374
|
+
let last = 0;
|
|
375
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
376
|
+
const ch = text.charAt(i);
|
|
377
|
+
if (ch === '(' || ch === '[' || ch === '{')
|
|
378
|
+
depth += 1;
|
|
379
|
+
else if (ch === ')' || ch === ']' || ch === '}')
|
|
380
|
+
depth -= 1;
|
|
381
|
+
else if (ch === sep && depth === 0) {
|
|
382
|
+
parts.push(text.slice(last, i));
|
|
383
|
+
last = i + 1;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
parts.push(text.slice(last));
|
|
387
|
+
return parts;
|
|
388
|
+
}
|
|
389
|
+
/** `a as b` -> the name the target exports and the name it lands under. */
|
|
390
|
+
function renamedEntry(entry) {
|
|
391
|
+
let text = entry.trim();
|
|
392
|
+
if (text === '')
|
|
393
|
+
return null;
|
|
394
|
+
const withoutType = text.replace(/^type\s+/, '');
|
|
395
|
+
text = withoutType;
|
|
396
|
+
const asAt = text.search(/\sas\s/);
|
|
397
|
+
if (asAt === -1)
|
|
398
|
+
return { local: text, exported: text };
|
|
399
|
+
const local = text.slice(0, asAt).trim();
|
|
400
|
+
const exported = text.slice(asAt + 4).trim();
|
|
401
|
+
if (local === '')
|
|
402
|
+
return null;
|
|
403
|
+
return { local, exported: exported === '' ? local : exported };
|
|
404
|
+
}
|
|
405
|
+
function importedName(entry) {
|
|
406
|
+
const parsed = renamedEntry(entry);
|
|
407
|
+
return parsed === null ? null : parsed.local;
|
|
408
|
+
}
|
|
409
|
+
function parseImportHead(head) {
|
|
410
|
+
let rest = head.trim();
|
|
411
|
+
let typeOnly = false;
|
|
412
|
+
if (/^type\b/.test(rest)) {
|
|
413
|
+
typeOnly = true;
|
|
414
|
+
rest = rest.slice(4).trim();
|
|
415
|
+
}
|
|
416
|
+
const names = [];
|
|
417
|
+
for (const part of splitTopLevel(rest, ',')) {
|
|
418
|
+
const clause = part.trim();
|
|
419
|
+
if (clause === '')
|
|
420
|
+
continue;
|
|
421
|
+
if (clause.startsWith('{')) {
|
|
422
|
+
const inner = clause.slice(1, clause.lastIndexOf('}'));
|
|
423
|
+
for (const entry of splitTopLevel(inner, ',')) {
|
|
424
|
+
const name = importedName(entry);
|
|
425
|
+
if (name !== null)
|
|
426
|
+
names.push(name);
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
names.push(clause.startsWith('*') ? '*' : 'default');
|
|
431
|
+
}
|
|
432
|
+
return { names, typeOnly };
|
|
433
|
+
}
|
|
434
|
+
function parseExportFromHead(head) {
|
|
435
|
+
let rest = head.trim();
|
|
436
|
+
let typeOnly = false;
|
|
437
|
+
if (/^type\b/.test(rest)) {
|
|
438
|
+
typeOnly = true;
|
|
439
|
+
rest = rest.slice(4).trim();
|
|
440
|
+
}
|
|
441
|
+
if (rest.startsWith('*')) {
|
|
442
|
+
const after = rest.slice(1).trim();
|
|
443
|
+
if (/^as\b/.test(after)) {
|
|
444
|
+
const alias = after.slice(2).trim();
|
|
445
|
+
return { importNames: ['*'], exportNames: alias === '' ? ['*'] : [alias], typeOnly };
|
|
446
|
+
}
|
|
447
|
+
return { importNames: ['*'], exportNames: ['*'], typeOnly };
|
|
448
|
+
}
|
|
449
|
+
if (rest.startsWith('{')) {
|
|
450
|
+
const inner = rest.slice(1, rest.lastIndexOf('}'));
|
|
451
|
+
const importNames = [];
|
|
452
|
+
const exportNames = [];
|
|
453
|
+
for (const entry of splitTopLevel(inner, ',')) {
|
|
454
|
+
const parsed = renamedEntry(entry);
|
|
455
|
+
if (parsed === null)
|
|
456
|
+
continue;
|
|
457
|
+
importNames.push(parsed.local);
|
|
458
|
+
exportNames.push(parsed.exported);
|
|
459
|
+
}
|
|
460
|
+
return { importNames, exportNames, typeOnly };
|
|
461
|
+
}
|
|
462
|
+
return { importNames: [], exportNames: [], typeOnly };
|
|
463
|
+
}
|
|
464
|
+
function unreadLabel(clauseText, stoppedAtBrace) {
|
|
465
|
+
return `${clauseText.trim()}${stoppedAtBrace ? ' {' : ''}`;
|
|
466
|
+
}
|
|
467
|
+
function parseExportDeclaration(head, clauseText, stoppedAtBrace, facts) {
|
|
468
|
+
let rest = head.trim();
|
|
469
|
+
let word = leadingWord(rest);
|
|
470
|
+
while (EXPORT_MODIFIERS.has(word)) {
|
|
471
|
+
rest = rest.slice(rest.indexOf(word) + word.length).trim();
|
|
472
|
+
word = leadingWord(rest);
|
|
473
|
+
}
|
|
474
|
+
if (word === 'default') {
|
|
475
|
+
facts.exports.push('default');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (word === 'const' || word === 'let' || word === 'var') {
|
|
479
|
+
const names = [];
|
|
480
|
+
for (const part of splitTopLevel(rest.slice(rest.indexOf(word) + word.length), ',')) {
|
|
481
|
+
const name = leadingWord(part);
|
|
482
|
+
if (name !== '')
|
|
483
|
+
names.push(name);
|
|
484
|
+
}
|
|
485
|
+
if (names.length > 0) {
|
|
486
|
+
facts.exports.push(...names);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
facts.unreadExports.push(unreadLabel(clauseText, stoppedAtBrace));
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (NAMED_DECLARATION_KEYWORDS.has(word)) {
|
|
493
|
+
let after = rest.slice(rest.indexOf(word) + word.length).trim();
|
|
494
|
+
if (after.startsWith('*'))
|
|
495
|
+
after = after.slice(1).trim();
|
|
496
|
+
const name = leadingWord(after);
|
|
497
|
+
if (name !== '') {
|
|
498
|
+
facts.exports.push(name);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
facts.unreadExports.push(unreadLabel(clauseText, stoppedAtBrace));
|
|
503
|
+
}
|
|
504
|
+
/** The depth-0 `from '...'` of a clause, if it has one. */
|
|
505
|
+
function findFromSpecifier(masked, from, to) {
|
|
506
|
+
let depth = 0;
|
|
507
|
+
let i = from;
|
|
508
|
+
while (i < to) {
|
|
509
|
+
const ch = masked.charAt(i);
|
|
510
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
511
|
+
depth += 1;
|
|
512
|
+
i += 1;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
516
|
+
depth -= 1;
|
|
517
|
+
i += 1;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (isIdentStart(ch)) {
|
|
521
|
+
const word = readIdent(masked, i);
|
|
522
|
+
if (depth === 0 && word === 'from') {
|
|
523
|
+
const quoteStart = skipSpace(masked, i + 4);
|
|
524
|
+
const quote = masked.charAt(quoteStart);
|
|
525
|
+
if (quoteStart < to && (quote === "'" || quote === '"')) {
|
|
526
|
+
const quoteEnd = masked.indexOf(quote, quoteStart + 1);
|
|
527
|
+
if (quoteEnd !== -1 && quoteEnd < to)
|
|
528
|
+
return { fromAt: i, quoteStart, quoteEnd };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
i += word.length;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
i += 1;
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
/** Whitespace is formatting, not signature: collapse it before comparing. */
|
|
539
|
+
function normalizeSignature(text) {
|
|
540
|
+
return text.trim().replace(/\s+/g, ' ');
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Record the declaration text against every export the clause introduced.
|
|
544
|
+
* Wrapping is what keeps this honest: the clause parsers below push export
|
|
545
|
+
* names from six different shapes, and one of them growing a seventh should
|
|
546
|
+
* not be able to silently drop its signature.
|
|
547
|
+
*/
|
|
548
|
+
function readClauseFacts(source, masked, start, clause, keyword, facts) {
|
|
549
|
+
const before = facts.exports.length;
|
|
550
|
+
readClauseExports(source, masked, start, clause, keyword, facts);
|
|
551
|
+
const signature = normalizeSignature(source.slice(start, clause.textEnd));
|
|
552
|
+
for (let i = before; i < facts.exports.length; i += 1) {
|
|
553
|
+
const name = facts.exports[i];
|
|
554
|
+
if (name !== undefined)
|
|
555
|
+
facts.exportSignatures.set(name, signature);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function readClauseExports(source, masked, start, clause, keyword, facts) {
|
|
559
|
+
const clauseText = source.slice(start, clause.textEnd);
|
|
560
|
+
const stoppedAtBrace = clause.stop === '{';
|
|
561
|
+
const headStart = start + keyword.length;
|
|
562
|
+
const spec = findFromSpecifier(masked, headStart, clause.textEnd);
|
|
563
|
+
if (spec !== null) {
|
|
564
|
+
const specifier = source.slice(spec.quoteStart + 1, spec.quoteEnd);
|
|
565
|
+
const head = masked.slice(headStart, spec.fromAt);
|
|
566
|
+
if (keyword === 'import') {
|
|
567
|
+
const { names, typeOnly } = parseImportHead(head);
|
|
568
|
+
facts.imports.push({ specifier, resolved: null, names, typeOnly, dynamic: false });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const { importNames, exportNames, typeOnly } = parseExportFromHead(head);
|
|
572
|
+
facts.imports.push({ specifier, resolved: null, names: importNames, typeOnly, dynamic: false });
|
|
573
|
+
facts.exports.push(...exportNames);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (keyword === 'import') {
|
|
577
|
+
// A side-effect import carries no symbol, but it is still an edge.
|
|
578
|
+
let quoteStart = skipSpace(masked, headStart);
|
|
579
|
+
if (readIdent(masked, quoteStart) === 'type')
|
|
580
|
+
quoteStart = skipSpace(masked, quoteStart + 4);
|
|
581
|
+
const quote = masked.charAt(quoteStart);
|
|
582
|
+
if (quote === "'" || quote === '"') {
|
|
583
|
+
const quoteEnd = masked.indexOf(quote, quoteStart + 1);
|
|
584
|
+
if (quoteEnd !== -1 && quoteEnd < clause.textEnd) {
|
|
585
|
+
facts.imports.push({
|
|
586
|
+
specifier: source.slice(quoteStart + 1, quoteEnd),
|
|
587
|
+
resolved: null,
|
|
588
|
+
names: [],
|
|
589
|
+
typeOnly: false,
|
|
590
|
+
dynamic: false,
|
|
591
|
+
});
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
facts.opaqueImports.push(clauseText.trim());
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const head = masked.slice(headStart, clause.textEnd);
|
|
599
|
+
const listHead = head.trim().replace(/^type\s+/, '');
|
|
600
|
+
if (listHead.startsWith('{')) {
|
|
601
|
+
for (const entry of splitTopLevel(listHead.slice(1, listHead.lastIndexOf('}')), ',')) {
|
|
602
|
+
const parsed = renamedEntry(entry);
|
|
603
|
+
if (parsed !== null)
|
|
604
|
+
facts.exports.push(parsed.exported);
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
parseExportDeclaration(head, clauseText, stoppedAtBrace, facts);
|
|
609
|
+
}
|
|
610
|
+
function readStatementClauses(source, masked, facts) {
|
|
611
|
+
let depth = 0;
|
|
612
|
+
let i = 0;
|
|
613
|
+
let prevChar = '';
|
|
614
|
+
// Start of file is a statement boundary; so is a line break, because ASI.
|
|
615
|
+
let sawNewline = true;
|
|
616
|
+
while (i < masked.length) {
|
|
617
|
+
const ch = masked.charAt(i);
|
|
618
|
+
if (ch === '\n') {
|
|
619
|
+
sawNewline = true;
|
|
620
|
+
i += 1;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (/\s/.test(ch)) {
|
|
624
|
+
i += 1;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
628
|
+
depth += 1;
|
|
629
|
+
prevChar = ch;
|
|
630
|
+
sawNewline = false;
|
|
631
|
+
i += 1;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
635
|
+
depth -= 1;
|
|
636
|
+
prevChar = ch;
|
|
637
|
+
sawNewline = false;
|
|
638
|
+
i += 1;
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (isIdentStart(ch)) {
|
|
642
|
+
const word = readIdent(masked, i);
|
|
643
|
+
const atBoundary = sawNewline || prevChar === '' || prevChar === ';' || prevChar === '{' || prevChar === '}';
|
|
644
|
+
const after = masked.charAt(skipSpace(masked, i + word.length));
|
|
645
|
+
const isDynamicOrMeta = word === 'import' && (after === '(' || after === '.');
|
|
646
|
+
if (depth === 0 &&
|
|
647
|
+
atBoundary &&
|
|
648
|
+
!isDynamicOrMeta &&
|
|
649
|
+
(word === 'import' || word === 'export')) {
|
|
650
|
+
const clause = readClause(masked, i, word);
|
|
651
|
+
readClauseFacts(source, masked, i, clause, word, facts);
|
|
652
|
+
i = clause.end;
|
|
653
|
+
prevChar = ';';
|
|
654
|
+
sawNewline = false;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
i += word.length;
|
|
658
|
+
prevChar = 'a';
|
|
659
|
+
sawNewline = false;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
prevChar = ch;
|
|
663
|
+
sawNewline = false;
|
|
664
|
+
i += 1;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function matchParen(masked, open) {
|
|
668
|
+
let depth = 0;
|
|
669
|
+
for (let i = open; i < masked.length; i += 1) {
|
|
670
|
+
const ch = masked.charAt(i);
|
|
671
|
+
if (ch === '(')
|
|
672
|
+
depth += 1;
|
|
673
|
+
else if (ch === ')') {
|
|
674
|
+
depth -= 1;
|
|
675
|
+
if (depth === 0)
|
|
676
|
+
return i;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return -1;
|
|
680
|
+
}
|
|
681
|
+
function readDynamicImports(source, masked, facts) {
|
|
682
|
+
const pattern = /\bimport\s*\(/g;
|
|
683
|
+
let match = pattern.exec(masked);
|
|
684
|
+
while (match !== null) {
|
|
685
|
+
const open = match.index + match[0].length - 1;
|
|
686
|
+
const close = matchParen(masked, open);
|
|
687
|
+
const inner = close === -1 ? masked.length : close;
|
|
688
|
+
const quoteStart = skipSpace(masked, open + 1);
|
|
689
|
+
const quote = masked.charAt(quoteStart);
|
|
690
|
+
const quoteEnd = quote === "'" || quote === '"' ? masked.indexOf(quote, quoteStart + 1) : -1;
|
|
691
|
+
if (quoteEnd !== -1 && quoteEnd < inner) {
|
|
692
|
+
facts.imports.push({
|
|
693
|
+
specifier: source.slice(quoteStart + 1, quoteEnd),
|
|
694
|
+
resolved: null,
|
|
695
|
+
names: ['*'],
|
|
696
|
+
typeOnly: false,
|
|
697
|
+
dynamic: true,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
facts.opaqueImports.push(`import(${source.slice(open + 1, inner).trim()})`);
|
|
702
|
+
}
|
|
703
|
+
pattern.lastIndex = close === -1 ? masked.length : close + 1;
|
|
704
|
+
match = pattern.exec(masked);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/** Read one file's imports and exports. Resolution is the graph's job, not this one's. */
|
|
708
|
+
export function parseModuleFacts(source, file) {
|
|
709
|
+
const facts = {
|
|
710
|
+
file,
|
|
711
|
+
exports: [],
|
|
712
|
+
imports: [],
|
|
713
|
+
exportSignatures: new Map(),
|
|
714
|
+
unreadExports: [],
|
|
715
|
+
opaqueImports: [],
|
|
716
|
+
unterminated: false,
|
|
717
|
+
};
|
|
718
|
+
const { masked, unterminated } = maskLiterals(source);
|
|
719
|
+
if (unterminated) {
|
|
720
|
+
facts.unterminated = true;
|
|
721
|
+
return facts;
|
|
722
|
+
}
|
|
723
|
+
readStatementClauses(source, masked, facts);
|
|
724
|
+
readDynamicImports(source, masked, facts);
|
|
725
|
+
return facts;
|
|
726
|
+
}
|
|
727
|
+
// ---------------------------------------------------------------------------
|
|
728
|
+
// Resolution
|
|
729
|
+
// ---------------------------------------------------------------------------
|
|
730
|
+
/** NodeNext writes `.js` and means the TypeScript source that compiles to it. */
|
|
731
|
+
const SOURCE_FOR_EMITTED = new Map([
|
|
732
|
+
['.js', ['.ts', '.tsx', '.js']],
|
|
733
|
+
['.jsx', ['.tsx', '.jsx']],
|
|
734
|
+
['.mjs', ['.mts', '.mjs']],
|
|
735
|
+
['.cjs', ['.cts', '.cjs']],
|
|
736
|
+
]);
|
|
737
|
+
/** The conventional emitted/authored pair, used to follow `dist/x.js` to `src/x.ts`. */
|
|
738
|
+
const BUILD_DIRECTORY = 'dist';
|
|
739
|
+
const SOURCE_DIRECTORY = 'src';
|
|
740
|
+
/**
|
|
741
|
+
* Extensions a relative import can carry that hold no compile-time symbols. An
|
|
742
|
+
* import of a stylesheet is a bundler instruction, not a module edge, so its
|
|
743
|
+
* absence from the graph is not a hole worth reporting.
|
|
744
|
+
*/
|
|
745
|
+
const SYMBOL_FREE_EXTENSIONS = new Set([
|
|
746
|
+
'.css',
|
|
747
|
+
'.scss',
|
|
748
|
+
'.sass',
|
|
749
|
+
'.less',
|
|
750
|
+
'.styl',
|
|
751
|
+
'.json',
|
|
752
|
+
'.json5',
|
|
753
|
+
'.yaml',
|
|
754
|
+
'.yml',
|
|
755
|
+
'.toml',
|
|
756
|
+
'.svg',
|
|
757
|
+
'.png',
|
|
758
|
+
'.jpg',
|
|
759
|
+
'.jpeg',
|
|
760
|
+
'.gif',
|
|
761
|
+
'.webp',
|
|
762
|
+
'.avif',
|
|
763
|
+
'.ico',
|
|
764
|
+
'.woff',
|
|
765
|
+
'.woff2',
|
|
766
|
+
'.ttf',
|
|
767
|
+
'.otf',
|
|
768
|
+
'.eot',
|
|
769
|
+
'.mp3',
|
|
770
|
+
'.mp4',
|
|
771
|
+
'.webm',
|
|
772
|
+
'.wav',
|
|
773
|
+
'.ogg',
|
|
774
|
+
'.txt',
|
|
775
|
+
'.md',
|
|
776
|
+
'.html',
|
|
777
|
+
'.wasm',
|
|
778
|
+
]);
|
|
779
|
+
const AUTHORED_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts'];
|
|
780
|
+
const EXTENSIONLESS_CANDIDATES = [
|
|
781
|
+
'.ts',
|
|
782
|
+
'.tsx',
|
|
783
|
+
'.mts',
|
|
784
|
+
'.cts',
|
|
785
|
+
'.js',
|
|
786
|
+
'.jsx',
|
|
787
|
+
'.mjs',
|
|
788
|
+
'.cjs',
|
|
789
|
+
];
|
|
790
|
+
const INDEX_NAMES = [
|
|
791
|
+
'index.ts',
|
|
792
|
+
'index.tsx',
|
|
793
|
+
'index.mts',
|
|
794
|
+
'index.cts',
|
|
795
|
+
'index.js',
|
|
796
|
+
'index.jsx',
|
|
797
|
+
];
|
|
798
|
+
/**
|
|
799
|
+
* Map a specifier onto a repo-relative file, or null. A bare specifier resolves
|
|
800
|
+
* to null by design: a package is not this repo, so it is out of scope rather
|
|
801
|
+
* than a failure. A *relative* specifier that resolves to null is a hole, and
|
|
802
|
+
* the graph records it as one.
|
|
803
|
+
*/
|
|
804
|
+
function candidatesFor(base) {
|
|
805
|
+
const candidates = [];
|
|
806
|
+
const ext = extname(base);
|
|
807
|
+
const emitted = SOURCE_FOR_EMITTED.get(ext);
|
|
808
|
+
if (emitted !== undefined) {
|
|
809
|
+
const stem = base.slice(0, base.length - ext.length);
|
|
810
|
+
for (const swapped of emitted)
|
|
811
|
+
candidates.push(stem + swapped);
|
|
812
|
+
}
|
|
813
|
+
else if (AUTHORED_EXTENSIONS.includes(ext)) {
|
|
814
|
+
candidates.push(base);
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
candidates.push(base);
|
|
818
|
+
for (const suffix of EXTENSIONLESS_CANDIDATES)
|
|
819
|
+
candidates.push(base + suffix);
|
|
820
|
+
}
|
|
821
|
+
for (const index of INDEX_NAMES)
|
|
822
|
+
candidates.push(`${base}/${index}`);
|
|
823
|
+
return candidates;
|
|
824
|
+
}
|
|
825
|
+
export function resolveSpecifier(fromFile, specifier, inScope) {
|
|
826
|
+
if (!specifier.startsWith('.'))
|
|
827
|
+
return null;
|
|
828
|
+
const base = join(dirname(fromFile), specifier);
|
|
829
|
+
for (const candidate of candidatesFor(base)) {
|
|
830
|
+
if (inScope.has(candidate))
|
|
831
|
+
return candidate;
|
|
832
|
+
}
|
|
833
|
+
// Build output stands in for the source that emits it. A module importing
|
|
834
|
+
// `../dist/paths.js` is coupled to `../src/paths.ts` just as tightly as if it
|
|
835
|
+
// had said so: change that signature and this importer breaks. The compiled
|
|
836
|
+
// copy is not in scope — it is generated — so follow the edge to the source.
|
|
837
|
+
if (base.includes(`/${BUILD_DIRECTORY}/`)) {
|
|
838
|
+
const asSource = base.replace(`/${BUILD_DIRECTORY}/`, `/${SOURCE_DIRECTORY}/`);
|
|
839
|
+
for (const candidate of candidatesFor(asSource)) {
|
|
840
|
+
if (inScope.has(candidate))
|
|
841
|
+
return candidate;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
// ---------------------------------------------------------------------------
|
|
847
|
+
// The graph
|
|
848
|
+
// ---------------------------------------------------------------------------
|
|
849
|
+
/**
|
|
850
|
+
* Build the graph from an in-memory corpus. Taking sources rather than a
|
|
851
|
+
* directory keeps this pure and testable; `collectSources` is the half that
|
|
852
|
+
* touches disk.
|
|
853
|
+
*/
|
|
854
|
+
export function buildSymbolGraph(sources) {
|
|
855
|
+
const inScope = new Set(sources.keys());
|
|
856
|
+
const modules = new Map();
|
|
857
|
+
const dependents = new Map();
|
|
858
|
+
const unanalyzed = [];
|
|
859
|
+
const unresolved = [];
|
|
860
|
+
const seenUnresolved = new Set();
|
|
861
|
+
const ordered = [...sources.keys()].sort();
|
|
862
|
+
for (const file of ordered) {
|
|
863
|
+
const source = sources.get(file);
|
|
864
|
+
if (source === undefined)
|
|
865
|
+
continue;
|
|
866
|
+
const facts = parseModuleFacts(source, file);
|
|
867
|
+
if (facts.unterminated) {
|
|
868
|
+
unanalyzed.push(file);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
for (const edge of facts.imports) {
|
|
872
|
+
edge.resolved = resolveSpecifier(file, edge.specifier, inScope);
|
|
873
|
+
if (edge.resolved === null) {
|
|
874
|
+
if (edge.specifier.startsWith('.') &&
|
|
875
|
+
!SYMBOL_FREE_EXTENSIONS.has(extname(edge.specifier))) {
|
|
876
|
+
const key = `${file}\u0000${edge.specifier}`;
|
|
877
|
+
if (!seenUnresolved.has(key)) {
|
|
878
|
+
seenUnresolved.add(key);
|
|
879
|
+
unresolved.push({ from: file, specifier: edge.specifier });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const list = dependents.get(edge.resolved) ?? [];
|
|
885
|
+
list.push({
|
|
886
|
+
from: file,
|
|
887
|
+
names: edge.names,
|
|
888
|
+
typeOnly: edge.typeOnly,
|
|
889
|
+
specifier: edge.specifier,
|
|
890
|
+
dynamic: edge.dynamic,
|
|
891
|
+
});
|
|
892
|
+
dependents.set(edge.resolved, list);
|
|
893
|
+
}
|
|
894
|
+
modules.set(file, facts);
|
|
895
|
+
}
|
|
896
|
+
return { modules, dependents, unanalyzed, unresolved };
|
|
897
|
+
}
|
|
898
|
+
// ---------------------------------------------------------------------------
|
|
899
|
+
// Reading a corpus off disk
|
|
900
|
+
// ---------------------------------------------------------------------------
|
|
901
|
+
export const DEFAULT_SOURCE_EXTENSIONS = [
|
|
902
|
+
'.ts',
|
|
903
|
+
'.tsx',
|
|
904
|
+
'.mts',
|
|
905
|
+
'.cts',
|
|
906
|
+
'.js',
|
|
907
|
+
'.jsx',
|
|
908
|
+
'.mjs',
|
|
909
|
+
'.cjs',
|
|
910
|
+
];
|
|
911
|
+
/** Directories whose contents are never the repo's own authored source. */
|
|
912
|
+
export const DEFAULT_SKIPPED_DIRECTORIES = [
|
|
913
|
+
'.git',
|
|
914
|
+
'node_modules',
|
|
915
|
+
'dist',
|
|
916
|
+
'build',
|
|
917
|
+
'coverage',
|
|
918
|
+
'workspaces',
|
|
919
|
+
'state',
|
|
920
|
+
'.venv',
|
|
921
|
+
'venv',
|
|
922
|
+
'__pycache__',
|
|
923
|
+
'.next',
|
|
924
|
+
'.turbo',
|
|
925
|
+
];
|
|
926
|
+
function readEntries(absolute) {
|
|
927
|
+
try {
|
|
928
|
+
return readdirSync(absolute, { withFileTypes: true });
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
// A directory that cannot be listed is a directory this graph cannot speak for.
|
|
932
|
+
return [];
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/** Read every authored source under `rootDir`, keyed by repo-relative path. */
|
|
936
|
+
export function collectSources(rootDir, options = {}) {
|
|
937
|
+
const extensions = options.extensions ?? DEFAULT_SOURCE_EXTENSIONS;
|
|
938
|
+
const skipped = new Set(options.skipDirectories ?? DEFAULT_SKIPPED_DIRECTORIES);
|
|
939
|
+
const sources = new Map();
|
|
940
|
+
const walk = (relativeDir) => {
|
|
941
|
+
const absolute = relativeDir === '' ? rootDir : `${rootDir}/${relativeDir}`;
|
|
942
|
+
for (const entry of readEntries(absolute)) {
|
|
943
|
+
const relative = relativeDir === '' ? entry.name : `${relativeDir}/${entry.name}`;
|
|
944
|
+
if (entry.isSymbolicLink())
|
|
945
|
+
continue;
|
|
946
|
+
if (entry.isDirectory()) {
|
|
947
|
+
if (skipped.has(entry.name))
|
|
948
|
+
continue;
|
|
949
|
+
walk(relative);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (!entry.isFile())
|
|
953
|
+
continue;
|
|
954
|
+
if (!extensions.some((suffix) => entry.name.endsWith(suffix)))
|
|
955
|
+
continue;
|
|
956
|
+
try {
|
|
957
|
+
sources.set(relative, readFileSync(`${rootDir}/${relative}`, 'utf8'));
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
// A file that cannot be read is not a file this graph can speak for.
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
walk('');
|
|
965
|
+
return sources;
|
|
966
|
+
}
|