ts-axi 0.1.10 → 0.1.11
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 +28 -2
- package/dist/src/args.js +13 -0
- package/dist/src/cli.js +15 -4
- package/dist/src/commands/common.js +27 -5
- package/dist/src/commands/def.js +4 -3
- package/dist/src/commands/home.js +6 -12
- package/dist/src/commands/impact.js +6 -2
- package/dist/src/commands/outline.js +9 -5
- package/dist/src/commands/refs.js +5 -4
- package/dist/src/fs.js +68 -0
- package/dist/src/git.js +2 -1
- package/dist/src/gittree.js +277 -0
- package/dist/src/project.js +67 -32
- package/dist/src/render.js +14 -1
- package/dist/src/scope.js +4 -3
- package/dist/src/templates.js +9 -8
- package/dist/src/walk.js +12 -30
- package/dist/src/workspace.js +21 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ The hook runs `ts-axi` with no arguments: it reads only the file tree and git st
|
|
|
86
86
|
| `config` | Show effective settings with their source; `config set <key> <value>` and `config unset <key>` write `.ts-axi.json` |
|
|
87
87
|
| `setup <hooks\|status\|uninstall>` | Opt-in SessionStart hook for Claude Code, Codex and OpenCode that runs the home view at session start. `--project` for the current repository only |
|
|
88
88
|
|
|
89
|
-
`ts-axi <command> --help` prints a concise, complete flag reference. `-P <tsconfig>` selects a project on any command that may analyse types; `--json` prints the decoded payload.
|
|
89
|
+
`ts-axi <command> --help` prints a concise, complete flag reference. `-P <tsconfig>` selects a project on any command that may analyse types; `--ref <rev>` answers `def`, `refs`, `callers` and `outline` at a git revision ([below](#reading-another-revision---ref)); `--json` prints the decoded payload.
|
|
90
90
|
|
|
91
91
|
### A review, end to end
|
|
92
92
|
|
|
@@ -196,6 +196,31 @@ scope: nx workspace - 30 of 5433 files (seeds 1; importers 29)
|
|
|
196
196
|
|
|
197
197
|
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.
|
|
198
198
|
|
|
199
|
+
## Reading another revision (`--ref`)
|
|
200
|
+
|
|
201
|
+
`def`, `refs`, `callers` and `outline` take `--ref <rev>` (a branch, tag, `origin/main`, `HEAD~3`, a SHA) and answer from that commit's tree, read straight from git objects. Nothing is checked out, written or fetched, and the working tree has no effect on the answer: a triage agent can ask about the deployed code while the checkout is on another branch or dirty.
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
ts-axi callers Cart.total --ref origin/main
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
ref: origin/main @ 5474e0f
|
|
209
|
+
symbol: Cart.total
|
|
210
|
+
...
|
|
211
|
+
help[1]:
|
|
212
|
+
- Run `ts-axi refs src/lib/cart.ts:32 --ref origin/main` for every Reference (not only calls)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- The rev is resolved once to a SHA (`ref: <rev> @ <sha>`), so a `git fetch` during the run cannot mix versions. Paths in the output look as they do without `--ref`, and every follow-up hint keeps `--ref`.
|
|
216
|
+
- A file that is not in the revision is absent even if it exists locally, and a file deleted locally is still there: `file not in origin/main: src/new.ts`.
|
|
217
|
+
- `node_modules` is not in git, so dependency types come from the working tree; the output says `node_modules: working tree (not in git)` when it read any.
|
|
218
|
+
- A partial clone never fetches: a blob that is not available locally is a `GIT_ERROR` (`git fetch` or a full clone fixes it). Fetch the ref yourself first if it may be stale.
|
|
219
|
+
- `.ts-axi.json` is read from the working tree, like `--test-glob`.
|
|
220
|
+
- `impact` already compares git revisions (`ts-axi impact origin/main...HEAD`) and rejects `--ref`, as do `config` and `setup`.
|
|
221
|
+
|
|
222
|
+
Cost: the tree listing and one batched blob read add about 0.1 s on a small repo and 0.1-0.3 s on a ~5k-file Nx monorepo.
|
|
223
|
+
|
|
199
224
|
## Test files
|
|
200
225
|
|
|
201
226
|
A file counts as a test by path pattern. Defaults: `**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`, `**/test/**`, `**/tests/**`, `**/e2e/**`, `**/spec/**`, `**/test.*`, `**/*_test.*`. The home view shows `test_globs` and `test_files` so a wrong rule is visible.
|
|
@@ -236,7 +261,7 @@ corepack pnpm install
|
|
|
236
261
|
corepack pnpm run check # typecheck, tests, build, skill freshness
|
|
237
262
|
```
|
|
238
263
|
|
|
239
|
-
`skills/ts-axi/SKILL.md` is generated: `corepack pnpm run build:skill`. See `PLAN.md` for the spec, `GLOSSARY.md` for the vocabulary and `docs/adr/` for the
|
|
264
|
+
`skills/ts-axi/SKILL.md` is generated: `corepack pnpm run build:skill`. See `PLAN.md` for the spec, `GLOSSARY.md` for the vocabulary and `docs/adr/` for the design decisions (TypeScript LanguageService over ts-morph / tree-sitter; importer subgraph over a configured project list; `--ref` over git objects instead of a worktree).
|
|
240
265
|
|
|
241
266
|
### Tests
|
|
242
267
|
|
|
@@ -244,6 +269,7 @@ corepack pnpm run check # typecheck, tests, build, skill freshness
|
|
|
244
269
|
| --- | --- | --- |
|
|
245
270
|
| `test/unit.test.ts`, `test/workspace.test.ts` | `pnpm test` | target parsing, signatures, change classification, diff parsing, settings, glob matching, reference classification, workspace detection, import resolution |
|
|
246
271
|
| `test/commands.test.ts` | `pnpm test` | the whole CLI through `main()` over five fixtures copied to temp git repos: `test/fixtures/project` (TS, interface + implementations, barrel, JS consumer, `any` access), `test/fixtures/workspace` (Nx + pnpm workspace, `paths`, references-only tsconfigs), `test/fixtures/legacy-js` (AngularJS, Meteor, prototype, IIFE, CommonJS), `test/fixtures/commonjs` (`require('..')` shim, expando methods, `.d.ts` twin), `test/fixtures/esm-js` (`"type": "module"`, `index.js` + `index.d.ts`) |
|
|
272
|
+
| `test/ref.test.ts`, `test/ref-leak.test.ts`, `test/gittree.test.ts` | `pnpm test` | `--ref`: answers on a poisoned working tree equal a clean checkout (four fixtures), no read below the repo root bypasses the git tree (`node:fs` and `ts.sys` spied), tree listing, blob batches, symlinks, submodules, bad revisions |
|
|
247
273
|
| `test/integration/*.test.ts` | `pnpm run test:integration` | the built binary against the vendored Astro + React sample in `test/integration/sample` (installs its own dependencies once; copied to a temp git repo per run, see `test/integration/README.md`) |
|
|
248
274
|
|
|
249
275
|
CI runs the first two on Node 22 and 24, the built CLI on Node 20, and the integration suite on Node 24.
|
package/dist/src/args.js
CHANGED
|
@@ -19,6 +19,14 @@ const POSITIONAL = {
|
|
|
19
19
|
"--symbol": "pass the symbol as a positional argument",
|
|
20
20
|
"--name": "pass the symbol as a positional argument",
|
|
21
21
|
};
|
|
22
|
+
/** Commands that take `--ref <rev>`. */
|
|
23
|
+
export const REF_COMMANDS = ["def", "refs", "callers", "outline"];
|
|
24
|
+
function refHint(command, rev) {
|
|
25
|
+
const value = rev && !rev.startsWith("-") ? rev : "<ref>";
|
|
26
|
+
if (command === "ts-axi impact")
|
|
27
|
+
return `Run \`ts-axi impact ${value}...HEAD\` - impact takes git revisions as its range`;
|
|
28
|
+
return `--ref works with ${REF_COMMANDS.join(", ")} only`;
|
|
29
|
+
}
|
|
22
30
|
/**
|
|
23
31
|
* Strict declarative parser: every flag must be declared, unknown flags fail
|
|
24
32
|
* loud with the valid set inline (AXI principle 6). Supports `--flag value`,
|
|
@@ -38,8 +46,11 @@ export function parseArgs(args, specs, command) {
|
|
|
38
46
|
positionals: [],
|
|
39
47
|
};
|
|
40
48
|
const unknown = [];
|
|
49
|
+
let refValue;
|
|
41
50
|
for (let i = 0; i < args.length; i++) {
|
|
42
51
|
const token = args[i];
|
|
52
|
+
if (token === "--ref" || token.startsWith("--ref="))
|
|
53
|
+
refValue = token.startsWith("--ref=") ? token.slice(6) : args[i + 1];
|
|
43
54
|
if (token === "--") {
|
|
44
55
|
parsed.positionals.push(...args.slice(i + 1));
|
|
45
56
|
break;
|
|
@@ -98,7 +109,9 @@ export function parseArgs(args, specs, command) {
|
|
|
98
109
|
const positional = unknown
|
|
99
110
|
.filter((flag) => POSITIONAL[flag])
|
|
100
111
|
.map((flag) => `${flag} is not supported; ${POSITIONAL[flag]}`);
|
|
112
|
+
const ref = unknown.includes("--ref") ? [refHint(command, refValue)] : [];
|
|
101
113
|
throw usageError(`unknown flag${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")} for \`${command}\``, [
|
|
114
|
+
...ref,
|
|
102
115
|
...renamed,
|
|
103
116
|
...positional,
|
|
104
117
|
`valid flags for \`${command}\`: ${valid} (--help always allowed)`,
|
package/dist/src/cli.js
CHANGED
|
@@ -7,7 +7,9 @@ import { outlineCommand, OUTLINE_HELP } from "./commands/outline.js";
|
|
|
7
7
|
import { callersCommand, CALLERS_HELP, refsCommand, REFS_HELP } from "./commands/refs.js";
|
|
8
8
|
import { setupCommand, SETUP_HELP } from "./commands/setup.js";
|
|
9
9
|
import { AxiError, exitCodeForError } from "./errors.js";
|
|
10
|
-
import {
|
|
10
|
+
import { refHeader } from "./commands/common.js";
|
|
11
|
+
import { diskFs, setSourceFs } from "./fs.js";
|
|
12
|
+
import { out, renderErrorOutput, setHintRef, setJsonMode, toJson } from "./render.js";
|
|
11
13
|
import { VERSION } from "./version.js";
|
|
12
14
|
export const DESCRIPTION = "TypeScript/JavaScript symbol CLI for agents - definitions, references, callers, file outlines and change impact (what a diff affects) from the TypeScript compiler. Prefer it over grep for 'who uses this'.";
|
|
13
15
|
export const TOP_HELP = `usage: ts-axi [command] [args] [flags]
|
|
@@ -15,13 +17,14 @@ commands[8]:
|
|
|
15
17
|
(none)=project overview, def, refs, callers, outline, impact, config, setup (opt-in session hooks)
|
|
16
18
|
targets:
|
|
17
19
|
name (save), member (User.save) or position (src/a.ts:42 or src/a.ts:42:7)
|
|
18
|
-
flags[
|
|
19
|
-
-P <tsconfig> (after the command; default nearest tsconfig.json), --json (anywhere), --help, -v/-V/--version
|
|
20
|
+
flags[5]:
|
|
21
|
+
-P <tsconfig> (after the command; default nearest tsconfig.json), --ref <rev> (def/refs/callers/outline; read a git revision instead of the working tree), --json (anywhere), --help, -v/-V/--version
|
|
20
22
|
examples:
|
|
21
23
|
ts-axi
|
|
22
24
|
ts-axi def User.save
|
|
23
25
|
ts-axi refs src/models/user.ts:12
|
|
24
26
|
ts-axi callers runSync --depth 2
|
|
27
|
+
ts-axi refs User.save --ref origin/main
|
|
25
28
|
ts-axi outline src/models/user.ts
|
|
26
29
|
ts-axi impact main...HEAD
|
|
27
30
|
ts-axi refs --help
|
|
@@ -36,7 +39,13 @@ const COMMAND_HELP = {
|
|
|
36
39
|
setup: SETUP_HELP,
|
|
37
40
|
};
|
|
38
41
|
function command(name, handler, ctx) {
|
|
39
|
-
return async (args) =>
|
|
42
|
+
return async (args) => {
|
|
43
|
+
if (args.includes("-h"))
|
|
44
|
+
return (COMMAND_HELP[name] ?? "").trimEnd();
|
|
45
|
+
const result = await handler(args, ctx);
|
|
46
|
+
// After the handler: the node_modules note depends on what it read.
|
|
47
|
+
return out(refHeader(), result);
|
|
48
|
+
};
|
|
40
49
|
}
|
|
41
50
|
function formatError(error) {
|
|
42
51
|
const axi = error instanceof AxiError
|
|
@@ -84,6 +93,8 @@ export async function main(options = {}) {
|
|
|
84
93
|
}
|
|
85
94
|
finally {
|
|
86
95
|
setJsonMode(false);
|
|
96
|
+
setSourceFs(diskFs);
|
|
97
|
+
setHintRef(undefined);
|
|
87
98
|
}
|
|
88
99
|
if (!json)
|
|
89
100
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import { setSourceFs, sourceFs } from "../fs.js";
|
|
2
|
+
import { openGitTree } from "../gittree.js";
|
|
3
|
+
import { block, setHintRef } from "../render.js";
|
|
4
4
|
import { dirname, posix, relative, resolve, sep } from "node:path";
|
|
5
5
|
import ts from "typescript";
|
|
6
6
|
import { enumFlag, flag, flags, bool } from "../args.js";
|
|
@@ -18,7 +18,28 @@ export const TARGET_FLAGS = [
|
|
|
18
18
|
{ name: "--exported", boolean: true },
|
|
19
19
|
{ name: "--fuzzy", boolean: true },
|
|
20
20
|
{ name: "-P" },
|
|
21
|
+
{ name: "--ref" },
|
|
21
22
|
];
|
|
23
|
+
/**
|
|
24
|
+
* `--ref <rev>`: read the repo from that commit's git tree for the rest of the run. Returns the
|
|
25
|
+
* context with the cwd mapped into the tree (no disk realpath of the working tree).
|
|
26
|
+
*/
|
|
27
|
+
export function useRef(ctx, parsed) {
|
|
28
|
+
const rev = flag(parsed, "--ref");
|
|
29
|
+
if (rev === undefined)
|
|
30
|
+
return ctx;
|
|
31
|
+
const fs = openGitTree(ctx.cwd, rev);
|
|
32
|
+
setSourceFs(fs);
|
|
33
|
+
setHintRef(rev);
|
|
34
|
+
return { ...ctx, cwd: fs.ref.cwd };
|
|
35
|
+
}
|
|
36
|
+
/** The header block of a `--ref` run: `ref: <rev> @ <short sha>`, plus where node_modules came from. */
|
|
37
|
+
export function refHeader() {
|
|
38
|
+
const ref = sourceFs().ref;
|
|
39
|
+
if (!ref)
|
|
40
|
+
return undefined;
|
|
41
|
+
return block({ ref: `${ref.rev} @ ${ref.sha.slice(0, 7)}`, ...(ref.nodeModulesRead ? { node_modules: "working tree (not in git)" } : {}) });
|
|
42
|
+
}
|
|
22
43
|
export function readFilters(parsed, extraKinds = []) {
|
|
23
44
|
const raw = flag(parsed, "--kind");
|
|
24
45
|
const kind = raw === undefined ? undefined : enumFlag(parsed, "--kind", [...KINDS, ...extraKinds], KINDS[0]);
|
|
@@ -180,8 +201,9 @@ function filterSuffixOf(label, target) {
|
|
|
180
201
|
}
|
|
181
202
|
export function resolvePositionFile(cwd, file) {
|
|
182
203
|
const abs = resolve(cwd, file);
|
|
183
|
-
if (!
|
|
184
|
-
|
|
204
|
+
if (!sourceFs().exists(abs)) {
|
|
205
|
+
const ref = sourceFs().ref;
|
|
206
|
+
throw usageError(ref ? `file not in ${ref.rev}: ${file}` : `file not found: ${file}`, ["paths are relative to the current directory"]);
|
|
185
207
|
}
|
|
186
208
|
return abs;
|
|
187
209
|
}
|
package/dist/src/commands/def.js
CHANGED
|
@@ -2,10 +2,10 @@ import { bool, expectPositionals, flag, parseArgs } from "../args.js";
|
|
|
2
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
|
-
import { BODY_LINES, declarationAt, filterSuffix, MATCH_ROWS, matchRows, narrowHints, numberedLines, open, positionStartDir, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, readFilters, } from "./common.js";
|
|
5
|
+
import { BODY_LINES, declarationAt, filterSuffix, MATCH_ROWS, matchRows, narrowHints, numberedLines, open, positionStartDir, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, readFilters, useRef, } from "./common.js";
|
|
6
6
|
export const DEF_HELP = `usage: ts-axi def <name | Class.member | path:line[:col]> [flags]
|
|
7
|
-
flags[
|
|
8
|
-
--kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace|state>, --in <dir|glob> (repeatable; restrict to files), --exported, --fuzzy (substring, case-insensitive), --full (whole body; default first ${BODY_LINES} lines), -P <tsconfig>, --json
|
|
7
|
+
flags[8]:
|
|
8
|
+
--kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace|state>, --in <dir|glob> (repeatable; restrict to files), --exported, --fuzzy (substring, case-insensitive), --full (whole body; default first ${BODY_LINES} lines), -P <tsconfig>, --ref <rev> (read the code at a git revision; working tree ignored), --json
|
|
9
9
|
output:
|
|
10
10
|
1 match = file, line, kind, exported, signature + body fenced with file line numbers
|
|
11
11
|
several = matches[n]{at,kind,signature} (at = path:line) (at most ${MATCH_ROWS}; matches_total + truncated past that) + the narrowing forms (--in, --kind, Class.member, --exported) with how many each keeps
|
|
@@ -49,6 +49,7 @@ export async function defCommand(args, ctx) {
|
|
|
49
49
|
const target = parseTarget(raw, { fuzzy: f.fuzzy });
|
|
50
50
|
const full = bool(parsed, "--full");
|
|
51
51
|
const project = flag(parsed, "-P");
|
|
52
|
+
ctx = useRef(ctx, parsed);
|
|
52
53
|
const opened = open(ctx.cwd, parsed, positionStartDir(ctx.cwd, target));
|
|
53
54
|
const label = targetLabel(target);
|
|
54
55
|
if (target.kind === "position") {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sourceFs } from "../fs.js";
|
|
2
2
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { expectPositionals, flag, flags, parseArgs } from "../args.js";
|
|
4
4
|
import { AxiError, usageError } from "../errors.js";
|
|
@@ -18,20 +18,14 @@ const HOME_SPECS = [
|
|
|
18
18
|
];
|
|
19
19
|
/** Does `dir` hold a TS/JS source within `depth` levels (shared skip rule)? */
|
|
20
20
|
function hasSources(dir, depth, root = dir) {
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
21
|
+
const entries = sourceFs().readdir(dir);
|
|
28
22
|
for (const entry of entries) {
|
|
29
|
-
if (entry.isFile
|
|
23
|
+
if (entry.isFile && SOURCE_EXT.test(entry.name))
|
|
30
24
|
return true;
|
|
31
25
|
}
|
|
32
26
|
if (depth === 0)
|
|
33
27
|
return false;
|
|
34
|
-
return entries.some((e) => e.isDirectory
|
|
28
|
+
return entries.some((e) => e.isDirectory && !skipDir(e.name, join(dir, e.name), root) && hasSources(join(dir, e.name), depth - 1, root));
|
|
35
29
|
}
|
|
36
30
|
/** Implicit project: every source below `root` from the shared listing (git, else a bounded fs walk). */
|
|
37
31
|
function walkSources(root) {
|
|
@@ -41,7 +35,7 @@ function findTsconfig(cwd) {
|
|
|
41
35
|
let dir = cwd;
|
|
42
36
|
for (;;) {
|
|
43
37
|
const candidate = join(dir, "tsconfig.json");
|
|
44
|
-
if (
|
|
38
|
+
if (sourceFs().exists(candidate))
|
|
45
39
|
return candidate;
|
|
46
40
|
const parent = dirname(dir);
|
|
47
41
|
if (parent === dir)
|
|
@@ -79,7 +73,7 @@ function resolveTsconfig(cwd, settings) {
|
|
|
79
73
|
// -P is relative to cwd, the .ts-axi.json value to the repo root.
|
|
80
74
|
const base = settings.sources.project === "flag" ? cwd : settings.root;
|
|
81
75
|
const path = resolve(base, settings.project);
|
|
82
|
-
if (!
|
|
76
|
+
if (!sourceFs().isFile(path)) {
|
|
83
77
|
throw usageError(`tsconfig not found: ${settings.project}`, [
|
|
84
78
|
settings.sources.project === "flag"
|
|
85
79
|
? "Pass an existing file to -P"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { sourceFs } from "../fs.js";
|
|
2
3
|
import { basename, dirname, join, relative, sep } from "node:path";
|
|
3
4
|
import ts from "typescript";
|
|
4
5
|
import { bool, flag, intFlag, parseArgs, parseFields } from "../args.js";
|
|
@@ -69,6 +70,7 @@ async function readStdin() {
|
|
|
69
70
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
70
71
|
return Buffer.concat(chunks).toString("utf8");
|
|
71
72
|
}
|
|
73
|
+
// Always the working tree: it is the "after" side of the diff (impact rejects --ref).
|
|
72
74
|
function readText(abs) {
|
|
73
75
|
try {
|
|
74
76
|
return readFileSync(abs, "utf8");
|
|
@@ -243,6 +245,7 @@ export function textualMatches(opened, semantic, names, known, skipLines, change
|
|
|
243
245
|
const files = [...semantic.projectFiles];
|
|
244
246
|
const projectSet = new Set(files);
|
|
245
247
|
walkExtra(opened.root, files);
|
|
248
|
+
sourceFs().prefetch(files);
|
|
246
249
|
const rows = [];
|
|
247
250
|
for (const abs of files) {
|
|
248
251
|
const text = opened.index.text(abs);
|
|
@@ -311,6 +314,7 @@ export function textualCount(opened, needle, changeSet = new Map(), onlyFile) {
|
|
|
311
314
|
const candidates = onlyFile !== undefined ? [onlyFile] : [...project];
|
|
312
315
|
if (token && onlyFile === undefined)
|
|
313
316
|
walkExtra(opened.root, candidates);
|
|
317
|
+
sourceFs().prefetch(candidates);
|
|
314
318
|
for (const abs of new Set(candidates)) {
|
|
315
319
|
const text = opened.index.text(abs);
|
|
316
320
|
if (text === undefined || !text.includes(name))
|
|
@@ -432,7 +436,7 @@ function specFilesHint(cwd, opened, skipped, noTests) {
|
|
|
432
436
|
const dir = dirname(tsconfig);
|
|
433
437
|
const name = basename(tsconfig);
|
|
434
438
|
const order = name === "tsconfig.lib.json" || name === "tsconfig.app.json" ? ["tsconfig.json", "tsconfig.spec.json"] : ["tsconfig.spec.json", "tsconfig.json"];
|
|
435
|
-
const candidates = order.map((n) => join(dir, n)).filter((c) => c !== tsconfig &&
|
|
439
|
+
const candidates = order.map((n) => join(dir, n)).filter((c) => c !== tsconfig && sourceFs().exists(c));
|
|
436
440
|
// Prefer the one that really includes a skipped spec file (an Nx tsconfig.json may only hold references).
|
|
437
441
|
const includes = (c) => {
|
|
438
442
|
try {
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sourceFs } from "../fs.js";
|
|
2
2
|
import { posix, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { bool, flag, parseArgs, parseFields } from "../args.js";
|
|
4
4
|
import { usageError } from "../errors.js";
|
|
5
5
|
import { hasGlobMagic, matchesGlob } from "../glob.js";
|
|
6
6
|
import { createSyntacticIndex, findRepoRoot, openProject, SOURCE_EXTENSIONS } from "../project.js";
|
|
7
7
|
import { block, helpBlock, out } from "../render.js";
|
|
8
|
+
import { useRef } from "./common.js";
|
|
8
9
|
const MAX_ROWS = 300;
|
|
9
10
|
export const OUTLINE_HELP = `usage: ts-axi outline <file | glob>... [flags]
|
|
10
|
-
flags[
|
|
11
|
-
--exported (alias --exports; exported Declarations only), --fields exported (adds an exported column), -P <tsconfig> (file list for globs), --json
|
|
11
|
+
flags[5]:
|
|
12
|
+
--exported (alias --exports; exported Declarations only), --fields exported (adds an exported column), -P <tsconfig> (file list for globs), --ref <rev> (the file at a git revision), --json
|
|
12
13
|
output:
|
|
13
14
|
one file = file + decls[n]{line,kind,name,signature}; several = decls[n]{at,kind,name,signature} (at = path:line); --exported keeps only exported Declarations, --fields exported adds the column
|
|
14
15
|
class members follow their class as Class.member; at most ${MAX_ROWS} rows
|
|
@@ -27,6 +28,7 @@ export async function outlineCommand(args, ctx) {
|
|
|
27
28
|
{ name: "--exported", alias: "--exports", boolean: true },
|
|
28
29
|
{ name: "--fields" },
|
|
29
30
|
{ name: "-P" },
|
|
31
|
+
{ name: "--ref" },
|
|
30
32
|
], COMMAND);
|
|
31
33
|
if (parsed.positionals.length === 0) {
|
|
32
34
|
throw usageError("missing argument", [`usage: ${COMMAND} <file | glob>... [--exported]`]);
|
|
@@ -34,14 +36,16 @@ export async function outlineCommand(args, ctx) {
|
|
|
34
36
|
const exportedOnly = bool(parsed, "--exported");
|
|
35
37
|
const showExported = parseFields(flag(parsed, "--fields"), ["exported"]).length > 0;
|
|
36
38
|
const override = flag(parsed, "-P");
|
|
39
|
+
ctx = useRef(ctx, parsed);
|
|
37
40
|
const root = findRepoRoot(ctx.cwd);
|
|
38
41
|
const cwdPrefix = relative(root, resolve(ctx.cwd)).split(sep).join("/");
|
|
39
42
|
const files = [];
|
|
40
43
|
const globs = parsed.positionals.filter(hasGlobMagic);
|
|
41
44
|
for (const raw of parsed.positionals.filter((p) => !hasGlobMagic(p))) {
|
|
42
45
|
const abs = resolve(ctx.cwd, raw);
|
|
43
|
-
if (!
|
|
44
|
-
|
|
46
|
+
if (!sourceFs().isFile(abs)) {
|
|
47
|
+
const ref = sourceFs().ref;
|
|
48
|
+
throw usageError(ref ? `file not in ${ref.rev}: ${raw}` : `file not found: ${raw}`, ["pass a source file or a quoted glob such as \"src/**/*.ts\""]);
|
|
45
49
|
}
|
|
46
50
|
if (!isSource(abs))
|
|
47
51
|
throw usageError(`not a TypeScript/JavaScript source file: ${raw}`);
|
|
@@ -11,11 +11,11 @@ import { byDepthThenPath } from "../walk.js";
|
|
|
11
11
|
import { detectWorkspace, isMultiProject } from "../workspace.js";
|
|
12
12
|
import { isTestFile } from "../settings.js";
|
|
13
13
|
import { isFunctionLike, kindFromScriptElement, parseTarget } from "../symbols.js";
|
|
14
|
-
import { contextOf, declarationAt, filterSuffix, lineOf, MATCH_ROWS, matchRows, narrowHints, open, positionStartDir, projectFields, readFilters, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, } from "./common.js";
|
|
14
|
+
import { contextOf, declarationAt, filterSuffix, lineOf, MATCH_ROWS, matchRows, narrowHints, open, positionStartDir, projectFields, readFilters, useRef, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, } from "./common.js";
|
|
15
15
|
const DEFAULT_LIMIT = 50;
|
|
16
16
|
const MAX_LIMIT = 500;
|
|
17
17
|
const DEPTH_ROWS = 50;
|
|
18
|
-
const SHARED_FLAGS = `--kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace|state> (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`;
|
|
18
|
+
const SHARED_FLAGS = `--kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace|state> (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>, --ref <rev> (read the code at a git revision; working tree ignored), --json`;
|
|
19
19
|
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
|
|
20
20
|
via_implementations: n (Impl.member) = References to implementations or overrides of the Symbol; not in refs_total
|
|
21
21
|
implementations: n = members that implement or override the Symbol (their Declarations are never refs)
|
|
@@ -26,7 +26,7 @@ const VIA_DOC = `via_interface: n (Iface.member) = References through an interfa
|
|
|
26
26
|
textual: n matches in m files = whole-word count of the name in Project files (JavaScript declarations and implicit projects; a state counts its full dotted name as a whole token, templates included; not for a state's resolve keys and hooks - injected by name, not imported); 'n matches in this file' for a non-exported local; above refs_total = uses the checker cannot resolve - see refs --grep
|
|
27
27
|
templates: n usages in m files = template uses (.html .htm .ejs .hbs .njk .pug) of a directive or component (<wap-select / wap-select / data-wap-select), its bindings (fallback-display-property= on that tag), a controller (ng-controller) or a filter (| money); refs --grep lists them in unresolved`;
|
|
28
28
|
export const REFS_HELP = `usage: ts-axi refs <name | Class.member | path:line[:col]> [flags]
|
|
29
|
-
flags[
|
|
29
|
+
flags[13]:
|
|
30
30
|
${SHARED_FLAGS}
|
|
31
31
|
--implementations (lists implementations[n]{file,line,symbol} instead of the count)
|
|
32
32
|
--grep (adds unresolved[n]{file,line,context} - textual matches of the name that are not References; also searches .astro/.vue/.svelte/.mdx/.html)
|
|
@@ -43,7 +43,7 @@ examples[4]:
|
|
|
43
43
|
ts-axi refs runSync --limit 200
|
|
44
44
|
`;
|
|
45
45
|
export const CALLERS_HELP = `usage: ts-axi callers <name | Class.member | path:line[:col]> [flags]
|
|
46
|
-
flags[
|
|
46
|
+
flags[12]:
|
|
47
47
|
${SHARED_FLAGS}
|
|
48
48
|
--depth <1-3> (default 1; levels > 1 print callers_depthN[n]{caller,file,count}, no locations)
|
|
49
49
|
output:
|
|
@@ -86,6 +86,7 @@ async function run(args, ctx, invoked) {
|
|
|
86
86
|
grep: invoked === "refs" && bool(parsed, "--grep"),
|
|
87
87
|
};
|
|
88
88
|
const started = performance.now();
|
|
89
|
+
ctx = useRef(ctx, parsed);
|
|
89
90
|
const opened = open(ctx.cwd, parsed, positionStartDir(ctx.cwd, target));
|
|
90
91
|
const warning = opened.warning ? { warning: opened.warning } : {};
|
|
91
92
|
// Resolve to exactly one Symbol before building a program.
|
package/dist/src/fs.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The file-system seam: every read of repo content goes through `sourceFs()`. `diskFs` is the
|
|
2
|
+
// working tree; `--ref` swaps in a git-tree view (src/gittree.ts). Node builtins only.
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
export const diskFs = {
|
|
7
|
+
exists: (abs) => existsSync(abs),
|
|
8
|
+
isFile(abs) {
|
|
9
|
+
try {
|
|
10
|
+
return statSync(abs).isFile();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
isDirectory(abs) {
|
|
17
|
+
try {
|
|
18
|
+
return statSync(abs).isDirectory();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
readFile(abs) {
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(abs, "utf8");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
readdir(abs) {
|
|
33
|
+
try {
|
|
34
|
+
return readdirSync(abs, { withFileTypes: true }).map((e) => ({ name: e.name, isFile: e.isFile(), isDirectory: e.isDirectory() }));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
realpath: (abs) => realpathSync(abs),
|
|
41
|
+
listFiles(root) {
|
|
42
|
+
if (!existsSync(join(root, ".git")))
|
|
43
|
+
return undefined;
|
|
44
|
+
try {
|
|
45
|
+
const raw = execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
46
|
+
cwd: root,
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
maxBuffer: 512 * 1024 * 1024,
|
|
49
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
50
|
+
});
|
|
51
|
+
// A nested repo shows up as one `dir/` entry; deleted-but-tracked files are dropped by the caller's exists check.
|
|
52
|
+
return [...new Set(raw.split("\0").filter((f) => f !== "" && !f.endsWith("/")))];
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
prefetch: () => { },
|
|
59
|
+
};
|
|
60
|
+
// One fs per run, set by the command that parsed `--ref` and reset by `main()`. A module-level
|
|
61
|
+
// value because the reads sit in ~40 free functions (tsconfig chains, workspace detection, TS hosts).
|
|
62
|
+
let active = diskFs;
|
|
63
|
+
export function sourceFs() {
|
|
64
|
+
return active;
|
|
65
|
+
}
|
|
66
|
+
export function setSourceFs(fs) {
|
|
67
|
+
active = fs;
|
|
68
|
+
}
|
package/dist/src/git.js
CHANGED
|
@@ -7,6 +7,7 @@ const TIMEOUT_MS = 20_000;
|
|
|
7
7
|
const MAX_BUFFER = 256 * 1024 * 1024;
|
|
8
8
|
/** `git diff` target for a repository with no commits yet. */
|
|
9
9
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
10
|
+
export const REV_HINT = "Run `git branch -a` or `git log --oneline -5` to pick an existing revision";
|
|
10
11
|
/** Run git; resolves with the exit code instead of throwing on non-zero. */
|
|
11
12
|
async function run(cwd, args) {
|
|
12
13
|
try {
|
|
@@ -44,7 +45,7 @@ function gitError(result, args) {
|
|
|
44
45
|
? ["Run `ts-axi impact -` with a unified diff on stdin instead"]
|
|
45
46
|
: /unknown revision|bad revision|invalid object name|no merge base/i.test(first)
|
|
46
47
|
? [
|
|
47
|
-
|
|
48
|
+
REV_HINT,
|
|
48
49
|
"Run `ts-axi impact` for uncommitted changes, or `ts-axi impact <base>...HEAD` for a branch",
|
|
49
50
|
]
|
|
50
51
|
: [];
|