codesynapt 0.0.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 +17 -0
- package/LICENSE +686 -0
- package/LICENSES.md +141 -0
- package/README.md +331 -0
- package/electron/main.cjs +2849 -0
- package/electron/plugin-loader.cjs +184 -0
- package/electron/preload.cjs +108 -0
- package/package.json +216 -0
- package/packages/core/bin/codesynapt-mcp.cjs +611 -0
- package/packages/core/bin/codesynapt.cjs +1933 -0
- package/packages/core/legacy.js +300 -0
- package/packages/core/lib/control-server.cjs +1539 -0
- package/packages/core/lib/embedding.cjs +89 -0
- package/packages/core/lib/logger.cjs +63 -0
- package/packages/core/lib/search-cache.cjs +140 -0
- package/packages/core/lib/search-worker.cjs +255 -0
- package/packages/core/lib/search.cjs +211 -0
- package/packages/core/lib/symbol-graph.cjs +402 -0
- package/packages/core/lib/symbol-parser-js.cjs +542 -0
- package/packages/core/lib/symbol-parser-misc.cjs +394 -0
- package/packages/core/lib/symbol-parser-py.cjs +215 -0
- package/packages/core/lib/symbol-parser-treesitter.cjs +658 -0
- package/packages/core/lib/symbol-parser-tsc.cjs +332 -0
- package/packages/core/monorepo.js +310 -0
- package/packages/core/parser.js +2234 -0
- package/packages/core/scanner.js +623 -0
- package/plugin-api/LICENSE +21 -0
- package/plugin-api/README.md +114 -0
- package/plugin-api/docs/01-getting-started.md +197 -0
- package/plugin-api/docs/02-concepts.md +269 -0
- package/plugin-api/docs/api-reference.md +463 -0
- package/plugin-api/docs/troubleshooting.md +332 -0
- package/plugin-api/docs/types/exporter.md +377 -0
- package/plugin-api/docs/types/theme.md +312 -0
- package/plugin-api/examples/hello-world-plugin/README.md +70 -0
- package/plugin-api/examples/hello-world-plugin/main.js +36 -0
- package/plugin-api/examples/hello-world-plugin/manifest.json +12 -0
- package/plugin-api/examples/mermaid-exporter/README.md +125 -0
- package/plugin-api/examples/mermaid-exporter/main.js +58 -0
- package/plugin-api/examples/mermaid-exporter/manifest.json +12 -0
- package/plugin-api/examples/rust-parser/README.md +71 -0
- package/plugin-api/examples/rust-parser/main.js +123 -0
- package/plugin-api/examples/rust-parser/manifest.json +12 -0
- package/plugin-api/examples/sunset-theme/README.md +95 -0
- package/plugin-api/examples/sunset-theme/manifest.json +12 -0
- package/plugin-api/examples/sunset-theme/theme.css +31 -0
- package/plugin-api/package.json +20 -0
- package/plugin-api/types.d.ts +395 -0
- package/public/app.js +6837 -0
- package/public/backend.js +285 -0
- package/public/index.html +647 -0
- package/public/plugin-host.js +321 -0
- package/public/style.css +4359 -0
- package/public/vendor/three.module.js +53044 -0
- package/scripts/competitor-watch.mjs +144 -0
- package/scripts/copy-vendor.js +21 -0
- package/scripts/download-bundled-node.cjs +53 -0
- package/scripts/fuses-after-pack.cjs +34 -0
- package/scripts/license-check.js +119 -0
- package/scripts/perf-test.js +200 -0
- package/server.js +132 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Common problems and their fixes.
|
|
4
|
+
|
|
5
|
+
- [Plugin doesn't show up](#plugin-doesnt-show-up)
|
|
6
|
+
- [Plugin appears in list but doesn't work](#plugin-appears-in-list-but-doesnt-work)
|
|
7
|
+
- [Error: invalid manifest](#error-invalid-manifest)
|
|
8
|
+
- [Error: entry path escapes plugin folder](#error-entry-path-escapes-plugin-folder)
|
|
9
|
+
- [Error: missing or invalid permission](#error-missing-or-invalid-permission)
|
|
10
|
+
- [Theme loads but looks wrong](#theme-loads-but-looks-wrong)
|
|
11
|
+
- [Exporter generates but downloads nothing](#exporter-generates-but-downloads-nothing)
|
|
12
|
+
- [Context menu item doesn't appear](#context-menu-item-doesnt-appear)
|
|
13
|
+
- [Changes don't take effect](#changes-dont-take-effect)
|
|
14
|
+
- [DevTools console errors](#devtools-console-errors)
|
|
15
|
+
- [Still stuck](#still-stuck)
|
|
16
|
+
|
|
17
|
+
## Plugin doesn't show up
|
|
18
|
+
|
|
19
|
+
**Check the folder structure.** Your plugin must be in **its own
|
|
20
|
+
subfolder** inside the plugins directory:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
plugins/
|
|
24
|
+
├── my-plugin/ ← ✅ this is right
|
|
25
|
+
│ ├── manifest.json
|
|
26
|
+
│ └── main.js
|
|
27
|
+
└── my-other-plugin/ ← ✅ second plugin, also a subfolder
|
|
28
|
+
├── manifest.json
|
|
29
|
+
└── theme.css
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Loose files at the top of `plugins/` are ignored.
|
|
33
|
+
|
|
34
|
+
**Check the plugin folder path.** filegraph3d shows the path in
|
|
35
|
+
Settings → Appearance → Open plugin folder…. Verify your files are
|
|
36
|
+
**in that exact directory**.
|
|
37
|
+
|
|
38
|
+
**Check the manifest file.** Open `manifest.json` and confirm:
|
|
39
|
+
- The file is literally named `manifest.json` (not `Manifest.json`,
|
|
40
|
+
not `manifest.JSON`).
|
|
41
|
+
- It's valid JSON — paste into [jsonlint.com](https://jsonlint.com/)
|
|
42
|
+
if unsure.
|
|
43
|
+
|
|
44
|
+
**Did you restart the app?** filegraph3d only scans for plugins at
|
|
45
|
+
startup. **Cmd/Ctrl+Q to fully quit**, then reopen — just closing the
|
|
46
|
+
window isn't enough on macOS.
|
|
47
|
+
|
|
48
|
+
## Plugin appears in list but doesn't work
|
|
49
|
+
|
|
50
|
+
The plugin was discovered but failed to validate or activate. Open
|
|
51
|
+
**DevTools** (View → Toggle Developer Tools) and check the Console
|
|
52
|
+
for messages like:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
[plugin-loader] skipping <id>: <reason>
|
|
56
|
+
[plugin:<id>] activate() threw: <error>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The reason will tell you exactly what's wrong. Common ones below.
|
|
60
|
+
|
|
61
|
+
## Error: invalid manifest
|
|
62
|
+
|
|
63
|
+
### `missing or invalid "id"`
|
|
64
|
+
|
|
65
|
+
Every plugin needs an `id`. Make sure your manifest has one:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"id": "my-plugin", ← required
|
|
70
|
+
...
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `invalid id "..." (use alphanumeric + dash/underscore)`
|
|
75
|
+
|
|
76
|
+
Plugin ids must match `[a-z0-9][a-z0-9-_]*` (lowercase alphanumeric,
|
|
77
|
+
dashes, underscores). Starting character must be a letter or digit.
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"id": "My Plugin!" ← ❌ spaces, capitals, punctuation
|
|
82
|
+
"id": "1plugin" ← ✅ ok (starts with digit)
|
|
83
|
+
"id": "my-plugin" ← ✅ standard
|
|
84
|
+
"id": "my_plugin_v2" ← ✅ ok
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `unknown type "..."`
|
|
89
|
+
|
|
90
|
+
The `type` field must be one of:
|
|
91
|
+
`theme`, `exporter`, `parser`, `layout`, `panel`, `action`.
|
|
92
|
+
|
|
93
|
+
Typos like `themer` or `exporters` will fail.
|
|
94
|
+
|
|
95
|
+
### `unknown permission "..."`
|
|
96
|
+
|
|
97
|
+
Permissions must come from this list:
|
|
98
|
+
`read-files`, `read-graph`, `modify-graph`, `ui-panel`,
|
|
99
|
+
`context-menu`, `export`, `parse`.
|
|
100
|
+
|
|
101
|
+
## Error: entry path escapes plugin folder
|
|
102
|
+
|
|
103
|
+
You have something like `"main": "../something"` in your manifest.
|
|
104
|
+
For security, plugins can only reference files inside their own folder.
|
|
105
|
+
Move the file into the plugin directory.
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"main": "../../shared.js" ← ❌ refuses to load
|
|
110
|
+
"main": "main.js" ← ✅ inside plugin folder
|
|
111
|
+
"main": "src/main.js" ← ✅ subdirectory is fine
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Error: missing or invalid permission
|
|
116
|
+
|
|
117
|
+
Trying to use an API method without declaring the matching permission:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
Plugin "my-plugin" attempted "registerContextMenuItem" but does not declare permission "context-menu"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Add the permission to your manifest:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
...
|
|
128
|
+
"permissions": ["context-menu"]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Then restart the app.
|
|
133
|
+
|
|
134
|
+
The full list:
|
|
135
|
+
- `read-files` → `ctx.graph.readFile(id)`
|
|
136
|
+
- `ui-panel` → `ctx.ui.registerPanel(...)`
|
|
137
|
+
- `context-menu` → `ctx.ui.registerContextMenuItem(...)`
|
|
138
|
+
- `export` → `ctx.exporters.register(...)`
|
|
139
|
+
- `parse` → `ctx.parsers.register(...)`
|
|
140
|
+
|
|
141
|
+
## Theme loads but looks wrong
|
|
142
|
+
|
|
143
|
+
### "Some panels look broken"
|
|
144
|
+
|
|
145
|
+
You probably only set a few variables. The app's UI uses ~30 CSS
|
|
146
|
+
variables; if you override only `--bg` and `--accent`, you'll inherit
|
|
147
|
+
the previous theme's other colors and they'll clash.
|
|
148
|
+
|
|
149
|
+
Set everything from this list (with sensible values):
|
|
150
|
+
|
|
151
|
+
```css
|
|
152
|
+
body[data-theme="my-theme"] {
|
|
153
|
+
--bg: ...
|
|
154
|
+
--bg-glass: ...
|
|
155
|
+
--fg: ...
|
|
156
|
+
--fg-dim: ...
|
|
157
|
+
--fg-mute: ...
|
|
158
|
+
--fg-faint: ...
|
|
159
|
+
--border: ...
|
|
160
|
+
--border-hot: ...
|
|
161
|
+
--border-edge: ...
|
|
162
|
+
--accent: ...
|
|
163
|
+
--accent-warm: ...
|
|
164
|
+
--accent-pink: ...
|
|
165
|
+
--decoration: 0 or 1
|
|
166
|
+
--radius: ...
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
See [theme.md](./types/theme.md) for the full list and what each does.
|
|
171
|
+
|
|
172
|
+
### "Light mode has weird specks/grain"
|
|
173
|
+
|
|
174
|
+
The grain overlay (`body::before`) uses blend-mode that looks fine on
|
|
175
|
+
dark but ugly on light. Disable it:
|
|
176
|
+
|
|
177
|
+
```css
|
|
178
|
+
body[data-theme="my-light-theme"] {
|
|
179
|
+
--grain: 0;
|
|
180
|
+
}
|
|
181
|
+
body[data-theme="my-light-theme"]::before {
|
|
182
|
+
opacity: 0 !important;
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### "Colors leak between themes"
|
|
187
|
+
|
|
188
|
+
You defined CSS variables at `:root` instead of inside
|
|
189
|
+
`body[data-theme="..."]`. The variables apply globally, polluting
|
|
190
|
+
other themes. Scope them properly:
|
|
191
|
+
|
|
192
|
+
```css
|
|
193
|
+
/* ❌ leaks */
|
|
194
|
+
:root {
|
|
195
|
+
--accent: #FF0000;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* ✅ scoped */
|
|
199
|
+
body[data-theme="my-theme"] {
|
|
200
|
+
--accent: #FF0000;
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Exporter generates but downloads nothing
|
|
205
|
+
|
|
206
|
+
### "Empty file downloads"
|
|
207
|
+
|
|
208
|
+
Your `generate` function isn't returning the string. Check:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
generate(graph) {
|
|
212
|
+
console.log('hello') // ❌ logged but never returned
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
generate(graph) {
|
|
218
|
+
return 'hello' // ✅
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### "Wrong extension on the file"
|
|
223
|
+
|
|
224
|
+
`extension` should be the bare suffix, no dot:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
extension: 'mmd' // ✅ → file.mmd
|
|
228
|
+
extension: '.mmd' // ❌ → file..mmd
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### "Unicode characters render as ???"
|
|
232
|
+
|
|
233
|
+
Add charset to the MIME type:
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
mimeType: 'text/plain; charset=utf-8'
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Context menu item doesn't appear
|
|
240
|
+
|
|
241
|
+
- Did you declare `"permissions": ["context-menu"]` in the manifest?
|
|
242
|
+
- Did you fully restart the app?
|
|
243
|
+
- Right-click directly on a **node** (sphere) in the graph — empty
|
|
244
|
+
space doesn't show node-context items.
|
|
245
|
+
- Open DevTools and check for plugin activation errors.
|
|
246
|
+
|
|
247
|
+
## Changes don't take effect
|
|
248
|
+
|
|
249
|
+
filegraph3d **does not** hot-reload plugins right now. Any change to
|
|
250
|
+
a plugin file requires:
|
|
251
|
+
|
|
252
|
+
1. **Quit completely** (Cmd/Ctrl+Q, not just close window).
|
|
253
|
+
2. **Reopen** the app.
|
|
254
|
+
|
|
255
|
+
If you change `manifest.json`, the same applies. There's no way to
|
|
256
|
+
reload just one plugin without restarting.
|
|
257
|
+
|
|
258
|
+
(Hot reload is on the roadmap.)
|
|
259
|
+
|
|
260
|
+
## DevTools console errors
|
|
261
|
+
|
|
262
|
+
Open DevTools: **View → Toggle Developer Tools** (or Cmd/Ctrl+Option+I).
|
|
263
|
+
|
|
264
|
+
Switch to the **Console** tab. Errors from your plugin are prefixed
|
|
265
|
+
with `[plugin:<id>]`. For example:
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
[plugin:my-plugin] activate() threw: TypeError: cannot read property 'foo' of undefined
|
|
269
|
+
at activate (blob:file:///...:5:23)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The line number points to your `main.js`. Click the link to jump to
|
|
273
|
+
the source.
|
|
274
|
+
|
|
275
|
+
Common errors:
|
|
276
|
+
|
|
277
|
+
### `Cannot read property '...' of undefined`
|
|
278
|
+
|
|
279
|
+
You accessed something that doesn't exist. Most often:
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
const node = ctx.graph.getNode(id)
|
|
283
|
+
ctx.log(node.loc) // ❌ if node is null, this throws
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Add a null check:
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
const node = ctx.graph.getNode(id)
|
|
290
|
+
if (node) ctx.log(node.loc)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### `Plugin "..." attempted "..." but does not declare permission`
|
|
294
|
+
|
|
295
|
+
Add the permission to your manifest. See
|
|
296
|
+
[Error: missing or invalid permission](#error-missing-or-invalid-permission).
|
|
297
|
+
|
|
298
|
+
### `Failed to fetch dynamically imported module`
|
|
299
|
+
|
|
300
|
+
Your `main.js` has a syntax error preventing it from loading. Check
|
|
301
|
+
it parses with `node --check main.js` if you have Node installed, or
|
|
302
|
+
paste it into [esprima.org/demo/validate.html](https://esprima.org/demo/validate.html).
|
|
303
|
+
|
|
304
|
+
### `await is only valid in async functions`
|
|
305
|
+
|
|
306
|
+
Mark your function as `async`:
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
generate: async (graph) => {
|
|
310
|
+
const content = await graph.readFile(...)
|
|
311
|
+
return content
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Still stuck
|
|
316
|
+
|
|
317
|
+
1. **Look at a working example.** Each `examples/*` plugin has a
|
|
318
|
+
README explaining how it's structured. Copy and modify.
|
|
319
|
+
|
|
320
|
+
2. **Compare with the type definitions.** Open
|
|
321
|
+
[`../types.d.ts`](../types.d.ts) and check that your code matches
|
|
322
|
+
the expected shape.
|
|
323
|
+
|
|
324
|
+
3. **File an issue.** [GitHub issues](https://github.com/YOUR_USER/filegraph3d/issues)
|
|
325
|
+
with:
|
|
326
|
+
- Your `manifest.json`
|
|
327
|
+
- Your `main.js` (or `theme.css`)
|
|
328
|
+
- The error from the DevTools console
|
|
329
|
+
- The filegraph3d version (Settings → About)
|
|
330
|
+
|
|
331
|
+
4. **Ask the community.** Plugin discussion lives at
|
|
332
|
+
[GitHub Discussions](https://github.com/YOUR_USER/filegraph3d/discussions).
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Exporter plugin
|
|
2
|
+
|
|
3
|
+
An exporter plugin adds a new entry to filegraph3d's export menu. Your
|
|
4
|
+
plugin receives the current graph and returns a string; the app handles
|
|
5
|
+
file downloads, MIME types, and the user-facing UI.
|
|
6
|
+
|
|
7
|
+
- [Minimal example](#minimal-example)
|
|
8
|
+
- [What `generate` receives](#what-generate-receives)
|
|
9
|
+
- [Example: Mermaid](#example-mermaid)
|
|
10
|
+
- [Example: GraphViz DOT](#example-graphviz-dot)
|
|
11
|
+
- [Example: Custom JSON](#example-custom-json)
|
|
12
|
+
- [Tips & patterns](#tips--patterns)
|
|
13
|
+
- [Common mistakes](#common-mistakes)
|
|
14
|
+
|
|
15
|
+
## Minimal example
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
plugins/my-exporter/
|
|
19
|
+
├── manifest.json
|
|
20
|
+
└── main.js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**`manifest.json`:**
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"id": "my-exporter",
|
|
28
|
+
"name": "My Exporter",
|
|
29
|
+
"version": "1.0.0",
|
|
30
|
+
"type": "exporter",
|
|
31
|
+
"main": "main.js",
|
|
32
|
+
"minAppVersion": "0.10.0",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"permissions": ["export"]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**`main.js`:**
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
export default {
|
|
42
|
+
activate(ctx) {
|
|
43
|
+
ctx.exporters.register({
|
|
44
|
+
name: 'My Format',
|
|
45
|
+
extension: 'txt',
|
|
46
|
+
mimeType: 'text/plain',
|
|
47
|
+
generate(graph) {
|
|
48
|
+
return `Exported ${graph.nodes.length} files`
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That's it. The user will see "My Format" in the export dropdown.
|
|
56
|
+
Clicking it generates a `.txt` file with one line of text.
|
|
57
|
+
|
|
58
|
+
## What `generate` receives
|
|
59
|
+
|
|
60
|
+
The `generate` function gets a [`GraphAPI`](../api-reference.md#graphapi)
|
|
61
|
+
object — read-only access to the current graph:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
generate(graph: GraphAPI): string | Promise<string>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You can use:
|
|
68
|
+
|
|
69
|
+
| Property/method | What it gives you |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `graph.root` | Absolute path of the opened folder |
|
|
72
|
+
| `graph.nodes` | Array of all nodes (each has `id`, `ext`, `size`, `loc`, `hex`) |
|
|
73
|
+
| `graph.edges` | Array of all edges (each has `s`, `t`, `k`) |
|
|
74
|
+
| `graph.selectedId` | Currently selected node id (or `null`) |
|
|
75
|
+
| `graph.activeSet` | Currently active file set (or `null` if disabled) |
|
|
76
|
+
| `graph.getNode(id)` | Lookup a single node |
|
|
77
|
+
| `graph.outgoing(id)` / `graph.incoming(id)` | Get edges from/to a node |
|
|
78
|
+
| `graph.readFile(id)` | Read file contents (requires `read-files` permission) |
|
|
79
|
+
|
|
80
|
+
`generate` can be `async` if you need to do anything that takes time —
|
|
81
|
+
for example reading file contents. The app shows a "Generating…"
|
|
82
|
+
state while it's running.
|
|
83
|
+
|
|
84
|
+
## Example: Mermaid
|
|
85
|
+
|
|
86
|
+
[Mermaid](https://mermaid.js.org/) diagrams render inside GitHub
|
|
87
|
+
Markdown, Notion, Obsidian, and many other tools.
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
export default {
|
|
91
|
+
activate(ctx) {
|
|
92
|
+
ctx.exporters.register({
|
|
93
|
+
name: 'Mermaid Diagram',
|
|
94
|
+
extension: 'mmd',
|
|
95
|
+
mimeType: 'text/plain',
|
|
96
|
+
generate(graph) {
|
|
97
|
+
const lines = ['graph LR']
|
|
98
|
+
|
|
99
|
+
// Mermaid node ids must be alphanumeric, so we map every
|
|
100
|
+
// file path to a safe alias.
|
|
101
|
+
const aliases = new Map()
|
|
102
|
+
const taken = new Set()
|
|
103
|
+
for (const node of graph.nodes) {
|
|
104
|
+
const base = (node.id.split('/').pop() || node.id)
|
|
105
|
+
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
106
|
+
.replace(/^(\d)/, '_$1')
|
|
107
|
+
let alias = base
|
|
108
|
+
let n = 2
|
|
109
|
+
while (taken.has(alias)) alias = `${base}${n++}`
|
|
110
|
+
aliases.set(node.id, alias)
|
|
111
|
+
taken.add(alias)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Declare each node with its file name as the visible label
|
|
115
|
+
for (const node of graph.nodes) {
|
|
116
|
+
const alias = aliases.get(node.id)
|
|
117
|
+
const label = (node.id.split('/').pop() || node.id)
|
|
118
|
+
.replace(/"/g, '\\"')
|
|
119
|
+
lines.push(` ${alias}["${label}"]`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Then edges
|
|
123
|
+
for (const e of graph.edges) {
|
|
124
|
+
const s = aliases.get(e.s)
|
|
125
|
+
const t = aliases.get(e.t)
|
|
126
|
+
if (s && t) lines.push(` ${s} --> ${t}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return lines.join('\n')
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
For graphs over a few hundred nodes, Mermaid can get slow — consider
|
|
137
|
+
filtering to just the active set:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
generate(graph) {
|
|
141
|
+
const active = graph.activeSet
|
|
142
|
+
const nodes = active
|
|
143
|
+
? graph.nodes.filter((n) => active.has(n.id))
|
|
144
|
+
: graph.nodes
|
|
145
|
+
// ...rest as above, using `nodes` instead of `graph.nodes`
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Example: GraphViz DOT
|
|
150
|
+
|
|
151
|
+
[GraphViz](https://graphviz.org/) DOT is the lingua franca of dependency
|
|
152
|
+
graphs. Render with `dot -Tsvg graph.dot -o graph.svg`.
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
export default {
|
|
156
|
+
activate(ctx) {
|
|
157
|
+
ctx.exporters.register({
|
|
158
|
+
name: 'GraphViz DOT',
|
|
159
|
+
extension: 'dot',
|
|
160
|
+
mimeType: 'text/vnd.graphviz',
|
|
161
|
+
generate(graph) {
|
|
162
|
+
const lines = ['digraph G {']
|
|
163
|
+
lines.push(' rankdir=LR;')
|
|
164
|
+
lines.push(' node [shape=box, fontname="JetBrains Mono", fontsize=10];')
|
|
165
|
+
|
|
166
|
+
// Nodes — use file ids as both the dot identifier (quoted) and label.
|
|
167
|
+
for (const node of graph.nodes) {
|
|
168
|
+
const id = JSON.stringify(node.id)
|
|
169
|
+
const ext = node.ext
|
|
170
|
+
// Color nodes by extension
|
|
171
|
+
const color = colorForExt(ext)
|
|
172
|
+
lines.push(` ${id} [label=${JSON.stringify(basename(node.id))}, color="${color}"];`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Edges
|
|
176
|
+
for (const e of graph.edges) {
|
|
177
|
+
lines.push(` ${JSON.stringify(e.s)} -> ${JSON.stringify(e.t)};`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
lines.push('}')
|
|
181
|
+
return lines.join('\n')
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function basename(p) {
|
|
188
|
+
return p.split('/').pop() || p
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function colorForExt(ext) {
|
|
192
|
+
const map = { js: '#F7DF1E', ts: '#3178C6', jsx: '#61DAFB',
|
|
193
|
+
tsx: '#3178C6', py: '#3776AB', css: '#264DE4' }
|
|
194
|
+
return map[ext] || '#999999'
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
DOT handles thousands of nodes gracefully — it's designed for this.
|
|
199
|
+
|
|
200
|
+
## Example: Custom JSON
|
|
201
|
+
|
|
202
|
+
If you want to feed the graph into your own tooling, a structured JSON
|
|
203
|
+
export is often the right choice. Make sure your shape is documented
|
|
204
|
+
or you'll forget what it means in six months.
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
export default {
|
|
208
|
+
activate(ctx) {
|
|
209
|
+
ctx.exporters.register({
|
|
210
|
+
name: 'JSON (analysis)',
|
|
211
|
+
extension: 'json',
|
|
212
|
+
mimeType: 'application/json',
|
|
213
|
+
generate(graph) {
|
|
214
|
+
const data = {
|
|
215
|
+
// Versioning your export format pays off the first time you
|
|
216
|
+
// change it.
|
|
217
|
+
schemaVersion: 1,
|
|
218
|
+
generatedAt: new Date().toISOString(),
|
|
219
|
+
root: graph.root,
|
|
220
|
+
stats: {
|
|
221
|
+
nodeCount: graph.nodes.length,
|
|
222
|
+
edgeCount: graph.edges.length,
|
|
223
|
+
},
|
|
224
|
+
nodes: graph.nodes.map((n) => ({
|
|
225
|
+
id: n.id,
|
|
226
|
+
ext: n.ext,
|
|
227
|
+
size: n.size,
|
|
228
|
+
loc: n.loc,
|
|
229
|
+
})),
|
|
230
|
+
edges: graph.edges.map((e) => ({
|
|
231
|
+
from: e.s,
|
|
232
|
+
to: e.t,
|
|
233
|
+
kind: e.k,
|
|
234
|
+
})),
|
|
235
|
+
}
|
|
236
|
+
return JSON.stringify(data, null, 2)
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Tips & patterns
|
|
244
|
+
|
|
245
|
+
### Respect the active set when present
|
|
246
|
+
|
|
247
|
+
If the user has curated an active set, they're telling you "the rest
|
|
248
|
+
isn't really part of my project right now." Most exports should filter
|
|
249
|
+
to it:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
const active = graph.activeSet // null if disabled
|
|
253
|
+
const nodes = active ? graph.nodes.filter((n) => active.has(n.id))
|
|
254
|
+
: graph.nodes
|
|
255
|
+
const edges = active
|
|
256
|
+
? graph.edges.filter((e) => active.has(e.s) && active.has(e.t))
|
|
257
|
+
: graph.edges
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Truncate huge exports
|
|
261
|
+
|
|
262
|
+
If your format struggles past 5k nodes (Mermaid does), warn the user
|
|
263
|
+
or truncate:
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
if (graph.nodes.length > 5000) {
|
|
267
|
+
ctx.toast(`Graph has ${graph.nodes.length} nodes — Mermaid may struggle. Consider using an active set.`)
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Toast access requires the plugin context, so capture it from
|
|
272
|
+
`activate`:
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
export default {
|
|
276
|
+
activate(ctx) {
|
|
277
|
+
this._ctx = ctx
|
|
278
|
+
ctx.exporters.register({
|
|
279
|
+
// ...
|
|
280
|
+
generate: (graph) => this.doExport(ctx, graph)
|
|
281
|
+
})
|
|
282
|
+
},
|
|
283
|
+
doExport(ctx, graph) {
|
|
284
|
+
if (graph.nodes.length > 5000) {
|
|
285
|
+
ctx.toast('Large graph — this may take a moment')
|
|
286
|
+
}
|
|
287
|
+
// ...
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Make output reproducible
|
|
293
|
+
|
|
294
|
+
Sort nodes/edges in a stable order. Otherwise diffing two exports of
|
|
295
|
+
the same graph produces noise:
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
const sortedNodes = [...graph.nodes].sort((a, b) => a.id.localeCompare(b.id))
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Don't read files unless you have to
|
|
302
|
+
|
|
303
|
+
Reading file contents costs IO. If your format only needs the
|
|
304
|
+
dependency structure, work from `graph.nodes` and `graph.edges` alone.
|
|
305
|
+
|
|
306
|
+
If you do need contents:
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
{
|
|
310
|
+
// ...
|
|
311
|
+
permissions: ['export', 'read-files']
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```js
|
|
316
|
+
generate: async (graph) => {
|
|
317
|
+
const lines = []
|
|
318
|
+
for (const node of graph.nodes) {
|
|
319
|
+
const content = await graph.readFile(node.id)
|
|
320
|
+
// ... do something
|
|
321
|
+
}
|
|
322
|
+
return lines.join('\n')
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Common mistakes
|
|
327
|
+
|
|
328
|
+
### "My exporter doesn't appear in the export menu"
|
|
329
|
+
|
|
330
|
+
- Did you declare `"permissions": ["export"]` in the manifest? Without
|
|
331
|
+
it, `ctx.exporters.register(...)` throws.
|
|
332
|
+
- Did you quit and restart the app? Plugins load at startup.
|
|
333
|
+
|
|
334
|
+
### "Generate runs but no file downloads"
|
|
335
|
+
|
|
336
|
+
Make sure you `return` (not `console.log`) the generated string. Async
|
|
337
|
+
generators must `return` a string (or `Promise<string>`).
|
|
338
|
+
|
|
339
|
+
```js
|
|
340
|
+
// ❌ no return — empty file
|
|
341
|
+
generate(graph) {
|
|
342
|
+
console.log('done!')
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ✅ returns the string
|
|
346
|
+
generate(graph) {
|
|
347
|
+
return 'hello'
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### "Output file has wrong extension"
|
|
352
|
+
|
|
353
|
+
`extension` in your registration is the bare suffix without a dot:
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
extension: 'mmd' // ✅
|
|
357
|
+
extension: '.mmd' // ❌ produces "file..mmd"
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### "Unicode characters appear as ???"
|
|
361
|
+
|
|
362
|
+
Make sure your `mimeType` indicates the encoding:
|
|
363
|
+
|
|
364
|
+
```js
|
|
365
|
+
mimeType: 'text/plain; charset=utf-8'
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Or use a more specific type that implies UTF-8 (e.g.
|
|
369
|
+
`application/json`).
|
|
370
|
+
|
|
371
|
+
## Next steps
|
|
372
|
+
|
|
373
|
+
- Try the [mermaid-exporter example](../../examples/mermaid-exporter/)
|
|
374
|
+
— a complete working version of the Mermaid exporter.
|
|
375
|
+
- Look at the [API reference](../api-reference.md) for everything
|
|
376
|
+
available on `graph`.
|
|
377
|
+
- Check [troubleshooting](../troubleshooting.md) when stuck.
|