@dogsbay/format-starlight 0.2.0-beta.10 → 0.2.0-beta.102

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.
@@ -143,18 +143,27 @@ function resolveWranglerConfig(source) {
143
143
  toml = toml.replace(/^(\s*)(.*compatibility_date.*)/m, "$1# Set this to today's date\n$1$2");
144
144
  jsonc = jsonc.replace(/^(\s*)(.*compatibility_date.*)/m, "$1// Set this to today's date\n$1$2");
145
145
  }
146
+ // A closing fence MUST be on its own line. It wasn't for the TOML
147
+ // half — `TOML.stringify` returns no trailing newline, so the fence
148
+ // was glued to the last config line (`remote = true```). The block
149
+ // then never closed and SWALLOWED THE REST OF THE PAGE: on
150
+ // ai-search/get-started/workers, everything after the binding config
151
+ // — prose, headings and three more components — became one giant code
152
+ // block. Normalising both halves means neither depends on what its
153
+ // serializer happens to append.
146
154
  const result = `<Tabs>
147
155
  <TabItem label="wrangler.jsonc">
148
156
 
149
157
  \`\`\`jsonc
150
- ${jsonc}
158
+ ${jsonc.replace(/\s*$/, "")}
151
159
  \`\`\`
152
160
 
153
161
  </TabItem>
154
162
  <TabItem label="wrangler.toml">
155
163
 
156
164
  \`\`\`toml
157
- ${toml}\`\`\`
165
+ ${toml.replace(/\s*$/, "")}
166
+ \`\`\`
158
167
 
159
168
  </TabItem>
160
169
  </Tabs>`;
@@ -323,13 +332,34 @@ export const cloudflareAdapter = {
323
332
  href: attrs.href,
324
333
  }),
325
334
  },
335
+ // Cloudflare's own alias: `export { default as LinkTitleCard } from
336
+ // "./components/cf/LinkCard.astro"` — the SAME component under a second
337
+ // name, used when the card wraps its description as children rather than
338
+ // passing it as an attribute. Unmapped, it leaked its opening tag onto the
339
+ // page as visible text (`<LinkTitleCard title="Get started" href=…`) and
340
+ // its body rendered as a code block. 300 uses across the corpus.
341
+ LinkTitleCard: {
342
+ type: "card",
343
+ container: true,
344
+ propsFromAttrs: (attrs) => ({
345
+ title: attrs.title || "",
346
+ href: attrs.href,
347
+ icon: attrs.icon,
348
+ }),
349
+ },
326
350
  RelatedProduct: {
327
351
  type: "card",
328
352
  container: true,
329
353
  propsFromAttrs: (attrs) => ({
330
354
  title: attrs.header || "",
331
355
  href: attrs.href,
332
- icon: attrs.product ? `icons/${attrs.product}.svg` : undefined,
356
+ // `/_assets/icons/…` — the mount convention, so the file the importer
357
+ // copies is the file this reference names. It used to be a bare
358
+ // `icons/x.svg`, which existed in neither output: Astro's <Icon> takes
359
+ // a registry NAME and silently rendered nothing, and Obsidian built an
360
+ // <img> against a vault path with no file behind it, so every related
361
+ // product showed a broken image with its title stretched down the page.
362
+ icon: attrs.product ? `/_assets/icons/${attrs.product}.svg` : undefined,
333
363
  }),
334
364
  },
335
365
  Description: {
@@ -414,6 +444,20 @@ export const cloudflareAdapter = {
414
444
  // <Plan type="all" /> → **Available on all plans**
415
445
  [/<Plan\s+type="all"\s*\/?>/g, "**Available on all plans**"],
416
446
  [/<Plan\s+type="([^"]*)"\s*\/?>/g, "**$1 plan**"],
447
+ // <Type text="string" /> → `string`
448
+ //
449
+ // An inline type-annotation badge next to a parameter or method — the
450
+ // component renders it as a styled <code>, so inline code is the faithful
451
+ // mapping, not an approximation. 249 uses in the AI Search partials alone
452
+ // (`string` ×93, `boolean` ×38, `object` ×28), all inside API reference
453
+ // parameter lists where the type IS the content.
454
+ [/<Type\s+text="([^"]*)"\s*\/>/g, "`$1`"],
455
+ // <MetaInfo text="optional" /> → optional
456
+ //
457
+ // The companion annotation: "optional", "default: 10", a units hint. It
458
+ // was being DELETED, which reads as though every parameter is required.
459
+ // The component is a muted inline span, so plain text is the mapping.
460
+ [/<MetaInfo\s+text="([^"]*)"\s*\/>/g, "$1"],
417
461
  // <InlineBadge ... /> → strip
