@clarify-labs/cli 0.10.0 → 0.10.2

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.
@@ -27,14 +27,20 @@ let _tailwindcss_vite = require("@tailwindcss/vite");
27
27
  _tailwindcss_vite = __toESM(_tailwindcss_vite, 1);
28
28
  let _vitejs_plugin_react = require("@vitejs/plugin-react");
29
29
  _vitejs_plugin_react = __toESM(_vitejs_plugin_react, 1);
30
+ let rehype_raw = require("rehype-raw");
31
+ rehype_raw = __toESM(rehype_raw, 1);
30
32
  let _mdx_js_mdx = require("@mdx-js/mdx");
31
- let github_slugger = require("github-slugger");
32
- github_slugger = __toESM(github_slugger, 1);
33
- let mdast_util_to_string = require("mdast-util-to-string");
33
+ let _shikijs_rehype = require("@shikijs/rehype");
34
+ _shikijs_rehype = __toESM(_shikijs_rehype, 1);
35
+ let rehype_slug = require("rehype-slug");
36
+ rehype_slug = __toESM(rehype_slug, 1);
34
37
  let shiki = require("shiki");
35
38
  let unist_util_visit = require("unist-util-visit");
36
39
  let _clarify_labs_renderer = require("@clarify-labs/renderer");
37
40
  let node_fs_promises = require("node:fs/promises");
41
+ let github_slugger = require("github-slugger");
42
+ github_slugger = __toESM(github_slugger, 1);
43
+ let mdast_util_to_string = require("mdast-util-to-string");
38
44
  let remark = require("remark");
39
45
  let gray_matter = require("gray-matter");
40
46
  gray_matter = __toESM(gray_matter, 1);
@@ -47,38 +53,11 @@ let _apidevtools_swagger_parser = require("@apidevtools/swagger-parser");
47
53
  _apidevtools_swagger_parser = __toESM(_apidevtools_swagger_parser, 1);
48
54
  let node_os = require("node:os");
49
55
  let node_module = require("node:module");
50
- //#region source/parsers/markdown/utils.ts
51
- function escapeHtml(text) {
52
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
53
- }
54
- //#endregion
55
56
  //#region source/parsers/markdown/mdx.ts
56
57
  function getLanguage(className) {
57
58
  return (Array.isArray(className) ? className : typeof className === "string" ? className.split(/\s+/) : []).find((item) => typeof item === "string" && item.startsWith("language-"))?.replace(/^language-/, "") || "txt";
58
59
  }
59
- function getTextContent(node) {
60
- if (!node) return "";
61
- if (typeof node.value === "string") return node.value;
62
- return node.children?.map(getTextContent).join("") ?? "";
63
- }
64
- function renderPlainCode(code) {
65
- return code.split("\n").map((line) => `<span>${escapeHtml(line)}</span>`).join("\n");
66
- }
67
- function isBundledLanguage(language) {
68
- return language in shiki.bundledLanguages;
69
- }
70
- var highlighter;
71
60
  var cssVariablesTheme = (0, shiki.createCssVariablesTheme)();
