From 83ef7c5204bf3b876b9ff776c133b886d9f68527 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Thu, 9 Jul 2026 14:58:02 -0400 Subject: [PATCH 1/4] deps: upgrade mdz --- .changeset/fiery-loops-sell.md | 5 ++ src/lib/DeclarationDetail.svelte | 42 +++++++----- src/lib/DocMdz.svelte | 20 ++++++ src/lib/declaration.svelte.ts | 23 +++++++ src/lib/vite_plugin_docs_mdz.ts | 107 +++++++++++++++++++++++++++++++ vite.config.ts | 9 ++- 6 files changed, 188 insertions(+), 18 deletions(-) create mode 100644 .changeset/fiery-loops-sell.md create mode 100644 src/lib/DocMdz.svelte create mode 100644 src/lib/vite_plugin_docs_mdz.ts diff --git a/.changeset/fiery-loops-sell.md b/.changeset/fiery-loops-sell.md new file mode 100644 index 000000000..742335626 --- /dev/null +++ b/.changeset/fiery-loops-sell.md @@ -0,0 +1,5 @@ +--- +'@fuzdev/fuz_ui': minor +--- + +deps: upgrade mdz diff --git a/src/lib/DeclarationDetail.svelte b/src/lib/DeclarationDetail.svelte index 2da7e608e..059b442a8 100644 --- a/src/lib/DeclarationDetail.svelte +++ b/src/lib/DeclarationDetail.svelte @@ -11,8 +11,10 @@ including parameters, props, members, overloads, intersects, and more. --> + +{#if nodes} + +{:else if content} + +{/if} diff --git a/src/lib/declaration.svelte.ts b/src/lib/declaration.svelte.ts index 8408b3d78..bdbba4ee5 100644 --- a/src/lib/declaration.svelte.ts +++ b/src/lib/declaration.svelte.ts @@ -9,6 +9,8 @@ import type { } from 'svelte-docinfo/types.js'; import {generateImport, getDisplayName} from 'svelte-docinfo/declaration-helpers.js'; +import type {MdzNode} from '@fuzdev/mdz/mdz.js'; + import type {Module} from './module.svelte.ts'; import {url_github_file} from '@fuzdev/fuz_util/package_helpers.ts'; @@ -23,6 +25,21 @@ import {url_github_file} from '@fuzdev/fuz_util/package_helpers.ts'; const field = (decl: DeclarationJsonInput, key: string): T | undefined => (decl as Record)[key] as T | undefined; +// Pre-parsed markdown trees. `vite_plugin_docs_mdz` adds a `${key}Nodes` sibling +// next to each markdown string field (e.g. `descriptionNodes` for `description`, +// `examplesNodes` for `examples`) so the docs render `` instead +// of parsing on the client. `undefined` when the build step didn't run (a plain +// `vite dev` without the plugin) or the source field was empty — callers fall +// back to the raw string via `DocMdz`. + +/** Read the `*Nodes` sibling of a single markdown string field. */ +export const field_nodes = (obj: unknown, key: string): Array | undefined => + (obj as Record | undefined> | null | undefined)?.[`${key}Nodes`]; + +/** Read the `*Nodes` sibling of a string-array field (`examples`/`seeAlso`). */ +export const list_nodes = (obj: unknown, key: string): Array> | undefined => + (obj as Record> | undefined> | null | undefined)?.[`${key}Nodes`]; + /** * Rich runtime representation of an exported declaration. * @@ -114,6 +131,12 @@ export class Declaration { since = $derived(this.declaration_json.since); examples = $derived(this.declaration_json.examples ?? []); see_also = $derived(this.declaration_json.seeAlso ?? []); + + // Build-time pre-parsed trees for the markdown getters above (see `field_nodes`). + doc_comment_nodes = $derived(field_nodes(this.declaration_json, 'docComment')); + return_description_nodes = $derived(field_nodes(this.declaration_json, 'returnDescription')); + examples_nodes = $derived(list_nodes(this.declaration_json, 'examples')); + see_also_nodes = $derived(list_nodes(this.declaration_json, 'seeAlso')); /** * Nested members for classes, interfaces, types, and enums. */ diff --git a/src/lib/vite_plugin_docs_mdz.ts b/src/lib/vite_plugin_docs_mdz.ts new file mode 100644 index 000000000..93098586a --- /dev/null +++ b/src/lib/vite_plugin_docs_mdz.ts @@ -0,0 +1,107 @@ +/** + * Vite plugin that pre-parses the markdown-bearing fields of + * `virtual:svelte-docinfo` at build time, so the API-docs pages render + * pre-parsed `MdzNode` trees instead of calling `mdz_parse` on every `` + * mount in the client. + * + * A docs module page mounts tens–100+ `` instances (one per declaration + * doc comment, per `@param`/`@returns` description, per `@example`, per + * `@see`), each of which parses its markdown string on hydration *and* on every + * client-side navigation (the `virtual:svelte-docinfo` payload is bundled into + * the client chunk via `library.ts`'s top-level import under `prerender` + + * `adapter-static`). This plugin moves that parse to build time. + * + * It runs as a `transform` on svelte-docinfo's own resolved virtual module — + * *not* a separate derived module — so HMR needs no extra wiring: when + * svelte-docinfo invalidates `virtual:svelte-docinfo` on a source change, Vite + * re-runs its `load` and then this `transform` with the fresh data. + * + * **Sibling, not replace.** For each markdown string field (`docComment`, + * `description`, `returnDescription`) it adds a `*Nodes` sibling + * (`docCommentNodes: MdzNode[]`, …); for the string-array fields it adds + * `examplesNodes: MdzNode[][]` and `seeAlsoNodes: MdzNode[][]` (the latter via + * `mdz_from_tsdoc`, matching the runtime ``). + * The raw strings stay, so nothing that reads them as strings breaks; the trade + * is payload size (pre-parsed JSON is ~2–5× the raw string) for client parse + * CPU. `declaration.svelte.ts` surfaces the `*Nodes` fields and + * `DeclarationDetail.svelte` passes them to ``. + * + * Registered after `svelte_docinfo()` in `vite.config.ts`. + * + * @module + */ + +import type {Plugin} from 'vite'; +import {mdz_parse, type MdzNode} from '@fuzdev/mdz/mdz.js'; +import {mdz_from_tsdoc} from '@fuzdev/mdz/tsdoc_mdz.js'; + +/** svelte-docinfo's resolved virtual id (the `\0` marker is Rollup's "mine"). */ +const RESOLVED_DOCINFO_ID = '\0virtual:svelte-docinfo'; + +/** String fields whose value is markdown — each gains a `${key}Nodes` sibling. */ +const MARKDOWN_STRING_KEYS = new Set(['docComment', 'description', 'returnDescription']); + +/** + * Recursively copy `value`, adding pre-parsed `*Nodes` siblings next to every + * markdown-bearing field. Handles arbitrary nesting (modules → declarations → + * parameters/props/overloads/members) without hard-coding paths — the field + * names are the same at every level. + */ +const augment = (value: unknown): unknown => { + if (Array.isArray(value)) return value.map(augment); + if (value === null || typeof value !== 'object') return value; + const obj = value as Record; + const out: Record = {}; + for (const [key, v] of Object.entries(obj)) { + out[key] = augment(v); + if (MARKDOWN_STRING_KEYS.has(key) && typeof v === 'string' && v.length > 0) { + out[`${key}Nodes`] = mdz_parse(v); + } + } + // `examples: string[]` → `examplesNodes: MdzNode[][]` + if (Array.isArray(obj.examples) && obj.examples.length > 0) { + out.examplesNodes = (obj.examples as Array).map((s): Array => mdz_parse(s)); + } + // `seeAlso: string[]` → `seeAlsoNodes: MdzNode[][]`, via the same `@see` + // bridge the runtime uses (``) + if (Array.isArray(obj.seeAlso) && obj.seeAlso.length > 0) { + out.seeAlsoNodes = (obj.seeAlso as Array).map((s): Array => + mdz_parse(mdz_from_tsdoc(s)), + ); + } + return out; +}; + +/** + * Creates the docs-mdz pre-parse plugin. Zero-config; register it *after* + * `svelte_docinfo()` so this transform sees the loaded virtual module. + */ +export const vite_plugin_docs_mdz = (): Plugin => ({ + name: 'vite-plugin-docs-mdz', + // run before core JS transforms so we see svelte-docinfo's raw module source + enforce: 'pre', + transform(code, id) { + if (id !== RESOLVED_DOCINFO_ID) return undefined; + // svelte-docinfo's `load` emits `export const modules = ;\nexport + // const diagnostics = …`. Slice out the `modules` JSON by those anchors — + // index-based, not a regex, so string values containing `;` are safe. + const prefix = 'export const modules = '; + const json_start = code.indexOf(prefix); + if (json_start === -1) return undefined; // unexpected shape — leave untouched + const value_start = json_start + prefix.length; + const value_end = code.indexOf(';\nexport const diagnostics =', value_start); + if (value_end === -1) return undefined; + + let modules: unknown; + try { + modules = JSON.parse(code.slice(value_start, value_end)); + } catch { + return undefined; // not the JSON shape we expect + } + const augmented = JSON.stringify(augment(modules)); + return { + code: code.slice(0, value_start) + augmented + code.slice(value_end), + map: null, + }; + }, +}); diff --git a/vite.config.ts b/vite.config.ts index 23e846690..db1aa6c09 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,9 +4,16 @@ import svelte_docinfo from 'svelte-docinfo/vite.js'; import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.ts'; import {vite_plugin_pkg_json} from './src/lib/vite_plugin_pkg_json.js'; +import {vite_plugin_docs_mdz} from './src/lib/vite_plugin_docs_mdz.js'; export default defineConfig(({mode}) => ({ - plugins: [sveltekit(), svelte_docinfo(), vite_plugin_fuz_css(), vite_plugin_pkg_json()], + plugins: [ + sveltekit(), + svelte_docinfo(), + vite_plugin_docs_mdz(), // pre-parses svelte-docinfo's markdown fields to MdzNode trees + vite_plugin_fuz_css(), + vite_plugin_pkg_json(), + ], // In test mode, use browser conditions so Svelte's mount() resolves to the client version resolve: mode === 'test' ? {conditions: ['browser']} : undefined, optimizeDeps: {exclude: ['@fuzdev/blake3_wasm']}, From c6eb94db37b248951046cc2c043f9dae08d55ad2 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Thu, 9 Jul 2026 15:04:30 -0400 Subject: [PATCH 2/4] wip --- src/lib/DeclarationDetail.svelte | 5 ++- src/lib/vite_plugin_docs_mdz.ts | 69 ++++++++++++++++++-------------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/lib/DeclarationDetail.svelte b/src/lib/DeclarationDetail.svelte index 059b442a8..814625602 100644 --- a/src/lib/DeclarationDetail.svelte +++ b/src/lib/DeclarationDetail.svelte @@ -301,7 +301,10 @@ including parameters, props, members, overloads, intersects, and more.

returns

{#if declaration.return_description} - + {/if} {/if} diff --git a/src/lib/vite_plugin_docs_mdz.ts b/src/lib/vite_plugin_docs_mdz.ts index 93098586a..853e39cea 100644 --- a/src/lib/vite_plugin_docs_mdz.ts +++ b/src/lib/vite_plugin_docs_mdz.ts @@ -38,37 +38,37 @@ import {mdz_from_tsdoc} from '@fuzdev/mdz/tsdoc_mdz.js'; /** svelte-docinfo's resolved virtual id (the `\0` marker is Rollup's "mine"). */ const RESOLVED_DOCINFO_ID = '\0virtual:svelte-docinfo'; -/** String fields whose value is markdown — each gains a `${key}Nodes` sibling. */ +/** Markdown string fields — each gains a `${key}Nodes: MdzNode[]` sibling. */ const MARKDOWN_STRING_KEYS = new Set(['docComment', 'description', 'returnDescription']); +/** Markdown string-array fields — each gains a `${key}Nodes: MdzNode[][]` sibling. */ +const MARKDOWN_LIST_KEYS = new Set(['examples', 'seeAlso']); /** - * Recursively copy `value`, adding pre-parsed `*Nodes` siblings next to every + * Recursively copy `value`, adding a pre-parsed `*Nodes` sibling next to every * markdown-bearing field. Handles arbitrary nesting (modules → declarations → * parameters/props/overloads/members) without hard-coding paths — the field - * names are the same at every level. + * names are the same at every level. Which fields carry markdown is implicitly + * shared with the reader (`DeclarationDetail.svelte`); a field rendered there + * but missing here simply falls back to runtime parsing via `DocMdz`. */ const augment = (value: unknown): unknown => { if (Array.isArray(value)) return value.map(augment); if (value === null || typeof value !== 'object') return value; - const obj = value as Record; const out: Record = {}; - for (const [key, v] of Object.entries(obj)) { + for (const [key, v] of Object.entries(value)) { out[key] = augment(v); if (MARKDOWN_STRING_KEYS.has(key) && typeof v === 'string' && v.length > 0) { out[`${key}Nodes`] = mdz_parse(v); + } else if (MARKDOWN_LIST_KEYS.has(key) && Array.isArray(v) && v.length > 0) { + // `seeAlso` refs go through the `@see` bridge first, matching the + // runtime `` + const to_tree = + key === 'seeAlso' + ? (s: string): Array => mdz_parse(mdz_from_tsdoc(s)) + : (s: string): Array => mdz_parse(s); + out[`${key}Nodes`] = (v as Array).map(to_tree); } } - // `examples: string[]` → `examplesNodes: MdzNode[][]` - if (Array.isArray(obj.examples) && obj.examples.length > 0) { - out.examplesNodes = (obj.examples as Array).map((s): Array => mdz_parse(s)); - } - // `seeAlso: string[]` → `seeAlsoNodes: MdzNode[][]`, via the same `@see` - // bridge the runtime uses (``) - if (Array.isArray(obj.seeAlso) && obj.seeAlso.length > 0) { - out.seeAlsoNodes = (obj.seeAlso as Array).map((s): Array => - mdz_parse(mdz_from_tsdoc(s)), - ); - } return out; }; @@ -87,21 +87,30 @@ export const vite_plugin_docs_mdz = (): Plugin => ({ // index-based, not a regex, so string values containing `;` are safe. const prefix = 'export const modules = '; const json_start = code.indexOf(prefix); - if (json_start === -1) return undefined; // unexpected shape — leave untouched - const value_start = json_start + prefix.length; - const value_end = code.indexOf(';\nexport const diagnostics =', value_start); - if (value_end === -1) return undefined; - - let modules: unknown; + const value_start = json_start === -1 ? -1 : json_start + prefix.length; + const value_end = + value_start === -1 ? -1 : code.indexOf(';\nexport const diagnostics =', value_start); + if (value_end === -1) { + // Anchors gone → svelte-docinfo changed its emitted module shape. Warn + // (don't fail the build) so the drift is visible; the docs still render, + // parsing markdown at runtime via `DocMdz`'s fallback. + this.warn( + 'could not locate the `modules` export in virtual:svelte-docinfo; ' + + 'markdown will be parsed at runtime. svelte-docinfo may have changed its module format.', + ); + return undefined; + } try { - modules = JSON.parse(code.slice(value_start, value_end)); - } catch { - return undefined; // not the JSON shape we expect + const modules = JSON.parse(code.slice(value_start, value_end)); + const augmented = JSON.stringify(augment(modules)); + return {code: code.slice(0, value_start) + augmented + code.slice(value_end), map: null}; + } catch (err) { + // A malformed slice or an unexpected mdz_parse failure degrades to + // runtime parsing rather than breaking the build. + this.warn( + `failed to pre-parse markdown fields (${(err as Error).message}); parsing at runtime.`, + ); + return undefined; } - const augmented = JSON.stringify(augment(modules)); - return { - code: code.slice(0, value_start) + augmented + code.slice(value_end), - map: null, - }; }, }); From a4b1698835e748336d723d3f4134f8b3f3bbc53f Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Thu, 9 Jul 2026 16:24:31 -0400 Subject: [PATCH 3/4] wip --- CLAUDE.md | 12 ++ src/lib/ApiModule.svelte | 9 +- src/lib/DeclarationDetail.svelte | 3 +- src/lib/module.svelte.ts | 7 +- src/lib/vite_plugin_docs_mdz.ts | 25 ++- src/routes/docs/tomes.ts | 11 +- .../docs/vite_plugin_docs_mdz/+page.svelte | 116 ++++++++++++++ src/test/vite_plugin_docs_mdz.test.ts | 146 ++++++++++++++++++ 8 files changed, 318 insertions(+), 11 deletions(-) create mode 100644 src/routes/docs/vite_plugin_docs_mdz/+page.svelte create mode 100644 src/test/vite_plugin_docs_mdz.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 2332477f3..3d7acff02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -216,6 +216,10 @@ re-strip drops the extras. See the `vite_plugin_pkg_json` tome for the pattern. - `vite_plugin_pkg_json.ts` - Vite plugin serving `virtual:pkg.json` (curated `PkgJson` from `@fuzdev/fuz_util/pkg_json.ts`) +- `vite_plugin_docs_mdz.ts` - Vite plugin that pre-parses svelte-docinfo's + markdown fields to `MdzNode` trees (`*Nodes` siblings on `virtual:svelte-docinfo`), + so API-docs pages render pre-parsed instead of parsing per `` mount (see + [mdz rendering](#mdz-rendering)) - `library.svelte.ts` - `Library` class wrapping library data - `declaration.svelte.ts` - `Declaration` class for code declarations (uses `generateImport`, `getDisplayName` from `svelte-docinfo/declaration-helpers.js`) @@ -252,6 +256,14 @@ preprocessor (it precompiles any static `` content to plain markup); fuz_ui authors no static mdz, so it's effectively a pass-through and needs no injection options. +The markdown **parsing** (not rendering) is moved to build time by +`vite_plugin_docs_mdz`: it pre-parses svelte-docinfo's markdown fields +(`docComment`, `description`, `returnDescription`, `moduleComment`, `examples`, +`seeAlso`) to `MdzNode` trees and adds them as `*Nodes` siblings, which the +`DocMdz` wrapper prefers (``), falling back to parsing the raw +string when the plugin didn't run. Rendering and the injection seam above stay +at runtime; only the parse moves. Requires the `nodes` prop from `@fuzdev/mdz`. + ## Context system All contexts use the standardized pattern via `context_helpers.ts`: diff --git a/src/lib/ApiModule.svelte b/src/lib/ApiModule.svelte index 612c67c17..ccf78fd29 100644 --- a/src/lib/ApiModule.svelte +++ b/src/lib/ApiModule.svelte @@ -1,12 +1,12 @@ + + +
+

+ is a Vite plugin that pre-parses the + markdown-bearing fields of virtual:svelte-docinfo at build time, so the + auto-generated API-docs pages render pre-parsed + MdzNode trees instead of + calling + mdz_parse on every <Mdz> mount in the browser. +

+

+ A single module's docs page mounts tens to a hundred-plus <Mdz> instances + -- one per declaration doc comment, per @param/@returns description, + per + @example, per @see. Without the plugin each one parses its markdown + string on hydration and on every client-side navigation (the + virtual:svelte-docinfo payload is bundled into the client chunk). This plugin + moves all of that parsing to build time. +

+
+ + + +

+ Register the plugin, , + after + svelte_docinfo() so its transform sees the loaded virtual module: +

+ +

+ It's zero-config and uses enforce: 'pre' so it transforms svelte-docinfo's raw + module source before Vite's core transforms touch it. Because it augments svelte-docinfo's + own resolved module in place -- rather than deriving a separate module -- HMR needs + no extra wiring: when svelte-docinfo invalidates the module on a source edit, Vite re-runs its + load and then this transform with the fresh data, so the pre-parsed + trees never go stale. +

+
+ + + +

+ For each markdown string field (docComment, description, + returnDescription, moduleComment) the plugin adds a + *Nodes sibling (docCommentNodes, ...) next to the raw string; for + the string-array fields (examples, seeAlso) it adds + examplesNodes and seeAlsoNodes. The raw strings stay in place, so + code that reads them as strings -- like search -- keeps working. +

+

+ The docs components read the trees through a small DocMdz wrapper that prefers + the pre-parsed nodes and falls back to parsing content at runtime. + So a plain vite dev without the plugin registered still renders -- it just parses + on the client, exactly as before. +

+ +{#if nodes} + +{:else if content} + +{/if}`} + /> +
+ + + +

+ Pre-parsed JSON is larger than the raw markdown string it replaces -- roughly double + uncompressed, and the trees ship in the same client chunk as the rest of the library metadata. + The win is trading that payload for zero client-side parse work across every declaration on + every page load and every client-side navigation. For a docs-heavy library the parse savings + are worth the bytes; if they aren't for your case, the plugin is opt-in -- drop it and the + DocMdz fallback parses at runtime instead. +

+
+ + +
diff --git a/src/test/vite_plugin_docs_mdz.test.ts b/src/test/vite_plugin_docs_mdz.test.ts new file mode 100644 index 000000000..0ae7522e9 --- /dev/null +++ b/src/test/vite_plugin_docs_mdz.test.ts @@ -0,0 +1,146 @@ +import {test, assert, describe} from 'vitest'; + +import {mdz_parse} from '@fuzdev/mdz/mdz.ts'; +import {mdz_from_tsdoc} from '@fuzdev/mdz/tsdoc_mdz.ts'; + +import {vite_plugin_docs_mdz} from '$lib/vite_plugin_docs_mdz.ts'; + +// The `transform` hook reads `this.warn` (a Rollup plugin context). Cast the +// returned plugin to just that hook and hand it a minimal mock context — no real +// Vite/Rollup instance needed (same approach as vite_plugin_pkg_json.test.ts). +interface MockCtx { + warn: (msg: string) => void; +} +interface PluginHooks { + transform: (this: MockCtx, code: string, id: string) => {code: string; map: null} | undefined; +} + +const create_mock_ctx = (): MockCtx & {warnings: Array} => { + const warnings: Array = []; + return {warnings, warn: (msg) => warnings.push(msg)}; +}; + +const RESOLVED_DOCINFO_ID = '\0virtual:svelte-docinfo'; + +// Build a svelte-docinfo-shaped virtual module around a `modules` array, matching +// the emit the plugin slices (`export const modules = …;\nexport const diagnostics = …`). +const make_module_code = (modules: unknown): string => + `export const modules = ${JSON.stringify( + modules, + )};\nexport const diagnostics = [];\nexport default {modules, diagnostics};`; + +// Re-extract the augmented `modules` array from the plugin's output using the +// same anchors, so tests assert on structure rather than substrings. +const extract_modules = (code: string): any => { + const prefix = 'export const modules = '; + const start = code.indexOf(prefix) + prefix.length; + const end = code.indexOf(';\nexport const diagnostics =', start); + return JSON.parse(code.slice(start, end)); +}; + +const run = (modules: unknown): {out: any; warnings: Array} => { + const plugin = vite_plugin_docs_mdz() as unknown as PluginHooks; + const ctx = create_mock_ctx(); + const result = plugin.transform.call(ctx, make_module_code(modules), RESOLVED_DOCINFO_ID); + assert.ok(result, 'transform returned a result'); + return {out: extract_modules(result.code), warnings: ctx.warnings}; +}; + +describe('augment', () => { + test('adds *Nodes siblings for every rendered markdown field, at any depth', () => { + const {out} = run([ + { + path: 'Foo.ts', + moduleComment: 'Module **docs**.', + declarations: [ + { + name: 'foo', + kind: 'function', + docComment: 'Does `foo`.', + returnDescription: 'a number', + parameters: [{name: 'x', type: 'number', description: 'the _x_'}], + examples: ['const a = foo(1);'], + seeAlso: ['`bar`'], + }, + ], + }, + ]); + const decl = out[0].declarations[0]; + // module-level coverage (the gap this plugin's key set was extended to close) + assert.deepEqual(out[0].moduleCommentNodes, mdz_parse('Module **docs**.')); + // declaration-level string fields + assert.deepEqual(decl.docCommentNodes, mdz_parse('Does `foo`.')); + assert.deepEqual(decl.returnDescriptionNodes, mdz_parse('a number')); + // nested parameter description (arbitrary depth, same field name) + assert.deepEqual(decl.parameters[0].descriptionNodes, mdz_parse('the _x_')); + // list fields → arrays of trees + assert.deepEqual(decl.examplesNodes, [mdz_parse('const a = foo(1);')]); + }); + + test('seeAlso goes through the @see bridge (not raw mdz_parse)', () => { + const {out} = run([ + {path: 'Foo.ts', declarations: [{name: 'foo', kind: 'function', seeAlso: ['`bar`']}]}, + ]); + const nodes = out[0].declarations[0].seeAlsoNodes[0]; + // the bridge turns `` `bar` `` into a doc link, which differs from parsing the raw string + assert.deepEqual(nodes, mdz_parse(mdz_from_tsdoc('`bar`'))); + assert.notDeepEqual(nodes, mdz_parse('`bar`')); + }); + + test('keeps the raw strings (sibling, not replace)', () => { + const {out} = run([{path: 'Foo.ts', moduleComment: 'Module **docs**.', declarations: []}]); + assert.strictEqual(out[0].moduleComment, 'Module **docs**.'); + }); + + test('does not pre-parse fields nothing renders as markdown (propertyDescriptions)', () => { + const {out} = run([ + { + path: 'Foo.ts', + declarations: [{name: 'foo', kind: 'function', propertyDescriptions: {a: 'the **a**'}}], + }, + ]); + assert.notProperty(out[0].declarations[0], 'propertyDescriptionsNodes'); + }); + + test('skips empty strings and empty arrays', () => { + const {out} = run([ + { + path: 'Foo.ts', + moduleComment: '', + declarations: [{name: 'foo', kind: 'function', examples: []}], + }, + ]); + assert.notProperty(out[0], 'moduleCommentNodes'); + assert.notProperty(out[0].declarations[0], 'examplesNodes'); + }); +}); + +describe('robustness', () => { + test('passes through modules it does not own', () => { + const plugin = vite_plugin_docs_mdz() as unknown as PluginHooks; + const ctx = create_mock_ctx(); + assert.strictEqual( + plugin.transform.call(ctx, 'export const x = 1;', '\0some-other-id'), + undefined, + ); + assert.strictEqual(ctx.warnings.length, 0); + }); + + test('warns and degrades to runtime parsing when the modules anchor is gone', () => { + const plugin = vite_plugin_docs_mdz() as unknown as PluginHooks; + const ctx = create_mock_ctx(); + const result = plugin.transform.call(ctx, 'export const nope = [];\n', RESOLVED_DOCINFO_ID); + assert.strictEqual(result, undefined, 'no transform → svelte-docinfo output serves as-is'); + assert.ok(ctx.warnings.some((w) => w.includes('could not locate the `modules` export'))); + }); + + test('warns and degrades when the sliced JSON is malformed', () => { + const plugin = vite_plugin_docs_mdz() as unknown as PluginHooks; + const ctx = create_mock_ctx(); + // valid anchors, but the value between them is not valid JSON + const code = 'export const modules = {oops;\nexport const diagnostics = [];'; + const result = plugin.transform.call(ctx, code, RESOLVED_DOCINFO_ID); + assert.strictEqual(result, undefined); + assert.ok(ctx.warnings.some((w) => w.includes('failed to pre-parse'))); + }); +}); From 7edc5bf8bf6dc6ba10fcb149c8733182ee5f0acd Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Thu, 9 Jul 2026 17:38:34 -0400 Subject: [PATCH 4/4] wip --- src/lib/vite_plugin_docs_mdz.ts | 6 ++++-- src/routes/docs/vite_plugin_docs_mdz/+page.svelte | 12 +++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/lib/vite_plugin_docs_mdz.ts b/src/lib/vite_plugin_docs_mdz.ts index 4c671a68c..0e2abac8c 100644 --- a/src/lib/vite_plugin_docs_mdz.ts +++ b/src/lib/vite_plugin_docs_mdz.ts @@ -22,8 +22,10 @@ * `examplesNodes: MdzNode[][]` and `seeAlsoNodes: MdzNode[][]` (the latter via * `mdz_from_tsdoc`, matching the runtime ``). * The raw strings stay, so nothing that reads them as strings breaks; the trade - * is payload size (pre-parsed JSON is ~2–5× the raw string) for client parse - * CPU. `declaration.svelte.ts` surfaces the `*Nodes` fields and + * is payload size (pre-parsed JSON runs ~3–5× the raw string for a typical doc + * comment, and higher for short one-line fields, where the per-node + * `type`/`start`/`end` bookkeeping dominates) for client parse CPU. + * `declaration.svelte.ts` surfaces the `*Nodes` fields and * `DeclarationDetail.svelte` passes them to ``. * * Registered after `svelte_docinfo()` in `vite.config.ts`. diff --git a/src/routes/docs/vite_plugin_docs_mdz/+page.svelte b/src/routes/docs/vite_plugin_docs_mdz/+page.svelte index e09b8416d..53e870a0c 100644 --- a/src/routes/docs/vite_plugin_docs_mdz/+page.svelte +++ b/src/routes/docs/vite_plugin_docs_mdz/+page.svelte @@ -97,11 +97,13 @@ export default defineConfig({

- Pre-parsed JSON is larger than the raw markdown string it replaces -- roughly double - uncompressed, and the trees ship in the same client chunk as the rest of the library metadata. - The win is trading that payload for zero client-side parse work across every declaration on - every page load and every client-side navigation. For a docs-heavy library the parse savings - are worth the bytes; if they aren't for your case, the plugin is opt-in -- drop it and the + Pre-parsed JSON is several times the size of the raw markdown string it replaces -- roughly + 3-5x for a typical doc comment, and more for short one-line fields (a bare + @param description), where the per-node type and offset bookkeeping dominates. + The trees ship in the same client chunk as the rest of the library metadata. The win is + trading that payload for zero client-side parse work across every declaration on every page + load and every client-side navigation. For a docs-heavy library the parse savings are worth + the bytes; if they aren't for your case, the plugin is opt-in -- drop it and the DocMdz fallback parses at runtime instead.