@sean.holung/minicode 0.3.1 → 0.3.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 +52 -42
- package/dist/scripts/run-benchmarks.js +147 -0
- package/dist/src/agent/config.js +149 -40
- package/dist/src/agent/editable-config.js +314 -0
- package/dist/src/analysis/structural-analysis.js +379 -0
- package/dist/src/benchmark/evaluator.js +79 -0
- package/dist/src/benchmark/index.js +4 -0
- package/dist/src/benchmark/reporter.js +177 -0
- package/dist/src/benchmark/runner.js +100 -0
- package/dist/src/benchmark/task-loader.js +78 -0
- package/dist/src/benchmark/types.js +5 -0
- package/dist/src/cli/args.js +10 -0
- package/dist/src/cli/config-slash-command.js +135 -0
- package/dist/src/cli/plugin-install.js +69 -0
- package/dist/src/index.js +76 -6
- package/dist/src/indexer/cache.js +6 -4
- package/dist/src/indexer/code-map.js +41 -13
- package/dist/src/indexer/plugins/typescript.js +70 -23
- package/dist/src/indexer/project-index.js +175 -36
- package/dist/src/indexer/symbol-names.js +92 -0
- package/dist/src/model-utils.js +18 -0
- package/dist/src/serve/agent-bridge.js +203 -24
- package/dist/src/serve/mcp-server.js +405 -0
- package/dist/src/serve/server.js +165 -10
- package/dist/src/serve/websocket.js +8 -0
- package/dist/src/shared/graph-styles.js +119 -0
- package/dist/src/tools/find-path.js +75 -0
- package/dist/src/tools/find-references.js +7 -2
- package/dist/src/tools/get-dependencies.js +3 -2
- package/dist/src/tools/read-symbol.js +12 -5
- package/dist/src/tools/registry.js +3 -1
- package/dist/src/tools/search-code-map.js +4 -2
- package/dist/src/ui/app.js +1 -1
- package/dist/src/ui/cli-ink.js +79 -4
- package/dist/src/ui/components/header-bar.js +6 -2
- package/dist/src/ui/state/ui-store.js +5 -0
- package/dist/src/web/app.js +1124 -176
- package/dist/src/web/index.html +113 -3
- package/dist/src/web/style.css +973 -55
- package/dist/tests/agent.test.js +31 -0
- package/dist/tests/analysis-helpers.test.js +89 -0
- package/dist/tests/analysis-ui.test.js +29 -0
- package/dist/tests/benchmark-harness.test.js +527 -0
- package/dist/tests/config-api.test.js +143 -0
- package/dist/tests/config-integration.test.js +751 -0
- package/dist/tests/config-slash-command.test.js +106 -0
- package/dist/tests/config.test.js +42 -1
- package/dist/tests/context-indicator.test.js +220 -0
- package/dist/tests/editable-config.test.js +109 -0
- package/dist/tests/find-path.test.js +183 -0
- package/dist/tests/focus-tracker.test.js +62 -0
- package/dist/tests/graph-onboarding.test.js +55 -0
- package/dist/tests/graph-styles.test.js +65 -0
- package/dist/tests/indexer.test.js +137 -0
- package/dist/tests/mcp-and-plugin.test.js +186 -0
- package/dist/tests/model-client-openai.test.js +29 -0
- package/dist/tests/model-selection.test.js +136 -0
- package/dist/tests/model-utils.test.js +22 -0
- package/dist/tests/reasoning-effort.test.js +264 -0
- package/dist/tests/run-benchmarks.test.js +161 -0
- package/dist/tests/search-code-map.test.js +18 -0
- package/dist/tests/serve.integration.test.js +218 -2
- package/dist/tests/session-ui.test.js +21 -0
- package/dist/tests/session.test.js +50 -0
- package/dist/tests/settings-ui.test.js +30 -0
- package/dist/tests/structural-analysis.test.js +218 -0
- package/node_modules/@minicode/agent-sdk/README.md +80 -51
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +16 -5
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +51 -33
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +14 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +3 -2
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.js +2 -0
- package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.d.ts +35 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.d.ts.map +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.js +64 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.js.map +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +7 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts +5 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +83 -11
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.d.ts +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.js +8 -1
- package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.js +4 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/agent.test.js +3 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/agent.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/guardrails.test.js +8 -2
- package/node_modules/@minicode/agent-sdk/dist/tests/guardrails.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -5
- package/plugin/.claude-plugin/plugin.json +12 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/CLAUDE.md +26 -0
- package/plugin/skills/analyze/SKILL.md +12 -0
- package/plugin/skills/focus/SKILL.md +20 -0
- package/plugin/skills/graph/SKILL.md +13 -0
- package/plugin/skills/symbols/SKILL.md +13 -0
package/dist/src/index.js
CHANGED
|
@@ -3,12 +3,14 @@ import process from "node:process";
|
|
|
3
3
|
import { writeFile } from "node:fs/promises";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { CodingAgent, createModelClient } from "@minicode/agent-sdk";
|
|
6
|
-
import {
|
|
6
|
+
import { loadAgentConfig, getConfigSetupMessage } from "./agent/config.js";
|
|
7
7
|
import { listSessions, loadSession, loadSessionByLabel, saveSession, } from "./session/session-store.js";
|
|
8
8
|
import { computeFileHashes, getWorkspaceCacheDir, loadIndex, saveIndex, } from "./indexer/cache.js";
|
|
9
9
|
import { buildProjectIndex } from "./indexer/project-index.js";
|
|
10
|
+
import { sortModelsAlphabetically } from "./model-utils.js";
|
|
10
11
|
import { createToolRegistry } from "./tools/registry.js";
|
|
11
12
|
import { CliUsageError, parseCliArgs, validateCliArgs, } from "./cli/args.js";
|
|
13
|
+
import { handleConfigSlashCommand } from "./cli/config-slash-command.js";
|
|
12
14
|
const EXIT_CODE_SUCCESS = 0;
|
|
13
15
|
const EXIT_CODE_RUNTIME_ERROR = 1;
|
|
14
16
|
const EXIT_CODE_USAGE_ERROR = 2;
|
|
@@ -18,6 +20,11 @@ function printBanner() {
|
|
|
18
20
|
}
|
|
19
21
|
async function createAgentRuntime(verbose, onProgress) {
|
|
20
22
|
const config = await loadAgentConfig();
|
|
23
|
+
const setupMessage = getConfigSetupMessage(config);
|
|
24
|
+
if (setupMessage) {
|
|
25
|
+
console.error(setupMessage);
|
|
26
|
+
process.exit(EXIT_CODE_USAGE_ERROR);
|
|
27
|
+
}
|
|
21
28
|
const modelClient = createModelClient(config);
|
|
22
29
|
let projectIndex;
|
|
23
30
|
try {
|
|
@@ -49,12 +56,12 @@ async function createAgentRuntime(verbose, onProgress) {
|
|
|
49
56
|
...(onProgress ? { onProgress } : {}),
|
|
50
57
|
});
|
|
51
58
|
}
|
|
52
|
-
return { agent: buildAgent(), config, toolRegistry, projectIndex, buildAgent };
|
|
59
|
+
return { agent: buildAgent(), config, modelClient, toolRegistry, projectIndex, buildAgent };
|
|
53
60
|
}
|
|
54
61
|
async function runInteractive(verbose, initialTask) {
|
|
55
62
|
const runtime = await createAgentRuntime(verbose, (msg) => console.error(` ${msg}`));
|
|
56
63
|
let { agent } = runtime;
|
|
57
|
-
const { config, buildAgent } = runtime;
|
|
64
|
+
const { config, modelClient, buildAgent } = runtime;
|
|
58
65
|
printBanner();
|
|
59
66
|
console.log(`Workspace: ${config.workspaceRoot}`);
|
|
60
67
|
console.log(`Provider: ${config.modelProvider}`);
|
|
@@ -98,12 +105,13 @@ async function runInteractive(verbose, initialTask) {
|
|
|
98
105
|
break;
|
|
99
106
|
}
|
|
100
107
|
if (trimmed === "/help") {
|
|
101
|
-
console.log('Commands: "/help", "/config", "/compact", "/save [label]", "/load [label]", "/sessions", "/exit"');
|
|
108
|
+
console.log('Commands: "/help", "/config [keys|get|set|unset]", "/compact", "/reasoning [level]", "/models", "/model [name]", "/save [label]", "/load [label]", "/sessions", "/exit"');
|
|
102
109
|
console.log("Start with --verbose or -v to log prompts, responses, and tool calls.");
|
|
103
110
|
continue;
|
|
104
111
|
}
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
const configCommand = await handleConfigSlashCommand(trimmed, { config });
|
|
113
|
+
if (configCommand.handled) {
|
|
114
|
+
console.log("\n" + (configCommand.message ?? "") + "\n");
|
|
107
115
|
continue;
|
|
108
116
|
}
|
|
109
117
|
if (trimmed === "/compact") {
|
|
@@ -120,6 +128,63 @@ async function runInteractive(verbose, initialTask) {
|
|
|
120
128
|
}
|
|
121
129
|
continue;
|
|
122
130
|
}
|
|
131
|
+
if (trimmed === "/reasoning" || trimmed.startsWith("/reasoning ")) {
|
|
132
|
+
const VALID_LEVELS = ["xhigh", "high", "medium", "low", "minimal", "none"];
|
|
133
|
+
const arg = trimmed.slice("/reasoning".length).trim().toLowerCase();
|
|
134
|
+
if (arg.length === 0) {
|
|
135
|
+
const current = agent.getReasoningEffort() ?? "(unset)";
|
|
136
|
+
console.log(`Current reasoning effort: ${current}`);
|
|
137
|
+
console.log(`Valid levels: ${VALID_LEVELS.join(", ")}, off`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (arg === "off") {
|
|
141
|
+
agent.setReasoningEffort(undefined);
|
|
142
|
+
console.log("Reasoning effort disabled.");
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (VALID_LEVELS.includes(arg)) {
|
|
146
|
+
agent.setReasoningEffort(arg);
|
|
147
|
+
console.log(`Reasoning effort set to: ${arg}`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
console.log(`Invalid reasoning level "${arg}". Valid levels: ${VALID_LEVELS.join(", ")}, off`);
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (trimmed === "/models") {
|
|
155
|
+
if (modelClient.listModels) {
|
|
156
|
+
console.log("Fetching models...");
|
|
157
|
+
const models = sortModelsAlphabetically(await modelClient.listModels());
|
|
158
|
+
if (models.length === 0) {
|
|
159
|
+
console.log("No models found (provider may not support listing).");
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log(`Available models (${models.length}):`);
|
|
163
|
+
for (const m of models) {
|
|
164
|
+
const marker = m.id === config.model ? " (active)" : "";
|
|
165
|
+
console.log(` ${m.id}${marker}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.log("Current provider does not support listing models.");
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (trimmed.startsWith("/model ")) {
|
|
175
|
+
const newModel = trimmed.slice("/model ".length).trim();
|
|
176
|
+
if (newModel.length === 0) {
|
|
177
|
+
console.log(`Current model: ${config.model}`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
config.model = newModel;
|
|
181
|
+
console.log(`Model switched to: ${newModel}`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (trimmed === "/model") {
|
|
185
|
+
console.log(`Current model: ${config.model}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
123
188
|
if (trimmed === "/save" || trimmed.startsWith("/save ")) {
|
|
124
189
|
const label = trimmed.slice("/save".length).trim() || undefined;
|
|
125
190
|
try {
|
|
@@ -210,6 +275,11 @@ async function runOneshot(params) {
|
|
|
210
275
|
async function main() {
|
|
211
276
|
const cliArgs = parseCliArgs(process.argv);
|
|
212
277
|
validateCliArgs(cliArgs);
|
|
278
|
+
if (cliArgs.pluginInstall) {
|
|
279
|
+
const { installPlugin } = await import("./cli/plugin-install.js");
|
|
280
|
+
await installPlugin();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
213
283
|
if (cliArgs.serve) {
|
|
214
284
|
const { runServe } = await import("./serve/server.js");
|
|
215
285
|
await runServe(cliArgs.verbose, cliArgs.port);
|
|
@@ -20,19 +20,19 @@ export function getWorkspaceCacheDir(workspaceRoot) {
|
|
|
20
20
|
function hashContent(content) {
|
|
21
21
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
22
22
|
}
|
|
23
|
-
async function collectSourceFiles(dir, root, files) {
|
|
23
|
+
async function collectSourceFiles(dir, root, files, validExtensions) {
|
|
24
24
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
25
25
|
for (const entry of entries) {
|
|
26
26
|
const fullPath = path.join(dir, entry.name);
|
|
27
27
|
if (entry.isDirectory()) {
|
|
28
28
|
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
29
|
-
await collectSourceFiles(fullPath, root, files);
|
|
29
|
+
await collectSourceFiles(fullPath, root, files, validExtensions);
|
|
30
30
|
}
|
|
31
31
|
continue;
|
|
32
32
|
}
|
|
33
33
|
if (entry.isFile()) {
|
|
34
34
|
const ext = path.extname(entry.name).toLowerCase();
|
|
35
|
-
if (
|
|
35
|
+
if (validExtensions.has(ext)) {
|
|
36
36
|
files.push(path.relative(root, fullPath));
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -43,8 +43,10 @@ async function collectSourceFiles(dir, root, files) {
|
|
|
43
43
|
*/
|
|
44
44
|
export async function computeFileHashes(workspaceRoot) {
|
|
45
45
|
const root = path.resolve(workspaceRoot);
|
|
46
|
+
const plugins = await loadPlugins(root);
|
|
47
|
+
const validExtensions = new Set(plugins.flatMap((p) => p.extensions));
|
|
46
48
|
const sourceFiles = [];
|
|
47
|
-
await collectSourceFiles(root, root, sourceFiles);
|
|
49
|
+
await collectSourceFiles(root, root, sourceFiles, validExtensions);
|
|
48
50
|
const hashes = new Map();
|
|
49
51
|
for (const relPath of sourceFiles) {
|
|
50
52
|
const absPath = path.join(root, relPath);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getSymbolDisplayName } from "./symbol-names.js";
|
|
1
2
|
const DEFAULT_TOKEN_BUDGET = 1500;
|
|
2
3
|
const APPROX_CHARS_PER_TOKEN = 4;
|
|
3
4
|
function estimateTokens(text) {
|
|
@@ -7,39 +8,63 @@ function formatSymbol(symbol, indent, isMethod) {
|
|
|
7
8
|
if (isMethod) {
|
|
8
9
|
return `${indent} ${symbol.signature}`;
|
|
9
10
|
}
|
|
10
|
-
return `${indent}${symbol.kind} ${symbol
|
|
11
|
+
return `${indent}${symbol.kind} ${getSymbolDisplayName(symbol)}\n${indent} ${symbol.signature}`;
|
|
11
12
|
}
|
|
12
13
|
function isEntryPointFile(filePath) {
|
|
13
14
|
const name = filePath.replace(/\\/g, "/");
|
|
14
15
|
return /(?:^|\/)index\.[jt]sx?$/.test(name);
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Build adjacency maps from edges for O(1) lookups per symbol.
|
|
19
|
+
*/
|
|
20
|
+
function buildAdjacencyMaps(edges) {
|
|
21
|
+
const byFrom = new Map();
|
|
22
|
+
const byTo = new Map();
|
|
23
|
+
for (const edge of edges) {
|
|
24
|
+
const fromList = byFrom.get(edge.from);
|
|
25
|
+
if (fromList)
|
|
26
|
+
fromList.push(edge);
|
|
27
|
+
else
|
|
28
|
+
byFrom.set(edge.from, [edge]);
|
|
29
|
+
const toList = byTo.get(edge.to);
|
|
30
|
+
if (toList)
|
|
31
|
+
toList.push(edge);
|
|
32
|
+
else
|
|
33
|
+
byTo.set(edge.to, [edge]);
|
|
34
|
+
}
|
|
35
|
+
return { byFrom, byTo };
|
|
36
|
+
}
|
|
16
37
|
/**
|
|
17
38
|
* Build the set of symbols related to focus symbols via dependency edges.
|
|
18
39
|
* Expands 1 hop outbound (what focus symbols depend on) and 1 hop inbound
|
|
19
40
|
* (what depends on focus symbols).
|
|
20
41
|
*/
|
|
21
|
-
function expandFocusSet(focusSymbols,
|
|
42
|
+
function expandFocusSet(focusSymbols, adjacency) {
|
|
22
43
|
const expanded = new Set(focusSymbols);
|
|
23
|
-
for (const
|
|
44
|
+
for (const sym of focusSymbols) {
|
|
24
45
|
// Outbound: focus symbol depends on something
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
const outEdges = adjacency.byFrom.get(sym);
|
|
47
|
+
if (outEdges) {
|
|
48
|
+
for (const edge of outEdges)
|
|
49
|
+
expanded.add(edge.to);
|
|
27
50
|
}
|
|
28
51
|
// Inbound: something depends on focus symbol
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
const inEdges = adjacency.byTo.get(sym);
|
|
53
|
+
if (inEdges) {
|
|
54
|
+
for (const edge of inEdges)
|
|
55
|
+
expanded.add(edge.from);
|
|
31
56
|
}
|
|
32
57
|
}
|
|
33
58
|
return expanded;
|
|
34
59
|
}
|
|
35
|
-
function createSymbolRanker(
|
|
60
|
+
function createSymbolRanker(adjacency, focusSymbols) {
|
|
36
61
|
const refCount = new Map();
|
|
37
|
-
for (const
|
|
38
|
-
refCount.set(
|
|
62
|
+
for (const [target, edges] of adjacency.byTo) {
|
|
63
|
+
refCount.set(target, edges.length);
|
|
39
64
|
}
|
|
40
65
|
// Expand focus set to include 1-hop neighbors in the dependency graph
|
|
41
66
|
const boosted = focusSymbols?.size
|
|
42
|
-
? expandFocusSet(focusSymbols,
|
|
67
|
+
? expandFocusSet(focusSymbols, adjacency)
|
|
43
68
|
: undefined;
|
|
44
69
|
return (a, b) => {
|
|
45
70
|
// Focus-boosted symbols always sort first
|
|
@@ -72,8 +97,11 @@ function createSymbolRanker(edges, focusSymbols) {
|
|
|
72
97
|
export function generateCodeMap(symbolsByFile, tokenBudget = DEFAULT_TOKEN_BUDGET, dependencyEdges, focusSymbols) {
|
|
73
98
|
const totalCount = [...symbolsByFile.values()].reduce((sum, syms) => sum + syms.length, 0);
|
|
74
99
|
const lines = ["# Project Code Map", ""];
|
|
100
|
+
const adjacency = dependencyEdges
|
|
101
|
+
? buildAdjacencyMaps(dependencyEdges)
|
|
102
|
+
: { byFrom: new Map(), byTo: new Map() };
|
|
75
103
|
const rank = dependencyEdges
|
|
76
|
-
? createSymbolRanker(
|
|
104
|
+
? createSymbolRanker(adjacency, focusSymbols)
|
|
77
105
|
: (a, b) => (a.exported === b.exported ? 0 : a.exported ? -1 : 1);
|
|
78
106
|
let totalTokens = estimateTokens(lines.join("\n"));
|
|
79
107
|
let truncatedSymbols = 0;
|
|
@@ -82,7 +110,7 @@ export function generateCodeMap(symbolsByFile, tokenBudget = DEFAULT_TOKEN_BUDGE
|
|
|
82
110
|
// When we have focus symbols, sort files so that files containing
|
|
83
111
|
// focused symbols come first in the code map.
|
|
84
112
|
const boosted = focusSymbols?.size && dependencyEdges
|
|
85
|
-
? expandFocusSet(focusSymbols,
|
|
113
|
+
? expandFocusSet(focusSymbols, adjacency)
|
|
86
114
|
: undefined;
|
|
87
115
|
const sortedFiles = [...symbolsByFile.keys()].sort((a, b) => {
|
|
88
116
|
if (boosted) {
|
|
@@ -54,6 +54,7 @@ function extractJSDoc(node, sourceFile) {
|
|
|
54
54
|
return cleaned.length > 0 ? cleaned : undefined;
|
|
55
55
|
}
|
|
56
56
|
function createPlugin() {
|
|
57
|
+
const astCache = new Map();
|
|
57
58
|
return {
|
|
58
59
|
name: "typescript",
|
|
59
60
|
extensions: EXTENSIONS,
|
|
@@ -63,6 +64,7 @@ function createPlugin() {
|
|
|
63
64
|
},
|
|
64
65
|
indexFile(filePath, content) {
|
|
65
66
|
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
67
|
+
astCache.set(filePath, sourceFile);
|
|
66
68
|
const symbols = [];
|
|
67
69
|
let currentClass = null;
|
|
68
70
|
function visit(node) {
|
|
@@ -230,44 +232,88 @@ function createPlugin() {
|
|
|
230
232
|
},
|
|
231
233
|
resolveDependencies(symbols, projectFiles) {
|
|
232
234
|
const symbolSet = new Set(symbols.map((s) => s.qualifiedName));
|
|
235
|
+
const symbolsByLookup = new Map();
|
|
233
236
|
const edges = [];
|
|
237
|
+
const edgeKeys = new Set();
|
|
234
238
|
const rootDir = "/project";
|
|
239
|
+
function addLookup(key, symbol) {
|
|
240
|
+
if (key.length === 0)
|
|
241
|
+
return;
|
|
242
|
+
const existing = symbolsByLookup.get(key);
|
|
243
|
+
if (existing) {
|
|
244
|
+
existing.push(symbol);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
symbolsByLookup.set(key, [symbol]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const symbol of symbols) {
|
|
251
|
+
const lookupKeys = new Set([
|
|
252
|
+
symbol.qualifiedName,
|
|
253
|
+
symbol.name,
|
|
254
|
+
symbol.originalQualifiedName ?? "",
|
|
255
|
+
symbol.displayName ?? "",
|
|
256
|
+
...(symbol.aliases ?? []),
|
|
257
|
+
]);
|
|
258
|
+
for (const key of lookupKeys) {
|
|
259
|
+
addLookup(key, symbol);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
235
262
|
function addEdge(from, to, kind) {
|
|
236
|
-
|
|
263
|
+
const edgeKey = `${from}->${to}:${kind}`;
|
|
264
|
+
if (symbolSet.has(from) && symbolSet.has(to) && !edgeKeys.has(edgeKey)) {
|
|
265
|
+
edgeKeys.add(edgeKey);
|
|
237
266
|
edges.push({ from, to, kind });
|
|
238
267
|
}
|
|
239
268
|
}
|
|
240
|
-
function
|
|
269
|
+
function resolveCandidates(rawName, filePath, kinds) {
|
|
270
|
+
const matches = symbolsByLookup.get(rawName) ?? [];
|
|
271
|
+
return matches.filter((symbol) => (filePath === undefined || symbol.filePath === filePath) &&
|
|
272
|
+
(kinds === undefined || kinds.includes(symbol.kind)));
|
|
273
|
+
}
|
|
274
|
+
function addResolvedEdges(rawFrom, rawTo, kind, filePath, targetKinds) {
|
|
275
|
+
const fromMatches = resolveCandidates(rawFrom, filePath);
|
|
276
|
+
const sameFileTargets = resolveCandidates(rawTo, filePath, targetKinds);
|
|
277
|
+
const toMatches = sameFileTargets.length > 0
|
|
278
|
+
? sameFileTargets
|
|
279
|
+
: resolveCandidates(rawTo, undefined, targetKinds);
|
|
280
|
+
for (const fromSymbol of fromMatches) {
|
|
281
|
+
for (const toSymbol of toMatches) {
|
|
282
|
+
addEdge(fromSymbol.qualifiedName, toSymbol.qualifiedName, kind);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function collectTypeRefs(node, from, filePath) {
|
|
241
287
|
if (ts.isTypeReferenceNode(node)) {
|
|
242
288
|
const name = node.typeName.getText();
|
|
243
|
-
|
|
289
|
+
addResolvedEdges(from, name, "references", filePath, ["type", "interface", "class"]);
|
|
244
290
|
if (ts.isQualifiedName(node.typeName)) {
|
|
245
291
|
const left = node.typeName.left;
|
|
246
292
|
if (ts.isIdentifier(left)) {
|
|
247
|
-
|
|
293
|
+
addResolvedEdges(from, left.getText(), "references", filePath, ["type", "interface", "class"]);
|
|
248
294
|
}
|
|
249
295
|
}
|
|
250
296
|
}
|
|
251
|
-
ts.forEachChild(node, (n) => collectTypeRefs(n, from));
|
|
297
|
+
ts.forEachChild(node, (n) => collectTypeRefs(n, from, filePath));
|
|
252
298
|
}
|
|
253
|
-
function collectCalls(node, from) {
|
|
299
|
+
function collectCalls(node, from, filePath) {
|
|
254
300
|
if (ts.isCallExpression(node)) {
|
|
255
301
|
const expr = node.expression;
|
|
256
302
|
if (ts.isIdentifier(expr)) {
|
|
257
|
-
|
|
303
|
+
addResolvedEdges(from, expr.getText(), "calls", filePath, ["function", "class", "variable"]);
|
|
258
304
|
}
|
|
259
305
|
}
|
|
260
306
|
if (ts.isNewExpression(node)) {
|
|
261
307
|
const expr = node.expression;
|
|
262
308
|
if (ts.isIdentifier(expr)) {
|
|
263
|
-
|
|
309
|
+
addResolvedEdges(from, expr.getText(), "calls", filePath, ["class", "function"]);
|
|
264
310
|
}
|
|
265
311
|
}
|
|
266
|
-
ts.forEachChild(node, (n) => collectCalls(n, from));
|
|
312
|
+
ts.forEachChild(node, (n) => collectCalls(n, from, filePath));
|
|
267
313
|
}
|
|
268
314
|
for (const [filePath, content] of projectFiles) {
|
|
269
315
|
const fullPath = path.join(rootDir, filePath);
|
|
270
|
-
const sourceFile = ts.createSourceFile(fullPath, content, ts.ScriptTarget.Latest, true);
|
|
316
|
+
const sourceFile = astCache.get(filePath) ?? ts.createSourceFile(fullPath, content, ts.ScriptTarget.Latest, true);
|
|
271
317
|
let currentClass = null;
|
|
272
318
|
function visitClassNode(node, name) {
|
|
273
319
|
const prevClass = currentClass;
|
|
@@ -284,7 +330,7 @@ function createPlugin() {
|
|
|
284
330
|
const kind = clause.token === ts.SyntaxKind.ExtendsKeyword
|
|
285
331
|
? "extends"
|
|
286
332
|
: "implements";
|
|
287
|
-
|
|
333
|
+
addResolvedEdges(name, target, kind, filePath, ["class", "interface"]);
|
|
288
334
|
}
|
|
289
335
|
}
|
|
290
336
|
}
|
|
@@ -300,9 +346,9 @@ function createPlugin() {
|
|
|
300
346
|
}
|
|
301
347
|
if (ts.isConstructorDeclaration(node)) {
|
|
302
348
|
const from = currentClass ? `${currentClass}.constructor` : "constructor";
|
|
303
|
-
if (
|
|
304
|
-
collectTypeRefs(node, from);
|
|
305
|
-
collectCalls(node, from);
|
|
349
|
+
if (resolveCandidates(from, filePath).length > 0) {
|
|
350
|
+
collectTypeRefs(node, from, filePath);
|
|
351
|
+
collectCalls(node, from, filePath);
|
|
306
352
|
}
|
|
307
353
|
return;
|
|
308
354
|
}
|
|
@@ -311,18 +357,18 @@ function createPlugin() {
|
|
|
311
357
|
? "[computed]"
|
|
312
358
|
: node.name.getText(sourceFile);
|
|
313
359
|
const from = currentClass ? `${currentClass}.${name}` : name;
|
|
314
|
-
if (
|
|
315
|
-
collectTypeRefs(node, from);
|
|
316
|
-
collectCalls(node, from);
|
|
360
|
+
if (resolveCandidates(from, filePath).length > 0) {
|
|
361
|
+
collectTypeRefs(node, from, filePath);
|
|
362
|
+
collectCalls(node, from, filePath);
|
|
317
363
|
}
|
|
318
364
|
return;
|
|
319
365
|
}
|
|
320
366
|
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
321
367
|
const name = node.name.getText(sourceFile);
|
|
322
368
|
const from = currentClass ? `${currentClass}.${name}` : name;
|
|
323
|
-
if (
|
|
324
|
-
collectTypeRefs(node, from);
|
|
325
|
-
collectCalls(node, from);
|
|
369
|
+
if (resolveCandidates(from, filePath).length > 0) {
|
|
370
|
+
collectTypeRefs(node, from, filePath);
|
|
371
|
+
collectCalls(node, from, filePath);
|
|
326
372
|
}
|
|
327
373
|
return;
|
|
328
374
|
}
|
|
@@ -333,9 +379,9 @@ function createPlugin() {
|
|
|
333
379
|
continue;
|
|
334
380
|
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
335
381
|
const name = decl.name.getText(sourceFile);
|
|
336
|
-
if (
|
|
337
|
-
collectTypeRefs(decl, name);
|
|
338
|
-
collectCalls(decl, name);
|
|
382
|
+
if (resolveCandidates(name, filePath).length > 0) {
|
|
383
|
+
collectTypeRefs(decl, name, filePath);
|
|
384
|
+
collectCalls(decl, name, filePath);
|
|
339
385
|
}
|
|
340
386
|
}
|
|
341
387
|
else if (ts.isClassExpression(init)) {
|
|
@@ -349,6 +395,7 @@ function createPlugin() {
|
|
|
349
395
|
}
|
|
350
396
|
visit(sourceFile);
|
|
351
397
|
}
|
|
398
|
+
astCache.clear();
|
|
352
399
|
return edges;
|
|
353
400
|
},
|
|
354
401
|
};
|