ts-axi 0.1.1 → 0.1.3

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.
@@ -1,19 +1,34 @@
1
- import { expectPositionals, flag, intFlag, parseArgs } from "../args.js";
2
- import { createSemanticProject } from "../project.js";
1
+ import { relative, sep } from "node:path";
2
+ import { bool, expectPositionals, flag, intFlag, parseArgs } from "../args.js";
3
+ import { importerClosure } from "../importers.js";
4
+ import { findOutsideProject, nearestTsconfig } from "../project.js";
5
+ import { viaLine } from "../references.js";
3
6
  import { block, helpBlock, out } from "../render.js";
7
+ import { importerIndexFor, REFS_HOPS, scopeFields, scopeHelp, semanticFor } from "../scope.js";
8
+ import { byDepthThenPath } from "../walk.js";
9
+ import { detectWorkspace, isMultiProject } from "../workspace.js";
4
10
  import { isTestFile } from "../settings.js";
5
11
  import { isFunctionLike, kindFromScriptElement, parseTarget } from "../symbols.js";
6
- import { contextOf, declarationAt, filterSuffix, lineOf, matchRows, narrowHints, open, positionStartDir, readFilters, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, } from "./common.js";
12
+ import { contextOf, declarationAt, filterSuffix, lineOf, matchRows, narrowHints, open, positionStartDir, projectFields, readFilters, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, } from "./common.js";
7
13
  const DEFAULT_LIMIT = 50;
8
14
  const MAX_LIMIT = 500;
9
15
  const DEPTH_ROWS = 50;
10
- const SHARED_FLAGS = `--kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace> (narrows a name), --in <dir|glob> (repeatable; Declarations in these files), --exported, --fuzzy, --limit <1-${MAX_LIMIT}> (default ${DEFAULT_LIMIT} rows), --test-glob <glob> (repeatable; extra Test file pattern), -P <tsconfig>, --json`;
16
+ const SHARED_FLAGS = `--kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace> (narrows a name), --in <dir|glob> (repeatable; Declarations in these files), --exported, --fuzzy, --limit <1-${MAX_LIMIT}> (default ${DEFAULT_LIMIT} rows), --test-glob <glob> (repeatable; extra Test file pattern), --via (adds via rows with a via column), -P <tsconfig>, --json`;
17
+ const VIA_DOC = `via_interface: n (Iface.member) = References through an interface or base member the Symbol implements or overrides - they MAY reach it; not in refs_total
18
+ via_implementations: n (Impl.member) = References to implementations or overrides of the Symbol; not in refs_total
19
+ implementations: n = members that implement or override the Symbol (their Declarations are never refs)
20
+ matches: 0 under -P lists found_outside_project[n]{file,line} when the name is declared outside that Project
21
+ refs_total: 0 under -P in a workspace lists found_outside_project[n]{file,line} = importers outside that Project
22
+ scope = in a monorepo without -P the program is the declaring file + its importers (scope_partial: true when the importer scan was capped)
23
+ tsconfig: x (n referenced configs) = a references-only tsconfig expanded to the configs it references`;
11
24
  export const REFS_HELP = `usage: ts-axi refs <name | Class.member | path:line[:col]> [flags]
12
- flags[9]:
25
+ flags[11]:
13
26
  ${SHARED_FLAGS}
27
+ --implementations (lists implementations[n]{file,line,symbol} instead of the count)
14
28
  --kind calls = same as \`ts-axi callers\`
15
29
  output:
16
30
  symbol, kind, declared_at, refs_total, files, tests, refs[n]{file,line,context} (Declarations excluded), elapsed_ms
31
+ ${VIA_DOC}
17
32
  an ambiguous name lists matches[n] + the disambiguated forms; nothing is analysed
18
33
  examples[4]:
19
34
  ts-axi refs User.save
@@ -22,12 +37,13 @@ examples[4]:
22
37
  ts-axi refs runSync --limit 200
23
38
  `;
