ts-axi 0.1.2 → 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.
- package/README.md +15 -1
- package/dist/src/commands/common.js +10 -1
- package/dist/src/commands/def.js +2 -2
- package/dist/src/commands/home.js +35 -43
- package/dist/src/commands/impact.js +13 -22
- package/dist/src/commands/refs.js +71 -9
- package/dist/src/importers.js +182 -0
- package/dist/src/project.js +110 -73
- package/dist/src/scope.js +119 -0
- package/dist/src/walk.js +96 -0
- package/dist/src/workspace.js +297 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,6 +75,20 @@ npx skills add cmosgh/ts-axi --skill ts-axi
|
|
|
75
75
|
|
|
76
76
|
<!-- TODO(phase 4): paste real examples for def / refs / outline / config from the fixture project. -->
|
|
77
77
|
|
|
78
|
+
## Monorepos
|
|
79
|
+
|
|
80
|
+
No configuration and no new files in your repo: ts-axi detects the layout from what is already there - `nx.json` (projects = `project.json` dirs, or a legacy `workspace.json`), `pnpm-workspace.yaml`, `package.json` `workspaces`, or a root `tsconfig.json` that only holds `references`. The home view prints `workspace: nx (14 projects)`.
|
|
81
|
+
|
|
82
|
+
Without `-P`, `refs`, `callers` and `impact` do not type-check the whole repo. They build the program from a subgraph: the declaring file (for `impact`, the touched files) plus every file that imports it, found by a fast text scan of import specifiers that follows `tsconfig.base.json` `paths`, workspace package names and `export * from` barrels. A caller in another project is found; the cost tracks the subgraph, not the repo. Each result says what was analysed:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
scope: nx workspace - 30 of 5433 files (seeds 1; importers 29)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`scope_partial: true` means the importer scan stopped at its cap (50k files or 3 s); the help line says what is missing. `-P <tsconfig>` still analyses exactly one project; when it finds nothing, `found_outside_project` lists the importers outside it. A `-P` tsconfig that only holds `references` (an Nx project's `tsconfig.json`) is expanded to the configs it references: `tsconfig: libs/shared/tsconfig.json (2 referenced configs)`.
|
|
89
|
+
|
|
90
|
+
Files are listed with `git ls-files` (tracked plus untracked, gitignore applied), so nested worktrees and ignored copies are never analysed. Outside git, a directory walk skips `node_modules`, build output, `.claude`, `.cursor`, `.idea`, `.vscode` and any nested checkout.
|
|
91
|
+
|
|
78
92
|
## Test files
|
|
79
93
|
|
|
80
94
|
A file counts as a test by path pattern. Defaults: `**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`, `**/test/**`, `**/tests/**`, `**/e2e/**`. The home view shows `test_globs` and `test_files` so a wrong rule is visible.
|
|
@@ -100,7 +114,7 @@ Any other key is a `VALIDATION_ERROR` naming the valid ones. `--test-glob <glob>
|
|
|
100
114
|
One dependency: the `typescript` compiler API (ADR in `docs/adr/0001-typescript-language-service-engine.md`).
|
|
101
115
|
|
|
102
116
|
- `outline`, `def` and the home view only parse files (`ts.createSourceFile`): no tsconfig needed, well under a second.
|
|
103
|
-
- `refs`, `callers` and `impact` build a `LanguageService` over the nearest `tsconfig.json` (or an implicit project of every source file when none exists, with a warning) and ask it for references and call hierarchy. Symbol identity comes from the checker, so `user.save` resolves to `User.save` through imports and re-exports.
|
|
117
|
+
- `refs`, `callers` and `impact` build a `LanguageService` over the nearest `tsconfig.json` (or an implicit project of every source file when none exists, with a warning; in a monorepo or an implicit project of more than 3000 files, over the importer subgraph described above) and ask it for references and call hierarchy. Symbol identity comes from the checker, so `user.save` resolves to `User.save` through imports and re-exports.
|
|
104
118
|
- `impact` reads the "before" version of touched files with `git show` and compares signatures to classify each change (`added`, `removed`, `signature`, `body`, `type`, `modified`). Uses it cannot follow (access through `any`, dynamic string keys) are reported as `blind_spots`, never silently dropped.
|
|
105
119
|
- Type errors in the analysed project never block anything; they are reported as a `diagnostics` count.
|
|
106
120
|
|
|
@@ -8,6 +8,7 @@ import { usageError } from "../errors.js";
|
|
|
8
8
|
import { hasGlobMagic, matchesGlob } from "../glob.js";
|
|
9
9
|
import { openProject } from "../project.js";
|
|
10
10
|
import { loadSettings } from "../settings.js";
|
|
11
|
+
import { byDepthThenPath } from "../walk.js";
|
|
11
12
|
import { clip, didYouMean, groupSymbols, KINDS, kindOf, signatureOf, } from "../symbols.js";
|
|
12
13
|
export const BODY_LINES = 60;
|
|
13
14
|
export const CONTEXT_MAX = 100;
|
|
@@ -71,7 +72,15 @@ export function resolveNameTarget(ctx, target, f) {
|
|
|
71
72
|
...(target.member !== undefined ? { member: target.member } : {}),
|
|
72
73
|
fuzzy: f.fuzzy,
|
|
73
74
|
});
|
|
74
|
-
|
|
75
|
+
// Shallowest path first: the canonical copy before nested duplicates.
|
|
76
|
+
return groupSymbols(applyFilters(ctx, found, f)).sort((a, b) => byDepthThenPath(a.primary.file, b.primary.file) || a.decls[0].nameLine - b.decls[0].nameLine);
|
|
77
|
+
}
|
|
78
|
+
/** `tsconfig: x (n referenced configs)` when a references-only tsconfig was expanded. */
|
|
79
|
+
export function projectFields(ctx) {
|
|
80
|
+
const { tsconfig, referenced } = ctx.index.project;
|
|
81
|
+
if (!tsconfig || referenced === undefined)
|
|
82
|
+
return {};
|
|
83
|
+
return { tsconfig: `${relative(ctx.cwd, tsconfig).split(sep).join("/")} (${referenced} referenced configs)` };
|
|
75
84
|
}
|
|
76
85
|
export function suggestions(ctx, target) {
|
|
77
86
|
const names = [...ctx.index.allNames()].filter((n) => (target.member === undefined ? !n.includes(".") : n.includes(".")));
|
package/dist/src/commands/def.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bool, expectPositionals, flag, parseArgs } from "../args.js";
|
|
2
|
-
import {
|
|
2
|
+
import { semanticFor } from "../scope.js";
|
|
3
3
|
import { block, fenced, helpBlock, out } from "../render.js";
|
|
4
4
|
import { isFunctionLike, kindFromScriptElement, parseTarget } from "../symbols.js";
|
|
5
5
|
import { BODY_LINES, declarationAt, filterSuffix, matchRows, narrowHints, numberedLines, open, positionStartDir, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, readFilters, } from "./common.js";
|
|
@@ -63,7 +63,7 @@ export async function defCommand(args, ctx) {
|
|
|
63
63
|
return out(block({ position: label, identifiers: at.candidates }), helpBlock(at.candidates.slice(0, 3).map((c) => `Run \`ts-axi def ${at.file}:${c.line}:${c.col}\` for ${c.name}`)));
|
|
64
64
|
}
|
|
65
65
|
// A reference, not a Declaration: ask the checker where it is declared.
|
|
66
|
-
const semantic =
|
|
66
|
+
const { semantic } = semanticFor(opened, [at.abs], 0);
|
|
67
67
|
const def = semantic.definitionAt(at.abs, at.pos);
|
|
68
68
|
const decl = def && def.line > 0 ? declarationAt(opened.index, def.absFile, def.pos, kindFromScriptElement(def.kind)) : undefined;
|
|
69
69
|
if (!decl) {
|
|
@@ -5,8 +5,9 @@ import { usageError } from "../errors.js";
|
|
|
5
5
|
import { currentBranch, dirtyFileCount, isGitRepo } from "../git.js";
|
|
6
6
|
import { block, helpBlock, out } from "../render.js";
|
|
7
7
|
import { SETTINGS_FILE, isTestFile, loadSettings, readSettingsFile } from "../settings.js";
|
|
8
|
+
import { listFiles, skipDir } from "../walk.js";
|
|
9
|
+
import { detectWorkspace, hasMultiProjectSignal, isMultiProject } from "../workspace.js";
|
|
8
10
|
const SOURCE_EXT = /\.(?:[cm]?[jt]s|[jt]sx)$/;
|
|
9
|
-
const SKIP_DIRS = new Set(["node_modules", "dist", "build", ".git"]);
|
|
10
11
|
/** The view runs at session start: bound the file walk by count and time. */
|
|
11
12
|
const WALK_CAP = 20_000;
|
|
12
13
|
const WALK_DEADLINE_MS = 600;
|
|
@@ -15,8 +16,8 @@ const HOME_SPECS = [
|
|
|
15
16
|
{ name: "--test-glob", repeatable: true },
|
|
16
17
|
{ name: "--project", alias: "-P" },
|
|
17
18
|
];
|
|
18
|
-
/** Does `dir` hold a TS/JS source within `depth` levels (
|
|
19
|
-
function hasSources(dir, depth) {
|
|
19
|
+
/** Does `dir` hold a TS/JS source within `depth` levels (shared skip rule)? */
|
|
20
|
+
function hasSources(dir, depth, root = dir) {
|
|
20
21
|
let entries;
|
|
21
22
|
try {
|
|
22
23
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -30,36 +31,11 @@ function hasSources(dir, depth) {
|
|
|
30
31
|
}
|
|
31
32
|
if (depth === 0)
|
|
32
33
|
return false;
|
|
33
|
-
return entries.some((e) => e.isDirectory() && e.name
|
|
34
|
+
return entries.some((e) => e.isDirectory() && !skipDir(e.name, join(dir, e.name), root) && hasSources(join(dir, e.name), depth - 1, root));
|
|
34
35
|
}
|
|
35
|
-
/** Implicit project: every source below `root
|
|
36
|
+
/** Implicit project: every source below `root` from the shared listing (git, else a bounded fs walk). */
|
|
36
37
|
function walkSources(root) {
|
|
37
|
-
|
|
38
|
-
const deadline = Date.now() + WALK_DEADLINE_MS;
|
|
39
|
-
let capped = false;
|
|
40
|
-
const stack = [root];
|
|
41
|
-
while (stack.length > 0 && !capped) {
|
|
42
|
-
const dir = stack.pop();
|
|
43
|
-
let entries;
|
|
44
|
-
try {
|
|
45
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
for (const entry of entries) {
|
|
51
|
-
if (entry.isDirectory()) {
|
|
52
|
-
if (!SKIP_DIRS.has(entry.name))
|
|
53
|
-
stack.push(join(dir, entry.name));
|
|
54
|
-
}
|
|
55
|
-
else if (entry.isFile() && SOURCE_EXT.test(entry.name)) {
|
|
56
|
-
files.push(join(dir, entry.name));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (files.length >= WALK_CAP || Date.now() > deadline)
|
|
60
|
-
capped = true;
|
|
61
|
-
}
|
|
62
|
-
return { files, capped };
|
|
38
|
+
return listFiles(root, (name) => SOURCE_EXT.test(name) && !/\.d\.[mc]?ts$/.test(name), { maxFiles: WALK_CAP, deadline: Date.now() + WALK_DEADLINE_MS });
|
|
63
39
|
}
|
|
64
40
|
function findTsconfig(cwd) {
|
|
65
41
|
let dir = cwd;
|
|
@@ -73,14 +49,16 @@ function findTsconfig(cwd) {
|
|
|
73
49
|
dir = parent;
|
|
74
50
|
}
|
|
75
51
|
}
|
|
76
|
-
async function tsconfigFiles(tsconfig) {
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
52
|
+
async function tsconfigFiles(tsconfig, root) {
|
|
53
|
+
// Loaded lazily: the TypeScript module is only needed when a tsconfig exists.
|
|
54
|
+
const { loadProject } = await import("../project.js");
|
|
55
|
+
try {
|
|
56
|
+
const project = loadProject(root, { path: tsconfig, implicit: false });
|
|
57
|
+
return { files: project.fileNames, capped: false, ...(project.referenced !== undefined ? { referenced: project.referenced } : {}) };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
80
60
|
return { files: [], capped: false };
|
|
81
|
-
|
|
82
|
-
const files = parsed.fileNames.filter((f) => !f.split(/[\\/]/).includes("node_modules"));
|
|
83
|
-
return { files, capped: false };
|
|
61
|
+
}
|
|
84
62
|
}
|
|
85
63
|
function rel(root, path) {
|
|
86
64
|
return relative(root, path).split(sep).join("/") || ".";
|
|
@@ -112,17 +90,24 @@ function resolveTsconfig(cwd, settings) {
|
|
|
112
90
|
}
|
|
113
91
|
return findTsconfig(cwd);
|
|
114
92
|
}
|
|
115
|
-
export async function homeCommand(args) {
|
|
93
|
+
export async function homeCommand(args, ctx) {
|
|
116
94
|
const parsed = parseArgs(args, HOME_SPECS, "ts-axi");
|
|
117
95
|
expectPositionals(parsed, 0, "ts-axi [-P <tsconfig>] [--test-glob <glob>]");
|
|
118
|
-
const cwd = process.cwd();
|
|
96
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
119
97
|
const project = flag(parsed, "--project");
|
|
120
98
|
const settings = loadSettings(cwd, {
|
|
121
99
|
testGlobs: flags(parsed, "--test-glob"),
|
|
122
100
|
...(project !== undefined ? { project } : {}),
|
|
123
101
|
});
|
|
124
102
|
const tsconfig = resolveTsconfig(cwd, settings);
|
|
125
|
-
|
|
103
|
+
// Same rule as the semantic commands: no explicit project + a multi-project workspace = workspace scope.
|
|
104
|
+
let workspace;
|
|
105
|
+
if (settings.project === undefined && (tsconfig === undefined || hasMultiProjectSignal(settings.root))) {
|
|
106
|
+
const detected = detectWorkspace(settings.root);
|
|
107
|
+
if (isMultiProject(detected.kind))
|
|
108
|
+
workspace = detected;
|
|
109
|
+
}
|
|
110
|
+
if (tsconfig === undefined && workspace === undefined && !hasSources(cwd, PROBE_DEPTH)) {
|
|
126
111
|
return out(block({ project: "none" }), helpBlock([
|
|
127
112
|
"Run `ts-axi outline <file>` on any .ts/.js file; it needs no project",
|
|
128
113
|
"Run `ts-axi --help` for all commands",
|
|
@@ -130,7 +115,9 @@ export async function homeCommand(args) {
|
|
|
130
115
|
}
|
|
131
116
|
const [git, counted] = await Promise.all([
|
|
132
117
|
gitSummary(cwd),
|
|
133
|
-
|
|
118
|
+
workspace
|
|
119
|
+
? Promise.resolve({ files: workspace.sourceFiles, capped: false })
|
|
120
|
+
: tsconfig !== undefined ? tsconfigFiles(tsconfig, settings.root) : Promise.resolve(walkSources(settings.root)),
|
|
134
121
|
]);
|
|
135
122
|
const testFiles = counted.files.filter((f) => isTestFile(rel(settings.root, f), settings)).length;
|
|
136
123
|
const plus = counted.capped ? "+" : "";
|
|
@@ -143,7 +130,12 @@ export async function homeCommand(args) {
|
|
|
143
130
|
help.push("Run `ts-axi outline <file>` to list the declarations in a file", "Run `ts-axi def <name>` to find where a symbol is declared", "Run `ts-axi config` to see or change the test-file globs", "Run `ts-axi --help` for all commands");
|
|
144
131
|
return out(block({
|
|
145
132
|
root: settings.root,
|
|
146
|
-
|
|
133
|
+
...(workspace ? { workspace: `${workspace.kind} (${workspace.projects.length} projects)` } : {}),
|
|
134
|
+
tsconfig: workspace
|
|
135
|
+
? "nearest per file - workspace scope"
|
|
136
|
+
: tsconfig === undefined
|
|
137
|
+
? "implicit"
|
|
138
|
+
: `${rel(settings.root, tsconfig)}${counted.referenced !== undefined ? ` (${counted.referenced} referenced configs)` : ""}`,
|
|
147
139
|
source_files: counted.capped ? `${counted.files.length}${plus}` : counted.files.length,
|
|
148
140
|
test_globs: settings.testGlobs.length,
|
|
149
141
|
test_files: counted.capped ? `${testFiles}${plus}` : testFiles,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, relative, sep } from "node:path";
|
|
3
3
|
import ts from "typescript";
|
|
4
4
|
import { bool, intFlag, parseArgs, flag } from "../args.js";
|
|
@@ -6,17 +6,18 @@ import { changedRanges, parseUnifiedDiff } from "../diff.js";
|
|
|
6
6
|
import { AxiError, usageError } from "../errors.js";
|
|
7
7
|
import { diffNameStatus, diffUnified, isGitRepo, resolveBaseRev, showFile, untrackedFiles } from "../git.js";
|
|
8
8
|
import { allocateBudget, classifyFile, compareChanges, formatRanges, inRanges, localOnlyLine, matchKey, reverseApply, wordMatches, } from "../impact.js";
|
|
9
|
-
import {
|
|
9
|
+
import { loadProject, SOURCE_EXTENSIONS } from "../project.js";
|
|
10
10
|
import { block, helpBlock, out } from "../render.js";
|
|
11
11
|
import { isTestFile } from "../settings.js";
|
|
12
12
|
import { isFunctionLike } from "../symbols.js";
|
|
13
|
-
import {
|
|
13
|
+
import { IMPACT_HOPS, scopeFields, scopeHelp, semanticFor } from "../scope.js";
|
|
14
|
+
import { walkFiles } from "../walk.js";
|
|
15
|
+
import { contextOf, lineOf, open, projectFields } from "./common.js";
|
|
14
16
|
const DEFAULT_LIMIT = 50;
|
|
15
17
|
const MAX_LIMIT = 500;
|
|
16
18
|
const SIDE_ROWS = 20;
|
|
17
19
|
/** Non-TS files `--grep` also searches: templates that use TS exports the checker never sees. */
|
|
18
20
|
const GREP_EXTRA = [".astro", ".vue", ".svelte", ".mdx", ".html"];
|
|
19
|
-
const SKIP_DIRS = new Set(["node_modules", "dist", "build", "coverage"]);
|
|
20
21
|
export const IMPACT_HELP = `usage: ts-axi impact [<git-range> | -] [flags]
|
|
21
22
|
input:
|
|
22
23
|
(none) = working tree vs HEAD (staged + unstaged + untracked); --staged = index vs HEAD
|
|
@@ -34,6 +35,7 @@ output:
|
|
|
34
35
|
blind_spots[n]{symbol,file,line,reason} = uses the checker cannot follow (any; string keys; unresolved export *)
|
|
35
36
|
skipped[n]{file,reason} = files in the diff that were not analysed (.astro, .json, outside the Project)
|
|
36
37
|
tests: 0 on an exported signature or body change = no Test file references it
|
|
38
|
+
scope = in a monorepo without -P the program is the touched files + two levels of importers (scope_partial: true when capped)
|
|
37
39
|
examples[5]:
|
|
38
40
|
ts-axi impact
|
|
39
41
|
ts-axi impact --staged
|
|
@@ -223,22 +225,7 @@ function blindSpots(semantic, names, changeSet) {
|
|
|
223
225
|
return spots;
|
|
224
226
|
}
|
|
225
227
|
function walkExtra(dir, outFiles) {
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
for (const entry of entries) {
|
|
234
|
-
if (entry.isDirectory()) {
|
|
235
|
-
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith("."))
|
|
236
|
-
walkExtra(join(dir, entry.name), outFiles);
|
|
237
|
-
}
|
|
238
|
-
else if (GREP_EXTRA.some((ext) => entry.name.endsWith(ext))) {
|
|
239
|
-
outFiles.push(join(dir, entry.name));
|
|
240
|
-
}
|
|
241
|
-
}
|
|
228
|
+
outFiles.push(...walkFiles(dir, { accept: (name) => GREP_EXTRA.some((ext) => name.endsWith(ext)) }).files);
|
|
242
229
|
}
|
|
243
230
|
function textualMatches(opened, semantic, names, known, skipLines, changeSet) {
|
|
244
231
|
const files = [...semantic.projectFiles];
|
|
@@ -324,7 +311,7 @@ function rerun(arg, parsed, extra) {
|
|
|
324
311
|
* them): name the sibling tsconfig that includes them so tests get counted.
|
|
325
312
|
*/
|
|
326
313
|
function specFilesHint(cwd, opened, skipped, noTests) {
|
|
327
|
-
const tsconfig = opened.
|
|
314
|
+
const tsconfig = opened.index.project.tsconfig;
|
|
328
315
|
if (!tsconfig)
|
|
329
316
|
return undefined;
|
|
330
317
|
const outside = skipped.filter((s) => s.reason === "not in the Project");
|
|
@@ -410,6 +397,7 @@ export async function impactCommand(args, ctx) {
|
|
|
410
397
|
const total = declChanges.length + fileLevel.length;
|
|
411
398
|
const header = {
|
|
412
399
|
...(warnings.length > 0 ? { warning: warnings.join("; ") } : {}),
|
|
400
|
+
...projectFields(opened),
|
|
413
401
|
base: input.base,
|
|
414
402
|
head: input.head,
|
|
415
403
|
changes: total,
|
|
@@ -432,7 +420,8 @@ export async function impactCommand(args, ctx) {
|
|
|
432
420
|
}), helpBlock(help));
|
|
433
421
|
}
|
|
434
422
|
// --- References (semantic, on the checkout) ---
|
|
435
|
-
|
|
423
|
+
// Scoped workspace: touched files + two levels of importers; deleted or renamed-away paths still resolve so their importers count.
|
|
424
|
+
const { semantic, scope } = semanticFor(opened, analysed.filter((f) => f.status !== "deleted").map((f) => opened.index.abs(f.path)), IMPACT_HOPS, analysed.flatMap((f) => (f.status === "deleted" ? [f.path] : f.oldPath ? [f.oldPath] : [])).map((p) => opened.index.abs(p)));
|
|
436
425
|
const rows = declChanges.map((change) => {
|
|
437
426
|
const source = change.after ?? change.before;
|
|
438
427
|
const live = change.after ? opened.index.declarations(opened.index.abs(change.file)).find((d) => matchKey(d) === change.key) : undefined;
|
|
@@ -536,6 +525,7 @@ export async function impactCommand(args, ctx) {
|
|
|
536
525
|
const specHint = specFilesHint(ctx.cwd, opened, skipped, listed.every((r) => fileCounts(r).tests === 0));
|
|
537
526
|
if (specHint)
|
|
538
527
|
help.push(specHint);
|
|
528
|
+
help.push(...scopeHelp(scope));
|
|
539
529
|
const notes = rows
|
|
540
530
|
.filter((r) => r.change.change === "signature" && r.bases.length > 0)
|
|
541
531
|
.map((r) => ({ symbol: r.symbol, note: `implements ${r.bases.join(" and ")}` }));
|
|
@@ -565,6 +555,7 @@ export async function impactCommand(args, ctx) {
|
|
|
565
555
|
...(grep ? capped("unresolved", unresolved) : {}),
|
|
566
556
|
...depthLevels(semantic, rows, depth),
|
|
567
557
|
...skippedBlock,
|
|
558
|
+
...scopeFields(scope),
|
|
568
559
|
elapsed_ms: Math.round(performance.now() - started),
|
|
569
560
|
...(diagnostics > 0 ? { diagnostics } : {}),
|
|
570
561
|
}), helpBlock(help));
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { relative, sep } from "node:path";
|
|
2
2
|
import { bool, expectPositionals, flag, intFlag, parseArgs } from "../args.js";
|
|
3
|
-
import {
|
|
3
|
+
import { importerClosure } from "../importers.js";
|
|
4
|
+
import { findOutsideProject, nearestTsconfig } from "../project.js";
|
|
4
5
|
import { viaLine } from "../references.js";
|
|
5
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";
|
|
6
10
|
import { isTestFile } from "../settings.js";
|
|
7
11
|
import { isFunctionLike, kindFromScriptElement, parseTarget } from "../symbols.js";
|
|
8
|
-
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";
|
|
9
13
|
const DEFAULT_LIMIT = 50;
|
|
10
14
|
const MAX_LIMIT = 500;
|
|
11
15
|
const DEPTH_ROWS = 50;
|
|
@@ -13,7 +17,10 @@ const SHARED_FLAGS = `--kind <function|class|method|property|variable|interface|
|
|
|
13
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
|
|
14
18
|
via_implementations: n (Impl.member) = References to implementations or overrides of the Symbol; not in refs_total
|
|
15
19
|
implementations: n = members that implement or override the Symbol (their Declarations are never refs)
|
|
16
|
-
matches: 0 under -P lists found_outside_project[n]{file,line} when the name is declared outside that Project
|
|
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`;
|
|
17
24
|
export const REFS_HELP = `usage: ts-axi refs <name | Class.member | path:line[:col]> [flags]
|
|
18
25
|
flags[11]:
|
|
19
26
|
${SHARED_FLAGS}
|
|
@@ -103,7 +110,7 @@ async function run(args, ctx, invoked) {
|
|
|
103
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)}`)));
|
|
104
111
|
}
|
|
105
112
|
const decl = groups[0].decls[0];
|
|
106
|
-
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)))] };
|
|
107
114
|
}
|
|
108
115
|
else {
|
|
109
116
|
const at = resolvePosition(opened.index, ctx.cwd, target);
|
|
@@ -115,17 +122,62 @@ async function run(args, ctx, invoked) {
|
|
|
115
122
|
}
|
|
116
123
|
resolved = at.kind === "decl" ? { absFile: at.abs, pos: at.decl.namePos, decl: at.decl } : { absFile: at.abs, pos: at.pos };
|
|
117
124
|
}
|
|
118
|
-
|
|
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;
|
|
119
134
|
const def = semantic.definitionAt(resolved.absFile, resolved.pos);
|
|
120
135
|
const decl = resolved.decl ??
|
|
121
136
|
(def && def.line > 0 ? declarationAt(opened.index, def.absFile, def.pos, kindFromScriptElement(def.kind)) : undefined);
|
|
122
137
|
const kind = decl?.kind ?? kindFromScriptElement(def?.kind ?? semantic.quickInfo(resolved.absFile, resolved.pos)?.kind ?? "");
|
|
123
138
|
const symbol = decl?.qualifiedName ?? def?.name ?? label;
|
|
124
139
|
const declaredAt = decl ? `${decl.file}:${decl.nameLine}` : def && def.line > 0 ? `${def.file}:${def.line}` : "(outside the Project)";
|
|
125
|
-
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 };
|
|
126
143
|
return mode === "calls"
|
|
127
|
-
? renderCallers(semantic, opened, resolved, header, { limit, depth
|
|
128
|
-
: renderRefs(semantic, opened, resolved, header, {
|
|
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
|
+
};
|
|
129
181
|
}
|
|
130
182
|
function sortRows(rows) {
|
|
131
183
|
return rows.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.col - b.col);
|
|
@@ -173,9 +225,12 @@ function renderRefs(semantic, opened, resolved, header, o) {
|
|
|
173
225
|
help.push(`Run \`ts-axi refs ${o.declaredAt} --via\` for the ${via.length} via References (${viaNames(via)})`);
|
|
174
226
|
if (impls.length > 0 && !o.show.implementations)
|
|
175
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);
|
|
176
230
|
if (isFunctionLike(o.kind))
|
|
177
231
|
help.push(`Run \`ts-axi callers ${o.declaredAt}\` for call sites with their enclosing function`);
|
|
178
232
|
help.push(`Run \`ts-axi def ${o.declaredAt}\` for the declaration body`);
|
|
233
|
+
help.push(...scopeHelp(o.scope));
|
|
179
234
|
return out(block({
|
|
180
235
|
...header,
|
|
181
236
|
refs_total: rows.length,
|
|
@@ -196,6 +251,8 @@ function renderRefs(semantic, opened, resolved, header, o) {
|
|
|
196
251
|
})),
|
|
197
252
|
}),
|
|
198
253
|
...(truncated ? { truncated: true } : {}),
|
|
254
|
+
...outside.fields,
|
|
255
|
+
...scopeFields(o.scope),
|
|
199
256
|
...footer(semantic, o.started, fileList),
|
|
200
257
|
}), helpBlock(help));
|
|
201
258
|
}
|
|
@@ -233,7 +290,7 @@ function depthRows(sites) {
|
|
|
233
290
|
function renderCallers(semantic, opened, resolved, header, o) {
|
|
234
291
|
const sites = semantic.incomingCalls(resolved.absFile, resolved.pos);
|
|
235
292
|
if (sites === undefined) {
|
|
236
|
-
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`]));
|
|
237
294
|
}
|
|
238
295
|
const rows = sortRows(sites.filter((s) => s.via === undefined));
|
|
239
296
|
const via = sortRows(sites.filter((s) => s.via !== undefined));
|
|
@@ -262,7 +319,10 @@ function renderCallers(semantic, opened, resolved, header, o) {
|
|
|
262
319
|
const top = shown.find((r) => r.caller !== "(top-level)");
|
|
263
320
|
if (top && o.depth === 1)
|
|
264
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);
|
|
265
324
|
help.push(`Run \`ts-axi refs ${o.declaredAt}\` for every Reference (not only calls)`);
|
|
325
|
+
help.push(...scopeHelp(o.scope));
|
|
266
326
|
return out(block({
|
|
267
327
|
...header,
|
|
268
328
|
refs_total: rows.length,
|
|
@@ -282,6 +342,8 @@ function renderCallers(semantic, opened, resolved, header, o) {
|
|
|
282
342
|
}),
|
|
283
343
|
...(truncated ? { truncated: true } : {}),
|
|
284
344
|
...levels,
|
|
345
|
+
...outside.fields,
|
|
346
|
+
...scopeFields(o.scope),
|
|
285
347
|
...footer(semantic, o.started, fileList),
|
|
286
348
|
}), helpBlock(help));
|
|
287
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
|
+
}
|