@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.
@@ -2,13 +2,16 @@ import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path
2
2
  import mdxPlugin from "@mdx-js/rollup";
3
3
  import tailwindcss from "@tailwindcss/vite";
4
4
  import react from "@vitejs/plugin-react";
5
+ import rehypeRaw from "rehype-raw";
5
6
  import { compile } from "@mdx-js/mdx";
6
- import GithubSlugger, { slug } from "github-slugger";
7
- import { toString } from "mdast-util-to-string";
8
- import { bundledLanguages, createCssVariablesTheme, createHighlighter } from "shiki";
7
+ import rehypeShiki from "@shikijs/rehype";
8
+ import rehypeSlug from "rehype-slug";
9
+ import { createCssVariablesTheme } from "shiki";
9
10
  import { visit } from "unist-util-visit";
10
- import { markdownRemarkPlugins, themeBootstrapScript, themePresets } from "@clarify-labs/renderer";
11
+ import { markdownRemarkPlugins, parseCodeMeta, themeBootstrapScript, themePresets } from "@clarify-labs/renderer";
11
12
  import { readFile, readdir, stat } from "node:fs/promises";
13
+ import GithubSlugger, { slug } from "github-slugger";
14
+ import { toString } from "mdast-util-to-string";
12
15
  import { remark } from "remark";
13
16
  import matter from "gray-matter";