418
462
  [/<InlineBadge\s[^>]*\/>/g, ""],
419
463
  // Fallback strip for self-closing components that may appear deeply indented
@@ -422,7 +466,7 @@ export const cloudflareAdapter = {
422
466
  [/<AutoconfigDiagram[\s\S]*?\/>/g, ""],
423
467
  [/<GitHubCode\s[^>]*\/>/g, ""],
424
468
  [/<ResourcesBySelector[\s\S]*?\/>/g, ""],
425
- [/<MetaInfo\s[^>]*\/>/g, ""],
469
+ [/<MetaInfo\s[^>]*\/>/g, ""], // fallback: forms with no `text` attribute
426
470
  [/<DirectoryListing[\s\S]*?\/>/g, ""],
427
471
  [/<ExternalResources\s[^>]*\/>/g, ""],
428
472
  [/<YouTubeVideos\s[^>]*\/>/g, ""],
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  import type { FormatPlugin } from "@dogsbay/types";
2
+ import type { SiteAdapter } from "./adapter.js";
3
+ export declare const STARLIGHT_ADAPTERS: Record<string, SiteAdapter>;
2
4
  export declare const plugin: FormatPlugin;
package/dist/cli.js CHANGED
@@ -5,9 +5,10 @@ import { starlightToTree as starlightToTreeLegacy, extractFrontmatter } from "./
5
5
  import { starlightToTree as starlightToTreeMdx } from "./parse-mdx.js";
6
6
  import { cloudflareAdapter } from "./adapters/cloudflare.js";
7
7
  import { buildNavFromDirectory, buildNavFromConfig } from "./nav.js";
8
- const ADAPTERS = {
8
+ export const STARLIGHT_ADAPTERS = {
9
9
  cloudflare: cloudflareAdapter,
10
10
  };
11
+ const ADAPTERS = STARLIGHT_ADAPTERS;
11
12
  /**
12
13
  * Detect if a directory contains a Starlight docs source.
13
14
  *
@@ -136,9 +137,96 @@ async function importStarlight(source, opts) {
136
137
  if (opts.partialsDir) {
137
138
  partialsDir = opts.partialsDir;
138
139
  }
140
+ // Where the images live. Found the same way as partialsDir, because it is
141
+ // the same Astro layout: `src/content/docs` and `src/assets` are siblings
142
+ // under `src/`.
143
+ //
144
+ // Without this the conversion emitted `/_assets/images/x.png` refs and
145
+ // nothing ever copied a file there — every local image on a converted site
146
+ // was a 404. The reference survived and the file did not, which is why a
147
+ // page count, a retention score and a clean exit all looked fine.
148
+ let assetsDir;
149
+ searchDir = docsDir;
150
+ for (let depth = 0; depth < 10; depth++) {
151
+ const candidate = join(searchDir, "src", "assets");
152
+ if (existsSync(candidate)) {
153
+ assetsDir = candidate;
154
+ break;
155
+ }
156
+ const sibling = join(dirname(searchDir), "assets");
157
+ if (existsSync(sibling)) {
158
+ assetsDir = sibling;
159
+ break;
160
+ }
161
+ const parent = dirname(searchDir);
162
+ if (parent === searchDir)
163
+ break;
164
+ searchDir = parent;
165
+ }
166
+ // Product/brand icons, mounted under `_assets/icons` to match the refs the
167
+ // adapters emit for them.
168
+ let iconsDir;
169
+ searchDir = docsDir;
170
+ for (let depth = 0; depth < 10; depth++) {
171
+ const candidate = join(searchDir, "src", "icons");
172
+ if (existsSync(candidate)) {
173
+ iconsDir = candidate;
174
+ break;
175
+ }
176
+ const parent = dirname(searchDir);
177
+ if (parent === searchDir)
178
+ break;
179
+ searchDir = parent;
180
+ }
181
+ // Component sources, for recovering an accessible description from a
182
+ // component we cannot render. Same sibling layout as assets and partials.
183
+ let componentsDir;
184
+ searchDir = docsDir;
185
+ for (let depth = 0; depth < 10; depth++) {
186
+ const candidate = join(searchDir, "src", "components");
187
+ if (existsSync(candidate)) {
188
+ componentsDir = candidate;
189
+ break;
190
+ }
191
+ const parent = dirname(searchDir);
192
+ if (parent === searchDir)
193
+ break;
194
+ searchDir = parent;
195
+ }
196
+ /**
197
+ * The `aria-label` (or `<figcaption>`) a component declares about itself.
198
+ *
199
+ * Cloudflare's diagrams are Astro components — inline SVG, a web component,
200
+ * CSS animation — with no image file to carry across. But their `aria-label`
201
+ * is a full description written by their docs team, and it is the
202
+ * information the diagram exists to convey. Recovering it turns a silent
203
+ * hole into a described placeholder; nothing else about the component can
204
+ * survive a conversion to markdown.
205
+ */
206
+ const describeComponent = (tag) => {
207
+ if (!componentsDir)
208
+ return undefined;
209
+ const file = join(componentsDir, `${tag}.astro`);
210
+ if (!existsSync(file))
211
+ return undefined;
212
+ const source = readFileSync(file, "utf-8");
213
+ const aria = source.match(/aria-label="([^"]{20,})"/);
214
+ if (aria)
215
+ return aria[1].replace(/\s+/g, " ").trim();
216
+ const caption = source.match(/<figcaption[^>]*>([^<]{20,})<\/figcaption>/);
217
+ return caption ? caption[1].replace(/\s+/g, " ").trim() : undefined;
218
+ };
139
219
  // Parse all pages
140
220
  const mdxFiles = walkMdxFiles(docsDir, docsDir);
141
221
  const pages = [];
222
+ /** Components no builtin or adapter mapping claimed. */
223
+ const dropped = new Map();
224
+ const record = (tag, slug) => {
225
+ const entry = dropped.get(tag) ?? { count: 0, pages: new Set() };
226
+ entry.count++;
227
+ entry.pages.add(slug);
228
+ dropped.set(tag, entry);
229
+ };
142
230
  // Custom taxonomy names from dogsbay.config.yml's `taxonomies:` block
143
231
  // (threaded through cli's import-content.ts).
144
232
  const taxonomyNames = Array.isArray(opts.taxonomyNames)
@@ -151,7 +239,24 @@ async function importStarlight(source, opts) {
151
239
  // Fall back to legacy regex parser with --legacy-parser flag
152
240
  const useLegacy = opts.legacyParser === true;
153
241
  const starlightToTree = useLegacy ? starlightToTreeLegacy : starlightToTreeMdx;
154
- const tree = starlightToTree(body, { adapter, partialsDir });
242
+ const tree = starlightToTree(body, {
243
+ adapter,
244
+ partialsDir,
245
+ onDrop: (tag) => record(tag, slug),
246
+ describeComponent,
247
+ });
248
+ // Catch what the callback cannot: a component nested inside a MAPPED
249
+ // container is not dropped, it LEAKS — its opening tag lands in the page
250
+ // as visible text and its body often becomes a code block, because nothing
251
+ // claimed the block. `<LinkTitleCard>` inside `<CardGrid>` did exactly
252
+ // that on Cloudflare's AI Search overview while the drop warning stayed
253
+ // silent.
254
+ //
255
+ // Scanning the finished tree catches every route into the output, present
256
+ // and future, in one place. `code` nodes are skipped: a code sample
257
+ // containing `<Response>` is content, not a leak.
258
+ for (const tag of leakedComponentTags(tree))
259
+ record(tag, slug);
155
260
  // Extract headings from tree
156
261
  const headings = tree
157
262
  .filter((n) => n.type === "heading")
@@ -215,16 +320,85 @@ async function importStarlight(source, opts) {
215
320
  opts.siteName = indexPage.title;
216
321
  }
217
322
  }
218
- return { pages, nav };
323
+ // Say what was dropped, loudly and countably.
324
+ //
325
+ // A component nothing claimed is CONTENT THAT IS NOT ON THE PAGE — the
326
+ // Cloudflare AI Search overview lost its architecture diagram this way, and
327
+ // the only way anyone found out was comparing the converted page with the
328
+ // live one by eye. There is no ledger on this path, so the warning IS the
329
+ // report: it names each tag, how many times it appeared and how many pages
330
+ // it touched, so `convert … 2>&1 | grep "dropped"` answers "how much of this
331
+ // corpus does this affect" without reading any output files.
332
+ if (dropped.size > 0) {
333
+ const rows = [...dropped.entries()]
334
+ .sort((a, b) => b[1].count - a[1].count)
335
+ .map(([tag, d]) => ` <${tag}> ×${d.count} on ${d.pages.size} page(s): ${[...d.pages].slice(0, 3).join(", ")}${d.pages.size > 3 ? ", …" : ""}`);
336
+ const total = [...dropped.values()].reduce((n, d) => n + d.count, 0);
337
+ console.warn(`warning: ${total} unmapped component(s) — dropped, or leaked into the ` +
338
+ `page as literal text:\n${rows.join("\n")}\n` +
339
+ ` Map them with an adapter, or accept the loss knowingly.`);
340
+ }
341
+ return {
342
+ pages,
343
+ nav,
344
+ // Contents land in `public/_assets/`, matching the `/_assets/…` refs the
345
+ // parser now emits for the `~/assets/` alias.
346
+ ...(assetsDir || iconsDir
347
+ ? {
348
+ assetMounts: [
349
+ ...(assetsDir ? [{ dir: assetsDir }] : []),
350
+ ...(iconsDir ? [{ dir: iconsDir, into: "icons" }] : []),
351
+ ],
352
+ }
353
+ : {}),
354
+ };
355
+ }
356
+ /**
357
+ * Component tags left as literal text/HTML in a finished tree.
358
+ *
359
+ * A CamelCase tag only — `<ACCOUNT_ID>` and `<API_TOKEN>` are placeholders the
360
+ * docs write on purpose, and flagging those would bury the real signal.
361
+ */
362
+ function leakedComponentTags(nodes) {
363
+ const found = [];
364
+ const scan = (list) => {
365
+ for (const node of list) {
366
+ if (node.type === "code" || node.type === "diagram")
367
+ continue;
368
+ for (const text of [node.html, ...(node.inline ?? []).map((i) => i.text)]) {
369
+ if (typeof text !== "string")
370
+ continue;
371
+ // `</?` — a leak is often only the CLOSING tag, when the opener was
372
+ // consumed by the container above it but the closer had nothing to
373
+ // pair with. Matching openers alone missed exactly that case.
374
+ for (const m of text.matchAll(/<\/?([A-Z][a-z0-9]+[A-Za-z0-9]*)[\s/>]/g))
375
+ found.push(m[1]);
376
+ }
377
+ if (node.children)
378
+ scan(node.children);
379
+ }
380
+ };
381
+ scan(nodes);
382
+ return found;
219
383
  }
