@shanepadgett/tau-agent 0.27.0 → 0.28.1
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/extensions/context/sync.ts +1 -9
- package/extensions/context-pruning/prune.ts +2 -2
- package/extensions/explore/README.md +13 -21
- package/extensions/explore/ast/adapter.ts +97 -0
- package/extensions/explore/ast/engine.ts +470 -0
- package/extensions/explore/ast/format/ast-search.ts +97 -0
- package/extensions/explore/ast/format/composite.ts +26 -0
- package/extensions/explore/ast/format/context.ts +66 -0
- package/extensions/explore/ast/format/deps.ts +86 -0
- package/extensions/explore/ast/format/discover.ts +85 -0
- package/extensions/explore/ast/format/impact.ts +105 -0
- package/extensions/explore/ast/format/outline.ts +66 -0
- package/extensions/explore/ast/format/relationships.ts +81 -0
- package/extensions/explore/ast/format/show.ts +38 -0
- package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
- package/extensions/explore/ast/grammars/manifest.json +41 -0
- package/extensions/explore/ast/grammars/manifest.ts +99 -0
- package/extensions/explore/ast/grammars/odin.wasm +0 -0
- package/extensions/explore/ast/grammars/swift.wasm +0 -0
- package/extensions/explore/ast/graph/file-graph.ts +360 -0
- package/extensions/explore/ast/graph/relationships.ts +597 -0
- package/extensions/explore/ast/identity.ts +119 -0
- package/extensions/explore/ast/ir.ts +136 -0
- package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
- package/extensions/explore/ast/languages/csharp.ts +493 -0
- package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
- package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
- package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
- package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
- package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
- package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
- package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
- package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
- package/extensions/explore/ast/languages/go.ts +545 -0
- package/extensions/explore/ast/languages/java.ts +412 -0
- package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
- package/extensions/explore/ast/languages/kotlin.ts +611 -0
- package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
- package/extensions/explore/ast/languages/odin.ts +407 -0
- package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
- package/extensions/explore/ast/languages/rust.ts +506 -0
- package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
- package/extensions/explore/ast/languages/swift.ts +466 -0
- package/extensions/explore/ast/languages/tree.ts +220 -0
- package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
- package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
- package/extensions/explore/ast/languages/typescript.ts +751 -0
- package/extensions/explore/ast/markdown.ts +145 -0
- package/extensions/explore/ast/package-surface.ts +42 -0
- package/extensions/explore/ast/queries/ast-search.ts +306 -0
- package/extensions/explore/ast/queries/composite-target.ts +77 -0
- package/extensions/explore/ast/queries/context.ts +406 -0
- package/extensions/explore/ast/queries/discover.ts +427 -0
- package/extensions/explore/ast/queries/impact.ts +250 -0
- package/extensions/explore/ast/queries/outline.ts +203 -0
- package/extensions/explore/ast/queries/show.ts +262 -0
- package/extensions/explore/ast/query.ts +48 -0
- package/extensions/explore/ast/read/hook.ts +125 -0
- package/extensions/explore/ast/read/policy.ts +55 -0
- package/extensions/explore/ast/registry.ts +120 -0
- package/extensions/explore/ast/scan.ts +76 -0
- package/extensions/explore/ast/slice.ts +44 -0
- package/extensions/explore/ast/tools/ast-search.ts +138 -0
- package/extensions/explore/ast/tools/context.ts +126 -0
- package/extensions/explore/ast/tools/deps.ts +72 -0
- package/extensions/explore/ast/tools/discover.ts +208 -0
- package/extensions/explore/ast/tools/impact.ts +135 -0
- package/extensions/explore/ast/tools/outline.ts +200 -0
- package/extensions/explore/ast/tools/relationships.ts +160 -0
- package/extensions/explore/ast/tools/render.ts +121 -0
- package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
- package/extensions/explore/ast/tools/show.ts +116 -0
- package/extensions/explore/guidance.ts +52 -0
- package/extensions/explore/index.ts +82 -92
- package/extensions/explore/read/autoread.ts +118 -0
- package/extensions/explore/settings.ts +49 -31
- package/extensions/explore/traverse.ts +254 -91
- package/extensions/handoff/index.ts +1 -1
- package/extensions/publish/index.ts +18 -85
- package/extensions/publish/publish-ui.ts +0 -4
- package/extensions/soul/README.md +7 -4
- package/extensions/soul/index.ts +11 -5
- package/extensions/soul/prompt.ts +31 -13
- package/extensions/soul/settings.ts +7 -2
- package/extensions/subagent/agents/context-sync.md +6 -10
- package/extensions/subagent/agents/review.md +24 -55
- package/extensions/subagent/agents/scout.md +38 -115
- package/extensions/subagent/run.ts +1 -1
- package/extensions/tau-help/help.md +2 -4
- package/package.json +6 -4
- package/schemas/tau.schema.json +35 -30
- package/{extensions/explore → shared}/autoread.ts +21 -7
- package/shared/events.ts +1 -1
- package/shared/full-file-knowledge.ts +49 -0
- package/extensions/explore/ast-guidance.ts +0 -67
- package/extensions/explore/ast-languages.ts +0 -61
- package/extensions/explore/ast-tools.ts +0 -1994
- package/extensions/explore/ast-worker.ts +0 -1025
- package/extensions/explore/find.ts +0 -147
- package/extensions/explore/full-file-knowledge.ts +0 -234
- package/extensions/explore/grep.ts +0 -749
- package/extensions/explore/limits.ts +0 -22
- package/extensions/explore/ls.ts +0 -113
- package/extensions/explore/orientation-state.ts +0 -280
- package/extensions/explore/path-display.ts +0 -41
- package/extensions/explore/path-tree.ts +0 -150
- package/extensions/explore/read-cache.ts +0 -273
- package/extensions/explore/read-snapshots.ts +0 -57
- package/extensions/explore/read-stats-panel.ts +0 -176
- package/extensions/explore/read-stats.ts +0 -138
- package/extensions/explore/read.ts +0 -372
- package/extensions/explore/result.ts +0 -29
- package/native-bin/darwin-arm64/tau-ast +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { Node } from "web-tree-sitter";
|
|
2
|
+
import type { CallSite, Decl } from "../ir.ts";
|
|
3
|
+
|
|
4
|
+
export type DocSpan = {
|
|
5
|
+
docStartOffset: number;
|
|
6
|
+
docEndOffset: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function startLine(node: Node): number {
|
|
10
|
+
return node.startPosition.row + 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function endLine(node: Node): number {
|
|
14
|
+
const { row, column } = node.endPosition;
|
|
15
|
+
if (column === 0 && row > node.startPosition.row) return row;
|
|
16
|
+
return row + 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function newlineCount(source: string, from: number, to: number): number {
|
|
20
|
+
let count = 0;
|
|
21
|
+
const end = Math.min(to, source.length);
|
|
22
|
+
for (let i = Math.max(0, from); i < end; i += 1) {
|
|
23
|
+
if (source.charCodeAt(i) === 10) count += 1;
|
|
24
|
+
}
|
|
25
|
+
return count;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isCommentType(type: string, commentTypes: readonly string[]): boolean {
|
|
29
|
+
return commentTypes.includes(type);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Contiguous leading comment siblings; ≤1 blank line between comments and before decl.
|
|
34
|
+
* `skipTypes` walks back through those sibling types first (e.g. TS decorators, Rust attributes).
|
|
35
|
+
* `commentTypes` defaults to tree-sitter `comment`; grammars that split line/block must pass their own.
|
|
36
|
+
*/
|
|
37
|
+
export function docSpanBefore(
|
|
38
|
+
node: Node,
|
|
39
|
+
source: string,
|
|
40
|
+
skipTypes: readonly string[] = [],
|
|
41
|
+
// Most grammars use `comment`; rust/java/kotlin pass their own line/block kinds.
|
|
42
|
+
commentTypes: readonly string[] = ["comment"],
|
|
43
|
+
): DocSpan | undefined {
|
|
44
|
+
let cursor = node.previousSibling;
|
|
45
|
+
while (cursor !== null && skipTypes.includes(cursor.type)) {
|
|
46
|
+
cursor = cursor.previousSibling;
|
|
47
|
+
}
|
|
48
|
+
if (cursor === null || !isCommentType(cursor.type, commentTypes)) return undefined;
|
|
49
|
+
if (newlineCount(source, cursor.endIndex, node.startIndex) > 2) return undefined;
|
|
50
|
+
|
|
51
|
+
const docEndOffset = cursor.endIndex;
|
|
52
|
+
let docStartOffset = cursor.startIndex;
|
|
53
|
+
let gapRightStart = cursor.startIndex;
|
|
54
|
+
|
|
55
|
+
cursor = cursor.previousSibling;
|
|
56
|
+
while (cursor !== null && isCommentType(cursor.type, commentTypes)) {
|
|
57
|
+
if (newlineCount(source, cursor.endIndex, gapRightStart) > 2) break;
|
|
58
|
+
docStartOffset = cursor.startIndex;
|
|
59
|
+
gapRightStart = cursor.startIndex;
|
|
60
|
+
cursor = cursor.previousSibling;
|
|
61
|
+
}
|
|
62
|
+
return { docStartOffset, docEndOffset };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Trailing comment children of a preceding container (Kotlin package_header / import_list quirk).
|
|
67
|
+
* Same blank-line rule as sibling docs.
|
|
68
|
+
*/
|
|
69
|
+
export function docSpanTrailingChild(
|
|
70
|
+
container: Node,
|
|
71
|
+
before: Node,
|
|
72
|
+
source: string,
|
|
73
|
+
commentTypes: readonly string[],
|
|
74
|
+
): DocSpan | undefined {
|
|
75
|
+
const named = container.namedChildren;
|
|
76
|
+
if (named.length === 0) return undefined;
|
|
77
|
+
const lastIndex = named.length - 1;
|
|
78
|
+
const lastComment = named[lastIndex];
|
|
79
|
+
if (lastComment === undefined || !isCommentType(lastComment.type, commentTypes)) return undefined;
|
|
80
|
+
if (newlineCount(source, lastComment.endIndex, before.startIndex) > 2) return undefined;
|
|
81
|
+
|
|
82
|
+
let firstIndex = lastIndex;
|
|
83
|
+
while (firstIndex - 1 >= 0) {
|
|
84
|
+
const prev = named[firstIndex - 1];
|
|
85
|
+
const current = named[firstIndex];
|
|
86
|
+
if (prev === undefined || current === undefined || !isCommentType(prev.type, commentTypes)) break;
|
|
87
|
+
if (newlineCount(source, prev.endIndex, current.startIndex) > 2) break;
|
|
88
|
+
firstIndex -= 1;
|
|
89
|
+
}
|
|
90
|
+
const first = named[firstIndex];
|
|
91
|
+
if (first === undefined) return undefined;
|
|
92
|
+
return { docStartOffset: first.startIndex, docEndOffset: lastComment.endIndex };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Expand decl start left through contiguous previous siblings of the given types (e.g. Rust attributes). */
|
|
96
|
+
export function spanThroughPrevious(node: Node, types: readonly string[]): { startOffset: number; startLine: number } {
|
|
97
|
+
let startOffset = node.startIndex;
|
|
98
|
+
let line = startLine(node);
|
|
99
|
+
let cursor = node.previousSibling;
|
|
100
|
+
while (cursor !== null && types.includes(cursor.type)) {
|
|
101
|
+
startOffset = cursor.startIndex;
|
|
102
|
+
line = startLine(cursor);
|
|
103
|
+
cursor = cursor.previousSibling;
|
|
104
|
+
}
|
|
105
|
+
return { startOffset, startLine: line };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function applyDoc(decl: Decl, doc: DocSpan | undefined): Decl {
|
|
109
|
+
if (doc === undefined) return decl;
|
|
110
|
+
return {
|
|
111
|
+
...decl,
|
|
112
|
+
docStartOffset: doc.docStartOffset,
|
|
113
|
+
docEndOffset: doc.docEndOffset,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function unquote(text: string): string {
|
|
118
|
+
if (text.length < 2) return text;
|
|
119
|
+
const a = text[0];
|
|
120
|
+
const b = text[text.length - 1];
|
|
121
|
+
if ((a === '"' || a === "'" || a === "`") && a === b) return text.slice(1, -1);
|
|
122
|
+
return text;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function field(node: Node, name: string): Node | null {
|
|
126
|
+
return node.childForFieldName(name);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function nameText(node: Node | null | undefined): string {
|
|
130
|
+
if (node === null || node === undefined) return "";
|
|
131
|
+
return node.text;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function qualify(owner: string, name: string): string {
|
|
135
|
+
return owner.length === 0 ? name : `${owner}.${name}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type DeclDraft = Omit<Decl, "signatureEndOffset" | "children" | "calls" | "bases"> &
|
|
139
|
+
Partial<Pick<Decl, "signatureEndOffset" | "bodyStartOffset" | "bodyEndOffset" | "children" | "calls" | "bases">>;
|
|
140
|
+
|
|
141
|
+
export function finishDecl(partial: DeclDraft, body: Node | null): Decl {
|
|
142
|
+
const calls = partial.calls ?? [];
|
|
143
|
+
const bases = partial.bases ?? [];
|
|
144
|
+
if (body === null) {
|
|
145
|
+
return {
|
|
146
|
+
...partial,
|
|
147
|
+
signatureEndOffset: partial.signatureEndOffset ?? partial.endOffset,
|
|
148
|
+
children: partial.children ?? [],
|
|
149
|
+
calls,
|
|
150
|
+
bases,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
...partial,
|
|
155
|
+
signatureEndOffset: body.startIndex,
|
|
156
|
+
bodyStartOffset: body.startIndex,
|
|
157
|
+
bodyEndOffset: body.endIndex,
|
|
158
|
+
children: partial.children ?? [],
|
|
159
|
+
calls,
|
|
160
|
+
bases,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Walk a body for call extraction. Skips nested scope roots (`isNestedScope`)
|
|
166
|
+
* so nested callables own their own sites.
|
|
167
|
+
*/
|
|
168
|
+
export function walkBodyNodes(body: Node, isNestedScope: (node: Node) => boolean, visit: (node: Node) => void): void {
|
|
169
|
+
const go = (node: Node, isRoot: boolean): void => {
|
|
170
|
+
if (!isRoot && isNestedScope(node)) return;
|
|
171
|
+
visit(node);
|
|
172
|
+
for (const child of node.namedChildren) go(child, false);
|
|
173
|
+
};
|
|
174
|
+
go(body, true);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function callSite(
|
|
178
|
+
name: string,
|
|
179
|
+
line: number,
|
|
180
|
+
startOffset: number,
|
|
181
|
+
endOffset: number,
|
|
182
|
+
kind: CallSite["kind"],
|
|
183
|
+
receiver = "",
|
|
184
|
+
): CallSite | undefined {
|
|
185
|
+
if (name.length === 0) return undefined;
|
|
186
|
+
return { name, receiver, line, startOffset, endOffset, kind };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Build a top-level-ish decl from a grammar node span + optional body. */
|
|
190
|
+
export function declFromNode(
|
|
191
|
+
node: Node,
|
|
192
|
+
owner: string,
|
|
193
|
+
kind: Decl["kind"],
|
|
194
|
+
name: string,
|
|
195
|
+
exported: boolean,
|
|
196
|
+
body: Node | null,
|
|
197
|
+
children: Decl[],
|
|
198
|
+
doc: DocSpan | undefined,
|
|
199
|
+
visibility: Decl["visibility"] = "public",
|
|
200
|
+
): Decl | undefined {
|
|
201
|
+
if (name.length === 0) return undefined;
|
|
202
|
+
return applyDoc(
|
|
203
|
+
finishDecl(
|
|
204
|
+
{
|
|
205
|
+
kind,
|
|
206
|
+
name,
|
|
207
|
+
qualifiedName: qualify(owner, name),
|
|
208
|
+
startLine: startLine(node),
|
|
209
|
+
endLine: endLine(node),
|
|
210
|
+
startOffset: node.startIndex,
|
|
211
|
+
endOffset: node.endIndex,
|
|
212
|
+
visibility,
|
|
213
|
+
exported,
|
|
214
|
+
children,
|
|
215
|
+
},
|
|
216
|
+
body,
|
|
217
|
+
),
|
|
218
|
+
doc,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import type { FileDepHost, FileDepResolver } from "../adapter.ts";
|
|
4
|
+
import {
|
|
5
|
+
externalId,
|
|
6
|
+
findAncestorFile,
|
|
7
|
+
firstExistingFile,
|
|
8
|
+
internalPaths,
|
|
9
|
+
isWithin,
|
|
10
|
+
packageNameFromBareSpecifier,
|
|
11
|
+
} from "./file-dep-util.ts";
|
|
12
|
+
|
|
13
|
+
const TS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".mtsx"] as const;
|
|
14
|
+
|
|
15
|
+
type TsPathMapping = {
|
|
16
|
+
pattern: string;
|
|
17
|
+
/** Directory paths are resolved against (configDir + baseUrl of defining tsconfig). */
|
|
18
|
+
baseDir: string;
|
|
19
|
+
targets: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type TsPathsConfig = {
|
|
23
|
+
mappings: TsPathMapping[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function pushTsStems(out: string[], stem: string): void {
|
|
27
|
+
for (const sourceExt of TS_SOURCE_EXTENSIONS) out.push(`${stem}${sourceExt}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sourceCandidatesFromSeed(seed: string): string[] {
|
|
31
|
+
const out = [seed];
|
|
32
|
+
const ext = extname(seed);
|
|
33
|
+
if (ext === ".js" || ext === ".mjs" || ext === ".cjs" || ext === ".jsx") {
|
|
34
|
+
pushTsStems(out, seed.slice(0, -ext.length));
|
|
35
|
+
if (ext === ".jsx") out.push(`${seed.slice(0, -ext.length)}.tsx`);
|
|
36
|
+
} else if (seed.endsWith(".d.ts")) {
|
|
37
|
+
pushTsStems(out, seed.slice(0, -".d.ts".length));
|
|
38
|
+
} else if (ext === "") {
|
|
39
|
+
for (const sourceExt of TS_SOURCE_EXTENSIONS) {
|
|
40
|
+
out.push(`${seed}${sourceExt}`, join(seed, `index${sourceExt}`));
|
|
41
|
+
}
|
|
42
|
+
} else if (TS_SOURCE_EXTENSIONS.includes(ext as (typeof TS_SOURCE_EXTENSIONS)[number])) {
|
|
43
|
+
// already exact
|
|
44
|
+
} else {
|
|
45
|
+
pushTsStems(out, seed);
|
|
46
|
+
for (const sourceExt of TS_SOURCE_EXTENSIONS) out.push(join(seed, `index${sourceExt}`));
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function resolveExistingSource(host: FileDepHost, seed: string): Promise<string | undefined> {
|
|
52
|
+
const candidates = sourceCandidatesFromSeed(seed);
|
|
53
|
+
const preferred = candidates.filter((path) => {
|
|
54
|
+
if (path.endsWith(".d.ts")) return false;
|
|
55
|
+
const ext = extname(path);
|
|
56
|
+
return ext !== ".js" && ext !== ".mjs" && ext !== ".cjs" && ext !== ".jsx";
|
|
57
|
+
});
|
|
58
|
+
const hit = await firstExistingFile(host, preferred);
|
|
59
|
+
if (hit !== undefined) return hit;
|
|
60
|
+
return firstExistingFile(host, candidates);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function resolveRelative(host: FileDepHost, baseDir: string, target: string): Promise<string | undefined> {
|
|
64
|
+
return resolveExistingSource(host, resolve(baseDir, target));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Enough JSONC for tsconfig: // line comments + trailing commas.
|
|
69
|
+
* Does not strip block comments with a regex — that eats path strings like `"./*"`.
|
|
70
|
+
*/
|
|
71
|
+
function parseJsonc(text: string): unknown {
|
|
72
|
+
const lines = text.split("\n").map((line) => {
|
|
73
|
+
let inString = false;
|
|
74
|
+
let escape = false;
|
|
75
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
76
|
+
const ch = line.charAt(i);
|
|
77
|
+
if (escape) {
|
|
78
|
+
escape = false;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (inString && ch === "\\") {
|
|
82
|
+
escape = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (ch === '"') {
|
|
86
|
+
inString = !inString;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!inString && ch === "/" && line.charAt(i + 1) === "/") {
|
|
90
|
+
return line.slice(0, i);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return line;
|
|
94
|
+
});
|
|
95
|
+
const noTrail = lines.join("\n").replace(/,(\s*[}\]])/gu, "$1");
|
|
96
|
+
return JSON.parse(noTrail) as unknown;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
100
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
101
|
+
return value as Record<string, unknown>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readPathsObject(value: unknown): Record<string, string[]> {
|
|
105
|
+
const obj = asRecord(value);
|
|
106
|
+
if (obj === undefined) return {};
|
|
107
|
+
const out: Record<string, string[]> = {};
|
|
108
|
+
for (const [pattern, targets] of Object.entries(obj)) {
|
|
109
|
+
if (!Array.isArray(targets)) continue;
|
|
110
|
+
const list = targets.filter((t): t is string => typeof t === "string" && t.length > 0);
|
|
111
|
+
if (list.length > 0) out[pattern] = list;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Match `spec` against a TS paths pattern. Returns the `*` capture, or `""` for
|
|
118
|
+
* exact patterns, or undefined on no match.
|
|
119
|
+
*/
|
|
120
|
+
function matchPathPattern(pattern: string, spec: string): string | undefined {
|
|
121
|
+
const star = pattern.indexOf("*");
|
|
122
|
+
if (star === -1) return pattern === spec ? "" : undefined;
|
|
123
|
+
const prefix = pattern.slice(0, star);
|
|
124
|
+
const suffix = pattern.slice(star + 1);
|
|
125
|
+
if (!spec.startsWith(prefix) || !spec.endsWith(suffix)) return undefined;
|
|
126
|
+
if (spec.length < prefix.length + suffix.length) return undefined;
|
|
127
|
+
return spec.slice(prefix.length, spec.length - suffix.length);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function applyStar(target: string, capture: string): string {
|
|
131
|
+
const star = target.indexOf("*");
|
|
132
|
+
if (star === -1) return target;
|
|
133
|
+
return `${target.slice(0, star)}${capture}${target.slice(star + 1)}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function loadTsconfigFile(path: string): Promise<Record<string, unknown> | undefined> {
|
|
137
|
+
let text: string;
|
|
138
|
+
try {
|
|
139
|
+
text = await readFile(path, "utf8");
|
|
140
|
+
} catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
return asRecord(parseJsonc(text));
|
|
145
|
+
} catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveExtendsPath(fromConfig: string, extendsSpec: string): string {
|
|
151
|
+
const parentDir = dirname(fromConfig);
|
|
152
|
+
if (extendsSpec.endsWith(".json")) return resolve(parentDir, extendsSpec);
|
|
153
|
+
return resolve(parentDir, `${extendsSpec}.json`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Nearest tsconfig/jsconfig paths + extends merge. Targets resolve against the
|
|
158
|
+
* defining config's directory + baseUrl (TypeScript rules).
|
|
159
|
+
*/
|
|
160
|
+
async function loadTsPathsConfig(
|
|
161
|
+
host: FileDepHost,
|
|
162
|
+
fromPath: string,
|
|
163
|
+
signal: AbortSignal,
|
|
164
|
+
): Promise<TsPathsConfig | undefined> {
|
|
165
|
+
const startDir = dirname(fromPath);
|
|
166
|
+
const tsconfig =
|
|
167
|
+
(await findAncestorFile(host, startDir, host.scopeRoot, "tsconfig.json")) ??
|
|
168
|
+
(await findAncestorFile(host, startDir, host.scopeRoot, "jsconfig.json"));
|
|
169
|
+
if (tsconfig === undefined) return undefined;
|
|
170
|
+
|
|
171
|
+
const memoKey = `ts-paths:${tsconfig}`;
|
|
172
|
+
const cached = host.memo.get(memoKey);
|
|
173
|
+
if (cached !== undefined) return cached as TsPathsConfig;
|
|
174
|
+
|
|
175
|
+
// Root → leaf so leaf overrides.
|
|
176
|
+
const chain: string[] = [];
|
|
177
|
+
let current: string | undefined = tsconfig;
|
|
178
|
+
const seen = new Set<string>();
|
|
179
|
+
while (current !== undefined && !seen.has(current)) {
|
|
180
|
+
signal.throwIfAborted();
|
|
181
|
+
seen.add(current);
|
|
182
|
+
chain.push(current);
|
|
183
|
+
const json = await loadTsconfigFile(current);
|
|
184
|
+
if (json === undefined) break;
|
|
185
|
+
const ext = json.extends;
|
|
186
|
+
if (typeof ext !== "string" || ext.length === 0) break;
|
|
187
|
+
const parent = resolveExtendsPath(current, ext);
|
|
188
|
+
if (!isWithin(host.scopeRoot, parent) && resolve(parent) !== resolve(host.scopeRoot)) break;
|
|
189
|
+
current = parent;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const byPattern = new Map<string, TsPathMapping>();
|
|
193
|
+
for (const configPath of [...chain].reverse()) {
|
|
194
|
+
signal.throwIfAborted();
|
|
195
|
+
const json = await loadTsconfigFile(configPath);
|
|
196
|
+
if (json === undefined) continue;
|
|
197
|
+
const compiler = asRecord(json.compilerOptions);
|
|
198
|
+
if (compiler === undefined) continue;
|
|
199
|
+
const configDir = dirname(configPath);
|
|
200
|
+
const baseUrl = typeof compiler.baseUrl === "string" && compiler.baseUrl.length > 0 ? compiler.baseUrl : ".";
|
|
201
|
+
const baseDir = resolve(configDir, baseUrl);
|
|
202
|
+
const paths = readPathsObject(compiler.paths);
|
|
203
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
204
|
+
byPattern.set(pattern, { pattern, baseDir, targets });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const mappings = [...byPattern.values()].sort(
|
|
209
|
+
(a, b) => b.pattern.length - a.pattern.length || (a.pattern < b.pattern ? -1 : 1),
|
|
210
|
+
);
|
|
211
|
+
const config: TsPathsConfig = { mappings };
|
|
212
|
+
host.memo.set(memoKey, config);
|
|
213
|
+
return config;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function resolveViaPaths(
|
|
217
|
+
host: FileDepHost,
|
|
218
|
+
fromPath: string,
|
|
219
|
+
specifier: string,
|
|
220
|
+
signal: AbortSignal,
|
|
221
|
+
): Promise<string | undefined> {
|
|
222
|
+
const config = await loadTsPathsConfig(host, fromPath, signal);
|
|
223
|
+
if (config === undefined || config.mappings.length === 0) return undefined;
|
|
224
|
+
|
|
225
|
+
for (const mapping of config.mappings) {
|
|
226
|
+
signal.throwIfAborted();
|
|
227
|
+
const capture = matchPathPattern(mapping.pattern, specifier);
|
|
228
|
+
if (capture === undefined) continue;
|
|
229
|
+
for (const target of mapping.targets) {
|
|
230
|
+
const mapped = applyStar(target, capture);
|
|
231
|
+
// Guard obvious escapes outside scope after mapping.
|
|
232
|
+
const seed = resolve(mapping.baseDir, mapped);
|
|
233
|
+
const scopeRel = relative(resolve(host.scopeRoot), seed);
|
|
234
|
+
if (scopeRel.startsWith(`..${sep}`) || scopeRel === "..") continue;
|
|
235
|
+
const file = await resolveExistingSource(host, seed);
|
|
236
|
+
if (file !== undefined) return file;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const resolveTypescriptFileDep: FileDepResolver = async (fromPath, specifier, host, signal) => {
|
|
243
|
+
signal.throwIfAborted();
|
|
244
|
+
const trimmed = specifier.trim();
|
|
245
|
+
if (trimmed.length === 0) return { kind: "unresolved" };
|
|
246
|
+
if (trimmed.startsWith("node:") || trimmed.startsWith("data:")) {
|
|
247
|
+
return externalId(packageNameFromBareSpecifier(trimmed));
|
|
248
|
+
}
|
|
249
|
+
if (trimmed.startsWith("./") || trimmed.startsWith("../")) {
|
|
250
|
+
const file = await resolveRelative(host, dirname(fromPath), trimmed);
|
|
251
|
+
if (file === undefined) return { kind: "unresolved" };
|
|
252
|
+
return internalPaths([file]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const aliased = await resolveViaPaths(host, fromPath, trimmed, signal);
|
|
256
|
+
if (aliased !== undefined) return internalPaths([aliased]);
|
|
257
|
+
|
|
258
|
+
return externalId(packageNameFromBareSpecifier(trimmed));
|
|
259
|
+
};
|