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.
Files changed (61) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +686 -0
  3. package/LICENSES.md +141 -0
  4. package/README.md +331 -0
  5. package/electron/main.cjs +2849 -0
  6. package/electron/plugin-loader.cjs +184 -0
  7. package/electron/preload.cjs +108 -0
  8. package/package.json +216 -0
  9. package/packages/core/bin/codesynapt-mcp.cjs +611 -0
  10. package/packages/core/bin/codesynapt.cjs +1933 -0
  11. package/packages/core/legacy.js +300 -0
  12. package/packages/core/lib/control-server.cjs +1539 -0
  13. package/packages/core/lib/embedding.cjs +89 -0
  14. package/packages/core/lib/logger.cjs +63 -0
  15. package/packages/core/lib/search-cache.cjs +140 -0
  16. package/packages/core/lib/search-worker.cjs +255 -0
  17. package/packages/core/lib/search.cjs +211 -0
  18. package/packages/core/lib/symbol-graph.cjs +402 -0
  19. package/packages/core/lib/symbol-parser-js.cjs +542 -0
  20. package/packages/core/lib/symbol-parser-misc.cjs +394 -0
  21. package/packages/core/lib/symbol-parser-py.cjs +215 -0
  22. package/packages/core/lib/symbol-parser-treesitter.cjs +658 -0
  23. package/packages/core/lib/symbol-parser-tsc.cjs +332 -0
  24. package/packages/core/monorepo.js +310 -0
  25. package/packages/core/parser.js +2234 -0
  26. package/packages/core/scanner.js +623 -0
  27. package/plugin-api/LICENSE +21 -0
  28. package/plugin-api/README.md +114 -0
  29. package/plugin-api/docs/01-getting-started.md +197 -0
  30. package/plugin-api/docs/02-concepts.md +269 -0
  31. package/plugin-api/docs/api-reference.md +463 -0
  32. package/plugin-api/docs/troubleshooting.md +332 -0
  33. package/plugin-api/docs/types/exporter.md +377 -0
  34. package/plugin-api/docs/types/theme.md +312 -0
  35. package/plugin-api/examples/hello-world-plugin/README.md +70 -0
  36. package/plugin-api/examples/hello-world-plugin/main.js +36 -0
  37. package/plugin-api/examples/hello-world-plugin/manifest.json +12 -0
  38. package/plugin-api/examples/mermaid-exporter/README.md +125 -0
  39. package/plugin-api/examples/mermaid-exporter/main.js +58 -0
  40. package/plugin-api/examples/mermaid-exporter/manifest.json +12 -0
  41. package/plugin-api/examples/rust-parser/README.md +71 -0
  42. package/plugin-api/examples/rust-parser/main.js +123 -0
  43. package/plugin-api/examples/rust-parser/manifest.json +12 -0
  44. package/plugin-api/examples/sunset-theme/README.md +95 -0
  45. package/plugin-api/examples/sunset-theme/manifest.json +12 -0
  46. package/plugin-api/examples/sunset-theme/theme.css +31 -0
  47. package/plugin-api/package.json +20 -0
  48. package/plugin-api/types.d.ts +395 -0
  49. package/public/app.js +6837 -0
  50. package/public/backend.js +285 -0
  51. package/public/index.html +647 -0
  52. package/public/plugin-host.js +321 -0
  53. package/public/style.css +4359 -0
  54. package/public/vendor/three.module.js +53044 -0
  55. package/scripts/competitor-watch.mjs +144 -0
  56. package/scripts/copy-vendor.js +21 -0
  57. package/scripts/download-bundled-node.cjs +53 -0
  58. package/scripts/fuses-after-pack.cjs +34 -0
  59. package/scripts/license-check.js +119 -0
  60. package/scripts/perf-test.js +200 -0
  61. 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.