@clarify-labs/cli 0.10.0 → 0.10.1

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.
@@ -67,14 +67,14 @@ function renderPlainCode(code) {
67
67
  function isBundledLanguage(language) {
68
68
  return language in shiki.bundledLanguages;
69
69
  }
70
- var highlighter;
70
+ var highlighterPromise;
71
71
  var cssVariablesTheme = (0, shiki.createCssVariablesTheme)();
72
72
  async function getHighlighter() {
73
- highlighter = highlighter ?? await (0, shiki.createHighlighter)({
73
+ highlighterPromise = highlighterPromise ?? (0, shiki.createHighlighter)({
74
74
  themes: [cssVariablesTheme],
75
75
  langs: []
76
76
  });
77
- return highlighter;
77
+ return highlighterPromise;
78
78
  }
79
79
  function renderHighlightedCode(html) {
80
80
  return (/<code[^>]*>([\s\S]*?)<\/code>/.exec(html)?.[1] ?? html).split("\n").map((line) => line.trimStart().startsWith("<span") ? line : `<span>${line}</span>`).join("\n");
@@ -336,12 +336,12 @@ function extractMdxSections(content) {
336
336
  return sections;
337
337
  }
338
338
  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
339
+ return sections.map((section) => ({
340
+ id: section.id,
341
+ title: section.title,
342
+ level: section.level,
343
+ badge: section.badge,
344
+ tags: section.tags
345
345
  }));
346
346
  }
347
347
  function normalizePath(path) {
@@ -413,16 +413,20 @@ async function findContentRoutes(dir, base = dir, options = {}) {
413
413
  }
414
414
  routes.push({
415
415
  kind: "mdx",
416
- title,
417
416
  path: cleanPath,
418
417
  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),
418
+ meta: {
419
+ title,
420
+ description: typeof page.frontmatter.description === "string" ? page.frontmatter.description : void 0,
421
+ keywords: frontmatterKeywords(page.frontmatter),
422
+ sections: extractMdxSections(page.content)
423
+ },
424
+ module: { virtualModuleId: "virtual:clarify-page/" + relativePath.replace(/\.mdx?$/, "").replace(/\/+/g, "/") },
425
+ source: {
426
+ filePath: fullPath,
427
+ frontmatter: page.frontmatter,
428
+ content: page.content
429
+ },
426
430
  diagnostic: mdxResult.ok ? void 0 : mdxResult.diagnostic
427
431
  });
428
432
  }
@@ -482,7 +486,7 @@ async function findLocalizedContentRoutes(contentRoot, i18n, options = {}) {
482
486
  path: localizedRoutePath(basePath, locale.code, i18n),
483
487
  basePath,
484
488
  locale: locale.code,
485
- virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.filePath)
489
+ module: { virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.source.filePath) }
486
490
  });
487
491
  }
488
492
  }
@@ -518,12 +522,12 @@ function buildNavigation(routes) {
518
522
  if (!node) {
519
523
  node = {
520
524
  path: pathSoFar,
521
- title: i === parts.length - 1 ? route.title : kebabToTitle(parts[i]),
525
+ title: i === parts.length - 1 ? route.meta.title : kebabToTitle(parts[i]),
522
526
  children: []
523
527
  };
524
528
  current.push(node);
525
529
  }
526
- if (i === parts.length - 1 && route.sections) node.sections = navigationSections(route.sections);
530
+ if (i === parts.length - 1 && route.meta.sections) node.sections = navigationSections(route.meta.sections);
527
531
  if (i < parts.length - 1) {
528
532
  node.children = node.children ?? [];
529
533
  current = node.children;
@@ -532,17 +536,22 @@ function buildNavigation(routes) {
532
536
  }
533
537
  return root;
534
538
  }
535
- function resolvePageItem(item) {
536
- if (typeof item === "string") return { pageRef: item };
539
+ function routeIntentFromPagesItem(item) {
540
+ if (typeof item === "string") return {
541
+ kind: "page",
542
+ ref: item
543
+ };
537
544
  if ("openapi" in item) return {
538
- openapiRef: item.openapi,
539
- openapiTagFilter: item.filter?.tags,
545
+ kind: "openapi",
546
+ ref: item.openapi,
547
+ tagFilter: item.filter?.tags,
540
548
  path: item.path,
541
549
  title: item.title,
542
550
  icon: item.icon
543
551
  };
544
552
  return {
545
- pageRef: item.page,
553
+ kind: "page",
554
+ ref: item.page,
546
555
  path: item.path,
547
556
  redirect: item.redirect,
548
557
  title: item.title,
@@ -557,25 +566,28 @@ function firstNavigationPath(nodes) {
557
566
  }
558
567
  return "/";
559
568
  }
560
- function collectExplicitPagePathItems(tabs) {
561
- const items = [];
569
+ function collectExplicitPagePathIntents(tabs) {
570
+ const intents = [];
562
571
  for (const tab of tabs ?? []) {
563
572
  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
- });
573
+ for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) {
574
+ const intent = routeIntentFromPagesItem(item);
575
+ if (intent.kind === "page" && intent.path) intents.push({
576
+ ...intent,
577
+ path: intent.path
578
+ });
579
+ }
568
580
  }
569
- return items;
581
+ return intents;
570
582
  }
571
583
  function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
572
- const pageItems = collectExplicitPagePathItems(tabs);
573
- if (!pageItems.length) return routes;
584
+ const pageIntents = collectExplicitPagePathIntents(tabs);
585
+ if (!pageIntents.length) return routes;
574
586
  const additions = [];
575
587
  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);
588
+ for (const intent of pageIntents) {
589
+ const sourceBasePath = routePathFromRef(intent.ref);
590
+ const targetBasePath = routePathFromRef(intent.ref, intent.path);
579
591
  if (sourceBasePath === targetBasePath) continue;
580
592
  for (const route of routes) {
581
593
  if (route.kind !== "mdx" || (route.basePath ?? route.path) !== sourceBasePath) continue;
@@ -599,40 +611,42 @@ function buildNavigationFromPagesConfig(routes, config) {
599
611
  return buildNavigationFromConfig(routes, config);
600
612
  }
601
613
  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
- }) };
614
+ return {
615
+ kind: "tabbed",
616
+ tabs: tabs.map((tab) => {
617
+ const children = buildNavigationFromPagesConfig(routes, tab.pages);
618
+ return {
619
+ type: "tab",
620
+ path: firstNavigationPath(children),
621
+ title: resolveLocalizedText(tab.tab, "", "") ?? "",
622
+ icon: tab.icon,
623
+ children
624
+ };
625
+ })
626
+ };
612
627
  }
613
628
  function buildNavigationFromConfig(routes, config) {
614
629
  const routeMap = new Map(routes.map((r) => [r.path, r]));
615
630
  return config.map((group) => {
616
631
  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);
632
+ const intent = routeIntentFromPagesItem(item);
633
+ if (intent.kind === "openapi") {
634
+ const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
620
635
  const route = routeMap.get(path);
621
636
  return {
622
637
  path,
623
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? openapiRef),
624
- icon,
625
- sections: route?.sections ? navigationSections(route.sections) : void 0
638
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
639
+ icon: intent.icon,
640
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
626
641
  };
627
642
  }
628
- const ref = pageRef ?? "";
629
- const path = routePathFromRef(ref, explicitPath);
643
+ const path = routePathFromRef(intent.ref, intent.path);
630
644
  const route = routeMap.get(path);
631
645
  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
646
+ path: intent.redirect ? normalizePath(intent.redirect) : path,
647
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
648
+ icon: intent.icon,
649
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
636
650
  };