24
39
  export const CALLERS_HELP = `usage: ts-axi callers <name | Class.member | path:line[:col]> [flags]
25
- flags[10]:
40
+ flags[11]:
26
41
  ${SHARED_FLAGS}
27
42
  --depth <1-3> (default 1; levels > 1 print callers_depthN[n]{caller,file,count}, no locations)
28
43
  output:
29
44
  symbol, kind, declared_at, refs_total, files, tests, refs[n]{file,line,caller,context}, elapsed_ms
30
45
  caller = enclosing function or method; (top-level) for module code
46
+ ${VIA_DOC}
31
47
  examples[3]:
32
48
  ts-axi callers runSync
33
49
  ts-axi callers User.save --depth 2
@@ -45,7 +61,8 @@ async function run(args, ctx, invoked) {
45
61
  ...TARGET_FLAGS,
46
62
  { name: "--limit" },
47
63
  { name: "--test-glob", repeatable: true },
48
- ...(invoked === "calls" ? [{ name: "--depth" }] : []),
64
+ { name: "--via", boolean: true },
65
+ ...(invoked === "calls" ? [{ name: "--depth" }] : [{ name: "--implementations", boolean: true }]),
49
66
  ];
50
67
  const parsed = parseArgs(args, specs, command);
51
68
  const [raw] = expectPositionals(parsed, 1, `${command} <name | Class.member | path:line[:col]> [flags]`);
@@ -57,6 +74,7 @@ async function run(args, ctx, invoked) {
57
74
  const label = targetLabel(target);
58
75
  const project = flag(parsed, "-P");
59
76
  const name = mode === "calls" ? "callers" : "refs";
77
+ const show = { via: bool(parsed, "--via"), implementations: invoked === "refs" && bool(parsed, "--implementations") };
60
78
  const started = performance.now();
61
79
  const opened = open(ctx.cwd, parsed, positionStartDir(ctx.cwd, target));
62
80
  const warning = opened.warning ? { warning: opened.warning } : {};
@@ -65,6 +83,23 @@ async function run(args, ctx, invoked) {
65
83
  if (target.kind === "name") {
66
84
  const groups = resolveNameTarget(opened, target, f);
67
85
  if (groups.length === 0) {
86
+ // Under an explicit Project the name may live in a sibling one: say so before suggesting --fuzzy.
87
+ const outside = project || opened.settings.project
88
+ ? findOutsideProject(opened.index, { name: target.name, ...(target.member !== undefined ? { member: target.member } : {}), fuzzy: f.fuzzy })
89
+ : [];
90
+ if (outside.length > 0) {
91
+ const tsconfig = nearestTsconfig(opened.index.abs(outside[0].file), opened.root);
92
+ const rel = tsconfig ? relative(ctx.cwd, tsconfig).split(sep).join("/") : undefined;
93
+ return out(block({
94
+ ...warning,
95
+ matches: 0,
96
+ found_outside_project: outside.map((d) => ({ file: d.file, line: d.nameLine })),
97
+ hint: `symbol is declared outside the selected project - run with -P ${rel ?? "<its tsconfig>"} or without -P`,
98
+ }), helpBlock([
99
+ ...(rel ? [`Run \`ts-axi ${name} ${label} -P ${rel}\``] : []),
100
+ `Run \`ts-axi ${name} ${outside[0].file}:${outside[0].nameLine}\` to use the nearest tsconfig of that file`,
101
+ ]));
102
+ }
68
103
  const suggested = suggestions(opened, target);