220
384
  export const plugin = {
221
385
  name: "starlight",
222
386
  canImport: true,
223
387
  canExport: false,
224
388
  detectSource: detectStarlightSource,
225
- exportOptions: [
226
- { flags: "--adapter <name>", description: "Site adapter for custom components (e.g. cloudflare)" },
389
+ // importOptions, not exportOptions: this plugin declares `canExport: false`,
390
+ // so an export-side declaration described a flag on a command that does not
391
+ // exist — and `convert`'s "unusable flag" warning reads the IMPORT list, so
392
+ // `--adapter cloudflare` was reported as ignored while being consumed. That
393
+ // warning is load-bearing on this corpus: the adapter is the difference
394
+ // between every `<Render>` partial resolving and none of them.
395
+ importOptions: [
396
+ {
397
+ flags: "--adapter <name>",
398
+ description: `Site adapter for custom components (${Object.keys(STARLIGHT_ADAPTERS).join(", ")})`,
399
+ },
227
400
  ],
401
+ exportOptions: [],
228
402
  async import(source, opts) {
229
403
  return importStarlight(source, opts);
230
404
  },
@@ -18,5 +18,28 @@ export interface ParseOptions {
18
18
  adapter?: SiteAdapter;
19
19
  /** Directory containing partial MDX files for <Render> resolution */
20
20
  partialsDir?: string;
21
+ /**
22
+ * Called with the tag name of every component DROPPED from the output —
23
+ * one that no builtin and no adapter mapping claimed.
24
+ *
25
+ * A dropped component is content that is not on the page. Cloudflare's
26
+ * `<AiSearchOverviewDiagram />` is the architecture diagram at the top of
27
+ * the AI Search overview; it vanished with no trace, and the only way to
28
+ * find out was to compare the converted page against the live one by eye.
29
+ * The importer knows at the moment it happens — this is how it says so.
30
+ */
31
+ onDrop?: (tag: string) => void;
32
+ /**
33
+ * Accessible description for a component we cannot render, if one can be
34
+ * found — used to leave a described placeholder instead of a hole.
35
+ *
36
+ * Cloudflare's diagrams are 478-line Astro components: inline SVG, a web
37
+ * component and CSS animation, with no image file anywhere. Nothing can
38
+ * convert that. But each carries an `aria-label` written by their docs team
39
+ * ("AI Search is a managed search primitive: you upload or connect your
40
+ * content…"), which IS the information the diagram conveys — and it was
41
+ * being thrown away with the markup.
42
+ */
43
+ describeComponent?: (tag: string) => string | undefined;
21
44
  }
22
45
  export declare function starlightToTree(source: string, options?: ParseOptions): TreeNode[];
package/dist/parse-mdx.js CHANGED
@@ -105,10 +105,21 @@ function applyInlineTransforms(source, adapter) {
105
105
  return result;
106
106
  }
107
107
  /**
108
- * Rewrite ~/assets/ Vite alias to relative path.
108
+ * Rewrite the `~/assets/` Vite alias to the `/_assets/` mount convention.
109
+ *
110
+ * It used to produce a bare relative `assets/…`, which resolves to nothing:
111
+ * the page lives at `/ai-search/get-started/workers`, so a relative
112
+ * `assets/images/x.png` is looked for under that directory, and no exporter
113
+ * ever put a file there. Every local image on a converted Starlight site was a
114
+ * 404 — the reference survived the conversion and the file did not.
115
+ *
116
+ * `/_assets/…` is the repo-wide convention (see `assetMounts` in
117
+ * format-astro's project emitter): a mounted directory's CONTENTS land in
118
+ * `public/_assets/`, and importers rewrite refs to match — the same thing the
119
+ * Docusaurus importer does with a `static/` dir.
109
120
  */
110
121
  function rewriteAssetPaths(source) {
111
- return source.replace(/~\/assets\//g, "assets/");
122
+ return source.replace(/~\/assets\//g, "/_assets/");
112
123
  }
113
124
  /**
114
125
  * Convert Starlight :::note / :::caution / :::tip / :::danger directives
@@ -149,7 +160,7 @@ function convertDirectives(source) {
149
160
  * Convert markdown-it tokens (including jsx_* tokens) to TreeNode[].
150
161
  * This replaces both segmentsToTree and tokensToTree from the old parser.
151
162
  */
152
- function tokensToTree(tokens, componentMap) {
163
+ function tokensToTree(tokens, componentMap, onDrop, describeComponent) {
153
164
  const tree = [];
154
165
  let i = 0;
155
166
  while (i < tokens.length) {
@@ -327,7 +338,7 @@ function tokensToTree(tokens, componentMap) {
327
338
  const children = [];
328
339
  i++;
329
340
  while (i < tokens.length && tokens[i].type !== "blockquote_close") {
330
- const nested = tokensToTree(tokens.slice(i, i + 1), componentMap);
341
+ const nested = tokensToTree(tokens.slice(i, i + 1), componentMap, onDrop, describeComponent);
331
342
  children.push(...nested);
332
343
  i++;
333
344
  }
@@ -339,7 +350,22 @@ function tokensToTree(tokens, componentMap) {
339
350
  // Check if this is a leftover JSX component that wasn't caught
340
351
  const trimmed = token.content.trim();
341
352
  if (trimmed.match(/^<[A-Z]/)) {
342
- // Likely a JSX component — skip it (already handled or unrecognized)
353
+ // A JSX component nothing claimed. Dropping it is the existing
354
+ // behaviour; reporting it is new — silence here is what let five
355
+ // diagrams disappear from a converted section unnoticed.
356
+ const tag = trimmed.match(/^<([A-Za-z0-9]+)/)?.[1] ?? "unknown";
357
+ const description = describeComponent?.(tag);
358
+ if (description) {
359
+ // Keep the MEANING even though the markup cannot travel.
360
+ tree.push({
361
+ type: "callout",
362
+ props: { variant: "note", title: "Diagram" },
363
+ children: [{ type: "paragraph", inline: [{ type: "text", text: description }] }],
364
+ });
365
+ }
366
+ else {
367
+ onDrop?.(tag);
368
+ }
343
369
  i++;
344
370
  continue;
345
371
  }
@@ -518,6 +544,8 @@ function groupConsecutiveCards(tree) {
518
544
  }
519
545
  export function starlightToTree(source, options) {
520
546
  const adapter = options?.adapter;
547
+ const onDrop = options?.onDrop;
548
+ const describeComponent = options?.describeComponent;
521
549
  const { components, selfClosing, map: componentMap } = buildComponentMap(adapter);
522
550
  // Strip frontmatter block before parsing. Without this, markdown-it sees
523
551
  // `---...---` as thematic-break + paragraph + thematic-break, which leaks the
@@ -548,7 +576,7 @@ export function starlightToTree(source, options) {
548
576
  // Parse into tokens (including jsx_* tokens)
549
577
  const tokens = md.parse(cleaned, {});
550
578
  // Convert tokens to tree
551
- const tree = tokensToTree(tokens, componentMap);
579
+ const tree = tokensToTree(tokens, componentMap, onDrop, describeComponent);
552
580
  const unwrapped = unwrapStepsOrderedList(tree);
553
581
  return groupConsecutiveCards(unwrapped);
554
582
  }
package/dist/parse.js CHANGED
@@ -140,11 +140,13 @@ function buildComponentMap(adapter) {
140
140
  function stripImports(source) {
141
141
  // Strip single-line imports: import { X } from "y";
142
142
  // Strip multi-line imports: import {\n X,\n Y,\n} from "y";
143
- // Rewrite ~/assets/ Vite alias to relative assets/ path
143
+ // Rewrite the ~/assets/ Vite alias to the `/_assets/` mount convention —
144
+ // see the note on rewriteAssetPaths in parse-mdx.ts. A bare relative
145
+ // `assets/…` resolved against the page's own directory and 404'd.
144
146
  return source
145
147
  .replace(/^import\s+\{[^}]*\}\s+from\s+["'][^"']*["']\s*;?\s*$/gm, "")
146
148
  .replace(/^import\s+\{[\s\S]*?\}\s+from\s+["'][^"']*["']\s*;?\s*$/gm, "")
147
- .replace(/~\/assets\//g, "assets/")
149
+ .replace(/~\/assets\//g, "/_assets/")
148
150
  .replace(/\{\/\*[\s\S]*?\*\/\}/g, "")
149
151
  .replace(/\n{3,}/g, "\n\n");
150
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/format-starlight",
3
- "version": "0.2.0-beta.10",
3
+ "version": "0.2.0-beta.102",
4
4
  "description": "Starlight MDX importer for Dogsbay — converts Starlight sources (with JSX components, Render partials, and adapter-specific extensions) into TreeNode[]",
5
5
  "type": "module",
6
6
  "exports": {
@@ -15,16 +15,16 @@
15
15
  "@iarna/toml": "^2.2.5",
16
16
  "gray-matter": "^4.0.3",
17
17
  "jsonc-parser": "^3.3.1",
18
- "markdown-it": "^14.1.0",
19
- "@dogsbay/markdown-it-mdx-jsx": "0.2.0-beta.10",
20
- "@dogsbay/types": "0.2.0-beta.10"
18
+ "markdown-it": "^14.3.0",
19
+ "@dogsbay/markdown-it-mdx-jsx": "0.2.0-beta.102",
20
+ "@dogsbay/types": "0.2.0-beta.102"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/iarna__toml": "^2.0.5",
24
24
  "@types/markdown-it": "^14.1.2",
25
- "@types/node": "^22.0.0",
26
- "typescript": "^5.8.0",
27
- "vitest": "^3.2.0"
25
+ "@types/node": "^22.20.1",
26
+ "typescript": "^5.9.3",
27
+ "vitest": "^4.1.10"
28
28
  },
29
29
  "license": "MIT",
30
30
  "repository": {