637
651
  });
638
652
  return {
@@ -658,16 +672,15 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
658
672
  const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
659
673
  return config.map((group) => {
660
674
  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);
675
+ const intent = routeIntentFromPagesItem(item);
676
+ const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
664
677
  const route = routeMap.get(basePath);
665
678
  const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
666
679
  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
680
+ path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, i18n) : void 0) ?? path,
681
+ title: resolveLocalizedText(intent.title, locale, i18n.defaultLocale) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
682
+ icon: intent.icon,
683
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
671
684
  };
672
685
  });
673
686
  return {
@@ -680,10 +693,10 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
680
693
  }
681
694
  function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
682
695
  if (!i18n) return void 0;
683
- const result = {};
696
+ const locales = {};
684
697
  for (const locale of i18n.locales) {
685
698
  const localeRoutes = routes.filter((route) => route.locale === locale.code);
686
- result[locale.code] = { tabs: tabs.map((tab) => {
699
+ locales[locale.code] = { tabs: tabs.map((tab) => {
687
700
  const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code, i18n);
688
701
  return {
689
702
  type: "tab",
@@ -694,17 +707,20 @@ function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
694
707
  };
695
708
  }) };
696
709
  }
697
- return result;
710
+ return {
711
+ kind: "localized-tabbed",
712
+ locales
713
+ };
698
714
  }
699
715
  /** Serialize a ContentRoute into the minimal shape exposed to tooling. */
700
716
  function toDevRouteEntry(route) {
701
717
  return {
702
718
  path: route.path,
703
- filePath: route.filePath,
719
+ filePath: route.source.filePath,
704
720
  locale: route.locale,
705
721
  basePath: route.basePath,
706
722
  kind: route.kind,
707
- title: route.title
723
+ title: route.meta.title
708
724
  };
709
725
  }
710
726
  /**
@@ -718,7 +734,7 @@ function toDevRouteEntry(route) {
718
734
  * or the first match.
719
735
  */
720
736
  function resolveRouteForFile(filePath, routes, defaultLocale) {
721
- const matches = routes.filter((r) => r.filePath === filePath);
737
+ const matches = routes.filter((r) => r.source.filePath === filePath);
722
738
  if (matches.length === 0) return void 0;
723
739
  if (matches.length === 1) return matches[0];
724
740
  const fileLocale = inferLocaleFromPath(filePath);
@@ -1143,6 +1159,7 @@ var VIRTUAL_CONFIG = "virtual:clarify/config";
1143
1159
  var VIRTUAL_ROUTES = "virtual:clarify/routes";
1144
1160
  var VIRTUAL_SERVER_ROUTES = "virtual:clarify/routes/server";
1145
1161
  var VIRTUAL_OPENAPI = "virtual:clarify/openapi";
1162
+ var VIRTUAL_OPENAPI_SERVER = "virtual:clarify/openapi/server";
1146
1163
  var VIRTUAL_SLOTS = "virtual:clarify/slots";
1147
1164
  var VIRTUAL_SLOT = "virtual:clarify/slot";
1148
1165
  var VIRTUAL_CLIENT_ENTRY = "virtual:clarify/entry-client";
@@ -1163,6 +1180,7 @@ var FIXED_VIRTUAL_IDS = [
1163
1180
  VIRTUAL_ROUTES,
1164
1181
  VIRTUAL_SERVER_ROUTES,
1165
1182
  VIRTUAL_OPENAPI,
1183
+ VIRTUAL_OPENAPI_SERVER,
1166
1184
  VIRTUAL_SLOTS,
1167
1185
  VIRTUAL_SLOT
1168
1186
  ];
@@ -1179,8 +1197,8 @@ function resolveVirtualModuleId(id, modules, routes) {
1179
1197
  for (const virtualId of FIXED_VIRTUAL_IDS) if (id === virtualId || id === resolveVirtualId(virtualId)) return resolveVirtualId(virtualId);
1180
1198
  const moduleId = stripVirtualPrefix(id);
1181
1199
  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);
1200
+ const route = routes.find((route) => route.module.virtualModuleId === id || route.module.virtualModuleId === moduleId);
1201
+ if (route) return resolveVirtualId(route.module.virtualModuleId);
1184
1202
  return null;
1185
1203
  }
1186
1204
  function generateConfigModule(projectConfig, buildOptions, version) {
@@ -1194,38 +1212,38 @@ function generateConfigModule(projectConfig, buildOptions, version) {
1194
1212
  function moduleSpecifier(value) {
1195
1213
  return JSON.stringify(value);
1196
1214
  }
1197
- function routeToRuntimeObject(route, component, mode) {
1198
- const obj = {
1215
+ function routeToRuntimeManifestEntry(route, component, mode) {
1216
+ const entry = {
1199
1217
  path: route.path,
1200
- title: route.title,
1218
+ title: route.meta.title,
1201
1219
  component,
1202
1220
  kind: route.kind
1203
1221
  };
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
1222
+ if (mode === "client") entry.lazy = true;
1223
+ if (route.basePath) entry.basePath = route.basePath;
1224
+ if (route.locale) entry.locale = route.locale;
1225
+ if (route.isFallback) entry.isFallback = true;
1226
+ if (route.isBareAlias) entry.isBareAlias = true;
1227
+ if (route.alternates) entry.alternates = route.alternates;
1228
+ if (route.meta.description) entry.description = route.meta.description;
1229
+ if (route.meta.keywords?.length) entry.keywords = route.meta.keywords;
1230
+ if (route.artifacts?.contentArtifactUrl) entry.contentArtifactUrl = route.artifacts.contentArtifactUrl;
1231
+ if (route.source?.sourceEditUrl) entry.sourceEditUrl = route.source.sourceEditUrl;
1232
+ if (route.meta.sections?.length) entry.sections = route.meta.sections.map((section) => ({
1233
+ id: section.id,
1234
+ title: section.title,
1235
+ level: section.level,
1236
+ badge: section.badge,
1237
+ tags: section.tags
1220
1238
  }));
1221
- return obj;
1239
+ return entry;
1222
1240
  }
1223
1241
  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 }) }))`;
1242
+ 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) => {
1243
+ 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
1244
  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: ")}`;
1245
+ const manifestEntry = routeToRuntimeManifestEntry(r, placeholder, mode);
1246
+ return ` ${JSON.stringify(manifestEntry, null, 0).replace(`"${placeholder}"`, component).replace(/"([A-Za-z_$][\w$]*)":/g, "$1: ")}`;
1229
1247
  }).join(",\n")}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
1230
1248
  }