69
104
  return out(block({ ...warning, matches: 0, ...(suggested.length > 0 ? { did_you_mean: suggested } : {}) }), helpBlock([
70
105
  ...suggested.slice(0, 2).map((s) => `Run \`ts-axi ${name} ${s}\``),
@@ -75,7 +110,7 @@ async function run(args, ctx, invoked) {
75
110
  return out(block({ ...warning, symbol: label, ambiguous: `${groups.length} Symbols - pick one`, matches: matchRows(groups) }), helpBlock(narrowHints(name, groups, f, `${label}${filterSuffix(f, project)}`)));
76
111
  }
77
112
  const decl = groups[0].decls[0];
78
- resolved = { absFile: opened.index.abs(decl.file), pos: decl.namePos, decl };
113
+ resolved = { absFile: opened.index.abs(decl.file), pos: decl.namePos, decl, seeds: [...new Set(groups[0].decls.map((d) => opened.index.abs(d.file)))] };
79
114
  }
80
115
  else {
81
116
  const at = resolvePosition(opened.index, ctx.cwd, target);
@@ -87,17 +122,62 @@ async function run(args, ctx, invoked) {
87
122
  }
88
123
  resolved = at.kind === "decl" ? { absFile: at.abs, pos: at.decl.namePos, decl: at.decl } : { absFile: at.abs, pos: at.pos };
89
124
  }
90
- const semantic = createSemanticProject(opened.index);
125
+ let built = semanticFor(opened, resolved.seeds ?? [resolved.absFile], resolved.decl ? REFS_HOPS : 0);
126
+ if (!resolved.decl && built.scope) {
127
+ // A use position: find its Declaration in a small program, then scope around the declaring file.
128
+ const at = built.semantic.definitionAt(resolved.absFile, resolved.pos);
129
+ if (at && at.line > 0)
130
+ resolved = { absFile: at.absFile, pos: at.pos };
131
+ built = semanticFor(opened, [resolved.absFile], REFS_HOPS);
132
+ }
133
+ const { semantic, scope } = built;
91
134
  const def = semantic.definitionAt(resolved.absFile, resolved.pos);
92
135
  const decl = resolved.decl ??
93
136
  (def && def.line > 0 ? declarationAt(opened.index, def.absFile, def.pos, kindFromScriptElement(def.kind)) : undefined);
94
137
  const kind = decl?.kind ?? kindFromScriptElement(def?.kind ?? semantic.quickInfo(resolved.absFile, resolved.pos)?.kind ?? "");
95
138
  const symbol = decl?.qualifiedName ?? def?.name ?? label;
96
139
  const declaredAt = decl ? `${decl.file}:${decl.nameLine}` : def && def.line > 0 ? `${def.file}:${def.line}` : "(outside the Project)";
97
- const header = { ...warning, symbol, kind, declared_at: declaredAt };
140
+ const header = { ...warning, ...projectFields(opened), symbol, kind, declared_at: declaredAt };
141
+ const explicit = Boolean(project || opened.settings.project);
142
+ const o = { started, symbol, declaredAt, show, scope, explicit, label, name, absFile: resolved.absFile };
98
143
  return mode === "calls"
99
- ? renderCallers(semantic, opened, resolved, header, { limit, depth, started, symbol, declaredAt })
100
- : renderRefs(semantic, opened, resolved, header, { limit, started, symbol, declaredAt, kind });
144
+ ? renderCallers(semantic, opened, resolved, header, { ...o, limit, depth })
145
+ : renderRefs(semantic, opened, resolved, header, { ...o, limit, kind });
146
+ }
147
+ const OUTSIDE_ROWS = 5;
148
+ /**
149
+ * No References under an explicit Project: importers of the declaring file (or of a barrel
150
+ * re-exporting it) elsewhere in the workspace, from the syntactic importer index.
151
+ */
152
+ function outsideImporters(opened, o) {
153
+ if (!o.explicit)
154
+ return { fields: {}, help: [] };
155
+ const workspace = detectWorkspace(opened.root);
156
+ if (!isMultiProject(workspace.kind))
157
+ return { fields: {}, help: [] };
158
+ const index = importerIndexFor(opened.index, workspace);
159
+ const closure = importerClosure(index, [o.absFile], REFS_HOPS);
160
+ const inProject = new Set(opened.index.files);
161
+ const barrels = new Set(closure.barrels);
162
+ const targets = [o.absFile, ...closure.barrels];
163
+ const needle = o.symbol.split(".").pop();
164
+ const rows = closure.importers
165
+ .filter((f) => !inProject.has(f) && !barrels.has(f) && (opened.index.text(f) ?? "").includes(needle))
166
+ .map((f) => {
167
+ const line = targets.flatMap((t) => index.importersOf(t)).find((i) => i.file === f)?.line ?? 1;
168
+ return { file: opened.index.rel(f), line };
169
+ })
170
+ .sort((a, b) => byDepthThenPath(a.file, b.file));
171
+ if (rows.length === 0)
172
+ return { fields: {}, help: [] };
173
+ return {
174
+ fields: {
175
+ found_outside_project: rows.slice(0, OUTSIDE_ROWS),
176
+ ...(rows.length > OUTSIDE_ROWS ? { found_outside_total: rows.length } : {}),
177
+ hint: `${rows.length} files outside the selected project import it - run without -P for workspace scope`,
178
+ },
179
+ help: [`Run \`ts-axi ${o.name} ${o.label}\` without -P to search the whole workspace`],
180
+ };
101
181
  }
102
182
  function sortRows(rows) {
103
183
  return rows.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.col - b.col);
@@ -113,32 +193,72 @@ function footer(semantic, started, touched) {
113
193
  const diagnostics = semantic.diagnosticsCount(touched);
114
194
  return { elapsed_ms: Math.round(performance.now() - started), ...(diagnostics > 0 ? { diagnostics } : {}) };
115
195
  }
196
+ /** `via_interface` / `via_implementations` counts, plus the hint to see their rows. */
197
+ function viaFields(via, bases) {
198
+ const base = via.filter((v) => v.through === "base");
199
+ const impl = via.filter((v) => v.through === "implementation");
200
+ return {
201
+ ...(bases.length > 0 || base.length > 0 ? { via_interface: viaLine(base.length, [...bases, ...base.map((v) => v.via)]) } : {}),
202
+ ...(impl.length > 0 ? { via_implementations: viaLine(impl.length, impl.map((v) => v.via)) } : {}),
203
+ };
204
+ }
205
+ /** Direct rows, plus via rows (with a `via` column) under `--via`. */
206
+ function mergedRows(direct, via, show) {
207
+ return show.via ? sortRows([...direct, ...via]) : direct;
208
+ }
116
209
  function renderRefs(semantic, opened, resolved, header, o) {
117
- const rows = sortRows(semantic.findReferences(resolved.absFile, resolved.pos).filter((r) => !r.isDefinition));
210
+ const split = semantic.referenceSplit(resolved.absFile, resolved.pos);
211
+ const rows = sortRows([...split.direct]);
212
+ const via = sortRows([...split.via]);
213
+ const impls = sortRows([...split.implementations]);
118
214
  const { files, tests, fileList } = summary(opened, rows);
119
- const shown = rows.slice(0, o.limit);
120
- const truncated = shown.length < rows.length;
215
+ const listed = mergedRows(rows, via, o.show);
216
+ const shown = listed.slice(0, o.limit);
217
+ const truncated = shown.length < listed.length;
121
218
  const help = [];
122
219
  if (truncated) {
123
220
  help.push(o.limit < MAX_LIMIT
124
- ? `Showing ${shown.length} of ${rows.length}; run \`ts-axi refs ${o.declaredAt} --limit ${Math.min(MAX_LIMIT, rows.length)}\` for more`
125
- : `Showing ${shown.length} of ${rows.length}; narrow by reading one file at a time`);
221
+ ? `Showing ${shown.length} of ${listed.length}; run \`ts-axi refs ${o.declaredAt}${o.show.via ? " --via" : ""} --limit ${Math.min(MAX_LIMIT, listed.length)}\` for more`
222
+ : `Showing ${shown.length} of ${listed.length}; narrow by reading one file at a time`);
126
223
  }
224
+ if (via.length > 0 && !o.show.via)
225
+ help.push(`Run \`ts-axi refs ${o.declaredAt} --via\` for the ${via.length} via References (${viaNames(via)})`);
226
+ if (impls.length > 0 && !o.show.implementations)
227
+ help.push(`Run \`ts-axi refs ${o.declaredAt} --implementations\` to list the ${impls.length} implementations`);
228
+ const outside = rows.length === 0 ? outsideImporters(opened, o) : { fields: {}, help: [] };
229
+ help.unshift(...outside.help);
127
230
  if (isFunctionLike(o.kind))
128
231
  help.push(`Run \`ts-axi callers ${o.declaredAt}\` for call sites with their enclosing function`);
129
232
  help.push(`Run \`ts-axi def ${o.declaredAt}\` for the declaration body`);
233
+ help.push(...scopeHelp(o.scope));
130
234
  return out(block({
131
235
  ...header,
132
236
  refs_total: rows.length,
133
237
  files,
134
238
  tests,
135
- ...(rows.length === 0
239
+ ...viaFields(via, split.bases),
240
+ ...(impls.length > 0
241
+ ? { implementations: o.show.implementations ? impls.map((r) => ({ file: r.file, line: r.line, symbol: r.name })) : impls.length }
242
+ : {}),
243
+ ...(shown.length === 0
136
244
  ? {}
137
- : { refs: shown.map((r) => ({ file: r.file, line: r.line, context: contextCell(opened, r) })) }),
245
+ : {
246
+ refs: shown.map((r) => ({
247
+ file: r.file,
248
+ line: r.line,
249
+ ...(o.show.via ? { via: r.via ?? "-" } : {}),
250
+ context: contextCell(opened, r),
251
+ })),
252
+ }),
138
253
  ...(truncated ? { truncated: true } : {}),
254
+ ...outside.fields,
255
+ ...scopeFields(o.scope),
139
256
  ...footer(semantic, o.started, fileList),
140
257
  }), helpBlock(help));
141
258
  }
