icopilot 2.2.0
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/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/icopilot.js +6 -0
- package/dist/acp/router.js +123 -0
- package/dist/acp/schema.js +53 -0
- package/dist/agents/aggregator.js +187 -0
- package/dist/agents/custom-agents.js +97 -0
- package/dist/agents/goal-driven.js +411 -0
- package/dist/agents/multi-repo.js +350 -0
- package/dist/agents/parallel-runner.js +181 -0
- package/dist/agents/router.js +144 -0
- package/dist/agents/self-heal.js +481 -0
- package/dist/agents/tdd-agent.js +278 -0
- package/dist/api/github-models.js +158 -0
- package/dist/bridge/ide-bridge.js +479 -0
- package/dist/cloud/routine-executor.js +34 -0
- package/dist/cloud/routine-scheduler.js +67 -0
- package/dist/cloud/routine-storage.js +297 -0
- package/dist/commands/acp-cmd.js +143 -0
- package/dist/commands/actions-cmd.js +624 -0
- package/dist/commands/agent-cmd.js +144 -0
- package/dist/commands/alias-cmd.js +132 -0
- package/dist/commands/bookmark-cmd.js +77 -0
- package/dist/commands/changelog-cmd.js +99 -0
- package/dist/commands/changes-cmd.js +120 -0
- package/dist/commands/clipboard-cmd.js +217 -0
- package/dist/commands/cloud-routine-cmd.js +265 -0
- package/dist/commands/codegen-cmd.js +544 -0
- package/dist/commands/compare-cmd.js +116 -0
- package/dist/commands/context-cmd.js +247 -0
- package/dist/commands/context-viz-cmd.js +43 -0
- package/dist/commands/conventions-cmd.js +116 -0
- package/dist/commands/cost-cmd.js +51 -0
- package/dist/commands/deps-cmd.js +294 -0
- package/dist/commands/diagram-cmd.js +658 -0
- package/dist/commands/diff-review-cmd.js +92 -0
- package/dist/commands/doc-cmd.js +412 -0
- package/dist/commands/doctor-cmd.js +152 -0
- package/dist/commands/editor-cmd.js +49 -0
- package/dist/commands/env-cmd.js +86 -0
- package/dist/commands/explain-cmd.js +78 -0
- package/dist/commands/explain-shell-cmd.js +22 -0
- package/dist/commands/explore-cmd.js +231 -0
- package/dist/commands/feedback-cmd.js +98 -0
- package/dist/commands/fix-cmd.js +17 -0
- package/dist/commands/generate-cmd.js +38 -0
- package/dist/commands/git-extra.js +197 -0
- package/dist/commands/git-log-cmd.js +98 -0
- package/dist/commands/git-undo-cmd.js +137 -0
- package/dist/commands/git.js +155 -0
- package/dist/commands/history-cmd.js +122 -0
- package/dist/commands/index-cmd.js +65 -0
- package/dist/commands/init-cmd.js +73 -0
- package/dist/commands/lint-cmd.js +133 -0
- package/dist/commands/memory-cmd.js +98 -0
- package/dist/commands/metrics-cmd.js +97 -0
- package/dist/commands/mode-prefix.js +30 -0
- package/dist/commands/multi-cmd.js +44 -0
- package/dist/commands/notify-cmd.js +204 -0
- package/dist/commands/profile-cmd.js +101 -0
- package/dist/commands/prompts.js +17 -0
- package/dist/commands/rag-cmd.js +60 -0
- package/dist/commands/readme-cmd.js +564 -0
- package/dist/commands/reasoning-cmd.js +34 -0
- package/dist/commands/refactor-cmd.js +96 -0
- package/dist/commands/release-cmd.js +450 -0
- package/dist/commands/repo-cmd.js +195 -0
- package/dist/commands/route-cmd.js +21 -0
- package/dist/commands/schedule-cmd.js +109 -0
- package/dist/commands/search-cmd.js +47 -0
- package/dist/commands/security-cmd.js +156 -0
- package/dist/commands/settings-cmd.js +238 -0
- package/dist/commands/skill-cmd.js +338 -0
- package/dist/commands/slash.js +2721 -0
- package/dist/commands/snippets-cmd.js +83 -0
- package/dist/commands/space-cmd.js +92 -0
- package/dist/commands/stash-cmd.js +156 -0
- package/dist/commands/stats-cmd.js +36 -0
- package/dist/commands/style-cmd.js +85 -0
- package/dist/commands/suggest-cmd.js +40 -0
- package/dist/commands/summary-cmd.js +138 -0
- package/dist/commands/task-cmd.js +58 -0
- package/dist/commands/team-memory-cmd.js +97 -0
- package/dist/commands/template-cmd.js +475 -0
- package/dist/commands/test-cmd.js +146 -0
- package/dist/commands/todo-cmd.js +172 -0
- package/dist/commands/tokens-cmd.js +277 -0
- package/dist/commands/trigger-cmd.js +147 -0
- package/dist/commands/undo-cmd.js +18 -0
- package/dist/commands/voice-cmd.js +89 -0
- package/dist/commands/watch-cmd.js +110 -0
- package/dist/commands/web-cmd.js +183 -0
- package/dist/commands/worktree-cmd.js +119 -0
- package/dist/config-profile.js +66 -0
- package/dist/config.js +288 -0
- package/dist/context/compactor.js +53 -0
- package/dist/context/dep-context.js +329 -0
- package/dist/context/file-refs.js +54 -0
- package/dist/context/git-context.js +229 -0
- package/dist/context/image-input.js +66 -0
- package/dist/context/memory.js +55 -0
- package/dist/context/persistent-memory.js +104 -0
- package/dist/context/pinned.js +96 -0
- package/dist/context/priority.js +150 -0
- package/dist/context/read-only.js +48 -0
- package/dist/context/smart-files.js +286 -0
- package/dist/context/team-memory.js +156 -0
- package/dist/extensions/loader.js +149 -0
- package/dist/extensions/marketplace.js +49 -0
- package/dist/extensions/slack-provider.js +181 -0
- package/dist/extensions/team.js +56 -0
- package/dist/extensions/teams-provider.js +222 -0
- package/dist/extensions/voice.js +18 -0
- package/dist/hooks/lifecycle.js +215 -0
- package/dist/hooks/precommit.js +463 -0
- package/dist/index/embeddings.js +23 -0
- package/dist/index/indexer.js +86 -0
- package/dist/index/retrieve.js +20 -0
- package/dist/index/store.js +95 -0
- package/dist/index.js +286 -0
- package/dist/intelligence/dead-code.js +457 -0
- package/dist/intelligence/error-watch.js +263 -0
- package/dist/intelligence/navigation.js +141 -0
- package/dist/intelligence/stack-trace.js +210 -0
- package/dist/intelligence/symbol-index.js +410 -0
- package/dist/knowledge/auto-memory.js +412 -0
- package/dist/knowledge/conventions.js +475 -0
- package/dist/knowledge/corrections.js +213 -0
- package/dist/knowledge/rag.js +450 -0
- package/dist/knowledge/style-learner.js +324 -0
- package/dist/logger.js +35 -0
- package/dist/mcp/client.js +144 -0
- package/dist/mcp/config.js +24 -0
- package/dist/mcp/index.js +89 -0
- package/dist/modes/auto-compact.js +20 -0
- package/dist/modes/autopilot.js +157 -0
- package/dist/modes/background.js +82 -0
- package/dist/modes/interactive.js +187 -0
- package/dist/modes/oneshot.js +36 -0
- package/dist/modes/tui.js +265 -0
- package/dist/modes/turn.js +342 -0
- package/dist/notifications/manager.js +107 -0
- package/dist/plugins/marketplace.js +244 -0
- package/dist/providers/custom-provider.js +298 -0
- package/dist/providers/local-model.js +121 -0
- package/dist/routing/profiles.js +44 -0
- package/dist/routing/router.js +18 -0
- package/dist/sandbox/container.js +151 -0
- package/dist/security/audit.js +237 -0
- package/dist/security/content-filter.js +449 -0
- package/dist/security/proxy.js +301 -0
- package/dist/security/retention.js +281 -0
- package/dist/security/roles.js +252 -0
- package/dist/server/api-server.js +679 -0
- package/dist/session/bookmarks.js +72 -0
- package/dist/session/cloud-session.js +291 -0
- package/dist/session/handoff.js +405 -0
- package/dist/session/manager.js +35 -0
- package/dist/session/session.js +296 -0
- package/dist/session/share.js +313 -0
- package/dist/session/undo-journal.js +91 -0
- package/dist/snippets/store.js +60 -0
- package/dist/spaces/space-config.js +156 -0
- package/dist/spaces/space.js +220 -0
- package/dist/stats/store.js +101 -0
- package/dist/tools/apply-patch.js +134 -0
- package/dist/tools/auto-check.js +218 -0
- package/dist/tools/diff-edit.js +150 -0
- package/dist/tools/diff-prompt.js +36 -0
- package/dist/tools/edit-file.js +66 -0
- package/dist/tools/file-ops.js +205 -0
- package/dist/tools/glob.js +17 -0
- package/dist/tools/grep.js +56 -0
- package/dist/tools/image.js +194 -0
- package/dist/tools/list-directory.js +228 -0
- package/dist/tools/memory.js +17 -0
- package/dist/tools/multi-edit.js +299 -0
- package/dist/tools/policy.js +95 -0
- package/dist/tools/registry.js +484 -0
- package/dist/tools/retry.js +74 -0
- package/dist/tools/run-in-terminal.js +162 -0
- package/dist/tools/safety.js +64 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/search-symbols.js +212 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/web.js +167 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/render.js +96 -0
- package/dist/ui/screen.js +13 -0
- package/dist/ui/theme.js +56 -0
- package/dist/util/browser.js +34 -0
- package/dist/util/completion.js +350 -0
- package/dist/util/cost.js +28 -0
- package/dist/util/keybindings.js +113 -0
- package/dist/util/lazy.js +26 -0
- package/dist/util/perf.js +25 -0
- package/dist/util/token-worker.js +11 -0
- package/dist/util/tokens.js +50 -0
- package/dist/workflows/builtins.js +128 -0
- package/dist/workflows/engine.js +496 -0
- package/dist/workflows/file-trigger.js +197 -0
- package/package.json +79 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { theme } from '../ui/theme.js';
|
|
5
|
+
const MAX_NODES = 20;
|
|
6
|
+
const OMITTED_NODE_KEY = '__omitted__';
|
|
7
|
+
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs']);
|
|
8
|
+
const SKIP_DIRS = new Set([
|
|
9
|
+
'.git',
|
|
10
|
+
'.next',
|
|
11
|
+
'.turbo',
|
|
12
|
+
'build',
|
|
13
|
+
'coverage',
|
|
14
|
+
'dist',
|
|
15
|
+
'node_modules',
|
|
16
|
+
'out',
|
|
17
|
+
]);
|
|
18
|
+
const FLOW_ENTRY_NAMES = new Set([
|
|
19
|
+
'main',
|
|
20
|
+
'run',
|
|
21
|
+
'start',
|
|
22
|
+
'bootstrap',
|
|
23
|
+
'createProgram',
|
|
24
|
+
'handleSlash',
|
|
25
|
+
]);
|
|
26
|
+
export function diagramCommand(args, cwd) {
|
|
27
|
+
const [subcommand, ...rest] = args;
|
|
28
|
+
const scope = rest.join(' ').trim() || undefined;
|
|
29
|
+
if (!subcommand) {
|
|
30
|
+
return generateDiagram(cwd, { type: 'architecture' });
|
|
31
|
+
}
|
|
32
|
+
switch (subcommand.toLowerCase()) {
|
|
33
|
+
case 'architecture':
|
|
34
|
+
return generateDiagram(cwd, { type: 'architecture', scope });
|
|
35
|
+
case 'deps':
|
|
36
|
+
return generateDiagram(cwd, { type: 'deps', scope });
|
|
37
|
+
case 'classes':
|
|
38
|
+
return generateDiagram(cwd, { type: 'classes', scope });
|
|
39
|
+
case 'flow':
|
|
40
|
+
return generateDiagram(cwd, { type: 'flow', scope });
|
|
41
|
+
default:
|
|
42
|
+
return `${theme.warn('usage: /diagram [deps|classes [file]|flow [function]]')}\n`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function generateDiagram(rootDir, opts) {
|
|
46
|
+
const cwd = path.resolve(rootDir);
|
|
47
|
+
const scopeTarget = resolveScopeTarget(cwd, opts.scope);
|
|
48
|
+
const files = opts.type === 'flow'
|
|
49
|
+
? collectSourceFiles(cwd)
|
|
50
|
+
: collectSourceFiles(cwd, scopeTarget ?? undefined);
|
|
51
|
+
const parsedFiles = parseProjectFiles(cwd, files);
|
|
52
|
+
switch (opts.type) {
|
|
53
|
+
case 'architecture':
|
|
54
|
+
return buildArchitectureDiagram(cwd, parsedFiles);
|
|
55
|
+
case 'deps':
|
|
56
|
+
return buildDependencyDiagram(parsedFiles);
|
|
57
|
+
case 'classes':
|
|
58
|
+
return buildClassDiagram(parsedFiles);
|
|
59
|
+
case 'flow':
|
|
60
|
+
return buildFlowDiagram(cwd, parsedFiles, opts.scope);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function buildArchitectureDiagram(rootDir, parsedFiles) {
|
|
64
|
+
const entryFiles = new Set(detectEntryFiles(rootDir, parsedFiles));
|
|
65
|
+
const nodes = new Map();
|
|
66
|
+
const edges = new Map();
|
|
67
|
+
for (const parsed of parsedFiles) {
|
|
68
|
+
const fromModule = detectModuleBoundary(parsed.relative);
|
|
69
|
+
upsertNode(nodes, fromModule, entryFiles.has(parsed.file) ? `${fromModule}<br/>entry` : fromModule, 1);
|
|
70
|
+
if (entryFiles.has(parsed.file)) {
|
|
71
|
+
incrementNode(nodes, fromModule, 3);
|
|
72
|
+
}
|
|
73
|
+
for (const imported of parsed.imports) {
|
|
74
|
+
const toModule = detectModuleBoundary(relativePath(rootDir, imported));
|
|
75
|
+
upsertNode(nodes, toModule, entryFiles.has(imported) ? `${toModule}<br/>entry` : toModule, 1);
|
|
76
|
+
incrementNode(nodes, fromModule, 1);
|
|
77
|
+
incrementNode(nodes, toModule, 1);
|
|
78
|
+
if (fromModule !== toModule) {
|
|
79
|
+
upsertEdge(edges, fromModule, toModule);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return renderDirectedGraph('graph TD', nodes, edges, {
|
|
84
|
+
emptyMessage: 'No module relationships found.',
|
|
85
|
+
omittedLabel: 'other modules',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function buildDependencyDiagram(parsedFiles) {
|
|
89
|
+
const nodes = new Map();
|
|
90
|
+
const edges = new Map();
|
|
91
|
+
for (const parsed of parsedFiles) {
|
|
92
|
+
upsertNode(nodes, parsed.relative, parsed.relative, 1);
|
|
93
|
+
for (const imported of parsed.imports) {
|
|
94
|
+
const relativeImport = toPosixPath(parsedFiles.find((item) => item.file === imported)?.relative ?? '');
|
|
95
|
+
if (!relativeImport)
|
|
96
|
+
continue;
|
|
97
|
+
upsertNode(nodes, relativeImport, relativeImport, 1);
|
|
98
|
+
incrementNode(nodes, parsed.relative, 1);
|
|
99
|
+
incrementNode(nodes, relativeImport, 1);
|
|
100
|
+
upsertEdge(edges, parsed.relative, relativeImport);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return renderDirectedGraph('graph LR', nodes, edges, {
|
|
104
|
+
emptyMessage: 'No file dependencies found.',
|
|
105
|
+
omittedLabel: 'other files',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function buildClassDiagram(parsedFiles) {
|
|
109
|
+
const nodes = new Map();
|
|
110
|
+
const edges = new Map();
|
|
111
|
+
const classByName = new Map();
|
|
112
|
+
for (const parsed of parsedFiles) {
|
|
113
|
+
for (const classInfo of parsed.classes) {
|
|
114
|
+
classByName.set(classInfo.name, classInfo);
|
|
115
|
+
upsertNode(nodes, classInfo.id, classInfo.name, 1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const parsed of parsedFiles) {
|
|
119
|
+
for (const classInfo of parsed.classes) {
|
|
120
|
+
if (!classInfo.extends)
|
|
121
|
+
continue;
|
|
122
|
+
const base = classByName.get(classInfo.extends);
|
|
123
|
+
const baseKey = base?.id ?? `external:${classInfo.extends}`;
|
|
124
|
+
const baseLabel = base?.name ?? `${classInfo.extends} (external)`;
|
|
125
|
+
upsertNode(nodes, baseKey, baseLabel, base ? 1 : 0);
|
|
126
|
+
incrementNode(nodes, classInfo.id, 1);
|
|
127
|
+
incrementNode(nodes, baseKey, 1);
|
|
128
|
+
upsertEdge(edges, baseKey, classInfo.id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return renderClassGraph(nodes, edges);
|
|
132
|
+
}
|
|
133
|
+
function buildFlowDiagram(rootDir, parsedFiles, scope) {
|
|
134
|
+
const entryFiles = new Set(detectEntryFiles(rootDir, parsedFiles));
|
|
135
|
+
const allFunctions = parsedFiles.flatMap((parsed) => parsed.functions);
|
|
136
|
+
const functionById = new Map(allFunctions.map((fn) => [fn.id, fn]));
|
|
137
|
+
const functionIdsBySimpleName = new Map();
|
|
138
|
+
const functionIdsByFullName = new Map();
|
|
139
|
+
for (const fn of allFunctions) {
|
|
140
|
+
functionIdsByFullName.set(fn.fullName, fn.id);
|
|
141
|
+
const ids = functionIdsBySimpleName.get(fn.simpleName) ?? [];
|
|
142
|
+
ids.push(fn.id);
|
|
143
|
+
functionIdsBySimpleName.set(fn.simpleName, ids);
|
|
144
|
+
}
|
|
145
|
+
const edges = new Map();
|
|
146
|
+
const incoming = new Map();
|
|
147
|
+
for (const fn of allFunctions) {
|
|
148
|
+
for (const call of fn.calls) {
|
|
149
|
+
const targetId = resolveFunctionCall(fn, call, functionIdsBySimpleName, functionIdsByFullName);
|
|
150
|
+
if (!targetId || targetId === fn.id || !functionById.has(targetId))
|
|
151
|
+
continue;
|
|
152
|
+
upsertEdge(edges, fn.id, targetId);
|
|
153
|
+
incoming.set(targetId, (incoming.get(targetId) ?? 0) + 1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const rootIds = selectFlowRoots(allFunctions, scope, entryFiles, functionIdsBySimpleName, functionIdsByFullName, incoming);
|
|
157
|
+
const reachableIds = collectReachableNodes(rootIds, edges);
|
|
158
|
+
const activeIds = reachableIds.size > 0 ? reachableIds : new Set(allFunctions.map((fn) => fn.id));
|
|
159
|
+
const nodes = new Map();
|
|
160
|
+
const filteredEdges = new Map();
|
|
161
|
+
for (const fn of allFunctions) {
|
|
162
|
+
if (!activeIds.has(fn.id))
|
|
163
|
+
continue;
|
|
164
|
+
const label = rootIds.has(fn.id) ? `${fn.fullName}<br/>entry` : fn.fullName;
|
|
165
|
+
upsertNode(nodes, fn.id, label, 1 + (incoming.get(fn.id) ?? 0) + fn.calls.length);
|
|
166
|
+
if (rootIds.has(fn.id)) {
|
|
167
|
+
incrementNode(nodes, fn.id, 3);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const edge of edges.values()) {
|
|
171
|
+
if (!activeIds.has(edge.from) || !activeIds.has(edge.to))
|
|
172
|
+
continue;
|
|
173
|
+
filteredEdges.set(`${edge.from}→${edge.to}`, edge);
|
|
174
|
+
}
|
|
175
|
+
return renderDirectedGraph('flowchart TD', nodes, filteredEdges, {
|
|
176
|
+
emptyMessage: scope ? `No function flow found for "${scope}".` : 'No function flow found.',
|
|
177
|
+
omittedLabel: 'other functions',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function parseProjectFiles(rootDir, files) {
|
|
181
|
+
const fileSet = new Set(files.map((file) => path.resolve(file)));
|
|
182
|
+
return files
|
|
183
|
+
.map((file) => parseSourceFile(rootDir, file, fileSet))
|
|
184
|
+
.sort((left, right) => left.relative.localeCompare(right.relative));
|
|
185
|
+
}
|
|
186
|
+
function parseSourceFile(rootDir, file, fileSet) {
|
|
187
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
188
|
+
const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, scriptKindForFile(file));
|
|
189
|
+
const imports = new Set();
|
|
190
|
+
const classes = [];
|
|
191
|
+
const functions = [];
|
|
192
|
+
const visit = (node, currentClass) => {
|
|
193
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
194
|
+
node.moduleSpecifier &&
|
|
195
|
+
ts.isStringLiteral(node.moduleSpecifier)) {
|
|
196
|
+
const resolved = resolveImportTarget(file, node.moduleSpecifier.text, fileSet);
|
|
197
|
+
if (resolved)
|
|
198
|
+
imports.add(resolved);
|
|
199
|
+
}
|
|
200
|
+
if (ts.isCallExpression(node)) {
|
|
201
|
+
const moduleSpecifier = extractCallImport(node);
|
|
202
|
+
if (moduleSpecifier) {
|
|
203
|
+
const resolved = resolveImportTarget(file, moduleSpecifier, fileSet);
|
|
204
|
+
if (resolved)
|
|
205
|
+
imports.add(resolved);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
209
|
+
const className = node.name.text;
|
|
210
|
+
const heritage = node.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
|
|
211
|
+
const extendsName = heritage?.types[0]?.expression.getText(sourceFile).trim();
|
|
212
|
+
classes.push({
|
|
213
|
+
id: `${relativePath(rootDir, file)}#${className}`,
|
|
214
|
+
name: className,
|
|
215
|
+
file,
|
|
216
|
+
extends: extendsName,
|
|
217
|
+
});
|
|
218
|
+
ts.forEachChild(node, (child) => visit(child, className));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (ts.isFunctionDeclaration(node) && node.name && node.body) {
|
|
222
|
+
functions.push(createFunctionInfo(rootDir, file, node.name.text, node.body, sourceFile, undefined, isNodeExported(node)));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (currentClass && ts.isMethodDeclaration(node) && node.body) {
|
|
226
|
+
const methodName = getNodeName(node.name);
|
|
227
|
+
if (methodName) {
|
|
228
|
+
functions.push(createFunctionInfo(rootDir, file, methodName, node.body, sourceFile, currentClass, isNodeExported(node)));
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (ts.isVariableDeclaration(node) && node.initializer && ts.isIdentifier(node.name)) {
|
|
233
|
+
if (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) {
|
|
234
|
+
const body = ts.isBlock(node.initializer.body)
|
|
235
|
+
? node.initializer.body
|
|
236
|
+
: ts.factory.createBlock([ts.factory.createReturnStatement(node.initializer.body)], true);
|
|
237
|
+
functions.push(createFunctionInfo(rootDir, file, node.name.text, body, sourceFile, currentClass, isNodeExported(node.parent?.parent)));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
ts.forEachChild(node, (child) => visit(child, currentClass));
|
|
242
|
+
};
|
|
243
|
+
visit(sourceFile);
|
|
244
|
+
return {
|
|
245
|
+
file,
|
|
246
|
+
relative: relativePath(rootDir, file),
|
|
247
|
+
imports: Array.from(imports).sort((left, right) => left.localeCompare(right)),
|
|
248
|
+
classes,
|
|
249
|
+
functions,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function createFunctionInfo(rootDir, file, simpleName, body, sourceFile, className, exported = false) {
|
|
253
|
+
const fullName = className ? `${className}.${simpleName}` : simpleName;
|
|
254
|
+
return {
|
|
255
|
+
id: `${relativePath(rootDir, file)}#${fullName}`,
|
|
256
|
+
simpleName,
|
|
257
|
+
fullName,
|
|
258
|
+
file,
|
|
259
|
+
className,
|
|
260
|
+
exported,
|
|
261
|
+
calls: collectCallNames(body, sourceFile, className),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function collectCallNames(body, sourceFile, className) {
|
|
265
|
+
const calls = new Set();
|
|
266
|
+
const visit = (node) => {
|
|
267
|
+
if (node !== body && (ts.isFunctionLike(node) || ts.isClassLike(node))) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (ts.isCallExpression(node)) {
|
|
271
|
+
const callName = getCallName(node, sourceFile, className);
|
|
272
|
+
if (callName)
|
|
273
|
+
calls.add(callName);
|
|
274
|
+
}
|
|
275
|
+
ts.forEachChild(node, visit);
|
|
276
|
+
};
|
|
277
|
+
ts.forEachChild(body, visit);
|
|
278
|
+
return Array.from(calls);
|
|
279
|
+
}
|
|
280
|
+
function getCallName(node, sourceFile, className) {
|
|
281
|
+
if (ts.isIdentifier(node.expression)) {
|
|
282
|
+
return node.expression.text;
|
|
283
|
+
}
|
|
284
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
285
|
+
if (node.expression.expression.kind === ts.SyntaxKind.ThisKeyword && className) {
|
|
286
|
+
return `${className}.${node.expression.name.text}`;
|
|
287
|
+
}
|
|
288
|
+
return node.expression.name.text;
|
|
289
|
+
}
|
|
290
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
291
|
+
const firstArg = node.arguments[0];
|
|
292
|
+
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
293
|
+
return `import:${firstArg.text}`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return node.expression.getText(sourceFile).trim() || null;
|
|
297
|
+
}
|
|
298
|
+
function resolveFunctionCall(source, call, functionIdsBySimpleName, functionIdsByFullName) {
|
|
299
|
+
if (call.startsWith('import:'))
|
|
300
|
+
return null;
|
|
301
|
+
if (functionIdsByFullName.has(call))
|
|
302
|
+
return functionIdsByFullName.get(call) ?? null;
|
|
303
|
+
if (source.className) {
|
|
304
|
+
const classQualified = `${source.className}.${call}`;
|
|
305
|
+
if (functionIdsByFullName.has(classQualified)) {
|
|
306
|
+
return functionIdsByFullName.get(classQualified) ?? null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const bySimple = functionIdsBySimpleName.get(call) ?? [];
|
|
310
|
+
if (bySimple.length === 1) {
|
|
311
|
+
return bySimple[0] ?? null;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
function selectFlowRoots(functions, scope, entryFiles, functionIdsBySimpleName, functionIdsByFullName, incoming) {
|
|
316
|
+
if (scope) {
|
|
317
|
+
const trimmedScope = scope.trim();
|
|
318
|
+
const exact = new Set();
|
|
319
|
+
const exactFull = functionIdsByFullName.get(trimmedScope);
|
|
320
|
+
if (exactFull)
|
|
321
|
+
exact.add(exactFull);
|
|
322
|
+
for (const [simpleName, ids] of functionIdsBySimpleName) {
|
|
323
|
+
if (simpleName === trimmedScope)
|
|
324
|
+
ids.forEach((id) => exact.add(id));
|
|
325
|
+
}
|
|
326
|
+
if (exact.size > 0)
|
|
327
|
+
return exact;
|
|
328
|
+
const fuzzy = new Set(functions
|
|
329
|
+
.filter((fn) => fn.fullName.toLowerCase().includes(trimmedScope.toLowerCase()) ||
|
|
330
|
+
fn.simpleName.toLowerCase().includes(trimmedScope.toLowerCase()))
|
|
331
|
+
.map((fn) => fn.id));
|
|
332
|
+
if (fuzzy.size > 0)
|
|
333
|
+
return fuzzy;
|
|
334
|
+
}
|
|
335
|
+
const preferred = functions
|
|
336
|
+
.filter((fn) => entryFiles.has(fn.file) || fn.exported || FLOW_ENTRY_NAMES.has(fn.simpleName))
|
|
337
|
+
.sort((left, right) => left.fullName.localeCompare(right.fullName))
|
|
338
|
+
.slice(0, 3)
|
|
339
|
+
.map((fn) => fn.id);
|
|
340
|
+
if (preferred.length > 0)
|
|
341
|
+
return new Set(preferred);
|
|
342
|
+
const byIncoming = functions
|
|
343
|
+
.filter((fn) => (incoming.get(fn.id) ?? 0) === 0)
|
|
344
|
+
.sort((left, right) => left.fullName.localeCompare(right.fullName))
|
|
345
|
+
.slice(0, 3)
|
|
346
|
+
.map((fn) => fn.id);
|
|
347
|
+
if (byIncoming.length > 0)
|
|
348
|
+
return new Set(byIncoming);
|
|
349
|
+
return new Set(functions.slice(0, 3).map((fn) => fn.id));
|
|
350
|
+
}
|
|
351
|
+
function collectReachableNodes(rootIds, edges) {
|
|
352
|
+
const adjacency = new Map();
|
|
353
|
+
for (const edge of edges.values()) {
|
|
354
|
+
const list = adjacency.get(edge.from) ?? [];
|
|
355
|
+
list.push(edge.to);
|
|
356
|
+
adjacency.set(edge.from, list);
|
|
357
|
+
}
|
|
358
|
+
const visited = new Set();
|
|
359
|
+
const queue = Array.from(rootIds);
|
|
360
|
+
while (queue.length > 0) {
|
|
361
|
+
const current = queue.shift();
|
|
362
|
+
if (!current || visited.has(current))
|
|
363
|
+
continue;
|
|
364
|
+
visited.add(current);
|
|
365
|
+
for (const next of adjacency.get(current) ?? []) {
|
|
366
|
+
if (!visited.has(next))
|
|
367
|
+
queue.push(next);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return visited;
|
|
371
|
+
}
|
|
372
|
+
function renderDirectedGraph(header, nodes, edges, options) {
|
|
373
|
+
const limited = limitGraph(nodes, edges);
|
|
374
|
+
const lines = ['```mermaid', header];
|
|
375
|
+
if (limited.nodes.length === 0) {
|
|
376
|
+
lines.push(` empty["${escapeMermaidLabel(options.emptyMessage)}"]`);
|
|
377
|
+
lines.push('```');
|
|
378
|
+
return `${lines.join('\n')}\n`;
|
|
379
|
+
}
|
|
380
|
+
for (const node of limited.nodes) {
|
|
381
|
+
lines.push(` ${mermaidId(node.key)}["${escapeMermaidLabel(node.label)}"]`);
|
|
382
|
+
}
|
|
383
|
+
if (limited.omittedCount > 0) {
|
|
384
|
+
lines.push(` ${mermaidId(OMITTED_NODE_KEY)}["${escapeMermaidLabel(`${limited.omittedCount} ${options.omittedLabel}`)}"]`);
|
|
385
|
+
lines.push(` %% ${limited.omittedCount} ${options.omittedLabel} omitted for readability`);
|
|
386
|
+
}
|
|
387
|
+
for (const edge of limited.edges) {
|
|
388
|
+
const label = edge.count > 1 ? `|${edge.count}| ` : '';
|
|
389
|
+
lines.push(` ${mermaidId(edge.from)} --> ${label}${mermaidId(edge.to)}`);
|
|
390
|
+
}
|
|
391
|
+
lines.push('```');
|
|
392
|
+
return `${lines.join('\n')}\n`;
|
|
393
|
+
}
|
|
394
|
+
function renderClassGraph(nodes, edges) {
|
|
395
|
+
const limited = limitGraph(nodes, edges);
|
|
396
|
+
const lines = ['```mermaid', 'classDiagram'];
|
|
397
|
+
if (limited.nodes.length === 0) {
|
|
398
|
+
lines.push(' class EmptyDiagram {');
|
|
399
|
+
lines.push(' No classes found');
|
|
400
|
+
lines.push(' }');
|
|
401
|
+
lines.push('```');
|
|
402
|
+
return `${lines.join('\n')}\n`;
|
|
403
|
+
}
|
|
404
|
+
for (const node of limited.nodes) {
|
|
405
|
+
lines.push(` class ${mermaidId(node.key)}["${escapeMermaidLabel(node.label)}"]`);
|
|
406
|
+
}
|
|
407
|
+
if (limited.omittedCount > 0) {
|
|
408
|
+
lines.push(` class ${mermaidId(OMITTED_NODE_KEY)} {`);
|
|
409
|
+
lines.push(` +${limited.omittedCount} more classes omitted`);
|
|
410
|
+
lines.push(' }');
|
|
411
|
+
}
|
|
412
|
+
for (const edge of limited.edges) {
|
|
413
|
+
lines.push(` ${mermaidId(edge.from)} <|-- ${mermaidId(edge.to)}`);
|
|
414
|
+
}
|
|
415
|
+
lines.push('```');
|
|
416
|
+
return `${lines.join('\n')}\n`;
|
|
417
|
+
}
|
|
418
|
+
function limitGraph(nodes, edges) {
|
|
419
|
+
const sortedNodes = Array.from(nodes.values()).sort((left, right) => right.weight - left.weight || left.label.localeCompare(right.label));
|
|
420
|
+
if (sortedNodes.length <= MAX_NODES) {
|
|
421
|
+
return { nodes: sortedNodes, edges: Array.from(edges.values()), omittedCount: 0 };
|
|
422
|
+
}
|
|
423
|
+
const keptNodes = sortedNodes.slice(0, MAX_NODES - 1);
|
|
424
|
+
const keptKeys = new Set(keptNodes.map((node) => node.key));
|
|
425
|
+
const omittedCount = sortedNodes.length - keptNodes.length;
|
|
426
|
+
const limitedEdges = new Map();
|
|
427
|
+
for (const edge of edges.values()) {
|
|
428
|
+
const from = keptKeys.has(edge.from) ? edge.from : OMITTED_NODE_KEY;
|
|
429
|
+
const to = keptKeys.has(edge.to) ? edge.to : OMITTED_NODE_KEY;
|
|
430
|
+
if (from === to && from === OMITTED_NODE_KEY)
|
|
431
|
+
continue;
|
|
432
|
+
const key = `${from}→${to}`;
|
|
433
|
+
const current = limitedEdges.get(key);
|
|
434
|
+
if (current) {
|
|
435
|
+
current.count += edge.count;
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
limitedEdges.set(key, { from, to, count: edge.count });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
nodes: keptNodes,
|
|
443
|
+
edges: Array.from(limitedEdges.values()),
|
|
444
|
+
omittedCount,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function upsertNode(nodes, key, label, weight) {
|
|
448
|
+
const current = nodes.get(key);
|
|
449
|
+
if (current) {
|
|
450
|
+
current.weight += weight;
|
|
451
|
+
if (label.includes('<br/>entry'))
|
|
452
|
+
current.label = label;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
nodes.set(key, { key, label, weight });
|
|
456
|
+
}
|
|
457
|
+
function incrementNode(nodes, key, weight) {
|
|
458
|
+
const current = nodes.get(key);
|
|
459
|
+
if (current)
|
|
460
|
+
current.weight += weight;
|
|
461
|
+
}
|
|
462
|
+
function upsertEdge(edges, from, to) {
|
|
463
|
+
const key = `${from}→${to}`;
|
|
464
|
+
const current = edges.get(key);
|
|
465
|
+
if (current) {
|
|
466
|
+
current.count += 1;
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
edges.set(key, { from, to, count: 1 });
|
|
470
|
+
}
|
|
471
|
+
function collectSourceFiles(rootDir, scopeTarget) {
|
|
472
|
+
const start = scopeTarget ?? rootDir;
|
|
473
|
+
if (!fs.existsSync(start))
|
|
474
|
+
return [];
|
|
475
|
+
const stat = fs.statSync(start);
|
|
476
|
+
if (stat.isFile()) {
|
|
477
|
+
return isIncludedSourceFile(start, rootDir, true) ? [path.resolve(start)] : [];
|
|
478
|
+
}
|
|
479
|
+
const files = [];
|
|
480
|
+
const walk = (currentDir) => {
|
|
481
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
482
|
+
if (entry.isDirectory()) {
|
|
483
|
+
if (SKIP_DIRS.has(entry.name))
|
|
484
|
+
continue;
|
|
485
|
+
walk(path.join(currentDir, entry.name));
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const file = path.join(currentDir, entry.name);
|
|
489
|
+
if (isIncludedSourceFile(file, rootDir, currentDir === start && Boolean(scopeTarget))) {
|
|
490
|
+
files.push(path.resolve(file));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
walk(start);
|
|
495
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
496
|
+
}
|
|
497
|
+
function isIncludedSourceFile(file, rootDir, explicitScope) {
|
|
498
|
+
const extension = path.extname(file).toLowerCase();
|
|
499
|
+
if (!SOURCE_EXTENSIONS.has(extension))
|
|
500
|
+
return false;
|
|
501
|
+
if (file.endsWith('.d.ts'))
|
|
502
|
+
return false;
|
|
503
|
+
if (explicitScope)
|
|
504
|
+
return true;
|
|
505
|
+
const normalized = toPosixPath(relativePath(rootDir, file));
|
|
506
|
+
return !(normalized.includes('/tests/') ||
|
|
507
|
+
normalized.includes('/__tests__/') ||
|
|
508
|
+
/\.test\.[cm]?[jt]sx?$/u.test(normalized) ||
|
|
509
|
+
/\.spec\.[cm]?[jt]sx?$/u.test(normalized));
|
|
510
|
+
}
|
|
511
|
+
function resolveScopeTarget(rootDir, scope) {
|
|
512
|
+
if (!scope)
|
|
513
|
+
return null;
|
|
514
|
+
const resolved = path.resolve(rootDir, scope);
|
|
515
|
+
if (fs.existsSync(resolved))
|
|
516
|
+
return resolved;
|
|
517
|
+
const normalizedScope = toPosixPath(scope).replace(/^\.\//u, '');
|
|
518
|
+
const candidates = collectSourceFiles(rootDir).filter((file) => {
|
|
519
|
+
const relative = toPosixPath(relativePath(rootDir, file));
|
|
520
|
+
return relative === normalizedScope || relative.endsWith(`/${normalizedScope}`);
|
|
521
|
+
});
|
|
522
|
+
return candidates[0] ?? null;
|
|
523
|
+
}
|
|
524
|
+
function detectEntryFiles(rootDir, parsedFiles) {
|
|
525
|
+
const byRelative = new Map(parsedFiles.map((parsed) => [toPosixPath(parsed.relative), parsed.file]));
|
|
526
|
+
const candidates = new Set();
|
|
527
|
+
const packageJsonPath = path.join(rootDir, 'package.json');
|
|
528
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
529
|
+
try {
|
|
530
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
531
|
+
for (const value of extractPackageEntryValues(pkg)) {
|
|
532
|
+
const resolved = resolveEntryCandidate(value, byRelative);
|
|
533
|
+
if (resolved)
|
|
534
|
+
candidates.add(resolved);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
// ignore invalid package.json
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
for (const candidate of ['src/index.ts', 'src/main.ts', 'src/cli.ts', 'index.ts', 'main.ts']) {
|
|
542
|
+
const resolved = resolveEntryCandidate(candidate, byRelative);
|
|
543
|
+
if (resolved)
|
|
544
|
+
candidates.add(resolved);
|
|
545
|
+
}
|
|
546
|
+
return Array.from(candidates);
|
|
547
|
+
}
|
|
548
|
+
function extractPackageEntryValues(pkg) {
|
|
549
|
+
const values = [];
|
|
550
|
+
if (pkg.main)
|
|
551
|
+
values.push(pkg.main);
|
|
552
|
+
if (typeof pkg.bin === 'string') {
|
|
553
|
+
values.push(pkg.bin);
|
|
554
|
+
}
|
|
555
|
+
else if (pkg.bin) {
|
|
556
|
+
values.push(...Object.values(pkg.bin));
|
|
557
|
+
}
|
|
558
|
+
return values;
|
|
559
|
+
}
|
|
560
|
+
function resolveEntryCandidate(candidate, byRelative) {
|
|
561
|
+
const relative = toPosixPath(candidate).replace(/^\.\/+/u, '');
|
|
562
|
+
const direct = byRelative.get(relative);
|
|
563
|
+
if (direct)
|
|
564
|
+
return direct;
|
|
565
|
+
const asTs = relative.replace(/\.js$/u, '.ts');
|
|
566
|
+
if (byRelative.has(asTs))
|
|
567
|
+
return byRelative.get(asTs) ?? null;
|
|
568
|
+
const withoutBinPrefix = relative.replace(/^bin\//u, 'src/');
|
|
569
|
+
if (byRelative.has(withoutBinPrefix))
|
|
570
|
+
return byRelative.get(withoutBinPrefix) ?? null;
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
function detectModuleBoundary(relativeFile) {
|
|
574
|
+
const normalized = toPosixPath(relativeFile);
|
|
575
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
576
|
+
if (parts.length === 0)
|
|
577
|
+
return normalized;
|
|
578
|
+
if (parts[0] === 'src' && parts.length >= 3) {
|
|
579
|
+
return `src/${parts[1]}`;
|
|
580
|
+
}
|
|
581
|
+
if (parts[0] === 'src') {
|
|
582
|
+
return normalized;
|
|
583
|
+
}
|
|
584
|
+
if (parts.length >= 2) {
|
|
585
|
+
return parts[0];
|
|
586
|
+
}
|
|
587
|
+
return normalized;
|
|
588
|
+
}
|
|
589
|
+
function resolveImportTarget(fromFile, moduleSpecifier, fileSet) {
|
|
590
|
+
if (!moduleSpecifier.startsWith('.'))
|
|
591
|
+
return null;
|
|
592
|
+
const base = path.resolve(path.dirname(fromFile), moduleSpecifier);
|
|
593
|
+
const candidates = [
|
|
594
|
+
base,
|
|
595
|
+
...Array.from(SOURCE_EXTENSIONS, (extension) => `${base}${extension}`),
|
|
596
|
+
...Array.from(SOURCE_EXTENSIONS, (extension) => path.join(base, `index${extension}`)),
|
|
597
|
+
];
|
|
598
|
+
if (/\.[cm]?[jt]sx?$/u.test(moduleSpecifier)) {
|
|
599
|
+
const extless = base.replace(/\.[^.]+$/u, '');
|
|
600
|
+
candidates.push(...Array.from(SOURCE_EXTENSIONS, (extension) => `${extless}${extension}`));
|
|
601
|
+
}
|
|
602
|
+
for (const candidate of candidates) {
|
|
603
|
+
const resolved = path.resolve(candidate);
|
|
604
|
+
if (fileSet.has(resolved))
|
|
605
|
+
return resolved;
|
|
606
|
+
}
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
function extractCallImport(node) {
|
|
610
|
+
const firstArg = node.arguments[0];
|
|
611
|
+
if (!firstArg || !ts.isStringLiteral(firstArg))
|
|
612
|
+
return null;
|
|
613
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
614
|
+
return firstArg.text;
|
|
615
|
+
}
|
|
616
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
617
|
+
return firstArg.text;
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
function isNodeExported(node) {
|
|
622
|
+
if (!node)
|
|
623
|
+
return false;
|
|
624
|
+
return Boolean(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
|
|
625
|
+
}
|
|
626
|
+
function getNodeName(name) {
|
|
627
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
628
|
+
return name.text;
|
|
629
|
+
}
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
function scriptKindForFile(file) {
|
|
633
|
+
const extension = path.extname(file).toLowerCase();
|
|
634
|
+
switch (extension) {
|
|
635
|
+
case '.tsx':
|
|
636
|
+
return ts.ScriptKind.TSX;
|
|
637
|
+
case '.jsx':
|
|
638
|
+
return ts.ScriptKind.JSX;
|
|
639
|
+
case '.js':
|
|
640
|
+
case '.mjs':
|
|
641
|
+
case '.cjs':
|
|
642
|
+
return ts.ScriptKind.JS;
|
|
643
|
+
default:
|
|
644
|
+
return ts.ScriptKind.TS;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function relativePath(rootDir, file) {
|
|
648
|
+
return toPosixPath(path.relative(rootDir, file) || path.basename(file));
|
|
649
|
+
}
|
|
650
|
+
function toPosixPath(value) {
|
|
651
|
+
return value.replace(/\\/gu, '/');
|
|
652
|
+
}
|
|
653
|
+
function mermaidId(key) {
|
|
654
|
+
return key.replace(/[^A-Za-z0-9_]/gu, '_');
|
|
655
|
+
}
|
|
656
|
+
function escapeMermaidLabel(value) {
|
|
657
|
+
return value.replace(/"/gu, '"');
|
|
658
|
+
}
|