1231
1249
  function createClientEntryModule(options = {}) {
@@ -1234,9 +1252,15 @@ import '@clarify-labs/renderer/style.css';
1234
1252
  import { render } from '@clarify-labs/renderer/client';
1235
1253
  import { routes, navigation } from '${VIRTUAL_ROUTES}';
1236
1254
  import { config } from '${VIRTUAL_CONFIG}';
1237
- import { openApis } from '${VIRTUAL_OPENAPI}';
1255
+ import { openApiSpecs } from '${VIRTUAL_OPENAPI}';
1238
1256
  import { runtimeSlots } from '${VIRTUAL_SLOTS}';
1239
- render({ config, routes, navigation, openApis, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
1257
+ const renderOptions = { config, routes, navigation, openApiSpecs, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} };
1258
+ render(renderOptions);
1259
+ if (import.meta.hot) {
1260
+ import.meta.hot.accept('${VIRTUAL_ROUTES}', (mod) => {
1261
+ render({ ...renderOptions, routes: mod.routes, navigation: mod.navigation });
1262
+ });
1263
+ }`;
1240
1264
  }
1241
1265
  /**
1242
1266
  * Collect every plugin's `slots` declarations into a single runtime registry
@@ -1265,7 +1289,7 @@ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
1265
1289
  }
1266
1290
  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
1291
  const componentPath = root + slot.component;
1268
- const entry = `{ plugin: ${JSON.stringify(plugin)}, component: () => import(${moduleSpecifier(componentPath)}) }`;
1292
+ const entry = `{ plugin: ${JSON.stringify(plugin)}, loadComponent: () => import(${moduleSpecifier(componentPath)}) }`;
1269
1293
  const list = grouped.get(slot.name) ?? [];
1270
1294
  list.push(entry);
1271
1295
  grouped.set(slot.name, list);
@@ -1294,18 +1318,34 @@ function buildVirtualModules(args) {
1294
1318
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, "client"));
1295
1319
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, "server"));
1296
1320
  modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
1297
- modules.set(VIRTUAL_OPENAPI, "export const openApis = {};");
1321
+ modules.set(VIRTUAL_OPENAPI, "export const openApiSpecs = {};");
1298
1322
  modules.set(VIRTUAL_SLOT, "export { useSlot } from '@clarify-labs/renderer';\n");
1299
1323
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1300
1324
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1301
1325
  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);
1326
+ const moduleContent = route.diagnostic ? generateMdxErrorModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1327
+ modules.set(route.module.virtualModuleId, moduleContent);
1304
1328
  }
1305
1329
  return modules;
1306
1330
  }
1307
1331
  //#endregion
1308
1332
  //#region source/core/adapters.ts
1333
+ function routeStructure(route) {
1334
+ const { content: _content, ...source } = route.source;
1335
+ return {
1336
+ ...route,
1337
+ source
1338
+ };
1339
+ }
1340
+ function createDevStructureSnapshot(routes, navigation) {
1341
+ return {
1342
+ routes: JSON.stringify(routes.map(routeStructure)),
1343
+ navigation: JSON.stringify(navigation)
1344
+ };
1345
+ }
1346
+ function hasDevStructureChanged(before, after) {
1347
+ return before.routes !== after.routes || before.navigation !== after.navigation;
1348
+ }
1309
1349
  function invalidateVirtualModules(engine, server) {
1310
1350
  for (const id of engine.modules.keys()) {
1311
1351
  const moduleNode = server.moduleGraph.getModuleById(resolveVirtualId(id));
@@ -1313,8 +1353,10 @@ function invalidateVirtualModules(engine, server) {
1313
1353
  }
1314
1354
  }
1315
1355
  async function refreshDevServer(engine, server) {
1356
+ const before = createDevStructureSnapshot(engine.routes, engine.navigation);
1316
1357
  await engine.refresh();
1317
1358
  invalidateVirtualModules(engine, server);
1359
+ return hasDevStructureChanged(before, createDevStructureSnapshot(engine.routes, engine.navigation));
1318
1360
  }
1319
1361
  function createNormalizedMdxContentPlugin(engine) {
1320
1362
  return {
@@ -1323,10 +1365,10 @@ function createNormalizedMdxContentPlugin(engine) {
1323
1365
  transform(_code, id) {
1324
1366
  if (!/\.mdx?(?:\?|$)/.test(id)) return null;
1325
1367
  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;
1368
+ const route = engine.routes.find((route) => route.kind === "mdx" && route.source.filePath === filePath);
1369
+ if (!route || route.source.content === void 0) return null;
1328
1370
  return {
1329
- code: route.content,
1371
+ code: route.source.content,
1330
1372
  map: null
1331
1373
  };
1332
1374
  }
@@ -1380,8 +1422,13 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1380
1422
  }
1381
1423
  const relativeContentFile = (0, node_path.relative)(engine.contentRoot, changedFile);
1382
1424
  if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !(0, node_path.isAbsolute)(relativeContentFile)) || !engine.hasContentRouteForFile(changedFile)) return;
1383
- await refreshDevServer(engine, ctx.server);
1384
- return [];
1425
+ if (await refreshDevServer(engine, ctx.server)) {
1426
+ const routesModule = ctx.server.moduleGraph.getModuleById(resolveVirtualId(VIRTUAL_ROUTES));
1427
+ if (routesModule) return [...ctx.modules, routesModule];
1428
+ ctx.server.ws.send({ type: "full-reload" });
1429
+ return [];
1430
+ }
1431
+ return ctx.modules;
1385
1432
  },
1386
1433
  resolveId(id) {
1387
1434
  return resolveVirtualModuleId(id, engine.modules, engine.routes);
@@ -1392,18 +1439,6 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1392
1439
  async configureServer(server) {
1393
1440
  server.watcher.add(engine.contentRoot);
1394
1441
  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
1442
  server.middlewares.use((req, res, next) => {
1408
1443
  if (req.url === "/dev/query-preview-route" && req.method === "POST") return handleDevRouteRequest(req, res, engine.routes, engine.runtimeContext());
1409
1444
  if (req.url === "/dev/project-info" && (req.method === "GET" || req.method === "HEAD")) return handleProjectInfoRequest(req, res, engine.runtimeContext());
@@ -1413,7 +1448,7 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1413
1448
  const changedFile = (0, node_path.isAbsolute)(filePath) ? filePath : (0, node_path.join)(engine.root, filePath);
1414
1449
  const relativeContentFile = (0, node_path.relative)(engine.contentRoot, changedFile);
1415
1450
  if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !(0, node_path.isAbsolute)(relativeContentFile))) return;
1416
- await refreshDevServer(engine, server);
1451
+ if (await refreshDevServer(engine, server)) server.ws.send({ type: "full-reload" });
1417
1452
  };
1418
1453
  server.watcher.on("add", handleContentTreeChange);
1419
1454
  server.watcher.on("unlink", handleContentTreeChange);
@@ -1480,7 +1515,7 @@ function createViteAdapter(engine) {
1480
1515
  }
1481
1516
  var cliPackageVersion = {
1482
1517
  name: "@clarify-labs/cli",
1483
- version: "0.10.0",
1518
+ version: "0.10.1",
1484
1519
  license: "AGPL-3.0-only",
1485
1520
  type: "module",
1486
1521
  repository: {
@@ -1586,11 +1621,14 @@ function routeToOpenAPIArtifactUrl(routePath) {
1586
1621
  return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi.json`;
1587
1622
  }
1588
1623
  function attachContentArtifactUrls(routes) {
1589
- for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path);
1624
+ for (const route of routes) route.artifacts = {
1625
+ ...route.artifacts,
1626
+ contentArtifactUrl: route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path)
1627
+ };
1590
1628
  }