14
17
  import { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
@@ -19,38 +22,11 @@ import { fileURLToPath, pathToFileURL } from "node:url";
19
22
  import SwaggerParser from "@apidevtools/swagger-parser";
20
23
  import { tmpdir } from "node:os";
21
24
  import { createRequire } from "node:module";
22
- //#region source/parsers/markdown/utils.ts
23
- function escapeHtml(text) {
24
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
25
- }
26
- //#endregion
27
25
  //#region source/parsers/markdown/mdx.ts
28
26
  function getLanguage(className) {
29
27
  return (Array.isArray(className) ? className : typeof className === "string" ? className.split(/\s+/) : []).find((item) => typeof item === "string" && item.startsWith("language-"))?.replace(/^language-/, "") || "txt";
30
28
  }
31
- function getTextContent(node) {
32
- if (!node) return "";
33
- if (typeof node.value === "string") return node.value;
34
- return node.children?.map(getTextContent).join("") ?? "";
35
- }
36
- function renderPlainCode(code) {
37
- return code.split("\n").map((line) => `<span>${escapeHtml(line)}</span>`).join("\n");
38
- }
39
- function isBundledLanguage(language) {
40
- return language in bundledLanguages;
41
- }
42
- var highlighter;
43
29
  var cssVariablesTheme = createCssVariablesTheme();
44
- async function getHighlighter() {
45
- highlighter = highlighter ?? await createHighlighter({
46
- themes: [cssVariablesTheme],
47
- langs: []
48
- });
49
- return highlighter;
50
- }
51
- function renderHighlightedCode(html) {
52
- return (/<code[^>]*>([\s\S]*?)<\/code>/.exec(html)?.[1] ?? html).split("\n").map((line) => line.trimStart().startsWith("<span") ? line : `<span>${line}</span>`).join("\n");
53
- }
54
30
  function rehypeParseCodeBlocks() {
55
31
  return (tree) => {
56
32
  visit(tree, "element", (node, _nodeIndex, parentNode) => {
@@ -68,61 +44,18 @@ function rehypeParseCodeBlocks() {
68
44
  });
69
45
  };
70
46
  }
71
- function rehypeShiki() {
72
- return async (tree) => {
73
- const shikiHighlighter = await getHighlighter();
74
- const languages = /* @__PURE__ */ new Set();
75
- visit(tree, "element", (node) => {
76
- if (node.tagName !== "pre") return;
77
- const codeNode = node.children?.[0];
78
- if (codeNode?.tagName !== "code") return;
79
- const language = typeof node.properties?.language === "string" ? node.properties.language : getLanguage(codeNode.properties?.className);
80
- if (language && language !== "txt" && isBundledLanguage(language)) languages.add(language);
81
- });
82
- if (languages.size > 0) await shikiHighlighter.loadLanguage(...languages);
83
- visit(tree, "element", (node) => {
84
- if (node.tagName !== "pre") return;
85
- const codeNode = node.children?.[0];
86
- if (codeNode?.tagName !== "code") return;
87
- node.properties = node.properties ?? {};
88
- codeNode.properties = codeNode.properties ?? {};
89
- const code = getTextContent(codeNode);
90
- const language = typeof node.properties.language === "string" ? node.properties.language : getLanguage(codeNode.properties.className);
91
- node.properties.code = code;
92
- codeNode.properties.code = code;
93
- codeNode.properties.language = language;
94
- let highlighted = renderPlainCode(code);
95
- if (language && language !== "txt") try {
96
- highlighted = renderHighlightedCode(shikiHighlighter.codeToHtml(code, {
97
- lang: language,
98
- theme: cssVariablesTheme
99
- }));
100
- } catch {
101
- highlighted = renderPlainCode(code);
102
- }
103
- codeNode.children = [{
104
- type: "text",
105
- value: highlighted
106
- }];
107
- });
108
- };
109
- }
110
- function rehypeSlugSections() {
111
- return (tree) => {
112
- const slugger = new GithubSlugger();
113
- visit(tree, "element", (node) => {
114
- if (node.tagName !== "h2" && node.tagName !== "h3") return;
115
- node.properties = node.properties ?? {};
116
- if (node.properties.id) return;
117
- node.properties.id = slugger.slug(toString(node));
118
- });
119
- };
120
- }
121
47
  var remarkPlugins = markdownRemarkPlugins;
122
48
  var rehypePlugins = [
123
- rehypeSlugSections,
49
+ rehypeSlug,
124
50
  rehypeParseCodeBlocks,
125
- rehypeShiki
51
+ [rehypeShiki, {
52
+ theme: cssVariablesTheme,
53
+ langs: [],
54
+ lazy: true,
55
+ addLanguageClass: true,
56
+ fallbackLanguage: "text",
57
+ parseMetaString: parseCodeMeta
58
+ }]
126
59
  ];
127
60
  function formatMdxDiagnostic(error) {
128
61
  if (error instanceof Error) return error.message;
@@ -134,7 +67,8 @@ function formatMdxDiagnostic(error) {
134
67
  *
135
68
  * `rehypePlugins` includes `rehypeShiki`, which loads grammar/WASM assets and
136
69
  * is expensive. The real highlighting happens at Vite build time via the
137
- * `@mdx-js/rollup` plugin (see `createMdxPlugin` in `core/adapters.ts`), which
70
+ * `@mdx-js/rollup` plugins (see `createMarkdownPlugin` and `createMdxPlugin`
71
+ * in `core/adapters.ts`), which
138
72
  * runs the full remark + rehype pipeline. Re-running it here, during route
139
73
  * discovery (Phase 3), would compile every `.mdx` file twice and invoke Shiki
140
74
  * once per file per `engine.refresh()` — see ARCHITECTURE.md §2.2 which states
@@ -283,6 +217,11 @@ function kebabToTitle(str) {
283
217
  function parseMdxTree(content) {
284
218
  return remark.parse(content);
285
219
  }
220
+ async function compileRouteDiagnostic(content, filePath, baseDir) {
221
+ if (extname(filePath).toLowerCase() !== ".mdx") return void 0;
222
+ const result = await compileMdxContent(content, filePath, baseDir);
223
+ return result.ok ? void 0 : result.diagnostic;
224
+ }
286
225
  /** 从内容中提取第一个 H1 标题 */
287
226
  function extractH1(content) {
288
227
  const tree = parseMdxTree(content);
@@ -308,12 +247,12 @@ function extractMdxSections(content) {
308
247
  return sections;
309
248
  }
310
249
  function navigationSections(sections) {
311
- return sections.map((s) => ({
312
- id: s.id,
313
- title: s.title,
314
- level: s.level,
315
- badge: s.badge,
316
- tags: s.tags
250
+ return sections.map((section) => ({
251
+ id: section.id,
252
+ title: section.title,
253
+ level: section.level,
254
+ badge: section.badge,
255
+ tags: section.tags
317
256
  }));
318
257
  }
319
258
  function normalizePath(path) {
@@ -377,7 +316,7 @@ async function findContentRoutes(dir, base = dir, options = {}) {
377
316
  frontmatter,
378
317
  content
379
318
  };
380
- const mdxResult = await compileMdxContent(page.content, fullPath, base);
319
+ const diagnostic = await compileRouteDiagnostic(page.content, fullPath, base);
381
320
  let title = typeof page.frontmatter.title === "string" ? page.frontmatter.title : "";
382
321
  if (!title) {
383
322
  const lastPart = pathParts[pathParts.length - 1] ?? "";
@@ -385,17 +324,21 @@ async function findContentRoutes(dir, base = dir, options = {}) {
385
324
  }
386
325
  routes.push({
387
326
  kind: "mdx",
388
- title,
389
327
  path: cleanPath,
390
328
  basePath: cleanPath,
391
- filePath: fullPath,
392
- virtualModuleId: "virtual:clarify-page/" + relativePath.replace(/\.mdx?$/, "").replace(/\/+/g, "/"),
393
- description: typeof page.frontmatter.description === "string" ? page.frontmatter.description : void 0,
394
- keywords: frontmatterKeywords(page.frontmatter),
395
- frontmatter: page.frontmatter,
396
- content: page.content,
397
- sections: extractMdxSections(page.content),
398
- diagnostic: mdxResult.ok ? void 0 : mdxResult.diagnostic
329
+ meta: {
330
+ title,
331
+ description: typeof page.frontmatter.description === "string" ? page.frontmatter.description : void 0,
332
+ keywords: frontmatterKeywords(page.frontmatter),
333
+ sections: extractMdxSections(page.content)
334
+ },
335
+ module: { virtualModuleId: "virtual:clarify-page/" + relativePath.replace(/\.mdx?$/, "").replace(/\/+/g, "/") },
336
+ source: {
337
+ filePath: fullPath,
338
+ frontmatter: page.frontmatter,
339
+ content: page.content
340
+ },
341
+ diagnostic
399
342
  });
400
343
  }
401
344
  }
@@ -454,7 +397,7 @@ async function findLocalizedContentRoutes(contentRoot, i18n, options = {}) {
454
397
  path: localizedRoutePath(basePath, locale.code, i18n),
455
398
  basePath,
456
399
  locale: locale.code,
457
- virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.filePath)
400
+ module: { virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.source.filePath) }
458
401
  });
459
402
  }
460
403
  }
@@ -490,12 +433,12 @@ function buildNavigation(routes) {
490
433
  if (!node) {
491
434
  node = {
492
435
  path: pathSoFar,
493
- title: i === parts.length - 1 ? route.title : kebabToTitle(parts[i]),
436
+ title: i === parts.length - 1 ? route.meta.title : kebabToTitle(parts[i]),
494
437
  children: []
495
438
  };
496
439
  current.push(node);
497
440
  }
498
- if (i === parts.length - 1 && route.sections) node.sections = navigationSections(route.sections);
441
+ if (i === parts.length - 1 && route.meta.sections) node.sections = navigationSections(route.meta.sections);
499
442
  if (i < parts.length - 1) {
500
443
  node.children = node.children ?? [];
501
444
  current = node.children;
@@ -504,17 +447,22 @@ function buildNavigation(routes) {
504
447
  }
505
448
  return root;
506
449
  }
507
- function resolvePageItem(item) {
508
- if (typeof item === "string") return { pageRef: item };
450
+ function routeIntentFromPagesItem(item) {
451
+ if (typeof item === "string") return {
452
+ kind: "page",
453
+ ref: item
454
+ };
509
455
  if ("openapi" in item) return {
510
- openapiRef: item.openapi,
511
- openapiTagFilter: item.filter?.tags,
456
+ kind: "openapi",
457
+ ref: item.openapi,
458
+ tagFilter: item.filter?.tags,
512
459
  path: item.path,
513
460
  title: item.title,
514
461
  icon: item.icon
515
462
  };
516
463
  return {
517
- pageRef: item.page,
464
+ kind: "page",
465
+ ref: item.page,
518
466
  path: item.path,
519
467
  redirect: item.redirect,
520
468
  title: item.title,
@@ -529,25 +477,28 @@ function firstNavigationPath(nodes) {
529
477
  }
530
478
  return "/";
531
479
  }
532
- function collectExplicitPagePathItems(tabs) {
533
- const items = [];
480
+ function collectExplicitPagePathIntents(tabs) {
481
+ const intents = [];
534
482
  for (const tab of tabs ?? []) {
535
483
  if (!tab.pages || tab.pages === "FileTree") continue;
536
- for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) items.push({
537
- pageRef: item.page,
538
- path: item.path
539
- });
484
+ for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) {
485
+ const intent = routeIntentFromPagesItem(item);
486
+ if (intent.kind === "page" && intent.path) intents.push({
487
+ ...intent,
488
+ path: intent.path
489
+ });
490
+ }
540
491
  }
541
- return items;
492
+ return intents;
542
493
  }
543
494
  function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
544
- const pageItems = collectExplicitPagePathItems(tabs);
545
- if (!pageItems.length) return routes;
495
+ const pageIntents = collectExplicitPagePathIntents(tabs);
496
+ if (!pageIntents.length) return routes;
546
497
  const additions = [];
547
498
  const existingKeys = new Set(routes.map((route) => `${route.locale ?? ""}:${route.basePath ?? route.path}`));
548
- for (const item of pageItems) {
549
- const sourceBasePath = routePathFromRef(item.pageRef);
550
- const targetBasePath = routePathFromRef(item.pageRef, item.path);
499
+ for (const intent of pageIntents) {
500
+ const sourceBasePath = routePathFromRef(intent.ref);
501
+ const targetBasePath = routePathFromRef(intent.ref, intent.path);
551
502
  if (sourceBasePath === targetBasePath) continue;
552
503
  for (const route of routes) {
553
504
  if (route.kind !== "mdx" || (route.basePath ?? route.path) !== sourceBasePath) continue;
@@ -571,40 +522,42 @@ function buildNavigationFromPagesConfig(routes, config) {
571
522
  return buildNavigationFromConfig(routes, config);
572
523
  }
573
524
  function buildNavigationFromTabsConfig(routes, tabs) {
574
- return { tabs: tabs.map((tab) => {
575
- const children = buildNavigationFromPagesConfig(routes, tab.pages);
576
- return {
577
- type: "tab",
578
- path: firstNavigationPath(children),
579
- title: resolveLocalizedText(tab.tab, "", "") ?? "",
580
- icon: tab.icon,
581
- children
582
- };
583
- }) };
525
+ return {
526
+ kind: "tabbed",
527
+ tabs: tabs.map((tab) => {
528
+ const children = buildNavigationFromPagesConfig(routes, tab.pages);
529
+ return {
530
+ type: "tab",
531
+ path: firstNavigationPath(children),
532
+ title: resolveLocalizedText(tab.tab, "", "") ?? "",
533
+ icon: tab.icon,
534
+ children
535
+ };
536
+ })
537
+ };
584
538
  }
585
539
  function buildNavigationFromConfig(routes, config) {
586
540
  const routeMap = new Map(routes.map((r) => [r.path, r]));
587
541
  return config.map((group) => {
588
542
  const children = group.pages.map((item) => {
589
- const { pageRef, openapiRef, openapiTagFilter, path: explicitPath, redirect, title, icon } = resolvePageItem(item);
590
- if (openapiRef) {
591
- const path = openAPIPagePathFromRef(openapiRef, openapiTagFilter, explicitPath);
543
+ const intent = routeIntentFromPagesItem(item);
544
+ if (intent.kind === "openapi") {
545
+ const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
592
546
  const route = routeMap.get(path);
593
547
  return {
594
548
  path,
595
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? openapiRef),
596
- icon,
597
- sections: route?.sections ? navigationSections(route.sections) : void 0
549
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
550
+ icon: intent.icon,
551
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
598
552
  };
599
553
  }
600
- const ref = pageRef ?? "";
601
- const path = routePathFromRef(ref, explicitPath);
554
+ const path = routePathFromRef(intent.ref, intent.path);
602
555
  const route = routeMap.get(path);
603
556
  return {
604
- path: redirect ? normalizePath(redirect) : path,
605
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? ref),
606
- icon,
607
- sections: route?.sections ? navigationSections(route.sections) : void 0
557
+ path: intent.redirect ? normalizePath(intent.redirect) : path,
558
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
559
+ icon: intent.icon,
560
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
608
561
  };
609
562
  });
610
563
  return {
@@ -630,16 +583,15 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
630
583
  const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
631
584
  return config.map((group) => {
632
585
  const children = group.pages.map((item) => {
633
- const { pageRef, openapiRef, openapiTagFilter, path: explicitPath, redirect, title, icon } = resolvePageItem(item);
634
- const ref = openapiRef ?? pageRef ?? "";
635
- const basePath = openapiRef ? openAPIPagePathFromRef(openapiRef, openapiTagFilter, explicitPath) : routePathFromRef(ref, explicitPath);
586
+ const intent = routeIntentFromPagesItem(item);
587
+ const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
636
588
  const route = routeMap.get(basePath);
637
589
  const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
638
590
  return {
639
- path: (redirect ? localizedRoutePath(routePathFromRef(redirect), locale, i18n) : void 0) ?? path,
640
- title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle(basePath.split("/").pop() ?? ref),
641
- icon,
642
- sections: route?.sections ? navigationSections(route.sections) : void 0
591
+ path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, i18n) : void 0) ?? path,
592
+ title: resolveLocalizedText(intent.title, locale, i18n.defaultLocale) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
593
+ icon: intent.icon,
594
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
643
595
  };
644
596
  });
645
597
  return {
@@ -652,10 +604,10 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
652
604
  }
653
605
  function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
654
606
  if (!i18n) return void 0;
655
- const result = {};
607
+ const locales = {};
656
608
  for (const locale of i18n.locales) {
657
609
  const localeRoutes = routes.filter((route) => route.locale === locale.code);
658
- result[locale.code] = { tabs: tabs.map((tab) => {
610
+ locales[locale.code] = { tabs: tabs.map((tab) => {
659
611
  const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code, i18n);
660
612
  return {
661
613
  type: "tab",
@@ -666,17 +618,20 @@ function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
666
618
  };
667
619
  }) };
668
620
  }
669
- return result;
621
+ return {
622
+ kind: "localized-tabbed",
623
+ locales
624
+ };
670
625
  }
671
626
  /** Serialize a ContentRoute into the minimal shape exposed to tooling. */
672
627
  function toDevRouteEntry(route) {
673
628
  return {
674
629
  path: route.path,
675
- filePath: route.filePath,
630
+ filePath: route.source.filePath,
676
631
  locale: route.locale,
677
632
  basePath: route.basePath,
678
633
  kind: route.kind,
679
- title: route.title
634
+ title: route.meta.title
680
635
  };
681
636
  }
682
637
  /**
@@ -690,7 +645,7 @@ function toDevRouteEntry(route) {
690
645
  * or the first match.
691
646
  */
692
647
  function resolveRouteForFile(filePath, routes, defaultLocale) {
693
- const matches = routes.filter((r) => r.filePath === filePath);
648
+ const matches = routes.filter((r) => r.source.filePath === filePath);
694
649
  if (matches.length === 0) return void 0;
695
650
  if (matches.length === 1) return matches[0];
696
651
  const fileLocale = inferLocaleFromPath(filePath);
@@ -1115,6 +1070,7 @@ var VIRTUAL_CONFIG = "virtual:clarify/config";
1115
1070
  var VIRTUAL_ROUTES = "virtual:clarify/routes";
1116
1071
  var VIRTUAL_SERVER_ROUTES = "virtual:clarify/routes/server";
1117
1072
  var VIRTUAL_OPENAPI = "virtual:clarify/openapi";
1073
+ var VIRTUAL_OPENAPI_SERVER = "virtual:clarify/openapi/server";
1118
1074
  var VIRTUAL_SLOTS = "virtual:clarify/slots";
1119
1075
  var VIRTUAL_SLOT = "virtual:clarify/slot";
1120
1076
  var VIRTUAL_CLIENT_ENTRY = "virtual:clarify/entry-client";
@@ -1135,6 +1091,7 @@ var FIXED_VIRTUAL_IDS = [
1135
1091
  VIRTUAL_ROUTES,
1136
1092
  VIRTUAL_SERVER_ROUTES,
1137
1093
  VIRTUAL_OPENAPI,
1094
+ VIRTUAL_OPENAPI_SERVER,
1138
1095
  VIRTUAL_SLOTS,
1139
1096
  VIRTUAL_SLOT
1140
1097
  ];
@@ -1151,8 +1108,8 @@ function resolveVirtualModuleId(id, modules, routes) {
1151
1108
  for (const virtualId of FIXED_VIRTUAL_IDS) if (id === virtualId || id === resolveVirtualId(virtualId)) return resolveVirtualId(virtualId);
1152
1109
  const moduleId = stripVirtualPrefix(id);
1153
1110
  if (modules.has(moduleId)) return resolveVirtualId(moduleId);
1154
- const route = routes.find((route) => route.virtualModuleId === id || route.virtualModuleId === moduleId);
1155
- if (route) return resolveVirtualId(route.virtualModuleId);
1111
+ const route = routes.find((route) => route.module.virtualModuleId === id || route.module.virtualModuleId === moduleId);
1112
+ if (route) return resolveVirtualId(route.module.virtualModuleId);
1156
1113
  return null;
1157
1114
  }
1158
1115
  function generateConfigModule(projectConfig, buildOptions, version) {
@@ -1166,38 +1123,38 @@ function generateConfigModule(projectConfig, buildOptions, version) {
1166
1123
  function moduleSpecifier(value) {
1167
1124
  return JSON.stringify(value);
1168
1125
  }
1169
- function routeToRuntimeObject(route, component, mode) {
1170
- const obj = {
1126
+ function routeToRuntimeManifestEntry(route, component, mode) {
1127
+ const entry = {
1171
1128
  path: route.path,
1172
- title: route.title,
1129
+ title: route.meta.title,
1173
1130
  component,
1174
1131
  kind: route.kind
1175
1132
  };
1176
- if (mode === "client") obj.lazy = true;
1177
- if (route.basePath) obj.basePath = route.basePath;
1178
- if (route.locale) obj.locale = route.locale;
1179
- if (route.isFallback) obj.isFallback = true;
1180
- if (route.isBareAlias) obj.isBareAlias = true;
1181
- if (route.alternates) obj.alternates = route.alternates;
1182
- if (route.description) obj.description = route.description;
1183
- if (route.keywords?.length) obj.keywords = route.keywords;
1184
- if (route.contentArtifactUrl) obj.contentArtifactUrl = route.contentArtifactUrl;
1185
- if (route.sourceUrl) obj.sourceUrl = route.sourceUrl;
1186
- if (route.sections?.length) obj.sections = route.sections.map((s) => ({
1187
- id: s.id,
1188
- title: s.title,
1189
- level: s.level,
1190
- badge: s.badge,
1191
- tags: s.tags
1133
+ if (mode === "client") entry.lazy = true;
1134
+ if (route.basePath) entry.basePath = route.basePath;
1135
+ if (route.locale) entry.locale = route.locale;
1136
+ if (route.isFallback) entry.isFallback = true;
1137
+ if (route.isBareAlias) entry.isBareAlias = true;
1138
+ if (route.alternates) entry.alternates = route.alternates;
1139
+ if (route.meta.description) entry.description = route.meta.description;
1140
+ if (route.meta.keywords?.length) entry.keywords = route.meta.keywords;
1141
+ if (route.artifacts?.contentArtifactUrl) entry.contentArtifactUrl = route.artifacts.contentArtifactUrl;
1142
+ if (route.source?.sourceEditUrl) entry.sourceEditUrl = route.source.sourceEditUrl;
1143
+ if (route.meta.sections?.length) entry.sections = route.meta.sections.map((section) => ({
1144
+ id: section.id,
1145
+ title: section.title,
1146
+ level: section.level,
1147
+ badge: section.badge,
1148
+ tags: section.tags
1192
1149
  }));
1193
- return obj;
1150
+ return entry;
1194
1151
  }
1195
1152
  function generateRoutesModule(routes, navigation, mode = "client") {
1196
- 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) => {
1197
- 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 }) }))`;
1153
+ 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) => {
1154
+ 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 }) }))`;
1198
1155
  const placeholder = `__COMPONENT_${i}__`;
1199
- const obj = routeToRuntimeObject(r, placeholder, mode);
1200
- return ` ${JSON.stringify(obj, null, 0).replace(`"${placeholder}"`, component).replace(/"([A-Za-z_$][\w$]*)":/g, "$1: ")}`;
1156
+ const manifestEntry = routeToRuntimeManifestEntry(r, placeholder, mode);
1157
+ return ` ${JSON.stringify(manifestEntry, null, 0).replace(`"${placeholder}"`, component).replace(/"([A-Za-z_$][\w$]*)":/g, "$1: ")}`;
1201
1158
  }).join(",\n")}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
1202
1159
  }
1203
1160
  function createClientEntryModule(options = {}) {
@@ -1206,9 +1163,15 @@ import '@clarify-labs/renderer/style.css';
1206
1163
  import { render } from '@clarify-labs/renderer/client';
1207
1164
  import { routes, navigation } from '${VIRTUAL_ROUTES}';
1208
1165
  import { config } from '${VIRTUAL_CONFIG}';
1209
- import { openApis } from '${VIRTUAL_OPENAPI}';
1166
+ import { openApiSpecs } from '${VIRTUAL_OPENAPI}';
1210
1167
  import { runtimeSlots } from '${VIRTUAL_SLOTS}';
1211
- render({ config, routes, navigation, openApis, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
1168
+ const renderOptions = { config, routes, navigation, openApiSpecs, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} };
1169
+ render(renderOptions);
1170
+ if (import.meta.hot) {
1171
+ import.meta.hot.accept('${VIRTUAL_ROUTES}', (mod) => {
1172
+ render({ ...renderOptions, routes: mod.routes, navigation: mod.navigation });
1173
+ });
1174
+ }`;
1212
1175
  }
1213
1176
  /**
1214
1177
  * Collect every plugin's `slots` declarations into a single runtime registry
@@ -1237,7 +1200,7 @@ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
1237
1200
  }
1238
1201
  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.`);
1239
1202
  const componentPath = root + slot.component;
1240
- const entry = `{ plugin: ${JSON.stringify(plugin)}, component: () => import(${moduleSpecifier(componentPath)}) }`;
1203
+ const entry = `{ plugin: ${JSON.stringify(plugin)}, loadComponent: () => import(${moduleSpecifier(componentPath)}) }`;
1241
1204
  const list = grouped.get(slot.name) ?? [];
1242
1205
  list.push(entry);
1243
1206
  grouped.set(slot.name, list);
@@ -1266,18 +1229,34 @@ function buildVirtualModules(args) {
1266
1229
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, "client"));
1267
1230
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, "server"));
1268
1231
  modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
1269
- modules.set(VIRTUAL_OPENAPI, "export const openApis = {};");
1232
+ modules.set(VIRTUAL_OPENAPI, "export const openApiSpecs = {};");
1270
1233
  modules.set(VIRTUAL_SLOT, "export { useSlot } from '@clarify-labs/renderer';\n");
1271
1234
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1272
1235
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1273
1236
  for (const route of args.routes) {
1274
- const moduleContent = route.diagnostic ? generateMdxErrorModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.filePath)};`;
1275
- modules.set(route.virtualModuleId, moduleContent);
1237
+ const moduleContent = route.diagnostic ? generateMdxErrorModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1238
+ modules.set(route.module.virtualModuleId, moduleContent);
1276
1239
  }
1277
1240
  return modules;
1278
1241
  }
1279
1242
  //#endregion
1280
1243
  //#region source/core/adapters.ts
1244
+ function routeStructure(route) {
1245
+ const { content: _content, ...source } = route.source;
1246
+ return {
1247
+ ...route,
1248
+ source
1249
+ };
1250
+ }
1251
+ function createDevStructureSnapshot(routes, navigation) {
1252
+ return {
1253
+ routes: JSON.stringify(routes.map(routeStructure)),
1254
+ navigation: JSON.stringify(navigation)
1255
+ };
1256
+ }
1257
+ function hasDevStructureChanged(before, after) {
1258
+ return before.routes !== after.routes || before.navigation !== after.navigation;
1259
+ }
1281
1260
  function invalidateVirtualModules(engine, server) {
1282
1261
  for (const id of engine.modules.keys()) {
1283
1262
  const moduleNode = server.moduleGraph.getModuleById(resolveVirtualId(id));
@@ -1285,20 +1264,22 @@ function invalidateVirtualModules(engine, server) {
1285
1264
  }
1286
1265
  }
1287
1266
  async function refreshDevServer(engine, server) {
1267
+ const before = createDevStructureSnapshot(engine.routes, engine.navigation);
1288
1268
  await engine.refresh();
1289
1269
  invalidateVirtualModules(engine, server);
1270
+ return hasDevStructureChanged(before, createDevStructureSnapshot(engine.routes, engine.navigation));
1290
1271
  }
1291
- function createNormalizedMdxContentPlugin(engine) {
1272
+ function createNormalizedContentPlugin(engine) {
1292
1273
  return {
1293
- name: "clarify:normalized-mdx-content",
1274
+ name: "clarify:normalized-content",
1294
1275
  enforce: "pre",
1295
1276
  transform(_code, id) {
1296
1277
  if (!/\.mdx?(?:\?|$)/.test(id)) return null;
1297
1278
  const filePath = id.replace(/\?.*$/, "");
1298
- const route = engine.routes.find((route) => route.kind === "mdx" && route.filePath === filePath);
1299
- if (!route || route.content === void 0) return null;
1279
+ const route = engine.routes.find((route) => route.kind === "mdx" && route.source.filePath === filePath);
1280
+ if (!route || route.source.content === void 0) return null;
1300
1281
  return {
1301
- code: route.content,
1282
+ code: route.source.content,
1302
1283
  map: null
1303
1284
  };
1304
1285
  }
@@ -1306,14 +1287,26 @@ function createNormalizedMdxContentPlugin(engine) {
1306
1287
  }
1307
1288
  function createMdxPlugin() {
1308
1289
  return mdxPlugin({
1309
- include: ["**/*.{md,mdx}"],
1290
+ include: ["**/*.mdx"],
1291
+ format: "mdx",
1310
1292
  jsxImportSource: "react",
1311
1293
  providerImportSource: "@clarify-labs/renderer",
1312
1294
  remarkPlugins,
1313
1295
  rehypePlugins
1314
1296
  });
1315
1297
  }
1316
- function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1298
+ function createMarkdownPlugin() {
1299
+ return mdxPlugin({
1300
+ include: ["**/*.md"],
1301
+ format: "md",
1302
+ jsxImportSource: "react",
1303
+ providerImportSource: "@clarify-labs/renderer",
1304
+ remarkPlugins,
1305
+ remarkRehypeOptions: { allowDangerousHtml: true },
1306
+ rehypePlugins: [rehypeRaw, ...rehypePlugins]
1307
+ });
1308
+ }
1309
+ function createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx) {
1317
1310
  let viteConfig;
1318
1311
  return {
1319
1312
  name: "clarify:core",
@@ -1352,8 +1345,13 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1352
1345
  }
1353
1346
  const relativeContentFile = relative(engine.contentRoot, changedFile);
1354
1347
  if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !isAbsolute(relativeContentFile)) || !engine.hasContentRouteForFile(changedFile)) return;
1355
- await refreshDevServer(engine, ctx.server);
1356
- return [];
1348
+ if (await refreshDevServer(engine, ctx.server)) {
1349
+ const routesModule = ctx.server.moduleGraph.getModuleById(resolveVirtualId(VIRTUAL_ROUTES));
1350
+ if (routesModule) return [...ctx.modules, routesModule];
1351
+ ctx.server.ws.send({ type: "full-reload" });
1352
+ return [];
1353
+ }
1354
+ return ctx.modules;
1357
1355
  },
1358
1356
  resolveId(id) {
1359
1357
  return resolveVirtualModuleId(id, engine.modules, engine.routes);
@@ -1364,18 +1362,6 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1364
1362
  async configureServer(server) {
1365
1363
  server.watcher.add(engine.contentRoot);
1366
1364
  if (engine.configFilePath) server.watcher.add(engine.configFilePath);
1367
- let reloadQueued = false;
1368
- const invalidateAndReload = () => {
1369
- if (reloadQueued) return;
1370
- reloadQueued = true;
1371
- queueMicrotask(() => {
1372
- reloadQueued = false;
1373
- invalidateVirtualModules(engine, server);
1374
- server.ws.send({ type: "full-reload" });
1375
- });
1376
- };
1377
- engine.ctx.onRoutesChange(invalidateAndReload);
1378
- engine.ctx.onNavigationChange(invalidateAndReload);
1379
1365
  server.middlewares.use((req, res, next) => {
1380
1366
  if (req.url === "/dev/query-preview-route" && req.method === "POST") return handleDevRouteRequest(req, res, engine.routes, engine.runtimeContext());
1381
1367
  if (req.url === "/dev/project-info" && (req.method === "GET" || req.method === "HEAD")) return handleProjectInfoRequest(req, res, engine.runtimeContext());
@@ -1385,7 +1371,7 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1385
1371
  const changedFile = isAbsolute(filePath) ? filePath : join(engine.root, filePath);
1386
1372
  const relativeContentFile = relative(engine.contentRoot, changedFile);
1387
1373
  if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !isAbsolute(relativeContentFile))) return;
1388
- await refreshDevServer(engine, server);
1374
+ if (await refreshDevServer(engine, server)) server.ws.send({ type: "full-reload" });
1389
1375
  };
1390
1376
  server.watcher.on("add", handleContentTreeChange);
1391
1377
  server.watcher.on("unlink", handleContentTreeChange);
@@ -1422,7 +1408,8 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1422
1408
  outputDirectory: viteConfig.build.outDir,
1423
1409
  ssrPlugins: [
1424
1410
  engine.createSSGVirtualPlugin(),
1425
- normalizedMdxContentPlugin,
1411
+ normalizedContentPlugin,
1412
+ markdown,
1426
1413
  mdx
1427
1414
  ]
1428
1415
  });
@@ -1439,20 +1426,22 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1439
1426
  * site itself - it only bridges Vite lifecycle hooks to the engine.
1440
1427
  */
1441
1428
  function createViteAdapter(engine) {
1442
- const normalizedMdxContentPlugin = createNormalizedMdxContentPlugin(engine);
1429
+ const normalizedContentPlugin = createNormalizedContentPlugin(engine);
1430
+ const markdown = createMarkdownPlugin();
1443
1431
  const mdx = createMdxPlugin();
1444
1432
  const tailwind = tailwindcss();
1445
1433
  return [
1446
1434
  ...react(),
1447
1435
  ...Array.isArray(tailwind) ? tailwind : [tailwind],
1448
- normalizedMdxContentPlugin,
1449
- createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx),
1436
+ normalizedContentPlugin,
1437
+ createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx),
1438
+ markdown,
1450
1439
  mdx
1451
1440
  ];
1452
1441
  }
1453
1442
  var cliPackageVersion = {
1454
1443
  name: "@clarify-labs/cli",
1455
- version: "0.10.0",
1444
+ version: "0.10.2",
1456
1445
  license: "AGPL-3.0-only",
1457
1446
  type: "module",
1458
1447
  repository: {
@@ -1493,6 +1482,7 @@ var cliPackageVersion = {
1493
1482
  "@clarify-labs/templates": "workspace:*",
1494
1483
  "@mdx-js/mdx": "^3.1.1",
1495
1484
  "@mdx-js/rollup": "^3.1.1",
1485
+ "@shikijs/rehype": "^4.3.0",
1496
1486
  "@tailwindcss/vite": "^4.3.2",
1497
1487
  "@vitejs/plugin-react": "^6.0.3",
1498
1488
  "cac": "^7.0.0",
@@ -1507,6 +1497,8 @@ var cliPackageVersion = {
1507
1497
  "react-router": "^7.0.0",
1508
1498
  "react-router-dom": "^7.0.0",
1509
1499
  "react-zoom-pan-pinch": "^4.0.3",
1500
+ "rehype-raw": "^7.0.0",
1501
+ "rehype-slug": "^6.0.0",
1510
1502
  "remark": "^15.0.1",
1511
1503
  "remark-gfm": "^4.0.1",
1512
1504
  "shiki": "4.3.0",
@@ -1558,11 +1550,14 @@ function routeToOpenAPIArtifactUrl(routePath) {
1558
1550
  return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi.json`;
1559
1551
  }
1560
1552
  function attachContentArtifactUrls(routes) {
1561
- for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path);
1553
+ for (const route of routes) route.artifacts = {
1554
+ ...route.artifacts,
1555
+ contentArtifactUrl: route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path)
1556
+ };
1562
1557
  }
1563
1558
  function readRouteContent(route) {
1564
- if (route.content !== void 0) return route.content;
1565
- throw new Error(`Route content is missing from route context: ${route.filePath}`);
1559
+ if (route.source.content !== void 0) return route.source.content;
1560
+ throw new Error(`Route content is missing from route context: ${route.source.filePath}`);
1566
1561
  }
1567
1562
  function readRouteArtifactContent(route) {
1568
1563
  const content = readRouteContent(route);
@@ -1572,16 +1567,17 @@ function isLlmsTxtRoute(route) {
1572
1567
  return !route.path.split("/").includes("404");
1573
1568
  }
1574
1569
  function llmsTxtDescription(route) {
1575
- if (route.description) return route.description;
1576
- const sections = route.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
1570
+ if (route.meta.description) return route.meta.description;
1571
+ const sections = route.meta.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
1577
1572
  if (sections?.length) return `Covers ${sections.join(", ")}.`;
1578
- if (route.keywords?.length) return `Related topics: ${route.keywords.join(", ")}.`;
1573
+ if (route.meta.keywords?.length) return `Related topics: ${route.meta.keywords.join(", ")}.`;
1579
1574
  if (route.kind === "openapi") return "OpenAPI artifact for machine-readable API reference data.";
1580
1575
  }
1581
1576
  function llmsTxtListItem(route, basePath) {
1582
- if (!route.contentArtifactUrl) return void 0;
1577
+ const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
1578
+ if (!contentArtifactUrl) return void 0;
1583
1579
  const description = llmsTxtDescription(route);
1584
- return description ? `- [${route.title}](${basePath}${route.contentArtifactUrl}): ${description}` : `- [${route.title}](${basePath}${route.contentArtifactUrl})`;
1580
+ return description ? `- [${route.meta.title}](${basePath}${contentArtifactUrl}): ${description}` : `- [${route.meta.title}](${basePath}${contentArtifactUrl})`;
1585
1581
  }
1586
1582
  function llmsTxtLocaleLabel(locale, projectConfig) {
1587
1583
  return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
@@ -1598,7 +1594,7 @@ function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
1598
1594
  const routesBySource = /* @__PURE__ */ new Map();
1599
1595
  const defaultLocale = projectConfig.i18n?.defaultLocale;
1600
1596
  for (const route of routes) {
1601
- const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.contentArtifactUrl ?? route.path}`;
1597
+ const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.artifacts?.contentArtifactUrl ?? route.path}`;
1602
1598
  const previousRoute = routesBySource.get(sourceKey);
1603
1599
  if (!previousRoute || previousRoute.locale && !route.locale) routesBySource.set(sourceKey, route);
1604
1600
  }
@@ -1667,9 +1663,9 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
1667
1663
  }
1668
1664
  if (contentPath.endsWith(".openapi.yaml")) {
1669
1665
  const jsonPath = contentPath.replace(/\.yaml$/, ".json");
1670
- const route = routes.find((r) => r.contentArtifactUrl === jsonPath);
1671
- if (route?.kind === "openapi" && route.content) {
1672
- const spec = JSON.parse(route.content);
1666
+ const route = routes.find((route) => route.artifacts?.contentArtifactUrl === jsonPath);
1667
+ if (route?.kind === "openapi" && route.source.content) {
1668
+ const spec = JSON.parse(route.source.content);
1673
1669
  res.statusCode = 200;
1674
1670
  res.setHeader("Content-Type", "text/yaml; charset=utf-8");
1675
1671
  res.end(stringify(spec, { lineWidth: 0 }), "utf8");
@@ -1677,7 +1673,7 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
1677
1673
  }
1678
1674
  return false;
1679
1675
  }
1680
- const route = routes.find((route) => route.contentArtifactUrl === contentPath);
1676
+ const route = routes.find((route) => route.artifacts?.contentArtifactUrl === contentPath);
1681
1677
  if (!route) return false;
1682
1678
  res.statusCode = 200;
1683
1679
  res.setHeader("Content-Type", resolveContentArtifactType(route));
@@ -1703,14 +1699,15 @@ function createContentArtifactsPlugin() {
1703
1699
  "build:assets": (ctx) => {
1704
1700
  const assets = [];
1705
1701
  for (const route of ctx.routes) {
1706
- if (!route.contentArtifactUrl) continue;
1702
+ const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
1703
+ if (!contentArtifactUrl) continue;
1707
1704
  assets.push({
1708
- fileName: route.contentArtifactUrl.replace(/^\//, ""),
1705
+ fileName: contentArtifactUrl.replace(/^\//, ""),
1709
1706
  source: readRouteArtifactContent(route)
1710
1707
  });
1711
- if (route.kind === "openapi" && route.content) {
1712
- const yamlFileName = route.contentArtifactUrl.replace(/\.json$/, ".yaml");
1713
- const spec = JSON.parse(route.content);
1708
+ if (route.kind === "openapi" && route.source.content) {
1709
+ const yamlFileName = contentArtifactUrl.replace(/\.json$/, ".yaml");
1710
+ const spec = JSON.parse(route.source.content);
1714
1711
  assets.push({
1715
1712
  fileName: yamlFileName.replace(/^\//, ""),
1716
1713
  source: stringify(spec, { lineWidth: 0 })
@@ -1796,6 +1793,7 @@ var OPENAPI_HTTP_METHODS = [
1796
1793
  "patch",
1797
1794
  "trace"
1798
1795
  ];
1796
+ var CLARIFY_OPENAPI_SECTION_ID_EXTENSION = "x-clarify-section-id";
1799
1797
  function errorMessage(error) {
1800
1798
  return error instanceof Error ? error.message : String(error);
1801
1799
  }
@@ -1822,10 +1820,10 @@ function findOpenAPIRoutes(dir, base = dir) {
1822
1820
  routes.push({
1823
1821
  path: cleanPath,
1824
1822
  basePath: cleanPath,
1825
- filePath: fullPath,
1826
- virtualModuleId: virtualModuleIdFromRef(ref),
1827
- title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
1828
- kind: "openapi"
1823
+ kind: "openapi",
1824
+ meta: { title: kebabToTitle(cleanPath.split("/").pop() ?? "API") },
1825
+ module: { virtualModuleId: virtualModuleIdFromRef(ref) },
1826
+ source: { filePath: fullPath }
1829
1827
  });
1830
1828
  }
1831
1829
  return routes;
@@ -1834,36 +1832,80 @@ function operationMatchesTags(operationTags, filterTags) {
1834
1832
  if (!filterTags?.length) return true;
1835
1833
  return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
1836
1834
  }
1835
+ function filterPathItemsByTags(items, tags) {
1836
+ const filteredItems = {};
1837
+ for (const path of Object.keys(items)) {
1838
+ const pathItem = items[path];
1839
+ if (!pathItem) continue;
1840
+ if (OPENAPI_HTTP_METHODS.map((method) => pathItem[method]).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredItems[path] = pathItem;
1841
+ }
1842
+ return filteredItems;
1843
+ }
1844
+ function createOperationSectionId(operation, method, path, kind) {
1845
+ return (typeof operation.operationId === "string" ? operation.operationId.trim() : "") || slug(`${kind === "webhook" ? "webhook " : ""}${method.toLowerCase()} ${path}`);
1846
+ }
1847
+ function operationSectionId(operation) {
1848
+ const sectionId = operation[CLARIFY_OPENAPI_SECTION_ID_EXTENSION];
1849
+ return typeof sectionId === "string" ? sectionId : "";
1850
+ }
1851
+ function normalizeOpenAPISpecSectionIds(spec) {
1852
+ const items = [...Object.entries(spec.paths ?? {}).map(([path, pathItem]) => ({
1853
+ path,
1854
+ pathItem,
1855
+ kind: "path"
1856
+ })), ...Object.entries(spec.webhooks ?? {}).map(([path, pathItem]) => ({
1857
+ path,
1858
+ pathItem,
1859
+ kind: "webhook"
1860
+ }))];
1861
+ for (const { path, pathItem, kind } of items) {
1862
+ if (!pathItem || typeof pathItem !== "object") continue;
1863
+ const operations = pathItem;
1864
+ for (const method of OPENAPI_HTTP_METHODS) {
1865
+ const op = operations[method];
1866
+ if (!op || typeof op !== "object") continue;
1867
+ op[CLARIFY_OPENAPI_SECTION_ID_EXTENSION] = createOperationSectionId(op, method, path, kind);
1868
+ }
1869
+ }
1870
+ return spec;
1871
+ }
1837
1872
  /** Filter an OpenAPI spec to only include paths that have at least one operation matching the given tags. */
1838
1873
  function filterSpecByTags(spec, tags) {
1839
- const filteredPaths = {};
1840
1874
  const paths = spec.paths ?? {};
1841
- for (const path of Object.keys(paths)) {
1842
- const pathItem = paths[path];
1843
- if (!pathItem) continue;
1844
- if (Object.values(pathItem).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredPaths[path] = pathItem;
1845
- }
1875
+ const webhooks = spec.webhooks ?? {};
1846
1876
  return {
1847
1877
  ...spec,
1848
- paths: filteredPaths
1878
+ paths: filterPathItemsByTags(paths, tags),
1879
+ webhooks: filterPathItemsByTags(webhooks, tags)
1849
1880
  };
1850
1881
  }
1851
1882
  /** Extract endpoint operations from an OpenAPI spec as page sections. */
1852
1883
  function extractOpenAPISections(spec, filterTags) {
1884
+ normalizeOpenAPISpecSectionIds(spec);
1853
1885
  const sections = [];
1854
- const paths = spec.paths ?? {};
1855
- for (const [path, pathItem] of Object.entries(paths)) {
1886
+ const items = [...Object.entries(spec.paths ?? {}).map(([path, pathItem]) => ({
1887
+ path,
1888
+ pathItem,
1889
+ kind: "path"
1890
+ })), ...Object.entries(spec.webhooks ?? {}).map(([path, pathItem]) => ({
1891
+ path,
1892
+ pathItem,
1893
+ kind: "webhook"
1894
+ }))];
1895
+ for (const { path, pathItem, kind } of items) {
1856
1896
  if (!pathItem) continue;
1897
+ const operations = pathItem;
1857
1898
  for (const method of OPENAPI_HTTP_METHODS) {
1858
- const op = pathItem[method];
1859
- if (!op || !operationMatchesTags(op.tags, filterTags)) continue;
1860
- const title = op.summary ?? `${method.toUpperCase()} ${path}`;
1899
+ const op = operations[method];
1900
+ const tags = Array.isArray(op?.tags) ? op.tags : void 0;
1901
+ if (!op || !operationMatchesTags(tags, filterTags)) continue;
1902
+ const title = typeof op.summary === "string" ? op.summary : `${method.toUpperCase()} ${path}`;
1861
1903
  sections.push({
1862
- id: slug(`${method} ${path}`),
1904
+ id: operationSectionId(op),
1863
1905
  title,
1864
1906
  level: 2,
1865
- badge: method.toUpperCase(),
1866
- tags: op.tags
1907
+ badge: kind === "webhook" ? "WEBHOOK" : method.toUpperCase(),
1908
+ tags
1867
1909
  });
1868
1910
  }
1869
1911
  }
@@ -1905,12 +1947,16 @@ async function readOpenAPISpec(filePath, contentProcessor) {
1905
1947
  //#endregion
1906
1948
  //#region source/plugins/openapi/virtual-modules.ts
1907
1949
  var openApiRegistryModuleId = VIRTUAL_OPENAPI;
1950
+ var openApiServerRegistryModuleId = VIRTUAL_OPENAPI_SERVER;
1908
1951
  var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
1909
1952
  function specVirtualModuleId(specKey) {
1910
1953
  return `${OPENAPI_SPEC_PREFIX}${specKey}`;
1911
1954
  }
1912
- function generateOpenAPIRegistryModule(openApis) {
1913
- return `export const openApis = ${JSON.stringify(openApis)};`;
1955
+ function generateOpenAPIRegistryModule(openApiSpecs) {
1956
+ return `export const openApiSpecs = {\n${Object.entries(openApiSpecs).map(([key, moduleId]) => ` ${JSON.stringify(key)}: () => import(${JSON.stringify(moduleId)}),`).join("\n")}\n};`;
1957
+ }
1958
+ function generateOpenAPIServerRegistryModule(openApiSpecs) {
1959
+ return `export const openApiSpecs = ${JSON.stringify(openApiSpecs)};`;
1914
1960
  }
1915
1961
  /** Generate a virtual module that exports a single spec as default. */
1916
1962
  function generateOpenAPISpecModule(spec) {
@@ -1918,13 +1964,11 @@ function generateOpenAPISpecModule(spec) {
1918
1964
  }
1919
1965
  function generateOpenAPIPageModule(opts) {
1920
1966
  return `import { createOpenApiRouteComponent } from '@clarify-labs/renderer';
1967
+ import spec from ${JSON.stringify(opts.specModuleId)};
1921
1968
 
1922
- export const routeData = ${JSON.stringify({
1923
- spec: opts.spec,
1924
- tagFilter: opts.tagFilter
1925
- })};
1969
+ export const routeData = {};
1926
1970
 
1927
- export default createOpenApiRouteComponent(routeData);
1971
+ export default createOpenApiRouteComponent({ ...routeData, spec });
1928
1972
  `;
1929
1973
  }
1930
1974
  function generateOpenAPIErrorModule(diagnostic) {
@@ -1943,115 +1987,212 @@ export default createContentDiagnosticComponent(contentDiagnostic);
1943
1987
  }
1944
1988
  //#endregion
1945
1989
  //#region source/plugins/openapi/index.ts
1946
- function collectOpenAPIPageItems(config) {
1947
- const items = [];
1990
+ function collectOpenAPIPageIntents(config) {
1991
+ const intents = [];
1948
1992
  function visitPages(pages) {
1949
1993
  if (!pages || pages === "FileTree") return;
1950
- 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);
1994
+ for (const group of pages) for (const item of group.pages) {
1995
+ const intent = routeIntentFromPagesItem(item);
1996
+ if (intent.kind === "openapi" && (intent.path || intent.tagFilter?.length)) intents.push(intent);
1997
+ }
1951
1998
  }
1952
1999
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
1953
- return items;
2000
+ return intents;
1954
2001
  }
1955
2002
  function virtualModuleIdFromPath(path) {
1956
2003
  return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
1957
2004
  }
2005
+ function cloneOpenAPIRoute(route, overrides = {}) {
2006
+ return {
2007
+ ...route,
2008
+ meta: {
2009
+ ...route.meta,
2010
+ sections: route.meta.sections ? [...route.meta.sections] : void 0
2011
+ },
2012
+ module: { ...route.module },
2013
+ source: { ...route.source },
2014
+ openapi: route.openapi ? {
2015
+ ...route.openapi,
2016
+ tagFilter: route.openapi.tagFilter ? [...route.openapi.tagFilter] : void 0
2017
+ } : void 0,
2018
+ alternates: route.alternates ? { ...route.alternates } : void 0,
2019
+ ...overrides
2020
+ };
2021
+ }
2022
+ function findOpenAPIRoutesForContentRoot(contentRoot, i18n) {
2023
+ if (!i18n) return findOpenAPIRoutes(contentRoot);
2024
+ const localizedRoutes = [];
2025
+ for (const locale of i18n.locales) {
2026
+ const localeRoot = join(contentRoot, locale.code);
2027
+ for (const route of findOpenAPIRoutes(localeRoot, localeRoot)) {
2028
+ const basePath = route.basePath ?? route.path;
2029
+ const path = localizedRoutePath(basePath, locale.code, i18n);
2030
+ localizedRoutes.push({
2031
+ ...route,
2032
+ path,
2033
+ basePath,
2034
+ locale: locale.code,
2035
+ module: { virtualModuleId: virtualModuleIdFromPath(path) }
2036
+ });
2037
+ }
2038
+ }
2039
+ if (i18n.missing === "fallback") {
2040
+ const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
2041
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
2042
+ for (const sourceRoute of defaultRoutes) {
2043
+ const basePath = sourceRoute.basePath ?? sourceRoute.path;
2044
+ for (const locale of i18n.locales) {
2045
+ const key = `${locale.code}:${basePath}`;
2046
+ if (routeByLocaleAndBase.has(key)) continue;
2047
+ const path = localizedRoutePath(basePath, locale.code, i18n);
2048
+ localizedRoutes.push(cloneOpenAPIRoute(sourceRoute, {
2049
+ path,
2050
+ locale: locale.code,
2051
+ module: { virtualModuleId: virtualModuleIdFromPath(path) },
2052
+ isFallback: true
2053
+ }));
2054
+ }
2055
+ }
2056
+ }
2057
+ return localizedRoutes;
2058
+ }
1958
2059
  function createConfiguredOpenAPIRoutes(routes, config) {
1959
- const pageItems = collectOpenAPIPageItems(config);
1960
- if (!pageItems.length) return routes;
2060
+ const openAPIIntents = collectOpenAPIPageIntents(config);
2061
+ if (!openAPIIntents.length) return routes;
1961
2062
  const additions = [];
1962
2063
  const existingKeys = new Set(routes.map((route) => `${route.locale ?? ""}:${route.basePath ?? route.path}`));
1963
- for (const item of pageItems) {
1964
- const tagFilter = item.filter?.tags?.filter(Boolean);
1965
- const sourceBasePath = openAPIPagePathFromRef(item.openapi);
1966
- const targetBasePath = openAPIPagePathFromRef(item.openapi, tagFilter, item.path);
2064
+ for (const intent of openAPIIntents) {
2065
+ const tagFilter = intent.tagFilter?.filter(Boolean);
2066
+ const sourceBasePath = openAPIPagePathFromRef(intent.ref);
2067
+ const targetBasePath = openAPIPagePathFromRef(intent.ref, tagFilter, intent.path);
1967
2068
  if (sourceBasePath === targetBasePath && !tagFilter?.length) continue;
1968
2069
  for (const route of routes) {
1969
2070
  if (route.kind !== "openapi" || (route.basePath ?? route.path) !== sourceBasePath) continue;
1970
2071
  const key = `${route.locale ?? ""}:${targetBasePath}`;
1971
2072
  if (existingKeys.has(key)) continue;
1972
2073
  existingKeys.add(key);
1973
- additions.push({
1974
- ...route,
1975
- path: route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath,
2074
+ const path = route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath;
2075
+ additions.push(cloneOpenAPIRoute(route, {
2076
+ path,
1976
2077
  basePath: targetBasePath,
1977
- title: route.title,
1978
- virtualModuleId: virtualModuleIdFromPath(targetBasePath),
1979
- openapiTagFilter: tagFilter?.length ? tagFilter : void 0
1980
- });
2078
+ module: { virtualModuleId: virtualModuleIdFromPath(path) },
2079
+ openapi: {
2080
+ ...route.openapi,
2081
+ tagFilter: tagFilter?.length ? tagFilter : void 0
2082
+ }
2083
+ }));
1981
2084
  }
1982
2085
  }
1983
2086
  const nextRoutes = [...routes, ...additions];
1984
- return config.i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n)) : nextRoutes;
2087
+ if (!config.i18n) return nextRoutes;
2088
+ const routesWithAlternates = nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n));
2089
+ const bareAliases = [];
2090
+ const seenBare = new Set(routesWithAlternates.map((route) => route.path));
2091
+ for (const route of routesWithAlternates) {
2092
+ if (route.locale !== config.i18n.defaultLocale) continue;
2093
+ const basePath = route.basePath ?? route.path;
2094
+ if (basePath === route.path || seenBare.has(basePath)) continue;
2095
+ seenBare.add(basePath);
2096
+ bareAliases.push(cloneOpenAPIRoute(route, {
2097
+ path: basePath,
2098
+ module: { virtualModuleId: virtualModuleIdFromPath(basePath) },
2099
+ isBareAlias: true
2100
+ }));
2101
+ }
2102
+ return [...routesWithAlternates, ...bareAliases];
1985
2103
  }
1986
- /** Derive a stable, deduplicated key from an absolute spec file path. */
1987
- function specFileKeyFromPath(filePath, projectRoot) {
2104
+ /** Derive a stable, deduplicated id from an absolute spec file path. */
2105
+ function sourceSpecIdFromPath(filePath, projectRoot) {
1988
2106
  return relative(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1989
2107
  }
2108
+ function routeSpecModuleIdFromSourceSpecId(sourceSpecId, tagFilter) {
2109
+ if (!tagFilter?.length) return sourceSpecId;
2110
+ return `${sourceSpecId}_tags_${tagFilter.map((tag) => tag.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "tag").join("_")}`;
2111
+ }
2112
+ function openAPIRegistryKeys(route) {
2113
+ const keys = /* @__PURE__ */ new Set();
2114
+ const path = (route.path || "/").replace(/^\//, "");
2115
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
2116
+ if (route.locale) keys.add(`virtual:clarify-page/${route.locale}/${base}`);
2117
+ if (path) keys.add(`virtual:clarify-page/${path}`);
2118
+ else keys.add("virtual:clarify-page/index");
2119
+ if (!route.locale && base) keys.add(`virtual:clarify-page/${base}`);
2120
+ return [...keys];
2121
+ }
1990
2122
  function createOpenAPIPlugin() {
1991
- /** specs keyed by specFileKey (deduplicated per source file). */
2123
+ /** specs keyed by sourceSpecId (deduplicated per source file). */
1992
2124
  const specs = /* @__PURE__ */ new Map();
2125
+ const specModules = /* @__PURE__ */ new Map();
1993
2126
  return {
1994
2127
  name: "clarify:openapi",
1995
2128
  hooks: {
1996
- "routes:discover": (input) => ({
2129
+ "routes:discover": (input, ctx) => ({
1997
2130
  ...input,
1998
- routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
2131
+ routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.i18n)]
1999
2132
  }),
2000
2133
  "routes:discovered": async (routes, ctx) => {
2001
2134
  specs.clear();
2135
+ specModules.clear();
2002
2136
  const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
2003
2137
  const specByFilePath = /* @__PURE__ */ new Map();
2004
2138
  for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
2005
- const specFromCache = specByFilePath.get(route.filePath);
2139
+ const specFromCache = specByFilePath.get(route.source.filePath);
2006
2140
  const processor = createProjectContentProcessor(ctx.plugins, ctx);
2007
2141
  const result = specFromCache ? {
2008
2142
  ok: true,
2009
2143
  spec: specFromCache
2010
- } : await readOpenAPISpec(route.filePath, processor);
2144
+ } : await readOpenAPISpec(route.source.filePath, processor);
2011
2145
  if (!result.ok) {
2012
2146
  route.diagnostic = result.diagnostic;
2013
- route.title = route.title || "OpenAPI parse error";
2014
- route.sections = [];
2147
+ route.meta.title = route.meta.title || "OpenAPI parse error";
2148
+ route.meta.sections = [];
2015
2149
  continue;
2016
2150
  }
2017
- const spec = result.spec;
2018
- specByFilePath.set(route.filePath, spec);
2019
- const specKey = specFileKeyFromPath(route.filePath, ctx.generateOptions.projectRoot);
2020
- route.specFileKey = specKey;
2021
- specs.set(specKey, {
2151
+ const spec = normalizeOpenAPISpecSectionIds(result.spec);
2152
+ specByFilePath.set(route.source.filePath, spec);
2153
+ const sourceSpecId = sourceSpecIdFromPath(route.source.filePath, ctx.generateOptions.projectRoot);
2154
+ route.openapi = {
2155
+ ...route.openapi,
2156
+ sourceSpecId
2157
+ };
2158
+ specs.set(sourceSpecId, {
2022
2159
  spec,
2023
- filePath: route.filePath
2160
+ filePath: route.source.filePath
2024
2161
  });
2025
- route.title = spec.info?.title ?? route.title;
2026
- route.description = spec.info?.description ?? route.description;
2027
- route.sections = extractOpenAPISections(spec, route.openapiTagFilter);
2028
- const pageSpec = route.openapiTagFilter?.length ? filterSpecByTags(spec, route.openapiTagFilter) : spec;
2029
- route.content = JSON.stringify(pageSpec);
2162
+ route.meta.title = spec.info?.title ?? route.meta.title;
2163
+ route.meta.description = spec.info?.description ?? route.meta.description;
2164
+ route.meta.sections = extractOpenAPISections(spec, route.openapi?.tagFilter);
2165
+ const pageSpec = route.openapi?.tagFilter?.length ? filterSpecByTags(spec, route.openapi.tagFilter) : spec;
2166
+ const routeSpecModuleId = routeSpecModuleIdFromSourceSpecId(sourceSpecId, route.openapi?.tagFilter);
2167
+ route.openapi.routeSpecModuleId = routeSpecModuleId;
2168
+ specModules.set(routeSpecModuleId, pageSpec);
2169
+ route.source.content = JSON.stringify(pageSpec);
2030
2170
  }
2031
2171
  return nextRoutes;
2032
2172
  },
2033
2173
  "modules:before": (modules, ctx) => {
2034
2174
  const registryEntries = {};
2175
+ const serverRegistryEntries = {};
2035
2176
  for (const route of ctx.routes) {
2036
- if (route.kind !== "openapi" || !route.specFileKey) continue;
2037
- const spec = specs.get(route.specFileKey)?.spec;
2177
+ if (route.kind !== "openapi" || !route.openapi?.sourceSpecId) continue;
2178
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2179
+ const spec = specModules.get(routeSpecModuleId) ?? specs.get(route.openapi.sourceSpecId)?.spec;
2038
2180
  if (!spec) continue;
2039
- const base = (route.basePath ?? route.path).replace(/^\//, "");
2040
- const registryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
2041
- registryEntries[registryKey] = spec;
2181
+ for (const registryKey of openAPIRegistryKeys(route)) {
2182
+ registryEntries[registryKey] = specVirtualModuleId(routeSpecModuleId);
2183
+ serverRegistryEntries[registryKey] = spec;
2184
+ }
2042
2185
  }
2043
2186
  modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
2044
- for (const [specKey, entry] of specs) modules.set(specVirtualModuleId(specKey), generateOpenAPISpecModule(entry.spec));
2187
+ modules.set(openApiServerRegistryModuleId, generateOpenAPIServerRegistryModule(serverRegistryEntries));
2188
+ for (const [routeSpecModuleId, spec] of specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2045
2189
  for (const route of ctx.routes) {
2046
2190
  if (route.kind !== "openapi") continue;
2047
- if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
2048
- else if (route.specFileKey) {
2049
- const entry = specs.get(route.specFileKey);
2050
- if (!entry) continue;
2051
- modules.set(route.virtualModuleId, generateOpenAPIPageModule({
2052
- spec: entry.spec,
2053
- tagFilter: route.openapiTagFilter
2054
- }));
2191
+ if (route.diagnostic) modules.set(route.module.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
2192
+ else if (route.openapi?.sourceSpecId) {
2193
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2194
+ if (!specModules.has(routeSpecModuleId)) continue;
2195
+ modules.set(route.module.virtualModuleId, generateOpenAPIPageModule({ specModuleId: specVirtualModuleId(routeSpecModuleId) }));
2055
2196
  }
2056
2197
  }
2057
2198
  return modules;
@@ -2119,11 +2260,11 @@ async function generatePagefindIndex(options, pagefind) {
2119
2260
  }
2120
2261
  function routeSearchContent(route) {
2121
2262
  return [
2122
- route.title,
2123
- route.description,
2124
- route.keywords?.join(" "),
2125
- route.sections?.map((section) => section.title).join(" "),
2126
- route.content
2263
+ route.meta.title,
2264
+ route.meta.description,
2265
+ route.meta.keywords?.join(" "),
2266
+ route.meta.sections?.map((section) => section.title).join(" "),
2267
+ route.source.content
2127
2268
  ].filter(Boolean).join("\n\n");
2128
2269
  }
2129
2270
  async function generateDevSearchIndex(ctx, root, pagefind) {
@@ -2145,7 +2286,7 @@ async function generateDevSearchIndex(ctx, root, pagefind) {
2145
2286
  url: route.path,
2146
2287
  content,
2147
2288
  language: toPagefindLanguage(route.locale ?? ctx.projectConfig.i18n?.defaultLocale),
2148
- meta: { title: route.title }
2289
+ meta: { title: route.meta.title }
2149
2290
  });
2150
2291
  assertNoPagefindErrors(`addCustomRecord ${route.path}`, result.errors);
2151
2292
  pageCount += 1;
@@ -2302,15 +2443,18 @@ function normalizeRepositoryUrl(repository) {
2302
2443
  function encodePath(path) {
2303
2444
  return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
2304
2445
  }
2305
- function createSourceUrl(filePath, contentRoot, source) {
2446
+ function createSourceEditUrl(filePath, contentRoot, source) {
2306
2447
  const repository = normalizeRepositoryUrl(source.repository);
2307
2448
  const branch = source.branch ?? "main";
2308
2449
  const sourcePath = [source.directory ? trimSlashes(source.directory) : "", relative(contentRoot, filePath).replaceAll("\\", "/")].filter(Boolean).join("/");
2309
2450
  return `${repository}/edit/${encodeURIComponent(branch)}/${encodePath(sourcePath)}`;
2310
2451
  }
2311
- function attachSourceUrls(routes, contentRoot, source) {
2452
+ function attachSourceEditUrls(routes, contentRoot, source) {
2312
2453
  if (!source) return;
2313
- for (const route of routes) route.sourceUrl = createSourceUrl(route.filePath, contentRoot, source);
2454
+ for (const route of routes) route.source = {
2455
+ ...route.source,
2456
+ sourceEditUrl: createSourceEditUrl(route.source.filePath, contentRoot, source)
2457
+ };
2314
2458
  }
2315
2459
  //#endregion
2316
2460
  //#region source/plugins/source-links/index.ts
@@ -2318,7 +2462,7 @@ function createSourceLinksPlugin() {
2318
2462
  return {
2319
2463
  name: "clarify:source-links",
2320
2464
  hooks: { "routes:discovered": (routes, ctx) => {
2321
- attachSourceUrls(routes, join(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
2465
+ attachSourceEditUrls(routes, join(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
2322
2466
  return routes;
2323
2467
  } }
2324
2468
  };
@@ -2475,9 +2619,9 @@ var CLARIFY_ENV_TEMPLATE = `// This file is generated by Clarify. Do not edit it
2475
2619
  // plugin and slot code, so the TypeScript language service can resolve them.
2476
2620
 
2477
2621
  declare module 'virtual:clarify/slot' {
2478
- import type { SlotContext } from '@clarify-labs/cli'
2622
+ import type { UISlotContext } from '@clarify-labs/cli'
2479
2623
 
2480
- export function useSlot(): SlotContext
2624
+ export function useSlot(): UISlotContext
2481
2625
  }
2482
2626
  `;
2483
2627
  function generateClarifyEnvDts(_plugins) {
@@ -2499,6 +2643,11 @@ function writeClarifyEnvDts(projectRoot, plugins) {
2499
2643
  writeFileSync(filePath, next, "utf-8");
2500
2644
  }
2501
2645
  //#endregion
2646
+ //#region source/parsers/markdown/utils.ts
2647
+ function escapeHtml(text) {
2648
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2649
+ }
2650
+ //#endregion
2502
2651
  //#region source/core/runtime/html-template.ts
2503
2652
  /**
2504
2653
  * HTML template manipulation helpers used during SSG.
@@ -2515,7 +2664,7 @@ function resolveProjectConfigFromContext(contextOrConfig) {
2515
2664
  }
2516
2665
  function routeTitle(contextOrConfig, route) {
2517
2666
  const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2518
- const title = route?.title?.trim();
2667
+ const title = route?.meta.title.trim();
2519
2668
  if (!title || title === projectConfig.title) return projectConfig.title;
2520
2669
  return `${title} - ${projectConfig.title}`;
2521
2670
  }
@@ -2563,8 +2712,8 @@ function injectSSRIntoTemplate(template, appHtml, contextOrConfig, route) {
2563
2712
  const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2564
2713
  let html = injectHtmlLocaleAttributes(template, contextOrConfig, route);
2565
2714
  html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(routeTitle(contextOrConfig, route))}</title>`);
2566
- html = setNamedMeta(html, "description", route?.description ?? projectConfig.description);
2567
- html = setNamedMeta(html, "keywords", route?.keywords?.join(", "));
2715
+ html = setNamedMeta(html, "description", route?.meta.description ?? projectConfig.description);
2716
+ html = setNamedMeta(html, "keywords", route?.meta.keywords?.join(", "));
2568
2717
  html = injectCanonicalUrl(html, contextOrConfig, route);
2569
2718
  const dataPagefindIgnore = route?.isBareAlias ? " data-pagefind-ignore" : "";
2570
2719
  html = html.replace(/<div id="root">([\s\S]*?)<\/div>/, `<div id="root"${dataPagefindIgnore}>${appHtml}</div>`);
@@ -2673,7 +2822,7 @@ function routeOutputFiles(outputDirectory, route) {
2673
2822
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
2674
2823
  import { routes, navigation } from 'virtual:clarify/routes/server';
2675
2824
  import { config } from 'virtual:clarify/config';
2676
- import { openApis } from 'virtual:clarify/openapi';
2825
+ import { openApiSpecs } from 'virtual:clarify/openapi/server';
2677
2826
  import { runtimeSlots } from 'virtual:clarify/slots';
2678
2827
 
2679
2828
  export async function render(url) {
@@ -2681,10 +2830,10 @@ export async function render(url) {
2681
2830
  // them synchronously (renderToString has no Suspense support).
2682
2831
  const entries = Object.values(runtimeSlots).flat()
2683
2832
  await Promise.all(entries.map(async (entry) => {
2684
- const mod = await entry.component()
2833
+ const mod = await entry.loadComponent()
2685
2834
  entry._resolved = mod.default
2686
2835
  }))
2687
- return renderToHTML({ config, routes, navigation, openApis, runtimeSlots, url, themeEditor: config.theme.editor });
2836
+ return renderToHTML({ config, routes, navigation, openApiSpecs, runtimeSlots, url, themeEditor: config.theme.editor });
2688
2837
  }`;
2689
2838
  function createTempEntryFile(content) {
2690
2839
  const entryPath = join(createClarifyTempDir("ssr"), "entry-server.ts");
@@ -2771,7 +2920,10 @@ var ClarifyContext = class {
2771
2920
  this.generateOptions = init.generateOptions;
2772
2921
  this.version = init.version;
2773
2922
  this.currentRoutes = init.routes ?? [];
2774
- this.currentNavigation = init.navigation ?? [];
2923
+ this.currentNavigation = init.navigation ?? {
2924
+ kind: "flat",
2925
+ nodes: []
2926
+ };
2775
2927
  this.currentPlugins = init.plugins ?? [];
2776
2928
  }
2777
2929
  get routes() {
@@ -2893,7 +3045,10 @@ var ClarifyEngine = class {
2893
3045
  generateOptions: placeholderOptions,
2894
3046
  version: cliPackageVersion,
2895
3047
  routes: [],
2896
- navigation: [],
3048
+ navigation: {
3049
+ kind: "flat",
3050
+ nodes: []
3051
+ },
2897
3052
  plugins: []
2898
3053
  });
2899
3054
  }
@@ -2996,9 +3151,9 @@ var ClarifyEngine = class {
2996
3151
  routes = await runPhase(plugins, "content:process", this.ctx, async () => {
2997
3152
  const pages = routes.map((route) => ({
2998
3153
  path: route.path,
2999
- filePath: route.filePath,
3000
- frontmatter: route.frontmatter ?? {},
3001
- content: route.content ?? ""
3154
+ filePath: route.source.filePath,
3155
+ frontmatter: route.source.frontmatter ?? {},
3156
+ content: route.source.content ?? ""
3002
3157
  }));
3003
3158
  const resolvedPages = await runHooks(plugins, "pages:resolved", pages, this.ctx);
3004
3159
  const pageByPath = new Map(resolvedPages.map((p) => [p.path, p]));
@@ -3007,13 +3162,22 @@ var ClarifyEngine = class {
3007
3162
  if (!page) return route;
3008
3163
  return {
3009
3164
  ...route,
3010
- frontmatter: page.frontmatter,
3011
- content: page.content
3165
+ source: {
3166
+ ...route.source,
3167
+ frontmatter: page.frontmatter,
3168
+ content: page.content
3169
+ }
3012
3170
  };
3013
3171
  });
3014
3172
  });
3015
3173
  routes = applyConfiguredPageRoutePaths(routes, tabs, i18n);
3016
- const navigation = tabs ? i18n ? buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) ?? {} : buildNavigationFromTabsConfig(routes, tabs) : buildNavigation(routes);
3174
+ const navigation = tabs ? i18n ? buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) ?? {
3175
+ kind: "localized-tabbed",
3176
+ locales: {}
3177
+ } : buildNavigationFromTabsConfig(routes, tabs) : {
3178
+ kind: "flat",
3179
+ nodes: buildNavigation(routes)
3180
+ };
3017
3181
  const resolved = await runHooks(plugins, "routes:resolved", {
3018
3182
  routes,
3019
3183
  navigation
@@ -3047,7 +3211,7 @@ var ClarifyEngine = class {
3047
3211
  writeClarifyEnvDts(this.root, this.plugins);
3048
3212
  }
3049
3213
  hasContentRouteForFile(filePath) {
3050
- return this.routes.some((route) => route.filePath === filePath);
3214
+ return this.routes.some((route) => route.source.filePath === filePath);
3051
3215
  }
3052
3216
  async collectBuildAssets() {
3053
3217
  if (!this.buildEnabled) return [];
@@ -3140,4 +3304,4 @@ function createClarifyEngine(options = {}) {
3140
3304
  //#endregion
3141
3305
  export { cliPackageVersion as a, defineConfig as c, createClarifyTempDir as i, createClarifyEngine as n, cliPackageVersionWithCaret as o, createClarifyRuntimeAliases as r, createViteAdapter as s, ClarifyEngine as t };
3142
3306
 
3143
- //# sourceMappingURL=engine-3f5Tpa-6.js.map
3307
+ //# sourceMappingURL=engine-CSSL_gGd.js.map