259
+ function viaNames(via) {
260
+ return [...new Set(via.map((v) => v.via))].join("; ");
261
+ }
142
262
  function itemKey(site) {
143
263
  return `${site.item.file}#${site.item.selectionSpan.start}`;
144
264
  }
@@ -170,15 +290,17 @@ function depthRows(sites) {
170
290
  function renderCallers(semantic, opened, resolved, header, o) {
171
291
  const sites = semantic.incomingCalls(resolved.absFile, resolved.pos);
172
292
  if (sites === undefined) {
173
- return out(block({ ...header, refs_total: 0, note: "not callable - no call hierarchy for this Symbol", ...footer(semantic, o.started, []) }), helpBlock([`Run \`ts-axi refs ${o.declaredAt}\` for every Reference`]));
293
+ return out(block({ ...header, refs_total: 0, note: "not callable - no call hierarchy for this Symbol", ...scopeFields(o.scope), ...footer(semantic, o.started, []) }), helpBlock([`Run \`ts-axi refs ${o.declaredAt}\` for every Reference`]));
174
294
  }
175
- const rows = sortRows([...sites]);
295
+ const rows = sortRows(sites.filter((s) => s.via === undefined));
296
+ const via = sortRows(sites.filter((s) => s.via !== undefined));
176
297
  const { files, tests, fileList } = summary(opened, rows);
177
- const shown = rows.slice(0, o.limit);
178
- const truncated = shown.length < rows.length;
298
+ const listed = mergedRows(rows, via, o.show);
299
+ const shown = listed.slice(0, o.limit);
300
+ const truncated = shown.length < listed.length;
179
301
  const levels = {};
180
302
  const visited = new Set();
181
- let level = rows;
303
+ let level = listed;
182
304
  for (let d = 2; d <= o.depth; d++) {
183
305
  level = nextLevel(semantic, level, visited);
184
306
  const agg = depthRows(level);
@@ -190,22 +312,38 @@ function renderCallers(semantic, opened, resolved, header, o) {
190
312
  }
191
313
  const help = [];
192
314
  if (truncated) {
193
- help.push(`Showing ${shown.length} of ${rows.length}; run \`ts-axi callers ${o.declaredAt} --limit ${Math.min(MAX_LIMIT, rows.length)}\` for more`);
315
+ help.push(`Showing ${shown.length} of ${listed.length}; run \`ts-axi callers ${o.declaredAt}${o.show.via ? " --via" : ""} --limit ${Math.min(MAX_LIMIT, listed.length)}\` for more`);
194
316
  }
317
+ if (via.length > 0 && !o.show.via)
318
+ help.push(`Run \`ts-axi callers ${o.declaredAt} --via\` for the ${via.length} via calls (${viaNames(via)})`);
195
319
  const top = shown.find((r) => r.caller !== "(top-level)");
196
320
  if (top && o.depth === 1)
197
321
  help.push(`Run \`ts-axi callers ${o.declaredAt} --depth 2\` for who calls the callers`);
322
+ const outside = rows.length === 0 ? outsideImporters(opened, o) : { fields: {}, help: [] };
323
+ help.unshift(...outside.help);
198
324
  help.push(`Run \`ts-axi refs ${o.declaredAt}\` for every Reference (not only calls)`);
325
+ help.push(...scopeHelp(o.scope));
199
326
  return out(block({
200
327
  ...header,
201
328
  refs_total: rows.length,
202
329
  files,
203
330
  tests,
204
- ...(rows.length === 0
331
+ ...viaFields(via, []),
332
+ ...(shown.length === 0
205
333
  ? {}
206
- : { refs: shown.map((r) => ({ file: r.file, line: r.line, caller: r.caller, context: contextCell(opened, r) })) }),
334
+ : {
335
+ refs: shown.map((r) => ({
336
+ file: r.file,
337
+ line: r.line,
338
+ caller: r.caller,
339
+ ...(o.show.via ? { via: r.via ?? "-" } : {}),
340
+ context: contextCell(opened, r),
341
+ })),
342
+ }),
207
343
  ...(truncated ? { truncated: true } : {}),
208
344
  ...levels,
345
+ ...outside.fields,
346
+ ...scopeFields(o.scope),
209
347
  ...footer(semantic, o.started, fileList),
210
348
  }), helpBlock(help));
211
349
  }
@@ -0,0 +1,182 @@
1
+ // Syntactic reverse-import graph (regex over file text, no AST): which workspace files import
2
+ // a given file, through relative paths, `paths` aliases, workspace package names and barrels.
3
+ import { dirname, resolve } from "node:path";
4
+ const PATTERNS = [
5
+ { re: /\bexport\s+(?:type\s+)?(?:\*(?:\s+as\s+[\w$]+)?|\{[^}]*\})\s*from\s*(['"])([^'"\n]+)\1/g, reexport: true },
6
+ { re: /\bimport\s+(?:type\s+)?[^'";]*?\bfrom\s*(['"])([^'"\n]+)\1/g, reexport: false },
7
+ { re: /\bimport\s*(['"])([^'"\n]+)\1/g, reexport: false },
8
+ { re: /\b(?:require|import|(?:vi|jest)\.(?:mock|doMock|requireActual|importActual))\s*\(\s*(['"])([^'"\n]+)\1/g, reexport: false },
9
+ ];
10
+ /** Static specifiers of `import`/`export ... from`, side-effect imports, `require()`, `import()`, `vi.mock()`. */
11
+ export function extractImports(text) {
12
+ if (!/\b(?:import|export|require|mock)\b/.test(text))
13
+ return [];
14
+ const edges = new Map();
15
+ let lineStarts;
16
+ const lineAt = (offset) => {
17
+ lineStarts ??= [0, ...[...text.matchAll(/\n/g)].map((m) => m.index + 1)];
18
+ let lo = 0;
19
+ let hi = lineStarts.length - 1;
20
+ while (lo < hi) {
21
+ const mid = (lo + hi + 1) >> 1;
22
+ if (lineStarts[mid] <= offset)
23
+ lo = mid;
24
+ else
25
+ hi = mid - 1;
26
+ }
27
+ return lo + 1;
28
+ };
29
+ for (const { re, reexport } of PATTERNS) {
30
+ for (const m of text.matchAll(re)) {
31
+ const specOffset = m.index + m[0].lastIndexOf(m[2]);
32
+ // A position matched by an earlier (re-export) pattern keeps that classification.
33
+ if (!edges.has(specOffset))
34
+ edges.set(specOffset, { spec: m[2], reexport, line: lineAt(specOffset) });
35
+ }
36
+ }
37
+ return [...edges.entries()].sort((a, b) => a[0] - b[0]).map(([, e]) => e);
38
+ }
39
+ const EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
40
+ /** Resolves a specifier to a file of `files` (absolute paths); anything else (node_modules, builtins) is undefined. */
41
+ export function createResolver(input, files) {
42
+ const tryPath = (p, depth = 0) => {
43
+ if (files.has(p))
44
+ return p;
45
+ const stem = p.replace(/\.(?:d\.)?[mc]?[jt]sx?$/, "");
46
+ for (const ext of EXTENSIONS)
47
+ if (files.has(stem + ext))
48
+ return stem + ext;
49
+ for (const ext of EXTENSIONS)
50
+ if (files.has(`${p}/index${ext}`))
51
+ return `${p}/index${ext}`;
52
+ // Build output in a package manifest: look for the source next to it.
53
+ if (depth === 0 && /[\\/](?:dist|build|lib|out)[\\/]/.test(p))
54
+ return tryPath(p.replace(/([\\/])(?:dist|build|lib|out)([\\/])/, "$1src$2"), 1);
55
+ return undefined;
56
+ };
57
+ const aliases = Object.entries(input.paths?.paths ?? {})
58
+ .map(([pattern, targets]) => {
59
+ const star = pattern.indexOf("*");
60
+ return { pattern, targets, prefix: star < 0 ? pattern : pattern.slice(0, star), suffix: star < 0 ? "" : pattern.slice(star + 1), wildcard: star >= 0 };
61
+ })
62
+ // Exact patterns first, then the longest prefix (TypeScript's order).
63
+ .sort((a, b) => Number(a.wildcard) - Number(b.wildcard) || b.prefix.length - a.prefix.length);
64
+ const aliasBase = input.paths?.base ?? "";
65
+ const fromAlias = (spec) => {
66
+ for (const a of aliases) {
67
+ let captured;
68
+ if (!a.wildcard) {
69
+ if (spec !== a.pattern)
70
+ continue;
71
+ captured = "";
72
+ }
73
+ else {
74
+ if (!spec.startsWith(a.prefix) || !spec.endsWith(a.suffix) || spec.length < a.prefix.length + a.suffix.length)
75
+ continue;
76
+ captured = spec.slice(a.prefix.length, spec.length - a.suffix.length);
77
+ }
78
+ for (const target of a.targets) {
79
+ const hit = tryPath(resolve(aliasBase, target.replace("*", captured)));
80
+ if (hit)
81
+ return hit;
82
+ }
83
+ }
84
+ return undefined;
85
+ };
86
+ const packages = [...input.packages].sort((a, b) => b.name.length - a.name.length);
87
+ const fromPackage = (spec) => {
88
+ const pkg = packages.find((p) => spec === p.name || spec.startsWith(`${p.name}/`));
89
+ if (!pkg)
90
+ return undefined;
91
+ const sub = spec === pkg.name ? "." : `./${spec.slice(pkg.name.length + 1)}`;
92
+ const candidates = [...(pkg.exports[sub] ?? [])];
93
+ for (const [key, targets] of Object.entries(pkg.exports)) {
94
+ const star = key.indexOf("*");
95
+ if (star < 0 || !sub.startsWith(key.slice(0, star)) || !sub.endsWith(key.slice(star + 1)))
96
+ continue;
97
+ const captured = sub.slice(star, sub.length - (key.length - star - 1));
98
+ candidates.push(...targets.map((t) => t.replace("*", captured)));
99
+ }
100
+ if (sub !== ".")
101
+ candidates.push(resolve(pkg.dir, sub), resolve(pkg.dir, "src", sub));
102
+ for (const c of candidates) {
103
+ const hit = tryPath(c);
104
+ if (hit)
105
+ return hit;
106
+ }
107
+ return undefined;
108
+ };
109
+ return (spec, fromFile) => {
110
+ if (spec.startsWith(".") || spec.startsWith("/"))
111
+ return tryPath(resolve(dirname(fromFile), spec));
112
+ return fromAlias(spec) ?? fromPackage(spec);
113
+ };
114
+ }
115
+ export const IMPORTER_MAX_FILES = 50_000;
116
+ export const IMPORTER_MAX_MS = 3_000;
117
+ /** Reads every candidate once (bounded) and inverts its resolved imports. */
118
+ export function buildImporterIndex(files, read, resolveSpec, options = {}) {
119
+ const maxFiles = options.maxFiles ?? IMPORTER_MAX_FILES;
120
+ const deadline = performance.now() + (options.maxMs ?? IMPORTER_MAX_MS);
121
+ const reverse = new Map();
122
+ let scanned = 0;
123
+ let partial = false;
124
+ for (const file of files) {
125
+ if (scanned >= maxFiles || performance.now() > deadline) {
126
+ partial = true;
127
+ break;
128
+ }
129
+ scanned++;
130
+ const text = read(file);
131
+ if (text === undefined)
132
+ continue;
133
+ const seen = new Set();
134
+ for (const edge of extractImports(text)) {
135
+ const target = resolveSpec(edge.spec, file);
136
+ if (!target || target === file)
137
+ continue;
138
+ const key = `${target}#${edge.reexport}`;
139
+ if (seen.has(key))
140
+ continue;
141
+ seen.add(key);
142
+ const list = reverse.get(target) ?? [];
143
+ list.push({ file, reexport: edge.reexport, line: edge.line });
144
+ reverse.set(target, list);
145
+ }
146
+ }
147
+ return { scanned, total: files.length, partial, importersOf: (file) => reverse.get(file) ?? [] };
148
+ }
149
+ /**
150
+ * Importers of `seeds` up to `hops` levels; a re-export (barrel) does not use a hop, so
151
+ * importers of a barrel that re-exports a seed count as direct importers.
152
+ */
153
+ export function importerClosure(index, seeds, hops) {
154
+ const seedSet = new Set(seeds);
155
+ const importers = new Set();
156
+ const barrels = new Set();
157
+ let frontier = [...seeds];
158
+ const expanded = new Set(seeds);
159
+ for (let hop = 0; hop < hops && frontier.length > 0; hop++) {
160
+ const next = [];
161
+ const queue = [...frontier];
162
+ while (queue.length > 0) {
163
+ const file = queue.pop();
164
+ for (const imp of index.importersOf(file)) {
165
+ if (seedSet.has(imp.file))
166
+ continue;
167
+ importers.add(imp.file);
168
+ if (imp.reexport)
169
+ barrels.add(imp.file);
170
+ if (expanded.has(imp.file))
171
+ continue;
172
+ expanded.add(imp.file);
173
+ if (imp.reexport)
174
+ queue.push(imp.file);
175
+ else
176
+ next.push(imp.file);
177
+ }
178
+ }
179
+ frontier = next;
180
+ }
181
+ return { importers: [...importers].sort(), barrels: [...barrels].sort() };
182
+ }