1591
1629
  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}`);
1630
+ if (route.source.content !== void 0) return route.source.content;
1631
+ throw new Error(`Route content is missing from route context: ${route.source.filePath}`);
1594
1632
  }
1595
1633
  function readRouteArtifactContent(route) {
1596
1634
  const content = readRouteContent(route);
@@ -1600,16 +1638,17 @@ function isLlmsTxtRoute(route) {
1600
1638
  return !route.path.split("/").includes("404");
1601
1639
  }
1602
1640
  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);
1641
+ if (route.meta.description) return route.meta.description;
1642
+ const sections = route.meta.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
1605
1643
  if (sections?.length) return `Covers ${sections.join(", ")}.`;
1606
- if (route.keywords?.length) return `Related topics: ${route.keywords.join(", ")}.`;
1644
+ if (route.meta.keywords?.length) return `Related topics: ${route.meta.keywords.join(", ")}.`;
1607
1645
  if (route.kind === "openapi") return "OpenAPI artifact for machine-readable API reference data.";
1608
1646
  }
1609
1647
  function llmsTxtListItem(route, basePath) {
1610
- if (!route.contentArtifactUrl) return void 0;
1648
+ const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
1649
+ if (!contentArtifactUrl) return void 0;
1611
1650
  const description = llmsTxtDescription(route);
1612
- return description ? `- [${route.title}](${basePath}${route.contentArtifactUrl}): ${description}` : `- [${route.title}](${basePath}${route.contentArtifactUrl})`;
1651
+ return description ? `- [${route.meta.title}](${basePath}${contentArtifactUrl}): ${description}` : `- [${route.meta.title}](${basePath}${contentArtifactUrl})`;
1613
1652
  }
1614
1653
  function llmsTxtLocaleLabel(locale, projectConfig) {
1615
1654
  return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
@@ -1626,7 +1665,7 @@ function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
1626
1665
  const routesBySource = /* @__PURE__ */ new Map();
1627
1666
  const defaultLocale = projectConfig.i18n?.defaultLocale;
1628
1667
  for (const route of routes) {
1629
- const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.contentArtifactUrl ?? route.path}`;
1668
+ const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.artifacts?.contentArtifactUrl ?? route.path}`;
1630
1669
  const previousRoute = routesBySource.get(sourceKey);
1631
1670
  if (!previousRoute || previousRoute.locale && !route.locale) routesBySource.set(sourceKey, route);
1632
1671
  }
@@ -1695,9 +1734,9 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
1695
1734
  }
1696
1735
  if (contentPath.endsWith(".openapi.yaml")) {
1697
1736
  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);
1737
+ const route = routes.find((route) => route.artifacts?.contentArtifactUrl === jsonPath);
1738
+ if (route?.kind === "openapi" && route.source.content) {
1739
+ const spec = JSON.parse(route.source.content);
1701
1740
  res.statusCode = 200;
1702
1741
  res.setHeader("Content-Type", "text/yaml; charset=utf-8");
1703
1742
  res.end((0, yaml.stringify)(spec, { lineWidth: 0 }), "utf8");
@@ -1705,7 +1744,7 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
1705
1744
  }
1706
1745
  return false;
1707
1746
  }
1708
- const route = routes.find((route) => route.contentArtifactUrl === contentPath);
1747
+ const route = routes.find((route) => route.artifacts?.contentArtifactUrl === contentPath);
1709
1748
  if (!route) return false;
1710
1749
  res.statusCode = 200;
1711
1750
  res.setHeader("Content-Type", resolveContentArtifactType(route));
@@ -1731,14 +1770,15 @@ function createContentArtifactsPlugin() {
1731
1770
  "build:assets": (ctx) => {
1732
1771
  const assets = [];
1733
1772
  for (const route of ctx.routes) {
1734
- if (!route.contentArtifactUrl) continue;
1773
+ const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
1774
+ if (!contentArtifactUrl) continue;
1735
1775
  assets.push({
1736
- fileName: route.contentArtifactUrl.replace(/^\//, ""),
1776
+ fileName: contentArtifactUrl.replace(/^\//, ""),
1737
1777
  source: readRouteArtifactContent(route)
1738
1778
  });
1739
- if (route.kind === "openapi" && route.content) {
1740
- const yamlFileName = route.contentArtifactUrl.replace(/\.json$/, ".yaml");
1741
- const spec = JSON.parse(route.content);
1779
+ if (route.kind === "openapi" && route.source.content) {
1780
+ const yamlFileName = contentArtifactUrl.replace(/\.json$/, ".yaml");
1781
+ const spec = JSON.parse(route.source.content);
1742
1782
  assets.push({
1743
1783
  fileName: yamlFileName.replace(/^\//, ""),
1744
1784
  source: (0, yaml.stringify)(spec, { lineWidth: 0 })
@@ -1824,6 +1864,7 @@ var OPENAPI_HTTP_METHODS = [
1824
1864
  "patch",
1825
1865
  "trace"
1826
1866
  ];
1867
+ var CLARIFY_OPENAPI_SECTION_ID_EXTENSION = "x-clarify-section-id";
1827
1868
  function errorMessage(error) {
1828
1869
  return error instanceof Error ? error.message : String(error);
1829
1870
  }
@@ -1850,10 +1891,10 @@ function findOpenAPIRoutes(dir, base = dir) {
1850
1891
  routes.push({
1851
1892
  path: cleanPath,
1852
1893
  basePath: cleanPath,
1853
- filePath: fullPath,
1854
- virtualModuleId: virtualModuleIdFromRef(ref),
1855
- title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
1856
- kind: "openapi"
1894
+ kind: "openapi",
1895
+ meta: { title: kebabToTitle(cleanPath.split("/").pop() ?? "API") },
1896
+ module: { virtualModuleId: virtualModuleIdFromRef(ref) },
1897
+ source: { filePath: fullPath }
1857
1898
  });
1858
1899
  }
1859
1900
  return routes;
@@ -1862,36 +1903,80 @@ function operationMatchesTags(operationTags, filterTags) {
1862
1903
  if (!filterTags?.length) return true;
1863
1904
  return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
1864
1905
  }
1906
+ function filterPathItemsByTags(items, tags) {
1907
+ const filteredItems = {};
1908
+ for (const path of Object.keys(items)) {
1909
+ const pathItem = items[path];
1910
+ if (!pathItem) continue;
1911
+ if (OPENAPI_HTTP_METHODS.map((method) => pathItem[method]).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredItems[path] = pathItem;
1912
+ }
1913
+ return filteredItems;
1914
+ }
1915
+ function createOperationSectionId(operation, method, path, kind) {
1916
+ return (typeof operation.operationId === "string" ? operation.operationId.trim() : "") || (0, github_slugger.slug)(`${kind === "webhook" ? "webhook " : ""}${method.toLowerCase()} ${path}`);
1917
+ }
1918
+ function operationSectionId(operation) {
1919
+ const sectionId = operation[CLARIFY_OPENAPI_SECTION_ID_EXTENSION];
1920
+ return typeof sectionId === "string" ? sectionId : "";
1921
+ }
1922
+ function normalizeOpenAPISpecSectionIds(spec) {
1923
+ const items = [...Object.entries(spec.paths ?? {}).map(([path, pathItem]) => ({
1924
+ path,
1925
+ pathItem,
1926
+ kind: "path"
1927
+ })), ...Object.entries(spec.webhooks ?? {}).map(([path, pathItem]) => ({
1928
+ path,
1929
+ pathItem,
1930
+ kind: "webhook"
1931
+ }))];
1932
+ for (const { path, pathItem, kind } of items) {
1933
+ if (!pathItem || typeof pathItem !== "object") continue;
1934
+ const operations = pathItem;
1935
+ for (const method of OPENAPI_HTTP_METHODS) {
1936
+ const op = operations[method];
1937
+ if (!op || typeof op !== "object") continue;
1938
+ op[CLARIFY_OPENAPI_SECTION_ID_EXTENSION] = createOperationSectionId(op, method, path, kind);
1939
+ }
1940
+ }
1941
+ return spec;
1942
+ }
1865
1943
  /** Filter an OpenAPI spec to only include paths that have at least one operation matching the given tags. */
1866
1944
  function filterSpecByTags(spec, tags) {
1867
- const filteredPaths = {};
1868
1945
  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
- }
1946
+ const webhooks = spec.webhooks ?? {};
1874
1947
  return {
1875
1948
  ...spec,
1876
- paths: filteredPaths
1949
+ paths: filterPathItemsByTags(paths, tags),
1950
+ webhooks: filterPathItemsByTags(webhooks, tags)
1877
1951
  };
1878
1952
  }
1879
1953
  /** Extract endpoint operations from an OpenAPI spec as page sections. */
1880
1954
  function extractOpenAPISections(spec, filterTags) {
1955
+ normalizeOpenAPISpecSectionIds(spec);
1881
1956
  const sections = [];
1882
- const paths = spec.paths ?? {};
1883
- for (const [path, pathItem] of Object.entries(paths)) {
1957
+ const items = [...Object.entries(spec.paths ?? {}).map(([path, pathItem]) => ({
1958
+ path,
1959
+ pathItem,
1960
+ kind: "path"
1961
+ })), ...Object.entries(spec.webhooks ?? {}).map(([path, pathItem]) => ({
1962
+ path,
1963
+ pathItem,
1964
+ kind: "webhook"
1965
+ }))];
1966
+ for (const { path, pathItem, kind } of items) {
1884
1967
  if (!pathItem) continue;
1968
+ const operations = pathItem;
1885
1969
  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}`;