72
- async function getHighlighter() {
73
- highlighter = highlighter ?? await (0, shiki.createHighlighter)({
74
- themes: [cssVariablesTheme],
75
- langs: []
76
- });
77
- return highlighter;
78
- }
79
- function renderHighlightedCode(html) {
80
- return (/<code[^>]*>([\s\S]*?)<\/code>/.exec(html)?.[1] ?? html).split("\n").map((line) => line.trimStart().startsWith("<span") ? line : `<span>${line}</span>`).join("\n");
81
- }
82
61
  function rehypeParseCodeBlocks() {
83
62
  return (tree) => {
84
63
  (0, unist_util_visit.visit)(tree, "element", (node, _nodeIndex, parentNode) => {
@@ -96,61 +75,18 @@ function rehypeParseCodeBlocks() {
96
75
  });
97
76
  };
98
77
  }
99
- function rehypeShiki() {
100
- return async (tree) => {
101
- const shikiHighlighter = await getHighlighter();
102
- const languages = /* @__PURE__ */ new Set();
103
- (0, unist_util_visit.visit)(tree, "element", (node) => {
104
- if (node.tagName !== "pre") return;
105
- const codeNode = node.children?.[0];
106
- if (codeNode?.tagName !== "code") return;
107
- const language = typeof node.properties?.language === "string" ? node.properties.language : getLanguage(codeNode.properties?.className);
108
- if (language && language !== "txt" && isBundledLanguage(language)) languages.add(language);
109
- });
110
- if (languages.size > 0) await shikiHighlighter.loadLanguage(...languages);
111
- (0, unist_util_visit.visit)(tree, "element", (node) => {
112
- if (node.tagName !== "pre") return;
113
- const codeNode = node.children?.[0];
114
- if (codeNode?.tagName !== "code") return;
115
- node.properties = node.properties ?? {};
116
- codeNode.properties = codeNode.properties ?? {};
117
- const code = getTextContent(codeNode);
118
- const language = typeof node.properties.language === "string" ? node.properties.language : getLanguage(codeNode.properties.className);
119
- node.properties.code = code;
120
- codeNode.properties.code = code;
121
- codeNode.properties.language = language;
122
- let highlighted = renderPlainCode(code);
123
- if (language && language !== "txt") try {
124
- highlighted = renderHighlightedCode(shikiHighlighter.codeToHtml(code, {
125
- lang: language,
126
- theme: cssVariablesTheme
127
- }));
128
- } catch {
129
- highlighted = renderPlainCode(code);
130
- }
131
- codeNode.children = [{
132
- type: "text",
133
- value: highlighted
134
- }];
135
- });
136
- };
137
- }
138
- function rehypeSlugSections() {
139
- return (tree) => {
140
- const slugger = new github_slugger.default();
141
- (0, unist_util_visit.visit)(tree, "element", (node) => {
142
- if (node.tagName !== "h2" && node.tagName !== "h3") return;
143
- node.properties = node.properties ?? {};
144
- if (node.properties.id) return;
145
- node.properties.id = slugger.slug((0, mdast_util_to_string.toString)(node));
146
- });
147
- };
148
- }
149
78
  var remarkPlugins = _clarify_labs_renderer.markdownRemarkPlugins;
150
79
  var rehypePlugins = [
151
- rehypeSlugSections,
80
+ rehype_slug.default,
152
81
  rehypeParseCodeBlocks,
153
- rehypeShiki
82
+ [_shikijs_rehype.default, {
83
+ theme: cssVariablesTheme,
84
+ langs: [],
85
+ lazy: true,
86
+ addLanguageClass: true,
87
+ fallbackLanguage: "text",
88
+ parseMetaString: _clarify_labs_renderer.parseCodeMeta
89
+ }]
154
90
  ];
155
91
  function formatMdxDiagnostic(error) {
156
92
  if (error instanceof Error) return error.message;
@@ -162,7 +98,8 @@ function formatMdxDiagnostic(error) {
162
98
  *
163
99
  * `rehypePlugins` includes `rehypeShiki`, which loads grammar/WASM assets and
164
100
  * is expensive. The real highlighting happens at Vite build time via the
165
- * `@mdx-js/rollup` plugin (see `createMdxPlugin` in `core/adapters.ts`), which
101
+ * `@mdx-js/rollup` plugins (see `createMarkdownPlugin` and `createMdxPlugin`
102
+ * in `core/adapters.ts`), which
166
103
  * runs the full remark + rehype pipeline. Re-running it here, during route
167
104
  * discovery (Phase 3), would compile every `.mdx` file twice and invoke Shiki
168
105
  * once per file per `engine.refresh()` — see ARCHITECTURE.md §2.2 which states
@@ -311,6 +248,11 @@ function kebabToTitle(str) {
311
248
  function parseMdxTree(content) {
312
249
  return remark.remark.parse(content);
313
250
  }
251
+ async function compileRouteDiagnostic(content, filePath, baseDir) {
252
+ if ((0, node_path.extname)(filePath).toLowerCase() !== ".mdx") return void 0;
253
+ const result = await compileMdxContent(content, filePath, baseDir);
254
+ return result.ok ? void 0 : result.diagnostic;
255
+ }
314
256
  /** 从内容中提取第一个 H1 标题 */
315
257
  function extractH1(content) {
316
258
  const tree = parseMdxTree(content);
@@ -336,12 +278,12 @@ function extractMdxSections(content) {
336
278
  return sections;
337
279
  }
338
280
  function navigationSections(sections) {
339
- return sections.map((s) => ({
340
- id: s.id,
341
- title: s.title,
342
- level: s.level,
343
- badge: s.badge,
344
- tags: s.tags
281
+ return sections.map((section) => ({
282
+ id: section.id,
283
+ title: section.title,
284
+ level: section.level,
285
+ badge: section.badge,
286
+ tags: section.tags
345
287
  }));
346
288
  }
347
289
  function normalizePath(path) {
@@ -405,7 +347,7 @@ async function findContentRoutes(dir, base = dir, options = {}) {
405
347
  frontmatter,
406
348
  content
407
349
  };
408
- const mdxResult = await compileMdxContent(page.content, fullPath, base);
350
+ const diagnostic = await compileRouteDiagnostic(page.content, fullPath, base);
409
351
  let title = typeof page.frontmatter.title === "string" ? page.frontmatter.title : "";
410
352
  if (!title) {
411
353
  const lastPart = pathParts[pathParts.length - 1] ?? "";
@@ -413,17 +355,21 @@ async function findContentRoutes(dir, base = dir, options = {}) {
413
355
  }
414
356
  routes.push({
415
357
  kind: "mdx",
416
- title,
417
358
  path: cleanPath,
418
359
  basePath: cleanPath,
419
- filePath: fullPath,
420
- virtualModuleId: "virtual:clarify-page/" + relativePath.replace(/\.mdx?$/, "").replace(/\/+/g, "/"),
421
- description: typeof page.frontmatter.description === "string" ? page.frontmatter.description : void 0,
422
- keywords: frontmatterKeywords(page.frontmatter),
423
- frontmatter: page.frontmatter,
424
- content: page.content,
425
- sections: extractMdxSections(page.content),
426
- diagnostic: mdxResult.ok ? void 0 : mdxResult.diagnostic
360
+ meta: {
361
+ title,
362
+ description: typeof page.frontmatter.description === "string" ? page.frontmatter.description : void 0,
363
+ keywords: frontmatterKeywords(page.frontmatter),
364
+ sections: extractMdxSections(page.content)
365
+ },
366
+ module: { virtualModuleId: "virtual:clarify-page/" + relativePath.replace(/\.mdx?$/, "").replace(/\/+/g, "/") },
367
+ source: {
368
+ filePath: fullPath,
369
+ frontmatter: page.frontmatter,
370
+ content: page.content
371
+ },
372
+ diagnostic
427
373
  });
428
374
  }
429
375
  }
@@ -482,7 +428,7 @@ async function findLocalizedContentRoutes(contentRoot, i18n, options = {}) {
482
428
  path: localizedRoutePath(basePath, locale.code, i18n),
483
429
  basePath,
484
430
  locale: locale.code,
485
- virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.filePath)
431
+ module: { virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.source.filePath) }
486
432
  });
487
433
  }
488
434
  }
@@ -518,12 +464,12 @@ function buildNavigation(routes) {
518
464
  if (!node) {
519
465
  node = {
520
466
  path: pathSoFar,
521
- title: i === parts.length - 1 ? route.title : kebabToTitle(parts[i]),
467
+ title: i === parts.length - 1 ? route.meta.title : kebabToTitle(parts[i]),
522
468
  children: []
523
469
  };
524
470
  current.push(node);
525
471
  }
526
- if (i === parts.length - 1 && route.sections) node.sections = navigationSections(route.sections);
472
+ if (i === parts.length - 1 && route.meta.sections) node.sections = navigationSections(route.meta.sections);
527
473
  if (i < parts.length - 1) {
528
474
  node.children = node.children ?? [];
529
475
  current = node.children;
@@ -532,17 +478,22 @@ function buildNavigation(routes) {
532
478
  }
533
479
  return root;
534
480
  }
535
- function resolvePageItem(item) {
536
- if (typeof item === "string") return { pageRef: item };
481
+ function routeIntentFromPagesItem(item) {
482
+ if (typeof item === "string") return {
483
+ kind: "page",
484
+ ref: item
485
+ };
537
486
  if ("openapi" in item) return {
538
- openapiRef: item.openapi,
539
- openapiTagFilter: item.filter?.tags,
487
+ kind: "openapi",
488
+ ref: item.openapi,
489
+ tagFilter: item.filter?.tags,
540
490
  path: item.path,
541
491
  title: item.title,
542
492
  icon: item.icon
543
493
  };
544
494
  return {
545
- pageRef: item.page,
495
+ kind: "page",
496
+ ref: item.page,
546
497
  path: item.path,
547
498
  redirect: item.redirect,
548
499
  title: item.title,
@@ -557,25 +508,28 @@ function firstNavigationPath(nodes) {
557
508
  }
558
509
  return "/";
559
510
  }
560
- function collectExplicitPagePathItems(tabs) {
561
- const items = [];
511
+ function collectExplicitPagePathIntents(tabs) {
512
+ const intents = [];
562
513
  for (const tab of tabs ?? []) {
563
514
  if (!tab.pages || tab.pages === "FileTree") continue;
564
- for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) items.push({
565
- pageRef: item.page,
566
- path: item.path
567
- });
515
+ for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) {
516
+ const intent = routeIntentFromPagesItem(item);
517
+ if (intent.kind === "page" && intent.path) intents.push({
518
+ ...intent,
519
+ path: intent.path
520
+ });
521
+ }
568
522
  }
569
- return items;
523
+ return intents;
570
524
  }
571
525
  function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
572
- const pageItems = collectExplicitPagePathItems(tabs);
573
- if (!pageItems.length) return routes;
526
+ const pageIntents = collectExplicitPagePathIntents(tabs);
527
+ if (!pageIntents.length) return routes;
574
528
  const additions = [];
575
529
  const existingKeys = new Set(routes.map((route) => `${route.locale ?? ""}:${route.basePath ?? route.path}`));
576
- for (const item of pageItems) {
577
- const sourceBasePath = routePathFromRef(item.pageRef);
578
- const targetBasePath = routePathFromRef(item.pageRef, item.path);
530
+ for (const intent of pageIntents) {
531
+ const sourceBasePath = routePathFromRef(intent.ref);
532
+ const targetBasePath = routePathFromRef(intent.ref, intent.path);
579
533
  if (sourceBasePath === targetBasePath) continue;
580
534
  for (const route of routes) {
581
535
  if (route.kind !== "mdx" || (route.basePath ?? route.path) !== sourceBasePath) continue;
@@ -599,40 +553,42 @@ function buildNavigationFromPagesConfig(routes, config) {
599
553
  return buildNavigationFromConfig(routes, config);
600
554
  }
601
555
  function buildNavigationFromTabsConfig(routes, tabs) {
602
- return { tabs: tabs.map((tab) => {
603
- const children = buildNavigationFromPagesConfig(routes, tab.pages);
604
- return {
605
- type: "tab",
606
- path: firstNavigationPath(children),
607
- title: resolveLocalizedText(tab.tab, "", "") ?? "",
608
- icon: tab.icon,
609
- children
610
- };
611
- }) };
556
+ return {
557
+ kind: "tabbed",
558
+ tabs: tabs.map((tab) => {
559
+ const children = buildNavigationFromPagesConfig(routes, tab.pages);
560
+ return {
561
+ type: "tab",
562
+ path: firstNavigationPath(children),
563
+ title: resolveLocalizedText(tab.tab, "", "") ?? "",
564
+ icon: tab.icon,
565
+ children
566
+ };
567
+ })
568
+ };
612
569
  }
613
570
  function buildNavigationFromConfig(routes, config) {
614
571
  const routeMap = new Map(routes.map((r) => [r.path, r]));
615
572
  return config.map((group) => {
616
573
  const children = group.pages.map((item) => {
617
- const { pageRef, openapiRef, openapiTagFilter, path: explicitPath, redirect, title, icon } = resolvePageItem(item);
618
- if (openapiRef) {
619
- const path = openAPIPagePathFromRef(openapiRef, openapiTagFilter, explicitPath);
574
+ const intent = routeIntentFromPagesItem(item);
575
+ if (intent.kind === "openapi") {
576
+ const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
620
577
  const route = routeMap.get(path);
621
578
  return {
622
579
  path,
623
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? openapiRef),
624
- icon,
625
- sections: route?.sections ? navigationSections(route.sections) : void 0
580
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
581
+ icon: intent.icon,
582
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
626
583
  };
627
584
  }
628
- const ref = pageRef ?? "";
629
- const path = routePathFromRef(ref, explicitPath);
585
+ const path = routePathFromRef(intent.ref, intent.path);
630
586
  const route = routeMap.get(path);
631
587
  return {
632
- path: redirect ? normalizePath(redirect) : path,
633
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? ref),
634
- icon,
635
- sections: route?.sections ? navigationSections(route.sections) : void 0
588
+ path: intent.redirect ? normalizePath(intent.redirect) : path,
589
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
590
+ icon: intent.icon,
591
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
636
592
  };
637
593
  });
638
594
  return {
@@ -658,16 +614,15 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
658
614
  const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
659
615
  return config.map((group) => {
660
616
  const children = group.pages.map((item) => {
661
- const { pageRef, openapiRef, openapiTagFilter, path: explicitPath, redirect, title, icon } = resolvePageItem(item);
662
- const ref = openapiRef ?? pageRef ?? "";
663
- const basePath = openapiRef ? openAPIPagePathFromRef(openapiRef, openapiTagFilter, explicitPath) : routePathFromRef(ref, explicitPath);
617
+ const intent = routeIntentFromPagesItem(item);
618
+ const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
664
619
  const route = routeMap.get(basePath);
665
620
  const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
666
621
  return {
667
- path: (redirect ? localizedRoutePath(routePathFromRef(redirect), locale, i18n) : void 0) ?? path,
668
- title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle(basePath.split("/").pop() ?? ref),
669
- icon,
670
- sections: route?.sections ? navigationSections(route.sections) : void 0
622
+ path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, i18n) : void 0) ?? path,
623
+ title: resolveLocalizedText(intent.title, locale, i18n.defaultLocale) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
624
+ icon: intent.icon,
625
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
671
626
  };
672
627
  });
673
628
  return {
@@ -680,10 +635,10 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
680
635
  }
681
636
  function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
682
637
  if (!i18n) return void 0;
683
- const result = {};
638
+ const locales = {};
684
639
  for (const locale of i18n.locales) {
685
640
  const localeRoutes = routes.filter((route) => route.locale === locale.code);
686
- result[locale.code] = { tabs: tabs.map((tab) => {
641
+ locales[locale.code] = { tabs: tabs.map((tab) => {
687
642
  const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code, i18n);
688
643
  return {
689
644
  type: "tab",
@@ -694,17 +649,20 @@ function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
694
649
  };
695
650
  }) };
696
651
  }
697
- return result;
652
+ return {
653
+ kind: "localized-tabbed",
654
+ locales
655
+ };
698
656
  }
699
657
  /** Serialize a ContentRoute into the minimal shape exposed to tooling. */
700
658
  function toDevRouteEntry(route) {
701
659
  return {
702
660
  path: route.path,
703
- filePath: route.filePath,
661
+ filePath: route.source.filePath,
704
662
  locale: route.locale,
705
663
  basePath: route.basePath,
706
664
  kind: route.kind,
707
- title: route.title
665
+ title: route.meta.title
708
666
  };
709
667
  }
710
668
  /**
@@ -718,7 +676,7 @@ function toDevRouteEntry(route) {
718
676
  * or the first match.
719
677
  */
720
678
  function resolveRouteForFile(filePath, routes, defaultLocale) {
721
- const matches = routes.filter((r) => r.filePath === filePath);
679
+ const matches = routes.filter((r) => r.source.filePath === filePath);
722
680
  if (matches.length === 0) return void 0;
723
681
  if (matches.length === 1) return matches[0];
724
682
  const fileLocale = inferLocaleFromPath(filePath);
@@ -1143,6 +1101,7 @@ var VIRTUAL_CONFIG = "virtual:clarify/config";
1143
1101
  var VIRTUAL_ROUTES = "virtual:clarify/routes";
1144
1102
  var VIRTUAL_SERVER_ROUTES = "virtual:clarify/routes/server";
1145
1103
  var VIRTUAL_OPENAPI = "virtual:clarify/openapi";
1104
+ var VIRTUAL_OPENAPI_SERVER = "virtual:clarify/openapi/server";
1146
1105
  var VIRTUAL_SLOTS = "virtual:clarify/slots";
1147
1106
  var VIRTUAL_SLOT = "virtual:clarify/slot";
1148
1107
  var VIRTUAL_CLIENT_ENTRY = "virtual:clarify/entry-client";
@@ -1163,6 +1122,7 @@ var FIXED_VIRTUAL_IDS = [
1163
1122
  VIRTUAL_ROUTES,
1164
1123
  VIRTUAL_SERVER_ROUTES,
1165
1124
  VIRTUAL_OPENAPI,
1125
+ VIRTUAL_OPENAPI_SERVER,
1166
1126
  VIRTUAL_SLOTS,
1167
1127
  VIRTUAL_SLOT
1168
1128
  ];
@@ -1179,8 +1139,8 @@ function resolveVirtualModuleId(id, modules, routes) {
1179
1139
  for (const virtualId of FIXED_VIRTUAL_IDS) if (id === virtualId || id === resolveVirtualId(virtualId)) return resolveVirtualId(virtualId);
1180
1140
  const moduleId = stripVirtualPrefix(id);
1181
1141
  if (modules.has(moduleId)) return resolveVirtualId(moduleId);
1182
- const route = routes.find((route) => route.virtualModuleId === id || route.virtualModuleId === moduleId);
1183
- if (route) return resolveVirtualId(route.virtualModuleId);
1142
+ const route = routes.find((route) => route.module.virtualModuleId === id || route.module.virtualModuleId === moduleId);
1143
+ if (route) return resolveVirtualId(route.module.virtualModuleId);
1184
1144
  return null;
1185
1145
  }
1186
1146
  function generateConfigModule(projectConfig, buildOptions, version) {
@@ -1194,38 +1154,38 @@ function generateConfigModule(projectConfig, buildOptions, version) {
1194
1154
  function moduleSpecifier(value) {
1195
1155
  return JSON.stringify(value);
1196
1156
  }
1197
- function routeToRuntimeObject(route, component, mode) {
1198
- const obj = {
1157
+ function routeToRuntimeManifestEntry(route, component, mode) {
1158
+ const entry = {
1199
1159
  path: route.path,
1200
- title: route.title,
1160
+ title: route.meta.title,
1201
1161
  component,
1202
1162
  kind: route.kind
1203
1163
  };
1204
- if (mode === "client") obj.lazy = true;
1205
- if (route.basePath) obj.basePath = route.basePath;
1206
- if (route.locale) obj.locale = route.locale;
1207
- if (route.isFallback) obj.isFallback = true;
1208
- if (route.isBareAlias) obj.isBareAlias = true;
1209
- if (route.alternates) obj.alternates = route.alternates;
1210
- if (route.description) obj.description = route.description;
1211
- if (route.keywords?.length) obj.keywords = route.keywords;
1212
- if (route.contentArtifactUrl) obj.contentArtifactUrl = route.contentArtifactUrl;
1213
- if (route.sourceUrl) obj.sourceUrl = route.sourceUrl;
1214
- if (route.sections?.length) obj.sections = route.sections.map((s) => ({
1215
- id: s.id,
1216
- title: s.title,
1217
- level: s.level,
1218
- badge: s.badge,
1219
- tags: s.tags
1164
+ if (mode === "client") entry.lazy = true;
1165
+ if (route.basePath) entry.basePath = route.basePath;
1166
+ if (route.locale) entry.locale = route.locale;
1167
+ if (route.isFallback) entry.isFallback = true;
1168
+ if (route.isBareAlias) entry.isBareAlias = true;
1169
+ if (route.alternates) entry.alternates = route.alternates;
1170
+ if (route.meta.description) entry.description = route.meta.description;
1171
+ if (route.meta.keywords?.length) entry.keywords = route.meta.keywords;
1172
+ if (route.artifacts?.contentArtifactUrl) entry.contentArtifactUrl = route.artifacts.contentArtifactUrl;
1173
+ if (route.source?.sourceEditUrl) entry.sourceEditUrl = route.source.sourceEditUrl;
1174
+ if (route.meta.sections?.length) entry.sections = route.meta.sections.map((section) => ({
1175
+ id: section.id,
1176
+ title: section.title,
1177
+ level: section.level,
1178
+ badge: section.badge,
1179
+ tags: section.tags
1220
1180
  }));
1221
- return obj;
1181
+ return entry;
1222
1182
  }
1223
1183
  function generateRoutesModule(routes, navigation, mode = "client") {
1224
- return `${mode === "server" ? routes.map((r, i) => `import Page${i} from ${moduleSpecifier(r.virtualModuleId)};`).join("\n") : `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';`}\n\nexport const routes = [\n${routes.map((r, i) => {
1225
- const component = mode === "server" ? `Page${i}` : `() => import(${moduleSpecifier(r.virtualModuleId)}).catch((error) => Promise.resolve({ default: createContentDiagnosticComponent({ kind: 'route-load', title: 'This page could not be loaded', message: error instanceof Error ? error.message : String(error), details: error instanceof Error ? error.stack : undefined }) }))`;
1184
+ return `${mode === "server" ? routes.map((r, i) => `import Page${i} from ${moduleSpecifier(r.module.virtualModuleId)};`).join("\n") : `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';`}\n\nexport const routes = [\n${routes.map((r, i) => {
1185
+ const component = mode === "server" ? `Page${i}` : `() => import(${moduleSpecifier(r.module.virtualModuleId)}).catch((error) => Promise.resolve({ default: createContentDiagnosticComponent({ kind: 'route-load', title: 'This page could not be loaded', message: error instanceof Error ? error.message : String(error), details: error instanceof Error ? error.stack : undefined }) }))`;
1226
1186
  const placeholder = `__COMPONENT_${i}__`;
1227
- const obj = routeToRuntimeObject(r, placeholder, mode);
1228
- return ` ${JSON.stringify(obj, null, 0).replace(`"${placeholder}"`, component).replace(/"([A-Za-z_$][\w$]*)":/g, "$1: ")}`;
1187
+ const manifestEntry = routeToRuntimeManifestEntry(r, placeholder, mode);
1188
+ return ` ${JSON.stringify(manifestEntry, null, 0).replace(`"${placeholder}"`, component).replace(/"([A-Za-z_$][\w$]*)":/g, "$1: ")}`;
1229
1189
  }).join(",\n")}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
1230
1190
  }
1231
1191
  function createClientEntryModule(options = {}) {
@@ -1234,9 +1194,15 @@ import '@clarify-labs/renderer/style.css';
1234
1194
  import { render } from '@clarify-labs/renderer/client';
1235
1195
  import { routes, navigation } from '${VIRTUAL_ROUTES}';
1236
1196
  import { config } from '${VIRTUAL_CONFIG}';
1237
- import { openApis } from '${VIRTUAL_OPENAPI}';
1197
+ import { openApiSpecs } from '${VIRTUAL_OPENAPI}';
1238
1198
  import { runtimeSlots } from '${VIRTUAL_SLOTS}';
1239
- render({ config, routes, navigation, openApis, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
1199
+ const renderOptions = { config, routes, navigation, openApiSpecs, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} };
1200
+ render(renderOptions);
1201
+ if (import.meta.hot) {
1202
+ import.meta.hot.accept('${VIRTUAL_ROUTES}', (mod) => {
1203
+ render({ ...renderOptions, routes: mod.routes, navigation: mod.navigation });
1204
+ });
1205
+ }`;
1240
1206
  }
1241
1207
  /**
1242
1208
  * Collect every plugin's `slots` declarations into a single runtime registry
@@ -1265,7 +1231,7 @@ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
1265
1231
  }
1266
1232
  if (!slot.component.startsWith("/")) throw new Error(`[clarify] Plugin "${plugin}" slot "${slot.name}" has an invalid component path "${slot.component}". Component paths must start with "/" to reference the project root.`);
1267
1233
  const componentPath = root + slot.component;
1268
- const entry = `{ plugin: ${JSON.stringify(plugin)}, component: () => import(${moduleSpecifier(componentPath)}) }`;
1234
+ const entry = `{ plugin: ${JSON.stringify(plugin)}, loadComponent: () => import(${moduleSpecifier(componentPath)}) }`;
1269
1235
  const list = grouped.get(slot.name) ?? [];
1270
1236
  list.push(entry);
1271
1237
  grouped.set(slot.name, list);
@@ -1294,18 +1260,34 @@ function buildVirtualModules(args) {
1294
1260
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, "client"));
1295
1261
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, "server"));
1296
1262
  modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
1297
- modules.set(VIRTUAL_OPENAPI, "export const openApis = {};");
1263
+ modules.set(VIRTUAL_OPENAPI, "export const openApiSpecs = {};");
1298
1264
  modules.set(VIRTUAL_SLOT, "export { useSlot } from '@clarify-labs/renderer';\n");
1299
1265
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1300
1266
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1301
1267
  for (const route of args.routes) {
1302
- const moduleContent = route.diagnostic ? generateMdxErrorModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.filePath)};`;
1303
- modules.set(route.virtualModuleId, moduleContent);
1268
+ const moduleContent = route.diagnostic ? generateMdxErrorModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1269
+ modules.set(route.module.virtualModuleId, moduleContent);
1304
1270
  }
1305
1271
  return modules;
1306
1272
  }
1307
1273
  //#endregion
1308
1274
  //#region source/core/adapters.ts
1275
+ function routeStructure(route) {
1276
+ const { content: _content, ...source } = route.source;
1277
+ return {
1278
+ ...route,
1279
+ source
1280
+ };
1281
+ }
1282
+ function createDevStructureSnapshot(routes, navigation) {
1283
+ return {
1284
+ routes: JSON.stringify(routes.map(routeStructure)),
1285
+ navigation: JSON.stringify(navigation)
1286
+ };
1287
+ }
1288
+ function hasDevStructureChanged(before, after) {
1289
+ return before.routes !== after.routes || before.navigation !== after.navigation;
1290
+ }
1309
1291
  function invalidateVirtualModules(engine, server) {
1310
1292
  for (const id of engine.modules.keys()) {
1311
1293
  const moduleNode = server.moduleGraph.getModuleById(resolveVirtualId(id));
@@ -1313,20 +1295,22 @@ function invalidateVirtualModules(engine, server) {
1313
1295
  }
1314
1296
  }
1315
1297
  async function refreshDevServer(engine, server) {
1298
+ const before = createDevStructureSnapshot(engine.routes, engine.navigation);
1316
1299
  await engine.refresh();
1317
1300
  invalidateVirtualModules(engine, server);
1301
+ return hasDevStructureChanged(before, createDevStructureSnapshot(engine.routes, engine.navigation));
1318
1302
  }
1319
- function createNormalizedMdxContentPlugin(engine) {
1303
+ function createNormalizedContentPlugin(engine) {
1320
1304
  return {
1321
- name: "clarify:normalized-mdx-content",
1305
+ name: "clarify:normalized-content",
1322
1306
  enforce: "pre",
1323
1307
  transform(_code, id) {
1324
1308
  if (!/\.mdx?(?:\?|$)/.test(id)) return null;
1325
1309
  const filePath = id.replace(/\?.*$/, "");
1326
- const route = engine.routes.find((route) => route.kind === "mdx" && route.filePath === filePath);
1327
- if (!route || route.content === void 0) return null;
1310
+ const route = engine.routes.find((route) => route.kind === "mdx" && route.source.filePath === filePath);
1311
+ if (!route || route.source.content === void 0) return null;
1328
1312
  return {
1329
- code: route.content,
1313
+ code: route.source.content,
1330
1314
  map: null
1331
1315
  };
1332
1316
  }
@@ -1334,14 +1318,26 @@ function createNormalizedMdxContentPlugin(engine) {
1334
1318
  }
1335
1319
  function createMdxPlugin() {
1336
1320
  return (0, _mdx_js_rollup.default)({
1337
- include: ["**/*.{md,mdx}"],
1321
+ include: ["**/*.mdx"],
1322
+ format: "mdx",
1338
1323
  jsxImportSource: "react",
1339
1324
  providerImportSource: "@clarify-labs/renderer",
1340
1325
  remarkPlugins,
1341
1326
  rehypePlugins
1342
1327
  });
1343
1328
  }
1344
- function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1329
+ function createMarkdownPlugin() {
1330
+ return (0, _mdx_js_rollup.default)({
1331
+ include: ["**/*.md"],
1332
+ format: "md",
1333
+ jsxImportSource: "react",
1334
+ providerImportSource: "@clarify-labs/renderer",
1335
+ remarkPlugins,
1336
+ remarkRehypeOptions: { allowDangerousHtml: true },
1337
+ rehypePlugins: [rehype_raw.default, ...rehypePlugins]
1338
+ });
1339
+ }
1340
+ function createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx) {
1345
1341
  let viteConfig;
1346
1342
  return {
1347
1343
  name: "clarify:core",
@@ -1380,8 +1376,13 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1380
1376
  }
1381
1377
  const relativeContentFile = (0, node_path.relative)(engine.contentRoot, changedFile);
1382
1378
  if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !(0, node_path.isAbsolute)(relativeContentFile)) || !engine.hasContentRouteForFile(changedFile)) return;
1383
- await refreshDevServer(engine, ctx.server);
1384
- return [];
1379
+ if (await refreshDevServer(engine, ctx.server)) {
1380
+ const routesModule = ctx.server.moduleGraph.getModuleById(resolveVirtualId(VIRTUAL_ROUTES));
1381
+ if (routesModule) return [...ctx.modules, routesModule];
1382
+ ctx.server.ws.send({ type: "full-reload" });
1383
+ return [];
1384
+ }
1385
+ return ctx.modules;
1385
1386
  },
1386
1387
  resolveId(id) {
1387
1388
  return resolveVirtualModuleId(id, engine.modules, engine.routes);
@@ -1392,18 +1393,6 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1392
1393
  async configureServer(server) {
1393
1394
  server.watcher.add(engine.contentRoot);
1394
1395
  if (engine.configFilePath) server.watcher.add(engine.configFilePath);
1395
- let reloadQueued = false;
1396
- const invalidateAndReload = () => {
1397
- if (reloadQueued) return;
1398
- reloadQueued = true;
1399
- queueMicrotask(() => {
1400
- reloadQueued = false;
1401
- invalidateVirtualModules(engine, server);
1402
- server.ws.send({ type: "full-reload" });
1403
- });
1404
- };
1405
- engine.ctx.onRoutesChange(invalidateAndReload);
1406
- engine.ctx.onNavigationChange(invalidateAndReload);
1407
1396
  server.middlewares.use((req, res, next) => {
1408
1397
  if (req.url === "/dev/query-preview-route" && req.method === "POST") return handleDevRouteRequest(req, res, engine.routes, engine.runtimeContext());
1409
1398
  if (req.url === "/dev/project-info" && (req.method === "GET" || req.method === "HEAD")) return handleProjectInfoRequest(req, res, engine.runtimeContext());
@@ -1413,7 +1402,7 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1413
1402
  const changedFile = (0, node_path.isAbsolute)(filePath) ? filePath : (0, node_path.join)(engine.root, filePath);
1414
1403
  const relativeContentFile = (0, node_path.relative)(engine.contentRoot, changedFile);
1415
1404
  if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !(0, node_path.isAbsolute)(relativeContentFile))) return;
1416
- await refreshDevServer(engine, server);
1405
+ if (await refreshDevServer(engine, server)) server.ws.send({ type: "full-reload" });
1417
1406
  };
1418
1407
  server.watcher.on("add", handleContentTreeChange);
1419
1408
  server.watcher.on("unlink", handleContentTreeChange);
@@ -1450,7 +1439,8 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1450
1439
  outputDirectory: viteConfig.build.outDir,
1451
1440
  ssrPlugins: [
1452
1441
  engine.createSSGVirtualPlugin(),
1453
- normalizedMdxContentPlugin,
1442
+ normalizedContentPlugin,
1443
+ markdown,
1454
1444
  mdx
1455
1445
  ]
1456
1446
  });
@@ -1467,20 +1457,22 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1467
1457
  * site itself - it only bridges Vite lifecycle hooks to the engine.
1468
1458
  */
1469
1459
  function createViteAdapter(engine) {
1470
- const normalizedMdxContentPlugin = createNormalizedMdxContentPlugin(engine);
1460
+ const normalizedContentPlugin = createNormalizedContentPlugin(engine);
1461
+ const markdown = createMarkdownPlugin();
1471
1462
  const mdx = createMdxPlugin();
1472
1463
  const tailwind = (0, _tailwindcss_vite.default)();
1473
1464
  return [
1474
1465
  ...(0, _vitejs_plugin_react.default)(),
1475
1466
  ...Array.isArray(tailwind) ? tailwind : [tailwind],
1476
- normalizedMdxContentPlugin,
1477
- createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx),
1467
+ normalizedContentPlugin,
1468
+ createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx),
1469
+ markdown,
1478
1470
  mdx
1479
1471
  ];
1480
1472
  }
1481
1473
  var cliPackageVersion = {
1482
1474
  name: "@clarify-labs/cli",
1483
- version: "0.10.0",
1475
+ version: "0.10.2",
1484
1476
  license: "AGPL-3.0-only",
1485
1477
  type: "module",
1486
1478
  repository: {
@@ -1521,6 +1513,7 @@ var cliPackageVersion = {
1521
1513
  "@clarify-labs/templates": "workspace:*",
1522
1514
  "@mdx-js/mdx": "^3.1.1",
1523
1515
  "@mdx-js/rollup": "^3.1.1",
1516
+ "@shikijs/rehype": "^4.3.0",
1524
1517
  "@tailwindcss/vite": "^4.3.2",
1525
1518
  "@vitejs/plugin-react": "^6.0.3",
1526
1519
  "cac": "^7.0.0",
@@ -1535,6 +1528,8 @@ var cliPackageVersion = {
1535
1528
  "react-router": "^7.0.0",
1536
1529
  "react-router-dom": "^7.0.0",
1537
1530
  "react-zoom-pan-pinch": "^4.0.3",
1531
+ "rehype-raw": "^7.0.0",
1532
+ "rehype-slug": "^6.0.0",
1538
1533
  "remark": "^15.0.1",
1539
1534
  "remark-gfm": "^4.0.1",
1540
1535
  "shiki": "4.3.0",
@@ -1586,11 +1581,14 @@ function routeToOpenAPIArtifactUrl(routePath) {
1586
1581
  return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi.json`;
1587
1582
  }
1588
1583
  function attachContentArtifactUrls(routes) {
1589
- for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path);
1584
+ for (const route of routes) route.artifacts = {
1585
+ ...route.artifacts,
1586
+ contentArtifactUrl: route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path)
1587
+ };
1590
1588
  }
1591
1589
  function readRouteContent(route) {
1592
- if (route.content !== void 0) return route.content;
1593
- throw new Error(`Route content is missing from route context: ${route.filePath}`);
1590
+ if (route.source.content !== void 0) return route.source.content;
1591
+ throw new Error(`Route content is missing from route context: ${route.source.filePath}`);
1594
1592
  }
1595
1593
  function readRouteArtifactContent(route) {
1596
1594
  const content = readRouteContent(route);
@@ -1600,16 +1598,17 @@ function isLlmsTxtRoute(route) {
1600
1598
  return !route.path.split("/").includes("404");
1601
1599
  }
1602
1600
  function llmsTxtDescription(route) {
1603
- if (route.description) return route.description;
1604
- const sections = route.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
1601
+ if (route.meta.description) return route.meta.description;
1602
+ const sections = route.meta.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
1605
1603
  if (sections?.length) return `Covers ${sections.join(", ")}.`;
1606
- if (route.keywords?.length) return `Related topics: ${route.keywords.join(", ")}.`;
1604
+ if (route.meta.keywords?.length) return `Related topics: ${route.meta.keywords.join(", ")}.`;
1607
1605
  if (route.kind === "openapi") return "OpenAPI artifact for machine-readable API reference data.";
1608
1606
  }
1609
1607
  function llmsTxtListItem(route, basePath) {
1610
- if (!route.contentArtifactUrl) return void 0;
1608
+ const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
1609
+ if (!contentArtifactUrl) return void 0;
1611
1610
  const description = llmsTxtDescription(route);
1612
- return description ? `- [${route.title}](${basePath}${route.contentArtifactUrl}): ${description}` : `- [${route.title}](${basePath}${route.contentArtifactUrl})`;
1611
+ return description ? `- [${route.meta.title}](${basePath}${contentArtifactUrl}): ${description}` : `- [${route.meta.title}](${basePath}${contentArtifactUrl})`;
1613
1612
  }
1614
1613
  function llmsTxtLocaleLabel(locale, projectConfig) {
1615
1614
  return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
@@ -1626,7 +1625,7 @@ function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
1626
1625
  const routesBySource = /* @__PURE__ */ new Map();
1627
1626
  const defaultLocale = projectConfig.i18n?.defaultLocale;
1628
1627
  for (const route of routes) {
1629
- const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.contentArtifactUrl ?? route.path}`;
1628
+ const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.artifacts?.contentArtifactUrl ?? route.path}`;
1630
1629
  const previousRoute = routesBySource.get(sourceKey);
1631
1630
  if (!previousRoute || previousRoute.locale && !route.locale) routesBySource.set(sourceKey, route);
1632
1631
  }
@@ -1695,9 +1694,9 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
1695
1694
  }
1696
1695
  if (contentPath.endsWith(".openapi.yaml")) {
1697
1696
  const jsonPath = contentPath.replace(/\.yaml$/, ".json");
1698
- const route = routes.find((r) => r.contentArtifactUrl === jsonPath);
1699
- if (route?.kind === "openapi" && route.content) {
1700
- const spec = JSON.parse(route.content);
1697
+ const route = routes.find((route) => route.artifacts?.contentArtifactUrl === jsonPath);
1698
+ if (route?.kind === "openapi" && route.source.content) {
1699
+ const spec = JSON.parse(route.source.content);
1701
1700
  res.statusCode = 200;
1702
1701
  res.setHeader("Content-Type", "text/yaml; charset=utf-8");
1703
1702
  res.end((0, yaml.stringify)(spec, { lineWidth: 0 }), "utf8");
@@ -1705,7 +1704,7 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
1705
1704
  }
1706
1705
  return false;
1707
1706
  }
1708
- const route = routes.find((route) => route.contentArtifactUrl === contentPath);
1707
+ const route = routes.find((route) => route.artifacts?.contentArtifactUrl === contentPath);
1709
1708
  if (!route) return false;
1710
1709
  res.statusCode = 200;
1711
1710
  res.setHeader("Content-Type", resolveContentArtifactType(route));
@@ -1731,14 +1730,15 @@ function createContentArtifactsPlugin() {
1731
1730
  "build:assets": (ctx) => {
1732
1731
  const assets = [];
1733
1732
  for (const route of ctx.routes) {
1734
- if (!route.contentArtifactUrl) continue;
1733
+ const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
1734
+ if (!contentArtifactUrl) continue;
1735
1735
  assets.push({
1736
- fileName: route.contentArtifactUrl.replace(/^\//, ""),
1736
+ fileName: contentArtifactUrl.replace(/^\//, ""),
1737
1737
  source: readRouteArtifactContent(route)
1738
1738
  });
1739
- if (route.kind === "openapi" && route.content) {
1740
- const yamlFileName = route.contentArtifactUrl.replace(/\.json$/, ".yaml");
1741
- const spec = JSON.parse(route.content);
1739
+ if (route.kind === "openapi" && route.source.content) {
1740
+ const yamlFileName = contentArtifactUrl.replace(/\.json$/, ".yaml");
1741
+ const spec = JSON.parse(route.source.content);
1742
1742
  assets.push({
1743
1743
  fileName: yamlFileName.replace(/^\//, ""),
1744
1744
  source: (0, yaml.stringify)(spec, { lineWidth: 0 })
@@ -1824,6 +1824,7 @@ var OPENAPI_HTTP_METHODS = [
1824
1824
  "patch",
1825
1825
  "trace"
1826
1826
  ];
1827
+ var CLARIFY_OPENAPI_SECTION_ID_EXTENSION = "x-clarify-section-id";
1827
1828
  function errorMessage(error) {
1828
1829
  return error instanceof Error ? error.message : String(error);
1829
1830
  }
@@ -1850,10 +1851,10 @@ function findOpenAPIRoutes(dir, base = dir) {
1850
1851
  routes.push({
1851
1852
  path: cleanPath,
1852
1853
  basePath: cleanPath,
1853
- filePath: fullPath,
1854
- virtualModuleId: virtualModuleIdFromRef(ref),
1855
- title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
1856
- kind: "openapi"
1854
+ kind: "openapi",
1855
+ meta: { title: kebabToTitle(cleanPath.split("/").pop() ?? "API") },
1856
+ module: { virtualModuleId: virtualModuleIdFromRef(ref) },
1857
+ source: { filePath: fullPath }
1857
1858
  });
1858
1859
  }
1859
1860
  return routes;
@@ -1862,36 +1863,80 @@ function operationMatchesTags(operationTags, filterTags) {
1862
1863
  if (!filterTags?.length) return true;
1863
1864
  return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
1864
1865
  }
1866
+ function filterPathItemsByTags(items, tags) {
1867
+ const filteredItems = {};
1868
+ for (const path of Object.keys(items)) {
1869
+ const pathItem = items[path];
1870
+ if (!pathItem) continue;
1871
+ if (OPENAPI_HTTP_METHODS.map((method) => pathItem[method]).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredItems[path] = pathItem;
1872
+ }
1873
+ return filteredItems;
1874
+ }
1875
+ function createOperationSectionId(operation, method, path, kind) {
1876
+ return (typeof operation.operationId === "string" ? operation.operationId.trim() : "") || (0, github_slugger.slug)(`${kind === "webhook" ? "webhook " : ""}${method.toLowerCase()} ${path}`);
1877
+ }
1878
+ function operationSectionId(operation) {
1879
+ const sectionId = operation[CLARIFY_OPENAPI_SECTION_ID_EXTENSION];
1880
+ return typeof sectionId === "string" ? sectionId : "";
1881
+ }
1882
+ function normalizeOpenAPISpecSectionIds(spec) {
1883
+ const items = [...Object.entries(spec.paths ?? {}).map(([path, pathItem]) => ({
1884
+ path,
1885
+ pathItem,
1886
+ kind: "path"
1887
+ })), ...Object.entries(spec.webhooks ?? {}).map(([path, pathItem]) => ({
1888
+ path,
1889
+ pathItem,
1890
+ kind: "webhook"
1891
+ }))];
1892
+ for (const { path, pathItem, kind } of items) {
1893
+ if (!pathItem || typeof pathItem !== "object") continue;
1894
+ const operations = pathItem;
1895
+ for (const method of OPENAPI_HTTP_METHODS) {
1896
+ const op = operations[method];
1897
+ if (!op || typeof op !== "object") continue;
1898
+ op[CLARIFY_OPENAPI_SECTION_ID_EXTENSION] = createOperationSectionId(op, method, path, kind);
1899
+ }
1900
+ }
1901
+ return spec;
1902
+ }
1865
1903
  /** Filter an OpenAPI spec to only include paths that have at least one operation matching the given tags. */
1866
1904
  function filterSpecByTags(spec, tags) {
1867
- const filteredPaths = {};
1868
1905
  const paths = spec.paths ?? {};
1869
- for (const path of Object.keys(paths)) {
1870
- const pathItem = paths[path];
1871
- if (!pathItem) continue;
1872
- if (Object.values(pathItem).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredPaths[path] = pathItem;
1873
- }
1906
+ const webhooks = spec.webhooks ?? {};
1874
1907
  return {
1875
1908
  ...spec,
1876
- paths: filteredPaths
1909
+ paths: filterPathItemsByTags(paths, tags),
1910
+ webhooks: filterPathItemsByTags(webhooks, tags)
1877
1911
  };
1878
1912
  }
1879
1913
  /** Extract endpoint operations from an OpenAPI spec as page sections. */
1880
1914
  function extractOpenAPISections(spec, filterTags) {
1915
+ normalizeOpenAPISpecSectionIds(spec);
1881
1916
  const sections = [];
1882
- const paths = spec.paths ?? {};
1883
- for (const [path, pathItem] of Object.entries(paths)) {
1917
+ const items = [...Object.entries(spec.paths ?? {}).map(([path, pathItem]) => ({
1918
+ path,
1919
+ pathItem,
1920
+ kind: "path"
1921
+ })), ...Object.entries(spec.webhooks ?? {}).map(([path, pathItem]) => ({
1922
+ path,
1923
+ pathItem,
1924
+ kind: "webhook"
1925
+ }))];
1926
+ for (const { path, pathItem, kind } of items) {
1884
1927
  if (!pathItem) continue;
1928
+ const operations = pathItem;
1885
1929
  for (const method of OPENAPI_HTTP_METHODS) {
1886
- const op = pathItem[method];
1887
- if (!op || !operationMatchesTags(op.tags, filterTags)) continue;
1888
- const title = op.summary ?? `${method.toUpperCase()} ${path}`;
1930
+ const op = operations[method];
1931
+ const tags = Array.isArray(op?.tags) ? op.tags : void 0;
1932
+ if (!op || !operationMatchesTags(tags, filterTags)) continue;
1933
+ const title = typeof op.summary === "string" ? op.summary : `${method.toUpperCase()} ${path}`;
1889
1934
  sections.push({
1890
- id: (0, github_slugger.slug)(`${method} ${path}`),
1935
+ id: operationSectionId(op),
1891
1936
  title,
1892
1937
  level: 2,
1893
- badge: method.toUpperCase(),
1894
- tags: op.tags
1938
+ badge: kind === "webhook" ? "WEBHOOK" : method.toUpperCase(),
1939
+ tags
1895
1940
  });
1896
1941
  }
1897
1942
  }
@@ -1933,12 +1978,16 @@ async function readOpenAPISpec(filePath, contentProcessor) {
1933
1978
  //#endregion
1934
1979
  //#region source/plugins/openapi/virtual-modules.ts
1935
1980
  var openApiRegistryModuleId = VIRTUAL_OPENAPI;
1981
+ var openApiServerRegistryModuleId = VIRTUAL_OPENAPI_SERVER;
1936
1982
  var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
1937
1983
  function specVirtualModuleId(specKey) {
1938
1984
  return `${OPENAPI_SPEC_PREFIX}${specKey}`;
1939
1985
  }
1940
- function generateOpenAPIRegistryModule(openApis) {
1941
- return `export const openApis = ${JSON.stringify(openApis)};`;
1986
+ function generateOpenAPIRegistryModule(openApiSpecs) {
1987
+ return `export const openApiSpecs = {\n${Object.entries(openApiSpecs).map(([key, moduleId]) => ` ${JSON.stringify(key)}: () => import(${JSON.stringify(moduleId)}),`).join("\n")}\n};`;
1988
+ }
1989
+ function generateOpenAPIServerRegistryModule(openApiSpecs) {
1990
+ return `export const openApiSpecs = ${JSON.stringify(openApiSpecs)};`;
1942
1991
  }
1943
1992
  /** Generate a virtual module that exports a single spec as default. */
1944
1993
  function generateOpenAPISpecModule(spec) {
@@ -1946,13 +1995,11 @@ function generateOpenAPISpecModule(spec) {
1946
1995
  }
1947
1996
  function generateOpenAPIPageModule(opts) {
1948
1997
  return `import { createOpenApiRouteComponent } from '@clarify-labs/renderer';
1998
+ import spec from ${JSON.stringify(opts.specModuleId)};
1949
1999
 
1950
- export const routeData = ${JSON.stringify({
1951
- spec: opts.spec,
1952
- tagFilter: opts.tagFilter
1953
- })};
2000
+ export const routeData = {};
1954
2001
 
1955
- export default createOpenApiRouteComponent(routeData);
2002
+ export default createOpenApiRouteComponent({ ...routeData, spec });
1956
2003
  `;
1957
2004
  }
1958
2005
  function generateOpenAPIErrorModule(diagnostic) {
@@ -1971,115 +2018,212 @@ export default createContentDiagnosticComponent(contentDiagnostic);
1971
2018
  }
1972
2019
  //#endregion
1973
2020
  //#region source/plugins/openapi/index.ts
1974
- function collectOpenAPIPageItems(config) {
1975
- const items = [];
2021
+ function collectOpenAPIPageIntents(config) {
2022
+ const intents = [];
1976
2023
  function visitPages(pages) {
1977
2024
  if (!pages || pages === "FileTree") return;
1978
- for (const group of pages) for (const item of group.pages) if (typeof item !== "string" && "openapi" in item && (item.path || item.filter?.tags?.length)) items.push(item);
2025
+ for (const group of pages) for (const item of group.pages) {
2026
+ const intent = routeIntentFromPagesItem(item);
2027
+ if (intent.kind === "openapi" && (intent.path || intent.tagFilter?.length)) intents.push(intent);
2028
+ }
1979
2029
  }
1980
2030
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
1981
- return items;
2031
+ return intents;
1982
2032
  }
1983
2033
  function virtualModuleIdFromPath(path) {
1984
2034
  return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
1985
2035
  }
2036
+ function cloneOpenAPIRoute(route, overrides = {}) {
2037
+ return {
2038
+ ...route,
2039
+ meta: {
2040
+ ...route.meta,
2041
+ sections: route.meta.sections ? [...route.meta.sections] : void 0
2042
+ },
2043
+ module: { ...route.module },
2044
+ source: { ...route.source },
2045
+ openapi: route.openapi ? {
2046
+ ...route.openapi,
2047
+ tagFilter: route.openapi.tagFilter ? [...route.openapi.tagFilter] : void 0
2048
+ } : void 0,
2049
+ alternates: route.alternates ? { ...route.alternates } : void 0,
2050
+ ...overrides
2051
+ };
2052
+ }
2053
+ function findOpenAPIRoutesForContentRoot(contentRoot, i18n) {
2054
+ if (!i18n) return findOpenAPIRoutes(contentRoot);
2055
+ const localizedRoutes = [];
2056
+ for (const locale of i18n.locales) {
2057
+ const localeRoot = (0, node_path.join)(contentRoot, locale.code);
2058
+ for (const route of findOpenAPIRoutes(localeRoot, localeRoot)) {
2059
+ const basePath = route.basePath ?? route.path;
2060
+ const path = localizedRoutePath(basePath, locale.code, i18n);
2061
+ localizedRoutes.push({
2062
+ ...route,
2063
+ path,
2064
+ basePath,
2065
+ locale: locale.code,
2066
+ module: { virtualModuleId: virtualModuleIdFromPath(path) }
2067
+ });
2068
+ }
2069
+ }
2070
+ if (i18n.missing === "fallback") {
2071
+ const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
2072
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
2073
+ for (const sourceRoute of defaultRoutes) {
2074
+ const basePath = sourceRoute.basePath ?? sourceRoute.path;
2075
+ for (const locale of i18n.locales) {
2076
+ const key = `${locale.code}:${basePath}`;
2077
+ if (routeByLocaleAndBase.has(key)) continue;
2078
+ const path = localizedRoutePath(basePath, locale.code, i18n);
2079
+ localizedRoutes.push(cloneOpenAPIRoute(sourceRoute, {
2080
+ path,
2081
+ locale: locale.code,
2082
+ module: { virtualModuleId: virtualModuleIdFromPath(path) },
2083
+ isFallback: true
2084
+ }));
2085
+ }
2086
+ }
2087
+ }
2088
+ return localizedRoutes;
2089
+ }
1986
2090
  function createConfiguredOpenAPIRoutes(routes, config) {
1987
- const pageItems = collectOpenAPIPageItems(config);
1988
- if (!pageItems.length) return routes;
2091
+ const openAPIIntents = collectOpenAPIPageIntents(config);
2092
+ if (!openAPIIntents.length) return routes;
1989
2093
  const additions = [];
1990
2094
  const existingKeys = new Set(routes.map((route) => `${route.locale ?? ""}:${route.basePath ?? route.path}`));
1991
- for (const item of pageItems) {
1992
- const tagFilter = item.filter?.tags?.filter(Boolean);
1993
- const sourceBasePath = openAPIPagePathFromRef(item.openapi);
1994
- const targetBasePath = openAPIPagePathFromRef(item.openapi, tagFilter, item.path);
2095
+ for (const intent of openAPIIntents) {
2096
+ const tagFilter = intent.tagFilter?.filter(Boolean);
2097
+ const sourceBasePath = openAPIPagePathFromRef(intent.ref);
2098
+ const targetBasePath = openAPIPagePathFromRef(intent.ref, tagFilter, intent.path);
1995
2099
  if (sourceBasePath === targetBasePath && !tagFilter?.length) continue;
1996
2100
  for (const route of routes) {
1997
2101
  if (route.kind !== "openapi" || (route.basePath ?? route.path) !== sourceBasePath) continue;
1998
2102
  const key = `${route.locale ?? ""}:${targetBasePath}`;
1999
2103
  if (existingKeys.has(key)) continue;
2000
2104
  existingKeys.add(key);
2001
- additions.push({
2002
- ...route,
2003
- path: route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath,
2105
+ const path = route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath;
2106
+ additions.push(cloneOpenAPIRoute(route, {
2107
+ path,
2004
2108
  basePath: targetBasePath,
2005
- title: route.title,
2006
- virtualModuleId: virtualModuleIdFromPath(targetBasePath),
2007
- openapiTagFilter: tagFilter?.length ? tagFilter : void 0
2008
- });
2109
+ module: { virtualModuleId: virtualModuleIdFromPath(path) },
2110
+ openapi: {
2111
+ ...route.openapi,
2112
+ tagFilter: tagFilter?.length ? tagFilter : void 0
2113
+ }
2114
+ }));
2009
2115
  }
2010
2116
  }
2011
2117
  const nextRoutes = [...routes, ...additions];
2012
- return config.i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n)) : nextRoutes;
2118
+ if (!config.i18n) return nextRoutes;
2119
+ const routesWithAlternates = nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n));
2120
+ const bareAliases = [];
2121
+ const seenBare = new Set(routesWithAlternates.map((route) => route.path));
2122
+ for (const route of routesWithAlternates) {
2123
+ if (route.locale !== config.i18n.defaultLocale) continue;
2124
+ const basePath = route.basePath ?? route.path;
2125
+ if (basePath === route.path || seenBare.has(basePath)) continue;
2126
+ seenBare.add(basePath);
2127
+ bareAliases.push(cloneOpenAPIRoute(route, {
2128
+ path: basePath,
2129
+ module: { virtualModuleId: virtualModuleIdFromPath(basePath) },
2130
+ isBareAlias: true
2131
+ }));
2132
+ }
2133
+ return [...routesWithAlternates, ...bareAliases];
2013
2134
  }
2014
- /** Derive a stable, deduplicated key from an absolute spec file path. */
2015
- function specFileKeyFromPath(filePath, projectRoot) {
2135
+ /** Derive a stable, deduplicated id from an absolute spec file path. */
2136
+ function sourceSpecIdFromPath(filePath, projectRoot) {
2016
2137
  return (0, node_path.relative)(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
2017
2138
  }
2139
+ function routeSpecModuleIdFromSourceSpecId(sourceSpecId, tagFilter) {
2140
+ if (!tagFilter?.length) return sourceSpecId;
2141
+ return `${sourceSpecId}_tags_${tagFilter.map((tag) => tag.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "tag").join("_")}`;
2142
+ }
2143
+ function openAPIRegistryKeys(route) {
2144
+ const keys = /* @__PURE__ */ new Set();
2145
+ const path = (route.path || "/").replace(/^\//, "");
2146
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
2147
+ if (route.locale) keys.add(`virtual:clarify-page/${route.locale}/${base}`);
2148
+ if (path) keys.add(`virtual:clarify-page/${path}`);
2149
+ else keys.add("virtual:clarify-page/index");
2150
+ if (!route.locale && base) keys.add(`virtual:clarify-page/${base}`);
2151
+ return [...keys];
2152
+ }
2018
2153
  function createOpenAPIPlugin() {
2019
- /** specs keyed by specFileKey (deduplicated per source file). */
2154
+ /** specs keyed by sourceSpecId (deduplicated per source file). */
2020
2155
  const specs = /* @__PURE__ */ new Map();
2156
+ const specModules = /* @__PURE__ */ new Map();
2021
2157
  return {
2022
2158
  name: "clarify:openapi",
2023
2159
  hooks: {
2024
- "routes:discover": (input) => ({
2160
+ "routes:discover": (input, ctx) => ({
2025
2161
  ...input,
2026
- routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
2162
+ routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.i18n)]
2027
2163
  }),
2028
2164
  "routes:discovered": async (routes, ctx) => {
2029
2165
  specs.clear();
2166
+ specModules.clear();
2030
2167
  const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
2031
2168
  const specByFilePath = /* @__PURE__ */ new Map();
2032
2169
  for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
2033
- const specFromCache = specByFilePath.get(route.filePath);
2170
+ const specFromCache = specByFilePath.get(route.source.filePath);
2034
2171
  const processor = createProjectContentProcessor(ctx.plugins, ctx);
2035
2172
  const result = specFromCache ? {
2036
2173
  ok: true,
2037
2174
  spec: specFromCache
2038
- } : await readOpenAPISpec(route.filePath, processor);
2175
+ } : await readOpenAPISpec(route.source.filePath, processor);
2039
2176
  if (!result.ok) {
2040
2177
  route.diagnostic = result.diagnostic;
2041
- route.title = route.title || "OpenAPI parse error";
2042
- route.sections = [];
2178
+ route.meta.title = route.meta.title || "OpenAPI parse error";
2179
+ route.meta.sections = [];
2043
2180
  continue;
2044
2181
  }
2045
- const spec = result.spec;
2046
- specByFilePath.set(route.filePath, spec);
2047
- const specKey = specFileKeyFromPath(route.filePath, ctx.generateOptions.projectRoot);
2048
- route.specFileKey = specKey;
2049
- specs.set(specKey, {
2182
+ const spec = normalizeOpenAPISpecSectionIds(result.spec);
2183
+ specByFilePath.set(route.source.filePath, spec);
2184
+ const sourceSpecId = sourceSpecIdFromPath(route.source.filePath, ctx.generateOptions.projectRoot);
2185
+ route.openapi = {
2186
+ ...route.openapi,
2187
+ sourceSpecId
2188
+ };
2189
+ specs.set(sourceSpecId, {
2050
2190
  spec,
2051
- filePath: route.filePath
2191
+ filePath: route.source.filePath
2052
2192
  });
2053
- route.title = spec.info?.title ?? route.title;
2054
- route.description = spec.info?.description ?? route.description;
2055
- route.sections = extractOpenAPISections(spec, route.openapiTagFilter);
2056
- const pageSpec = route.openapiTagFilter?.length ? filterSpecByTags(spec, route.openapiTagFilter) : spec;
2057
- route.content = JSON.stringify(pageSpec);
2193
+ route.meta.title = spec.info?.title ?? route.meta.title;
2194
+ route.meta.description = spec.info?.description ?? route.meta.description;
2195
+ route.meta.sections = extractOpenAPISections(spec, route.openapi?.tagFilter);
2196
+ const pageSpec = route.openapi?.tagFilter?.length ? filterSpecByTags(spec, route.openapi.tagFilter) : spec;
2197
+ const routeSpecModuleId = routeSpecModuleIdFromSourceSpecId(sourceSpecId, route.openapi?.tagFilter);
2198
+ route.openapi.routeSpecModuleId = routeSpecModuleId;
2199
+ specModules.set(routeSpecModuleId, pageSpec);
2200
+ route.source.content = JSON.stringify(pageSpec);
2058
2201
  }
2059
2202
  return nextRoutes;
2060
2203
  },
2061
2204
  "modules:before": (modules, ctx) => {
2062
2205
  const registryEntries = {};
2206
+ const serverRegistryEntries = {};
2063
2207
  for (const route of ctx.routes) {
2064
- if (route.kind !== "openapi" || !route.specFileKey) continue;
2065
- const spec = specs.get(route.specFileKey)?.spec;
2208
+ if (route.kind !== "openapi" || !route.openapi?.sourceSpecId) continue;
2209
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2210
+ const spec = specModules.get(routeSpecModuleId) ?? specs.get(route.openapi.sourceSpecId)?.spec;
2066
2211
  if (!spec) continue;
2067
- const base = (route.basePath ?? route.path).replace(/^\//, "");
2068
- const registryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
2069
- registryEntries[registryKey] = spec;
2212
+ for (const registryKey of openAPIRegistryKeys(route)) {
2213
+ registryEntries[registryKey] = specVirtualModuleId(routeSpecModuleId);
2214
+ serverRegistryEntries[registryKey] = spec;
2215
+ }
2070
2216
  }
2071
2217
  modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
2072
- for (const [specKey, entry] of specs) modules.set(specVirtualModuleId(specKey), generateOpenAPISpecModule(entry.spec));
2218
+ modules.set(openApiServerRegistryModuleId, generateOpenAPIServerRegistryModule(serverRegistryEntries));
2219
+ for (const [routeSpecModuleId, spec] of specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2073
2220
  for (const route of ctx.routes) {
2074
2221
  if (route.kind !== "openapi") continue;
2075
- if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
2076
- else if (route.specFileKey) {
2077
- const entry = specs.get(route.specFileKey);
2078
- if (!entry) continue;
2079
- modules.set(route.virtualModuleId, generateOpenAPIPageModule({
2080
- spec: entry.spec,
2081
- tagFilter: route.openapiTagFilter
2082
- }));
2222
+ if (route.diagnostic) modules.set(route.module.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
2223
+ else if (route.openapi?.sourceSpecId) {
2224
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2225
+ if (!specModules.has(routeSpecModuleId)) continue;
2226
+ modules.set(route.module.virtualModuleId, generateOpenAPIPageModule({ specModuleId: specVirtualModuleId(routeSpecModuleId) }));
2083
2227
  }
2084
2228
  }
2085
2229
  return modules;
@@ -2147,11 +2291,11 @@ async function generatePagefindIndex(options, pagefind) {
2147
2291
  }
2148
2292
  function routeSearchContent(route) {
2149
2293
  return [
2150
- route.title,
2151
- route.description,
2152
- route.keywords?.join(" "),
2153
- route.sections?.map((section) => section.title).join(" "),
2154
- route.content
2294
+ route.meta.title,
2295
+ route.meta.description,
2296
+ route.meta.keywords?.join(" "),
2297
+ route.meta.sections?.map((section) => section.title).join(" "),
2298
+ route.source.content
2155
2299
  ].filter(Boolean).join("\n\n");
2156
2300
  }
2157
2301
  async function generateDevSearchIndex(ctx, root, pagefind) {
@@ -2173,7 +2317,7 @@ async function generateDevSearchIndex(ctx, root, pagefind) {
2173
2317
  url: route.path,
2174
2318
  content,
2175
2319
  language: toPagefindLanguage(route.locale ?? ctx.projectConfig.i18n?.defaultLocale),
2176
- meta: { title: route.title }
2320
+ meta: { title: route.meta.title }
2177
2321
  });
2178
2322
  assertNoPagefindErrors(`addCustomRecord ${route.path}`, result.errors);
2179
2323
  pageCount += 1;
@@ -2330,15 +2474,18 @@ function normalizeRepositoryUrl(repository) {
2330
2474
  function encodePath(path) {
2331
2475
  return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
2332
2476
  }
2333
- function createSourceUrl(filePath, contentRoot, source) {
2477
+ function createSourceEditUrl(filePath, contentRoot, source) {
2334
2478
  const repository = normalizeRepositoryUrl(source.repository);
2335
2479
  const branch = source.branch ?? "main";
2336
2480
  const sourcePath = [source.directory ? trimSlashes(source.directory) : "", (0, node_path.relative)(contentRoot, filePath).replaceAll("\\", "/")].filter(Boolean).join("/");
2337
2481
  return `${repository}/edit/${encodeURIComponent(branch)}/${encodePath(sourcePath)}`;
2338
2482
  }
2339
- function attachSourceUrls(routes, contentRoot, source) {
2483
+ function attachSourceEditUrls(routes, contentRoot, source) {
2340
2484
  if (!source) return;
2341
- for (const route of routes) route.sourceUrl = createSourceUrl(route.filePath, contentRoot, source);
2485
+ for (const route of routes) route.source = {
2486
+ ...route.source,
2487
+ sourceEditUrl: createSourceEditUrl(route.source.filePath, contentRoot, source)
2488
+ };
2342
2489
  }
2343
2490
  //#endregion
2344
2491
  //#region source/plugins/source-links/index.ts
@@ -2346,7 +2493,7 @@ function createSourceLinksPlugin() {
2346
2493
  return {
2347
2494
  name: "clarify:source-links",
2348
2495
  hooks: { "routes:discovered": (routes, ctx) => {
2349
- attachSourceUrls(routes, (0, node_path.join)(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
2496
+ attachSourceEditUrls(routes, (0, node_path.join)(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
2350
2497
  return routes;
2351
2498
  } }
2352
2499
  };
@@ -2503,9 +2650,9 @@ var CLARIFY_ENV_TEMPLATE = `// This file is generated by Clarify. Do not edit it
2503
2650
  // plugin and slot code, so the TypeScript language service can resolve them.
2504
2651
 
2505
2652
  declare module 'virtual:clarify/slot' {
2506
- import type { SlotContext } from '@clarify-labs/cli'
2653
+ import type { UISlotContext } from '@clarify-labs/cli'
2507
2654
 
2508
- export function useSlot(): SlotContext
2655
+ export function useSlot(): UISlotContext
2509
2656
  }
2510
2657
  `;
2511
2658
  function generateClarifyEnvDts(_plugins) {
@@ -2527,6 +2674,11 @@ function writeClarifyEnvDts(projectRoot, plugins) {
2527
2674
  (0, node_fs.writeFileSync)(filePath, next, "utf-8");
2528
2675
  }
2529
2676
  //#endregion
2677
+ //#region source/parsers/markdown/utils.ts
2678
+ function escapeHtml(text) {
2679
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2680
+ }
2681
+ //#endregion
2530
2682
  //#region source/core/runtime/html-template.ts
2531
2683
  /**
2532
2684
  * HTML template manipulation helpers used during SSG.
@@ -2543,7 +2695,7 @@ function resolveProjectConfigFromContext(contextOrConfig) {
2543
2695
  }
2544
2696
  function routeTitle(contextOrConfig, route) {
2545
2697
  const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2546
- const title = route?.title?.trim();
2698
+ const title = route?.meta.title.trim();
2547
2699
  if (!title || title === projectConfig.title) return projectConfig.title;
2548
2700
  return `${title} - ${projectConfig.title}`;
2549
2701
  }
@@ -2591,8 +2743,8 @@ function injectSSRIntoTemplate(template, appHtml, contextOrConfig, route) {
2591
2743
  const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2592
2744
  let html = injectHtmlLocaleAttributes(template, contextOrConfig, route);
2593
2745
  html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(routeTitle(contextOrConfig, route))}</title>`);
2594
- html = setNamedMeta(html, "description", route?.description ?? projectConfig.description);
2595
- html = setNamedMeta(html, "keywords", route?.keywords?.join(", "));
2746
+ html = setNamedMeta(html, "description", route?.meta.description ?? projectConfig.description);
2747
+ html = setNamedMeta(html, "keywords", route?.meta.keywords?.join(", "));
2596
2748
  html = injectCanonicalUrl(html, contextOrConfig, route);
2597
2749
  const dataPagefindIgnore = route?.isBareAlias ? " data-pagefind-ignore" : "";
2598
2750
  html = html.replace(/<div id="root">([\s\S]*?)<\/div>/, `<div id="root"${dataPagefindIgnore}>${appHtml}</div>`);
@@ -2701,7 +2853,7 @@ function routeOutputFiles(outputDirectory, route) {
2701
2853
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
2702
2854
  import { routes, navigation } from 'virtual:clarify/routes/server';
2703
2855
  import { config } from 'virtual:clarify/config';
2704
- import { openApis } from 'virtual:clarify/openapi';
2856
+ import { openApiSpecs } from 'virtual:clarify/openapi/server';
2705
2857
  import { runtimeSlots } from 'virtual:clarify/slots';
2706
2858
 
2707
2859
  export async function render(url) {
@@ -2709,10 +2861,10 @@ export async function render(url) {
2709
2861
  // them synchronously (renderToString has no Suspense support).
2710
2862
  const entries = Object.values(runtimeSlots).flat()
2711
2863
  await Promise.all(entries.map(async (entry) => {
2712
- const mod = await entry.component()
2864
+ const mod = await entry.loadComponent()
2713
2865
  entry._resolved = mod.default
2714
2866
  }))
2715
- return renderToHTML({ config, routes, navigation, openApis, runtimeSlots, url, themeEditor: config.theme.editor });
2867
+ return renderToHTML({ config, routes, navigation, openApiSpecs, runtimeSlots, url, themeEditor: config.theme.editor });
2716
2868
  }`;
2717
2869
  function createTempEntryFile(content) {
2718
2870
  const entryPath = (0, node_path.join)(createClarifyTempDir("ssr"), "entry-server.ts");
@@ -2799,7 +2951,10 @@ var ClarifyContext = class {
2799
2951
  this.generateOptions = init.generateOptions;
2800
2952
  this.version = init.version;
2801
2953
  this.currentRoutes = init.routes ?? [];
2802
- this.currentNavigation = init.navigation ?? [];
2954
+ this.currentNavigation = init.navigation ?? {
2955
+ kind: "flat",
2956
+ nodes: []
2957
+ };
2803
2958
  this.currentPlugins = init.plugins ?? [];
2804
2959
  }
2805
2960
  get routes() {
@@ -2921,7 +3076,10 @@ var ClarifyEngine = class {
2921
3076
  generateOptions: placeholderOptions,
2922
3077
  version: cliPackageVersion,
2923
3078
  routes: [],
2924
- navigation: [],
3079
+ navigation: {
3080
+ kind: "flat",
3081
+ nodes: []
3082
+ },
2925
3083
  plugins: []
2926
3084
  });
2927
3085
  }
@@ -3024,9 +3182,9 @@ var ClarifyEngine = class {
3024
3182
  routes = await runPhase(plugins, "content:process", this.ctx, async () => {
3025
3183
  const pages = routes.map((route) => ({
3026
3184
  path: route.path,
3027
- filePath: route.filePath,
3028
- frontmatter: route.frontmatter ?? {},
3029
- content: route.content ?? ""
3185
+ filePath: route.source.filePath,
3186
+ frontmatter: route.source.frontmatter ?? {},
3187
+ content: route.source.content ?? ""
3030
3188
  }));
3031
3189
  const resolvedPages = await runHooks(plugins, "pages:resolved", pages, this.ctx);
3032
3190
  const pageByPath = new Map(resolvedPages.map((p) => [p.path, p]));
@@ -3035,13 +3193,22 @@ var ClarifyEngine = class {
3035
3193
  if (!page) return route;
3036
3194
  return {
3037
3195
  ...route,
3038
- frontmatter: page.frontmatter,
3039
- content: page.content
3196
+ source: {
3197
+ ...route.source,
3198
+ frontmatter: page.frontmatter,
3199
+ content: page.content
3200
+ }
3040
3201
  };
3041
3202
  });
3042
3203
  });
3043
3204
  routes = applyConfiguredPageRoutePaths(routes, tabs, i18n);
3044
- const navigation = tabs ? i18n ? buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) ?? {} : buildNavigationFromTabsConfig(routes, tabs) : buildNavigation(routes);
3205
+ const navigation = tabs ? i18n ? buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) ?? {
3206
+ kind: "localized-tabbed",
3207
+ locales: {}
3208
+ } : buildNavigationFromTabsConfig(routes, tabs) : {
3209
+ kind: "flat",
3210
+ nodes: buildNavigation(routes)
3211
+ };
3045
3212
  const resolved = await runHooks(plugins, "routes:resolved", {
3046
3213
  routes,
3047
3214
  navigation
@@ -3075,7 +3242,7 @@ var ClarifyEngine = class {
3075
3242
  writeClarifyEnvDts(this.root, this.plugins);
3076
3243
  }
3077
3244
  hasContentRouteForFile(filePath) {
3078
- return this.routes.some((route) => route.filePath === filePath);
3245
+ return this.routes.some((route) => route.source.filePath === filePath);
3079
3246
  }
3080
3247
  async collectBuildAssets() {
3081
3248
  if (!this.buildEnabled) return [];
@@ -3215,4 +3382,4 @@ Object.defineProperty(exports, "defineConfig", {
3215
3382
  }
3216
3383
  });
3217
3384
 
3218
- //# sourceMappingURL=engine-I04EZzjg.cjs.map
3385
+ //# sourceMappingURL=engine-qafLfnXQ.cjs.map