1970
+ const op = operations[method];
1971
+ const tags = Array.isArray(op?.tags) ? op.tags : void 0;
1972
+ if (!op || !operationMatchesTags(tags, filterTags)) continue;
1973
+ const title = typeof op.summary === "string" ? op.summary : `${method.toUpperCase()} ${path}`;
1889
1974
  sections.push({
1890
- id: (0, github_slugger.slug)(`${method} ${path}`),
1975
+ id: operationSectionId(op),
1891
1976
  title,
1892
1977
  level: 2,
1893
- badge: method.toUpperCase(),
1894
- tags: op.tags
1978
+ badge: kind === "webhook" ? "WEBHOOK" : method.toUpperCase(),
1979
+ tags
1895
1980
  });
1896
1981
  }
1897
1982
  }
@@ -1933,12 +2018,16 @@ async function readOpenAPISpec(filePath, contentProcessor) {
1933
2018
  //#endregion
1934
2019
  //#region source/plugins/openapi/virtual-modules.ts
1935
2020
  var openApiRegistryModuleId = VIRTUAL_OPENAPI;
2021
+ var openApiServerRegistryModuleId = VIRTUAL_OPENAPI_SERVER;
1936
2022
  var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
1937
2023
  function specVirtualModuleId(specKey) {
1938
2024
  return `${OPENAPI_SPEC_PREFIX}${specKey}`;
1939
2025
  }
1940
- function generateOpenAPIRegistryModule(openApis) {
1941
- return `export const openApis = ${JSON.stringify(openApis)};`;
2026
+ function generateOpenAPIRegistryModule(openApiSpecs) {
2027
+ return `export const openApiSpecs = {\n${Object.entries(openApiSpecs).map(([key, moduleId]) => ` ${JSON.stringify(key)}: () => import(${JSON.stringify(moduleId)}),`).join("\n")}\n};`;
2028
+ }
2029
+ function generateOpenAPIServerRegistryModule(openApiSpecs) {
2030
+ return `export const openApiSpecs = ${JSON.stringify(openApiSpecs)};`;
1942
2031
  }
1943
2032
  /** Generate a virtual module that exports a single spec as default. */
1944
2033
  function generateOpenAPISpecModule(spec) {
@@ -1946,13 +2035,11 @@ function generateOpenAPISpecModule(spec) {
1946
2035
  }
1947
2036
  function generateOpenAPIPageModule(opts) {
1948
2037
  return `import { createOpenApiRouteComponent } from '@clarify-labs/renderer';
2038
+ import spec from ${JSON.stringify(opts.specModuleId)};
1949
2039
 
1950
- export const routeData = ${JSON.stringify({
1951
- spec: opts.spec,
1952
- tagFilter: opts.tagFilter
1953
- })};
2040
+ export const routeData = {};
1954
2041
 
1955
- export default createOpenApiRouteComponent(routeData);
2042
+ export default createOpenApiRouteComponent({ ...routeData, spec });
1956
2043
  `;
1957
2044
  }
1958
2045
  function generateOpenAPIErrorModule(diagnostic) {
@@ -1971,115 +2058,212 @@ export default createContentDiagnosticComponent(contentDiagnostic);
1971
2058
  }
1972
2059
  //#endregion
1973
2060
  //#region source/plugins/openapi/index.ts
1974
- function collectOpenAPIPageItems(config) {
1975
- const items = [];
2061
+ function collectOpenAPIPageIntents(config) {
2062
+ const intents = [];
1976
2063
  function visitPages(pages) {
1977
2064
  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);
2065
+ for (const group of pages) for (const item of group.pages) {
2066
+ const intent = routeIntentFromPagesItem(item);
2067
+ if (intent.kind === "openapi" && (intent.path || intent.tagFilter?.length)) intents.push(intent);
2068
+ }
1979
2069
  }
1980
2070
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
1981
- return items;
2071
+ return intents;
1982
2072
  }
1983
2073
  function virtualModuleIdFromPath(path) {
1984
2074
  return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
1985
2075
  }
2076
+ function cloneOpenAPIRoute(route, overrides = {}) {
2077
+ return {
2078
+ ...route,
2079
+ meta: {
2080
+ ...route.meta,
2081
+ sections: route.meta.sections ? [...route.meta.sections] : void 0
2082
+ },
2083
+ module: { ...route.module },
2084
+ source: { ...route.source },
2085
+ openapi: route.openapi ? {
2086
+ ...route.openapi,
2087
+ tagFilter: route.openapi.tagFilter ? [...route.openapi.tagFilter] : void 0
2088
+ } : void 0,
2089
+ alternates: route.alternates ? { ...route.alternates } : void 0,
2090
+ ...overrides
2091
+ };
2092
+ }
2093
+ function findOpenAPIRoutesForContentRoot(contentRoot, i18n) {
2094
+ if (!i18n) return findOpenAPIRoutes(contentRoot);
2095
+ const localizedRoutes = [];
2096
+ for (const locale of i18n.locales) {
2097
+ const localeRoot = (0, node_path.join)(contentRoot, locale.code);
2098
+ for (const route of findOpenAPIRoutes(localeRoot, localeRoot)) {
2099
+ const basePath = route.basePath ?? route.path;
2100
+ const path = localizedRoutePath(basePath, locale.code, i18n);
2101
+ localizedRoutes.push({
2102
+ ...route,
2103
+ path,
2104
+ basePath,
2105
+ locale: locale.code,
2106
+ module: { virtualModuleId: virtualModuleIdFromPath(path) }
2107
+ });
2108
+ }
2109
+ }
2110
+ if (i18n.missing === "fallback") {
2111
+ const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
2112
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
2113
+ for (const sourceRoute of defaultRoutes) {
2114
+ const basePath = sourceRoute.basePath ?? sourceRoute.path;
2115
+ for (const locale of i18n.locales) {
2116
+ const key = `${locale.code}:${basePath}`;
2117
+ if (routeByLocaleAndBase.has(key)) continue;
2118
+ const path = localizedRoutePath(basePath, locale.code, i18n);
2119
+ localizedRoutes.push(cloneOpenAPIRoute(sourceRoute, {
2120
+ path,
2121
+ locale: locale.code,
2122
+ module: { virtualModuleId: virtualModuleIdFromPath(path) },
2123
+ isFallback: true
2124
+ }));
2125
+ }
2126
+ }
2127
+ }
2128
+ return localizedRoutes;
2129
+ }
1986
2130
  function createConfiguredOpenAPIRoutes(routes, config) {
1987
- const pageItems = collectOpenAPIPageItems(config);
1988
- if (!pageItems.length) return routes;
2131
+ const openAPIIntents = collectOpenAPIPageIntents(config);
2132
+ if (!openAPIIntents.length) return routes;
1989
2133
  const additions = [];
1990
2134
  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);
2135
+ for (const intent of openAPIIntents) {
2136
+ const tagFilter = intent.tagFilter?.filter(Boolean);
2137
+ const sourceBasePath = openAPIPagePathFromRef(intent.ref);
2138
+ const targetBasePath = openAPIPagePathFromRef(intent.ref, tagFilter, intent.path);
1995
2139
  if (sourceBasePath === targetBasePath && !tagFilter?.length) continue;
1996
2140
  for (const route of routes) {
1997
2141
  if (route.kind !== "openapi" || (route.basePath ?? route.path) !== sourceBasePath) continue;
1998
2142
  const key = `${route.locale ?? ""}:${targetBasePath}`;
1999
2143
  if (existingKeys.has(key)) continue;
2000
2144
  existingKeys.add(key);
2001
- additions.push({
2002
- ...route,
2003
- path: route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath,
2145
+ const path = route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath;
2146
+ additions.push(cloneOpenAPIRoute(route, {
2147
+ path,
2004
2148
  basePath: targetBasePath,
2005
- title: route.title,
2006
- virtualModuleId: virtualModuleIdFromPath(targetBasePath),
2007
- openapiTagFilter: tagFilter?.length ? tagFilter : void 0
2008
- });
2149
+ module: { virtualModuleId: virtualModuleIdFromPath(path) },
2150
+ openapi: {
2151
+ ...route.openapi,
2152
+ tagFilter: tagFilter?.length ? tagFilter : void 0
2153
+ }
2154
+ }));
2009
2155
  }
2010
2156
  }
2011
2157
  const nextRoutes = [...routes, ...additions];
2012
- return config.i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n)) : nextRoutes;
2158
+ if (!config.i18n) return nextRoutes;
2159
+ const routesWithAlternates = nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n));
2160
+ const bareAliases = [];
2161
+ const seenBare = new Set(routesWithAlternates.map((route) => route.path));
2162
+ for (const route of routesWithAlternates) {
2163
+ if (route.locale !== config.i18n.defaultLocale) continue;
2164
+ const basePath = route.basePath ?? route.path;
2165
+ if (basePath === route.path || seenBare.has(basePath)) continue;
2166
+ seenBare.add(basePath);
2167
+ bareAliases.push(cloneOpenAPIRoute(route, {
2168
+ path: basePath,
2169
+ module: { virtualModuleId: virtualModuleIdFromPath(basePath) },
2170
+ isBareAlias: true
2171
+ }));
2172
+ }
2173
+ return [...routesWithAlternates, ...bareAliases];
2013
2174
  }
2014
- /** Derive a stable, deduplicated key from an absolute spec file path. */
2015
- function specFileKeyFromPath(filePath, projectRoot) {
2175
+ /** Derive a stable, deduplicated id from an absolute spec file path. */
2176
+ function sourceSpecIdFromPath(filePath, projectRoot) {
2016
2177
  return (0, node_path.relative)(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
2017
2178
  }
2179
+ function routeSpecModuleIdFromSourceSpecId(sourceSpecId, tagFilter) {
2180
+ if (!tagFilter?.length) return sourceSpecId;
2181
+ return `${sourceSpecId}_tags_${tagFilter.map((tag) => tag.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "tag").join("_")}`;
2182
+ }
2183
+ function openAPIRegistryKeys(route) {
2184
+ const keys = /* @__PURE__ */ new Set();
2185
+ const path = (route.path || "/").replace(/^\//, "");
2186
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
2187
+ if (route.locale) keys.add(`virtual:clarify-page/${route.locale}/${base}`);
2188
+ if (path) keys.add(`virtual:clarify-page/${path}`);
2189
+ else keys.add("virtual:clarify-page/index");
2190
+ if (!route.locale && base) keys.add(`virtual:clarify-page/${base}`);
2191
+ return [...keys];
2192
+ }
2018
2193
  function createOpenAPIPlugin() {
2019
- /** specs keyed by specFileKey (deduplicated per source file). */
2194
+ /** specs keyed by sourceSpecId (deduplicated per source file). */
2020
2195
  const specs = /* @__PURE__ */ new Map();
2196
+ const specModules = /* @__PURE__ */ new Map();
2021
2197
  return {
2022
2198
  name: "clarify:openapi",
2023
2199
  hooks: {
2024
- "routes:discover": (input) => ({
2200
+ "routes:discover": (input, ctx) => ({
2025
2201
  ...input,
2026
- routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
2202
+ routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.i18n)]
2027
2203
  }),
2028
2204
  "routes:discovered": async (routes, ctx) => {
2029
2205
  specs.clear();
2206
+ specModules.clear();
2030
2207
  const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
2031
2208
  const specByFilePath = /* @__PURE__ */ new Map();
2032
2209
  for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
2033
- const specFromCache = specByFilePath.get(route.filePath);
2210
+ const specFromCache = specByFilePath.get(route.source.filePath);
2034
2211
  const processor = createProjectContentProcessor(ctx.plugins, ctx);
2035
2212
  const result = specFromCache ? {
2036
2213
  ok: true,
2037
2214
  spec: specFromCache
2038
- } : await readOpenAPISpec(route.filePath, processor);
2215
+ } : await readOpenAPISpec(route.source.filePath, processor);
2039
2216
  if (!result.ok) {
2040
2217
  route.diagnostic = result.diagnostic;
2041
- route.title = route.title || "OpenAPI parse error";
2042
- route.sections = [];
2218
+ route.meta.title = route.meta.title || "OpenAPI parse error";
2219
+ route.meta.sections = [];
2043
2220
  continue;
2044
2221
  }
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, {
2222
+ const spec = normalizeOpenAPISpecSectionIds(result.spec);
2223
+ specByFilePath.set(route.source.filePath, spec);
2224
+ const sourceSpecId = sourceSpecIdFromPath(route.source.filePath, ctx.generateOptions.projectRoot);
2225
+ route.openapi = {
2226
+ ...route.openapi,
2227
+ sourceSpecId
2228
+ };
2229
+ specs.set(sourceSpecId, {
2050
2230
  spec,
2051
- filePath: route.filePath
2231
+ filePath: route.source.filePath
2052
2232
  });
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);
2233
+ route.meta.title = spec.info?.title ?? route.meta.title;
2234
+ route.meta.description = spec.info?.description ?? route.meta.description;
2235
+ route.meta.sections = extractOpenAPISections(spec, route.openapi?.tagFilter);
2236
+ const pageSpec = route.openapi?.tagFilter?.length ? filterSpecByTags(spec, route.openapi.tagFilter) : spec;
2237
+ const routeSpecModuleId = routeSpecModuleIdFromSourceSpecId(sourceSpecId, route.openapi?.tagFilter);
2238
+ route.openapi.routeSpecModuleId = routeSpecModuleId;
2239
+ specModules.set(routeSpecModuleId, pageSpec);
2240
+ route.source.content = JSON.stringify(pageSpec);
2058
2241
  }
2059
2242
  return nextRoutes;
2060
2243
  },
2061
2244
  "modules:before": (modules, ctx) => {
2062
2245
  const registryEntries = {};
2246
+ const serverRegistryEntries = {};
2063
2247
  for (const route of ctx.routes) {
2064
- if (route.kind !== "openapi" || !route.specFileKey) continue;
2065
- const spec = specs.get(route.specFileKey)?.spec;
2248
+ if (route.kind !== "openapi" || !route.openapi?.sourceSpecId) continue;
2249
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2250
+ const spec = specModules.get(routeSpecModuleId) ?? specs.get(route.openapi.sourceSpecId)?.spec;
2066
2251
  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;
2252
+ for (const registryKey of openAPIRegistryKeys(route)) {
2253
+ registryEntries[registryKey] = specVirtualModuleId(routeSpecModuleId);
2254
+ serverRegistryEntries[registryKey] = spec;
2255
+ }
2070
2256
  }
2071
2257
  modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
2072
- for (const [specKey, entry] of specs) modules.set(specVirtualModuleId(specKey), generateOpenAPISpecModule(entry.spec));
2258
+ modules.set(openApiServerRegistryModuleId, generateOpenAPIServerRegistryModule(serverRegistryEntries));
2259
+ for (const [routeSpecModuleId, spec] of specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2073
2260
  for (const route of ctx.routes) {
2074
2261
  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
- }));
2262
+ if (route.diagnostic) modules.set(route.module.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
2263
+ else if (route.openapi?.sourceSpecId) {
2264
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2265
+ if (!specModules.has(routeSpecModuleId)) continue;
2266
+ modules.set(route.module.virtualModuleId, generateOpenAPIPageModule({ specModuleId: specVirtualModuleId(routeSpecModuleId) }));
2083
2267
  }
2084
2268
  }
2085
2269
  return modules;
@@ -2147,11 +2331,11 @@ async function generatePagefindIndex(options, pagefind) {
2147
2331
  }
2148
2332
  function routeSearchContent(route) {
2149
2333
  return [
2150
- route.title,
2151
- route.description,
2152
- route.keywords?.join(" "),
2153
- route.sections?.map((section) => section.title).join(" "),
2154
- route.content
2334
+ route.meta.title,
2335
+ route.meta.description,
2336
+ route.meta.keywords?.join(" "),
2337
+ route.meta.sections?.map((section) => section.title).join(" "),
2338
+ route.source.content
2155
2339
  ].filter(Boolean).join("\n\n");
2156
2340
  }
2157
2341
  async function generateDevSearchIndex(ctx, root, pagefind) {
@@ -2173,7 +2357,7 @@ async function generateDevSearchIndex(ctx, root, pagefind) {
2173
2357
  url: route.path,
2174
2358
  content,
2175
2359
  language: toPagefindLanguage(route.locale ?? ctx.projectConfig.i18n?.defaultLocale),
2176
- meta: { title: route.title }
2360
+ meta: { title: route.meta.title }
2177
2361
  });
2178
2362
  assertNoPagefindErrors(`addCustomRecord ${route.path}`, result.errors);
2179
2363
  pageCount += 1;
@@ -2330,15 +2514,18 @@ function normalizeRepositoryUrl(repository) {
2330
2514
  function encodePath(path) {
2331
2515
  return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
2332
2516
  }
2333
- function createSourceUrl(filePath, contentRoot, source) {
2517
+ function createSourceEditUrl(filePath, contentRoot, source) {
2334
2518
  const repository = normalizeRepositoryUrl(source.repository);
2335
2519
  const branch = source.branch ?? "main";
2336
2520
  const sourcePath = [source.directory ? trimSlashes(source.directory) : "", (0, node_path.relative)(contentRoot, filePath).replaceAll("\\", "/")].filter(Boolean).join("/");
2337
2521
  return `${repository}/edit/${encodeURIComponent(branch)}/${encodePath(sourcePath)}`;
2338
2522
  }
2339
- function attachSourceUrls(routes, contentRoot, source) {
2523
+ function attachSourceEditUrls(routes, contentRoot, source) {
2340
2524
  if (!source) return;
2341
- for (const route of routes) route.sourceUrl = createSourceUrl(route.filePath, contentRoot, source);
2525
+ for (const route of routes) route.source = {
2526
+ ...route.source,
2527
+ sourceEditUrl: createSourceEditUrl(route.source.filePath, contentRoot, source)
2528
+ };
2342
2529
  }
2343
2530
  //#endregion
2344
2531
  //#region source/plugins/source-links/index.ts
@@ -2346,7 +2533,7 @@ function createSourceLinksPlugin() {
2346
2533
  return {
2347
2534
  name: "clarify:source-links",
2348
2535
  hooks: { "routes:discovered": (routes, ctx) => {
2349
- attachSourceUrls(routes, (0, node_path.join)(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
2536
+ attachSourceEditUrls(routes, (0, node_path.join)(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
2350
2537
  return routes;
2351
2538
  } }
2352
2539
  };
@@ -2503,9 +2690,9 @@ var CLARIFY_ENV_TEMPLATE = `// This file is generated by Clarify. Do not edit it
2503
2690
  // plugin and slot code, so the TypeScript language service can resolve them.
2504
2691
 
2505
2692
  declare module 'virtual:clarify/slot' {
2506
- import type { SlotContext } from '@clarify-labs/cli'
2693
+ import type { UISlotContext } from '@clarify-labs/cli'
2507
2694
 
2508
- export function useSlot(): SlotContext
2695
+ export function useSlot(): UISlotContext
2509
2696
  }
2510
2697
  `;
2511
2698
  function generateClarifyEnvDts(_plugins) {
@@ -2543,7 +2730,7 @@ function resolveProjectConfigFromContext(contextOrConfig) {
2543
2730
  }
2544
2731
  function routeTitle(contextOrConfig, route) {
2545
2732
  const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2546
- const title = route?.title?.trim();
2733
+ const title = route?.meta.title.trim();
2547
2734
  if (!title || title === projectConfig.title) return projectConfig.title;
2548
2735
  return `${title} - ${projectConfig.title}`;
2549
2736
  }
@@ -2591,8 +2778,8 @@ function injectSSRIntoTemplate(template, appHtml, contextOrConfig, route) {
2591
2778
  const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2592
2779
  let html = injectHtmlLocaleAttributes(template, contextOrConfig, route);
2593
2780
  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(", "));
2781
+ html = setNamedMeta(html, "description", route?.meta.description ?? projectConfig.description);
2782
+ html = setNamedMeta(html, "keywords", route?.meta.keywords?.join(", "));
2596
2783
  html = injectCanonicalUrl(html, contextOrConfig, route);
2597
2784
  const dataPagefindIgnore = route?.isBareAlias ? " data-pagefind-ignore" : "";
2598
2785
  html = html.replace(/<div id="root">([\s\S]*?)<\/div>/, `<div id="root"${dataPagefindIgnore}>${appHtml}</div>`);
@@ -2701,7 +2888,7 @@ function routeOutputFiles(outputDirectory, route) {
2701
2888
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
2702
2889
  import { routes, navigation } from 'virtual:clarify/routes/server';
2703
2890
  import { config } from 'virtual:clarify/config';
2704
- import { openApis } from 'virtual:clarify/openapi';
2891
+ import { openApiSpecs } from 'virtual:clarify/openapi/server';
2705
2892
  import { runtimeSlots } from 'virtual:clarify/slots';
2706
2893
 
2707
2894
  export async function render(url) {
@@ -2709,10 +2896,10 @@ export async function render(url) {
2709
2896
  // them synchronously (renderToString has no Suspense support).
2710
2897
  const entries = Object.values(runtimeSlots).flat()
2711
2898
  await Promise.all(entries.map(async (entry) => {
2712
- const mod = await entry.component()
2899
+ const mod = await entry.loadComponent()
2713
2900
  entry._resolved = mod.default
2714
2901
  }))
2715
- return renderToHTML({ config, routes, navigation, openApis, runtimeSlots, url, themeEditor: config.theme.editor });
2902
+ return renderToHTML({ config, routes, navigation, openApiSpecs, runtimeSlots, url, themeEditor: config.theme.editor });
2716
2903
  }`;
2717
2904
  function createTempEntryFile(content) {
2718
2905
  const entryPath = (0, node_path.join)(createClarifyTempDir("ssr"), "entry-server.ts");
@@ -2799,7 +2986,10 @@ var ClarifyContext = class {
2799
2986
  this.generateOptions = init.generateOptions;
2800
2987
  this.version = init.version;
2801
2988
  this.currentRoutes = init.routes ?? [];
2802
- this.currentNavigation = init.navigation ?? [];
2989
+ this.currentNavigation = init.navigation ?? {
2990
+ kind: "flat",
2991
+ nodes: []
2992
+ };
2803
2993
  this.currentPlugins = init.plugins ?? [];
2804
2994
  }
2805
2995
  get routes() {
@@ -2921,7 +3111,10 @@ var ClarifyEngine = class {
2921
3111
  generateOptions: placeholderOptions,
2922
3112
  version: cliPackageVersion,
2923
3113
  routes: [],
2924
- navigation: [],
3114
+ navigation: {
3115
+ kind: "flat",
3116
+ nodes: []
3117
+ },
2925
3118
  plugins: []
2926
3119
  });
2927
3120
  }
@@ -3024,9 +3217,9 @@ var ClarifyEngine = class {
3024
3217
  routes = await runPhase(plugins, "content:process", this.ctx, async () => {
3025
3218
  const pages = routes.map((route) => ({
3026
3219
  path: route.path,
3027
- filePath: route.filePath,
3028
- frontmatter: route.frontmatter ?? {},
3029
- content: route.content ?? ""
3220
+ filePath: route.source.filePath,
3221
+ frontmatter: route.source.frontmatter ?? {},
3222
+ content: route.source.content ?? ""
3030
3223
  }));
3031
3224
  const resolvedPages = await runHooks(plugins, "pages:resolved", pages, this.ctx);
3032
3225
  const pageByPath = new Map(resolvedPages.map((p) => [p.path, p]));
@@ -3035,13 +3228,22 @@ var ClarifyEngine = class {
3035
3228
  if (!page) return route;
3036
3229
  return {
3037
3230
  ...route,
3038
- frontmatter: page.frontmatter,
3039
- content: page.content
3231
+ source: {
3232
+ ...route.source,
3233
+ frontmatter: page.frontmatter,
3234
+ content: page.content
3235
+ }
3040
3236
  };
3041
3237
  });
3042
3238
  });
3043
3239
  routes = applyConfiguredPageRoutePaths(routes, tabs, i18n);
3044
- const navigation = tabs ? i18n ? buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) ?? {} : buildNavigationFromTabsConfig(routes, tabs) : buildNavigation(routes);
3240
+ const navigation = tabs ? i18n ? buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) ?? {
3241
+ kind: "localized-tabbed",
3242
+ locales: {}
3243
+ } : buildNavigationFromTabsConfig(routes, tabs) : {
3244
+ kind: "flat",
3245
+ nodes: buildNavigation(routes)
3246
+ };
3045
3247
  const resolved = await runHooks(plugins, "routes:resolved", {
3046
3248
  routes,
3047
3249
  navigation
@@ -3075,7 +3277,7 @@ var ClarifyEngine = class {
3075
3277
  writeClarifyEnvDts(this.root, this.plugins);
3076
3278
  }
3077
3279
  hasContentRouteForFile(filePath) {
3078
- return this.routes.some((route) => route.filePath === filePath);
3280
+ return this.routes.some((route) => route.source.filePath === filePath);
3079
3281
  }
3080
3282
  async collectBuildAssets() {
3081
3283
  if (!this.buildEnabled) return [];
@@ -3215,4 +3417,4 @@ Object.defineProperty(exports, "defineConfig", {
3215
3417
  }
3216
3418
  });
3217
3419
 
3218
- //# sourceMappingURL=engine-I04EZzjg.cjs.map
3420
+ //# sourceMappingURL=engine-5bdNX2Oo.cjs.map