@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.
@@ -39,14 +39,14 @@ function renderPlainCode(code) {
39
39
  function isBundledLanguage(language) {
40
40
  return language in bundledLanguages;
41
41
  }
42
- var highlighter;
42
+ var highlighterPromise;
43
43
  var cssVariablesTheme = createCssVariablesTheme();
44
44
  async function getHighlighter() {
45
- highlighter = highlighter ?? await createHighlighter({
45
+ highlighterPromise = highlighterPromise ?? createHighlighter({
46
46
  themes: [cssVariablesTheme],
47
47
  langs: []
48
48
  });
49
- return highlighter;
49
+ return highlighterPromise;
50
50
  }
51
51
  function renderHighlightedCode(html) {
52
52
  return (/<code[^>]*>([\s\S]*?)<\/code>/.exec(html)?.[1] ?? html).split("\n").map((line) => line.trimStart().startsWith("<span") ? line : `<span>${line}</span>`).join("\n");
@@ -308,12 +308,12 @@ function extractMdxSections(content) {
308
308
  return sections;
309
309
  }
310
310
  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
311
+ return sections.map((section) => ({
312
+ id: section.id,
313
+ title: section.title,
314
+ level: section.level,
315
+ badge: section.badge,
316
+ tags: section.tags
317
317
  }));
318
318
  }
319
319
  function normalizePath(path) {
@@ -385,16 +385,20 @@ async function findContentRoutes(dir, base = dir, options = {}) {
385
385
  }
386
386
  routes.push({
387
387
  kind: "mdx",
388
- title,
389
388
  path: cleanPath,
390
389
  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),
390
+ meta: {
391
+ title,
392
+ description: typeof page.frontmatter.description === "string" ? page.frontmatter.description : void 0,
393
+ keywords: frontmatterKeywords(page.frontmatter),
394
+ sections: extractMdxSections(page.content)
395
+ },
396
+ module: { virtualModuleId: "virtual:clarify-page/" + relativePath.replace(/\.mdx?$/, "").replace(/\/+/g, "/") },
397
+ source: {
398
+ filePath: fullPath,
399
+ frontmatter: page.frontmatter,
400
+ content: page.content
401
+ },
398
402
  diagnostic: mdxResult.ok ? void 0 : mdxResult.diagnostic
399
403
  });
400
404
  }
@@ -454,7 +458,7 @@ async function findLocalizedContentRoutes(contentRoot, i18n, options = {}) {
454
458
  path: localizedRoutePath(basePath, locale.code, i18n),
455
459
  basePath,
456
460
  locale: locale.code,
457
- virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.filePath)
461
+ module: { virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.source.filePath) }
458
462
  });
459
463
  }
460
464
  }
@@ -490,12 +494,12 @@ function buildNavigation(routes) {
490
494
  if (!node) {
491
495
  node = {
492
496
  path: pathSoFar,
493
- title: i === parts.length - 1 ? route.title : kebabToTitle(parts[i]),
497
+ title: i === parts.length - 1 ? route.meta.title : kebabToTitle(parts[i]),
494
498
  children: []
495
499
  };
496
500
  current.push(node);
497
501
  }
498
- if (i === parts.length - 1 && route.sections) node.sections = navigationSections(route.sections);
502
+ if (i === parts.length - 1 && route.meta.sections) node.sections = navigationSections(route.meta.sections);
499
503
  if (i < parts.length - 1) {
500
504
  node.children = node.children ?? [];
501
505
  current = node.children;
@@ -504,17 +508,22 @@ function buildNavigation(routes) {
504
508
  }
505
509
  return root;
506
510
  }
507
- function resolvePageItem(item) {
508
- if (typeof item === "string") return { pageRef: item };
511
+ function routeIntentFromPagesItem(item) {
512
+ if (typeof item === "string") return {
513
+ kind: "page",
514
+ ref: item
515
+ };
509
516
  if ("openapi" in item) return {
510
- openapiRef: item.openapi,
511
- openapiTagFilter: item.filter?.tags,
517
+ kind: "openapi",
518
+ ref: item.openapi,
519
+ tagFilter: item.filter?.tags,
512
520
  path: item.path,
513
521
  title: item.title,
514
522
  icon: item.icon
515
523
  };
516
524
  return {
517
- pageRef: item.page,
525
+ kind: "page",
526
+ ref: item.page,
518
527
  path: item.path,
519
528
  redirect: item.redirect,
520
529
  title: item.title,
@@ -529,25 +538,28 @@ function firstNavigationPath(nodes) {
529
538
  }
530
539
  return "/";
531
540
  }
532
- function collectExplicitPagePathItems(tabs) {
533
- const items = [];
541
+ function collectExplicitPagePathIntents(tabs) {
542
+ const intents = [];
534
543
  for (const tab of tabs ?? []) {
535
544
  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
- });
545
+ for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) {
546
+ const intent = routeIntentFromPagesItem(item);
547
+ if (intent.kind === "page" && intent.path) intents.push({
548
+ ...intent,
549
+ path: intent.path
550
+ });
551
+ }
540
552
  }
541
- return items;
553
+ return intents;
542
554
  }
543
555
  function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
544
- const pageItems = collectExplicitPagePathItems(tabs);
545
- if (!pageItems.length) return routes;
556
+ const pageIntents = collectExplicitPagePathIntents(tabs);
557
+ if (!pageIntents.length) return routes;
546
558
  const additions = [];
547
559
  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);
560
+ for (const intent of pageIntents) {
561
+ const sourceBasePath = routePathFromRef(intent.ref);
562
+ const targetBasePath = routePathFromRef(intent.ref, intent.path);
551
563
  if (sourceBasePath === targetBasePath) continue;
552
564
  for (const route of routes) {
553
565
  if (route.kind !== "mdx" || (route.basePath ?? route.path) !== sourceBasePath) continue;
@@ -571,40 +583,42 @@ function buildNavigationFromPagesConfig(routes, config) {
571
583
  return buildNavigationFromConfig(routes, config);
572
584
  }
573
585
  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
- }) };
586
+ return {
587
+ kind: "tabbed",
588
+ tabs: tabs.map((tab) => {
589
+ const children = buildNavigationFromPagesConfig(routes, tab.pages);
590
+ return {
591
+ type: "tab",
592
+ path: firstNavigationPath(children),
593
+ title: resolveLocalizedText(tab.tab, "", "") ?? "",
594
+ icon: tab.icon,
595
+ children
596
+ };
597
+ })
598
+ };
584
599
  }
585
600
  function buildNavigationFromConfig(routes, config) {
586
601
  const routeMap = new Map(routes.map((r) => [r.path, r]));
587
602
  return config.map((group) => {
588
603
  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);
604
+ const intent = routeIntentFromPagesItem(item);
605
+ if (intent.kind === "openapi") {
606
+ const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
592
607
  const route = routeMap.get(path);
593
608
  return {
594
609
  path,
595
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? openapiRef),
596
- icon,
597
- sections: route?.sections ? navigationSections(route.sections) : void 0
610
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
611
+ icon: intent.icon,
612
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
598
613
  };
599
614
  }
600
- const ref = pageRef ?? "";
601
- const path = routePathFromRef(ref, explicitPath);
615
+ const path = routePathFromRef(intent.ref, intent.path);
602
616
  const route = routeMap.get(path);
603
617
  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
618
+ path: intent.redirect ? normalizePath(intent.redirect) : path,
619
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
620
+ icon: intent.icon,
621
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
608
622
  };
609
623
  });
610
624
  return {
@@ -630,16 +644,15 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
630
644
  const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
631
645
  return config.map((group) => {
632
646
  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);
647
+ const intent = routeIntentFromPagesItem(item);
648
+ const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
636
649
  const route = routeMap.get(basePath);
637
650
  const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
638
651
  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
652
+ path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, i18n) : void 0) ?? path,
653
+ title: resolveLocalizedText(intent.title, locale, i18n.defaultLocale) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
654
+ icon: intent.icon,
655
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
643
656
  };
644
657
  });
645
658
  return {
@@ -652,10 +665,10 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
652
665
  }
653
666
  function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
654
667
  if (!i18n) return void 0;
655
- const result = {};
668
+ const locales = {};
656
669
  for (const locale of i18n.locales) {
657
670
  const localeRoutes = routes.filter((route) => route.locale === locale.code);
658
- result[locale.code] = { tabs: tabs.map((tab) => {
671
+ locales[locale.code] = { tabs: tabs.map((tab) => {
659
672
  const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code, i18n);
660
673
  return {
661
674
  type: "tab",
@@ -666,17 +679,20 @@ function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
666
679
  };
667
680
  }) };
668
681
  }
669
- return result;
682
+ return {
683
+ kind: "localized-tabbed",
684
+ locales
685
+ };
670
686
  }
671
687
  /** Serialize a ContentRoute into the minimal shape exposed to tooling. */
672
688
  function toDevRouteEntry(route) {
673
689
  return {
674
690
  path: route.path,
675
- filePath: route.filePath,
691
+ filePath: route.source.filePath,
676
692
  locale: route.locale,
677
693
  basePath: route.basePath,
678
694
  kind: route.kind,
679
- title: route.title
695
+ title: route.meta.title
680
696
  };
681
697
  }
682
698
  /**
@@ -690,7 +706,7 @@ function toDevRouteEntry(route) {
690
706
  * or the first match.
691
707
  */
692
708
  function resolveRouteForFile(filePath, routes, defaultLocale) {
693
- const matches = routes.filter((r) => r.filePath === filePath);
709
+ const matches = routes.filter((r) => r.source.filePath === filePath);
694
710
  if (matches.length === 0) return void 0;
695
711
  if (matches.length === 1) return matches[0];
696
712
  const fileLocale = inferLocaleFromPath(filePath);
@@ -1115,6 +1131,7 @@ var VIRTUAL_CONFIG = "virtual:clarify/config";
1115
1131
  var VIRTUAL_ROUTES = "virtual:clarify/routes";
1116
1132
  var VIRTUAL_SERVER_ROUTES = "virtual:clarify/routes/server";
1117
1133
  var VIRTUAL_OPENAPI = "virtual:clarify/openapi";
1134
+ var VIRTUAL_OPENAPI_SERVER = "virtual:clarify/openapi/server";
1118
1135
  var VIRTUAL_SLOTS = "virtual:clarify/slots";
1119
1136
  var VIRTUAL_SLOT = "virtual:clarify/slot";
1120
1137
  var VIRTUAL_CLIENT_ENTRY = "virtual:clarify/entry-client";
@@ -1135,6 +1152,7 @@ var FIXED_VIRTUAL_IDS = [
1135
1152
  VIRTUAL_ROUTES,
1136
1153
  VIRTUAL_SERVER_ROUTES,
1137
1154
  VIRTUAL_OPENAPI,
1155
+ VIRTUAL_OPENAPI_SERVER,
1138
1156
  VIRTUAL_SLOTS,
1139
1157
  VIRTUAL_SLOT
1140
1158
  ];
@@ -1151,8 +1169,8 @@ function resolveVirtualModuleId(id, modules, routes) {
1151
1169
  for (const virtualId of FIXED_VIRTUAL_IDS) if (id === virtualId || id === resolveVirtualId(virtualId)) return resolveVirtualId(virtualId);
1152
1170
  const moduleId = stripVirtualPrefix(id);
1153
1171
  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);
1172
+ const route = routes.find((route) => route.module.virtualModuleId === id || route.module.virtualModuleId === moduleId);
1173
+ if (route) return resolveVirtualId(route.module.virtualModuleId);
1156
1174
  return null;
1157
1175
  }
1158
1176
  function generateConfigModule(projectConfig, buildOptions, version) {
@@ -1166,38 +1184,38 @@ function generateConfigModule(projectConfig, buildOptions, version) {
1166
1184
  function moduleSpecifier(value) {
1167
1185
  return JSON.stringify(value);
1168
1186
  }
1169
- function routeToRuntimeObject(route, component, mode) {
1170
- const obj = {
1187
+ function routeToRuntimeManifestEntry(route, component, mode) {
1188
+ const entry = {
1171
1189
  path: route.path,
1172
- title: route.title,
1190
+ title: route.meta.title,
1173
1191
  component,
1174
1192
  kind: route.kind
1175
1193
  };
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
1194
+ if (mode === "client") entry.lazy = true;
1195
+ if (route.basePath) entry.basePath = route.basePath;
1196
+ if (route.locale) entry.locale = route.locale;
1197
+ if (route.isFallback) entry.isFallback = true;
1198
+ if (route.isBareAlias) entry.isBareAlias = true;
1199
+ if (route.alternates) entry.alternates = route.alternates;
1200
+ if (route.meta.description) entry.description = route.meta.description;
1201
+ if (route.meta.keywords?.length) entry.keywords = route.meta.keywords;
1202
+ if (route.artifacts?.contentArtifactUrl) entry.contentArtifactUrl = route.artifacts.contentArtifactUrl;
1203
+ if (route.source?.sourceEditUrl) entry.sourceEditUrl = route.source.sourceEditUrl;
1204
+ if (route.meta.sections?.length) entry.sections = route.meta.sections.map((section) => ({
1205
+ id: section.id,
1206
+ title: section.title,
1207
+ level: section.level,
1208
+ badge: section.badge,
1209
+ tags: section.tags
1192
1210
  }));
1193
- return obj;
1211
+ return entry;
1194
1212
  }
1195
1213
  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 }) }))`;
1214
+ 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) => {
1215
+ 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
1216
  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: ")}`;
1217
+ const manifestEntry = routeToRuntimeManifestEntry(r, placeholder, mode);
1218
+ return ` ${JSON.stringify(manifestEntry, null, 0).replace(`"${placeholder}"`, component).replace(/"([A-Za-z_$][\w$]*)":/g, "$1: ")}`;
1201
1219
  }).join(",\n")}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
1202
1220
  }
1203
1221
  function createClientEntryModule(options = {}) {
@@ -1206,9 +1224,15 @@ import '@clarify-labs/renderer/style.css';
1206
1224
  import { render } from '@clarify-labs/renderer/client';
1207
1225
  import { routes, navigation } from '${VIRTUAL_ROUTES}';
1208
1226
  import { config } from '${VIRTUAL_CONFIG}';
1209
- import { openApis } from '${VIRTUAL_OPENAPI}';
1227
+ import { openApiSpecs } from '${VIRTUAL_OPENAPI}';
1210
1228
  import { runtimeSlots } from '${VIRTUAL_SLOTS}';
1211
- render({ config, routes, navigation, openApis, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
1229
+ const renderOptions = { config, routes, navigation, openApiSpecs, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} };
1230
+ render(renderOptions);
1231
+ if (import.meta.hot) {
1232
+ import.meta.hot.accept('${VIRTUAL_ROUTES}', (mod) => {
1233
+ render({ ...renderOptions, routes: mod.routes, navigation: mod.navigation });
1234
+ });
1235
+ }`;
1212
1236
  }
1213
1237
  /**
1214
1238
  * Collect every plugin's `slots` declarations into a single runtime registry
@@ -1237,7 +1261,7 @@ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
1237
1261
  }
1238
1262
  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
1263
  const componentPath = root + slot.component;
1240
- const entry = `{ plugin: ${JSON.stringify(plugin)}, component: () => import(${moduleSpecifier(componentPath)}) }`;
1264
+ const entry = `{ plugin: ${JSON.stringify(plugin)}, loadComponent: () => import(${moduleSpecifier(componentPath)}) }`;
1241
1265
  const list = grouped.get(slot.name) ?? [];
1242
1266
  list.push(entry);
1243
1267
  grouped.set(slot.name, list);
@@ -1266,18 +1290,34 @@ function buildVirtualModules(args) {
1266
1290
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, "client"));
1267
1291
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, "server"));
1268
1292
  modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
1269
- modules.set(VIRTUAL_OPENAPI, "export const openApis = {};");
1293
+ modules.set(VIRTUAL_OPENAPI, "export const openApiSpecs = {};");
1270
1294
  modules.set(VIRTUAL_SLOT, "export { useSlot } from '@clarify-labs/renderer';\n");
1271
1295
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1272
1296
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1273
1297
  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);
1298
+ const moduleContent = route.diagnostic ? generateMdxErrorModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1299
+ modules.set(route.module.virtualModuleId, moduleContent);
1276
1300
  }
1277
1301
  return modules;
1278
1302
  }
1279
1303
  //#endregion
1280
1304
  //#region source/core/adapters.ts
1305
+ function routeStructure(route) {
1306
+ const { content: _content, ...source } = route.source;
1307
+ return {
1308
+ ...route,
1309
+ source
1310
+ };
1311
+ }
1312
+ function createDevStructureSnapshot(routes, navigation) {
1313
+ return {
1314
+ routes: JSON.stringify(routes.map(routeStructure)),
1315
+ navigation: JSON.stringify(navigation)
1316
+ };
1317
+ }
1318
+ function hasDevStructureChanged(before, after) {
1319
+ return before.routes !== after.routes || before.navigation !== after.navigation;
1320
+ }
1281
1321
  function invalidateVirtualModules(engine, server) {
1282
1322
  for (const id of engine.modules.keys()) {
1283
1323
  const moduleNode = server.moduleGraph.getModuleById(resolveVirtualId(id));
@@ -1285,8 +1325,10 @@ function invalidateVirtualModules(engine, server) {
1285
1325
  }
1286
1326
  }
1287
1327
  async function refreshDevServer(engine, server) {
1328
+ const before = createDevStructureSnapshot(engine.routes, engine.navigation);
1288
1329
  await engine.refresh();
1289
1330
  invalidateVirtualModules(engine, server);
1331
+ return hasDevStructureChanged(before, createDevStructureSnapshot(engine.routes, engine.navigation));
1290
1332
  }
1291
1333
  function createNormalizedMdxContentPlugin(engine) {
1292
1334
  return {
@@ -1295,10 +1337,10 @@ function createNormalizedMdxContentPlugin(engine) {
1295
1337
  transform(_code, id) {
1296
1338
  if (!/\.mdx?(?:\?|$)/.test(id)) return null;
1297
1339
  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;
1340
+ const route = engine.routes.find((route) => route.kind === "mdx" && route.source.filePath === filePath);
1341
+ if (!route || route.source.content === void 0) return null;
1300
1342
  return {
1301
- code: route.content,
1343
+ code: route.source.content,
1302
1344
  map: null
1303
1345
  };
1304
1346
  }
@@ -1352,8 +1394,13 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1352
1394
  }
1353
1395
  const relativeContentFile = relative(engine.contentRoot, changedFile);
1354
1396
  if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !isAbsolute(relativeContentFile)) || !engine.hasContentRouteForFile(changedFile)) return;
1355
- await refreshDevServer(engine, ctx.server);
1356
- return [];
1397
+ if (await refreshDevServer(engine, ctx.server)) {
1398
+ const routesModule = ctx.server.moduleGraph.getModuleById(resolveVirtualId(VIRTUAL_ROUTES));
1399
+ if (routesModule) return [...ctx.modules, routesModule];
1400
+ ctx.server.ws.send({ type: "full-reload" });
1401
+ return [];
1402
+ }
1403
+ return ctx.modules;
1357
1404
  },
1358
1405
  resolveId(id) {
1359
1406
  return resolveVirtualModuleId(id, engine.modules, engine.routes);
@@ -1364,18 +1411,6 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1364
1411
  async configureServer(server) {
1365
1412
  server.watcher.add(engine.contentRoot);
1366
1413
  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
1414
  server.middlewares.use((req, res, next) => {
1380
1415
  if (req.url === "/dev/query-preview-route" && req.method === "POST") return handleDevRouteRequest(req, res, engine.routes, engine.runtimeContext());
1381
1416
  if (req.url === "/dev/project-info" && (req.method === "GET" || req.method === "HEAD")) return handleProjectInfoRequest(req, res, engine.runtimeContext());
@@ -1385,7 +1420,7 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1385
1420
  const changedFile = isAbsolute(filePath) ? filePath : join(engine.root, filePath);
1386
1421
  const relativeContentFile = relative(engine.contentRoot, changedFile);
1387
1422
  if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !isAbsolute(relativeContentFile))) return;
1388
- await refreshDevServer(engine, server);
1423
+ if (await refreshDevServer(engine, server)) server.ws.send({ type: "full-reload" });
1389
1424
  };
1390
1425
  server.watcher.on("add", handleContentTreeChange);
1391
1426
  server.watcher.on("unlink", handleContentTreeChange);
@@ -1452,7 +1487,7 @@ function createViteAdapter(engine) {
1452
1487
  }
1453
1488
  var cliPackageVersion = {
1454
1489
  name: "@clarify-labs/cli",
1455
- version: "0.10.0",
1490
+ version: "0.10.1",
1456
1491
  license: "AGPL-3.0-only",
1457
1492
  type: "module",
1458
1493
  repository: {
@@ -1558,11 +1593,14 @@ function routeToOpenAPIArtifactUrl(routePath) {
1558
1593
  return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi.json`;
1559
1594
  }
1560
1595
  function attachContentArtifactUrls(routes) {
1561
- for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path);
1596
+ for (const route of routes) route.artifacts = {
1597
+ ...route.artifacts,
1598
+ contentArtifactUrl: route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path)
1599
+ };
1562
1600
  }
1563
1601
  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}`);
1602
+ if (route.source.content !== void 0) return route.source.content;
1603
+ throw new Error(`Route content is missing from route context: ${route.source.filePath}`);
1566
1604
  }
1567
1605
  function readRouteArtifactContent(route) {
1568
1606
  const content = readRouteContent(route);
@@ -1572,16 +1610,17 @@ function isLlmsTxtRoute(route) {
1572
1610
  return !route.path.split("/").includes("404");
1573
1611
  }
1574
1612
  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);
1613
+ if (route.meta.description) return route.meta.description;
1614
+ const sections = route.meta.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
1577
1615
  if (sections?.length) return `Covers ${sections.join(", ")}.`;
1578
- if (route.keywords?.length) return `Related topics: ${route.keywords.join(", ")}.`;
1616
+ if (route.meta.keywords?.length) return `Related topics: ${route.meta.keywords.join(", ")}.`;
1579
1617
  if (route.kind === "openapi") return "OpenAPI artifact for machine-readable API reference data.";
1580
1618
  }
1581
1619
  function llmsTxtListItem(route, basePath) {
1582
- if (!route.contentArtifactUrl) return void 0;
1620
+ const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
1621
+ if (!contentArtifactUrl) return void 0;
1583
1622
  const description = llmsTxtDescription(route);
1584
- return description ? `- [${route.title}](${basePath}${route.contentArtifactUrl}): ${description}` : `- [${route.title}](${basePath}${route.contentArtifactUrl})`;
1623
+ return description ? `- [${route.meta.title}](${basePath}${contentArtifactUrl}): ${description}` : `- [${route.meta.title}](${basePath}${contentArtifactUrl})`;
1585
1624
  }
1586
1625
  function llmsTxtLocaleLabel(locale, projectConfig) {
1587
1626
  return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
@@ -1598,7 +1637,7 @@ function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
1598
1637
  const routesBySource = /* @__PURE__ */ new Map();
1599
1638
  const defaultLocale = projectConfig.i18n?.defaultLocale;
1600
1639
  for (const route of routes) {
1601
- const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.contentArtifactUrl ?? route.path}`;
1640
+ const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.artifacts?.contentArtifactUrl ?? route.path}`;
1602
1641
  const previousRoute = routesBySource.get(sourceKey);
1603
1642
  if (!previousRoute || previousRoute.locale && !route.locale) routesBySource.set(sourceKey, route);
1604
1643
  }
@@ -1667,9 +1706,9 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
1667
1706
  }
1668
1707
  if (contentPath.endsWith(".openapi.yaml")) {
1669
1708
  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);
1709
+ const route = routes.find((route) => route.artifacts?.contentArtifactUrl === jsonPath);
1710
+ if (route?.kind === "openapi" && route.source.content) {
1711
+ const spec = JSON.parse(route.source.content);
1673
1712
  res.statusCode = 200;
1674
1713
  res.setHeader("Content-Type", "text/yaml; charset=utf-8");
1675
1714
  res.end(stringify(spec, { lineWidth: 0 }), "utf8");
@@ -1677,7 +1716,7 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
1677
1716
  }
1678
1717
  return false;
1679
1718
  }
1680
- const route = routes.find((route) => route.contentArtifactUrl === contentPath);
1719
+ const route = routes.find((route) => route.artifacts?.contentArtifactUrl === contentPath);
1681
1720
  if (!route) return false;
1682
1721
  res.statusCode = 200;
1683
1722
  res.setHeader("Content-Type", resolveContentArtifactType(route));
@@ -1703,14 +1742,15 @@ function createContentArtifactsPlugin() {
1703
1742
  "build:assets": (ctx) => {
1704
1743
  const assets = [];
1705
1744
  for (const route of ctx.routes) {
1706
- if (!route.contentArtifactUrl) continue;
1745
+ const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
1746
+ if (!contentArtifactUrl) continue;
1707
1747
  assets.push({
1708
- fileName: route.contentArtifactUrl.replace(/^\//, ""),
1748
+ fileName: contentArtifactUrl.replace(/^\//, ""),
1709
1749
  source: readRouteArtifactContent(route)
1710
1750
  });
1711
- if (route.kind === "openapi" && route.content) {
1712
- const yamlFileName = route.contentArtifactUrl.replace(/\.json$/, ".yaml");
1713
- const spec = JSON.parse(route.content);
1751
+ if (route.kind === "openapi" && route.source.content) {
1752
+ const yamlFileName = contentArtifactUrl.replace(/\.json$/, ".yaml");
1753
+ const spec = JSON.parse(route.source.content);
1714
1754
  assets.push({
1715
1755
  fileName: yamlFileName.replace(/^\//, ""),
1716
1756
  source: stringify(spec, { lineWidth: 0 })
@@ -1796,6 +1836,7 @@ var OPENAPI_HTTP_METHODS = [
1796
1836
  "patch",
1797
1837
  "trace"
1798
1838
  ];
1839
+ var CLARIFY_OPENAPI_SECTION_ID_EXTENSION = "x-clarify-section-id";
1799
1840
  function errorMessage(error) {
1800
1841
  return error instanceof Error ? error.message : String(error);
1801
1842
  }
@@ -1822,10 +1863,10 @@ function findOpenAPIRoutes(dir, base = dir) {
1822
1863
  routes.push({
1823
1864
  path: cleanPath,
1824
1865
  basePath: cleanPath,
1825
- filePath: fullPath,
1826
- virtualModuleId: virtualModuleIdFromRef(ref),
1827
- title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
1828
- kind: "openapi"
1866
+ kind: "openapi",
1867
+ meta: { title: kebabToTitle(cleanPath.split("/").pop() ?? "API") },
1868
+ module: { virtualModuleId: virtualModuleIdFromRef(ref) },
1869
+ source: { filePath: fullPath }
1829
1870
  });
1830
1871
  }
1831
1872
  return routes;
@@ -1834,36 +1875,80 @@ function operationMatchesTags(operationTags, filterTags) {
1834
1875
  if (!filterTags?.length) return true;
1835
1876
  return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
1836
1877
  }
1878
+ function filterPathItemsByTags(items, tags) {
1879
+ const filteredItems = {};
1880
+ for (const path of Object.keys(items)) {
1881
+ const pathItem = items[path];
1882
+ if (!pathItem) continue;
1883
+ if (OPENAPI_HTTP_METHODS.map((method) => pathItem[method]).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredItems[path] = pathItem;
1884
+ }
1885
+ return filteredItems;
1886
+ }
1887
+ function createOperationSectionId(operation, method, path, kind) {
1888
+ return (typeof operation.operationId === "string" ? operation.operationId.trim() : "") || slug(`${kind === "webhook" ? "webhook " : ""}${method.toLowerCase()} ${path}`);
1889
+ }
1890
+ function operationSectionId(operation) {
1891
+ const sectionId = operation[CLARIFY_OPENAPI_SECTION_ID_EXTENSION];
1892
+ return typeof sectionId === "string" ? sectionId : "";
1893
+ }
1894
+ function normalizeOpenAPISpecSectionIds(spec) {
1895
+ const items = [...Object.entries(spec.paths ?? {}).map(([path, pathItem]) => ({
1896
+ path,
1897
+ pathItem,
1898
+ kind: "path"
1899
+ })), ...Object.entries(spec.webhooks ?? {}).map(([path, pathItem]) => ({
1900
+ path,
1901
+ pathItem,
1902
+ kind: "webhook"
1903
+ }))];
1904
+ for (const { path, pathItem, kind } of items) {
1905
+ if (!pathItem || typeof pathItem !== "object") continue;
1906
+ const operations = pathItem;
1907
+ for (const method of OPENAPI_HTTP_METHODS) {
1908
+ const op = operations[method];
1909
+ if (!op || typeof op !== "object") continue;
1910
+ op[CLARIFY_OPENAPI_SECTION_ID_EXTENSION] = createOperationSectionId(op, method, path, kind);
1911
+ }
1912
+ }
1913
+ return spec;
1914
+ }
1837
1915
  /** Filter an OpenAPI spec to only include paths that have at least one operation matching the given tags. */
1838
1916
  function filterSpecByTags(spec, tags) {
1839
- const filteredPaths = {};
1840
1917
  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
- }
1918
+ const webhooks = spec.webhooks ?? {};
1846
1919
  return {
1847
1920
  ...spec,
1848
- paths: filteredPaths
1921
+ paths: filterPathItemsByTags(paths, tags),
1922
+ webhooks: filterPathItemsByTags(webhooks, tags)
1849
1923
  };
1850
1924
  }
1851
1925
  /** Extract endpoint operations from an OpenAPI spec as page sections. */
1852
1926
  function extractOpenAPISections(spec, filterTags) {
1927
+ normalizeOpenAPISpecSectionIds(spec);
1853
1928
  const sections = [];
1854
- const paths = spec.paths ?? {};
1855
- for (const [path, pathItem] of Object.entries(paths)) {
1929
+ const items = [...Object.entries(spec.paths ?? {}).map(([path, pathItem]) => ({
1930
+ path,
1931
+ pathItem,
1932
+ kind: "path"
1933
+ })), ...Object.entries(spec.webhooks ?? {}).map(([path, pathItem]) => ({
1934
+ path,
1935
+ pathItem,
1936
+ kind: "webhook"
1937
+ }))];
1938
+ for (const { path, pathItem, kind } of items) {
1856
1939
  if (!pathItem) continue;
1940
+ const operations = pathItem;
1857
1941
  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}`;
1942
+ const op = operations[method];
1943
+ const tags = Array.isArray(op?.tags) ? op.tags : void 0;
1944
+ if (!op || !operationMatchesTags(tags, filterTags)) continue;
1945
+ const title = typeof op.summary === "string" ? op.summary : `${method.toUpperCase()} ${path}`;
1861
1946
  sections.push({
1862
- id: slug(`${method} ${path}`),
1947
+ id: operationSectionId(op),
1863
1948
  title,
1864
1949
  level: 2,
1865
- badge: method.toUpperCase(),
1866
- tags: op.tags
1950
+ badge: kind === "webhook" ? "WEBHOOK" : method.toUpperCase(),
1951
+ tags
1867
1952
  });
1868
1953
  }
1869
1954
  }
@@ -1905,12 +1990,16 @@ async function readOpenAPISpec(filePath, contentProcessor) {
1905
1990
  //#endregion
1906
1991
  //#region source/plugins/openapi/virtual-modules.ts
1907
1992
  var openApiRegistryModuleId = VIRTUAL_OPENAPI;
1993
+ var openApiServerRegistryModuleId = VIRTUAL_OPENAPI_SERVER;
1908
1994
  var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
1909
1995
  function specVirtualModuleId(specKey) {
1910
1996
  return `${OPENAPI_SPEC_PREFIX}${specKey}`;
1911
1997
  }
1912
- function generateOpenAPIRegistryModule(openApis) {
1913
- return `export const openApis = ${JSON.stringify(openApis)};`;
1998
+ function generateOpenAPIRegistryModule(openApiSpecs) {
1999
+ return `export const openApiSpecs = {\n${Object.entries(openApiSpecs).map(([key, moduleId]) => ` ${JSON.stringify(key)}: () => import(${JSON.stringify(moduleId)}),`).join("\n")}\n};`;
2000
+ }
2001
+ function generateOpenAPIServerRegistryModule(openApiSpecs) {
2002
+ return `export const openApiSpecs = ${JSON.stringify(openApiSpecs)};`;
1914
2003
  }
1915
2004
  /** Generate a virtual module that exports a single spec as default. */
1916
2005
  function generateOpenAPISpecModule(spec) {
@@ -1918,13 +2007,11 @@ function generateOpenAPISpecModule(spec) {
1918
2007
  }
1919
2008
  function generateOpenAPIPageModule(opts) {
1920
2009
  return `import { createOpenApiRouteComponent } from '@clarify-labs/renderer';
2010
+ import spec from ${JSON.stringify(opts.specModuleId)};
1921
2011
 
1922
- export const routeData = ${JSON.stringify({
1923
- spec: opts.spec,
1924
- tagFilter: opts.tagFilter
1925
- })};
2012
+ export const routeData = {};
1926
2013
 
1927
- export default createOpenApiRouteComponent(routeData);
2014
+ export default createOpenApiRouteComponent({ ...routeData, spec });
1928
2015
  `;
1929
2016
  }
1930
2017
  function generateOpenAPIErrorModule(diagnostic) {
@@ -1943,115 +2030,212 @@ export default createContentDiagnosticComponent(contentDiagnostic);
1943
2030
  }
1944
2031
  //#endregion
1945
2032
  //#region source/plugins/openapi/index.ts
1946
- function collectOpenAPIPageItems(config) {
1947
- const items = [];
2033
+ function collectOpenAPIPageIntents(config) {
2034
+ const intents = [];
1948
2035
  function visitPages(pages) {
1949
2036
  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);
2037
+ for (const group of pages) for (const item of group.pages) {
2038
+ const intent = routeIntentFromPagesItem(item);
2039
+ if (intent.kind === "openapi" && (intent.path || intent.tagFilter?.length)) intents.push(intent);
2040
+ }
1951
2041
  }
1952
2042
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
1953
- return items;
2043
+ return intents;
1954
2044
  }
1955
2045
  function virtualModuleIdFromPath(path) {
1956
2046
  return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
1957
2047
  }
2048
+ function cloneOpenAPIRoute(route, overrides = {}) {
2049
+ return {
2050
+ ...route,
2051
+ meta: {
2052
+ ...route.meta,
2053
+ sections: route.meta.sections ? [...route.meta.sections] : void 0
2054
+ },
2055
+ module: { ...route.module },
2056
+ source: { ...route.source },
2057
+ openapi: route.openapi ? {
2058
+ ...route.openapi,
2059
+ tagFilter: route.openapi.tagFilter ? [...route.openapi.tagFilter] : void 0
2060
+ } : void 0,
2061
+ alternates: route.alternates ? { ...route.alternates } : void 0,
2062
+ ...overrides
2063
+ };
2064
+ }
2065
+ function findOpenAPIRoutesForContentRoot(contentRoot, i18n) {
2066
+ if (!i18n) return findOpenAPIRoutes(contentRoot);
2067
+ const localizedRoutes = [];
2068
+ for (const locale of i18n.locales) {
2069
+ const localeRoot = join(contentRoot, locale.code);
2070
+ for (const route of findOpenAPIRoutes(localeRoot, localeRoot)) {
2071
+ const basePath = route.basePath ?? route.path;
2072
+ const path = localizedRoutePath(basePath, locale.code, i18n);
2073
+ localizedRoutes.push({
2074
+ ...route,
2075
+ path,
2076
+ basePath,
2077
+ locale: locale.code,
2078
+ module: { virtualModuleId: virtualModuleIdFromPath(path) }
2079
+ });
2080
+ }
2081
+ }
2082
+ if (i18n.missing === "fallback") {
2083
+ const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
2084
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
2085
+ for (const sourceRoute of defaultRoutes) {
2086
+ const basePath = sourceRoute.basePath ?? sourceRoute.path;
2087
+ for (const locale of i18n.locales) {
2088
+ const key = `${locale.code}:${basePath}`;
2089
+ if (routeByLocaleAndBase.has(key)) continue;
2090
+ const path = localizedRoutePath(basePath, locale.code, i18n);
2091
+ localizedRoutes.push(cloneOpenAPIRoute(sourceRoute, {
2092
+ path,
2093
+ locale: locale.code,
2094
+ module: { virtualModuleId: virtualModuleIdFromPath(path) },
2095
+ isFallback: true
2096
+ }));
2097
+ }
2098
+ }
2099
+ }
2100
+ return localizedRoutes;
2101
+ }
1958
2102
  function createConfiguredOpenAPIRoutes(routes, config) {
1959
- const pageItems = collectOpenAPIPageItems(config);
1960
- if (!pageItems.length) return routes;
2103
+ const openAPIIntents = collectOpenAPIPageIntents(config);
2104
+ if (!openAPIIntents.length) return routes;
1961
2105
  const additions = [];
1962
2106
  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);
2107
+ for (const intent of openAPIIntents) {
2108
+ const tagFilter = intent.tagFilter?.filter(Boolean);
2109
+ const sourceBasePath = openAPIPagePathFromRef(intent.ref);
2110
+ const targetBasePath = openAPIPagePathFromRef(intent.ref, tagFilter, intent.path);
1967
2111
  if (sourceBasePath === targetBasePath && !tagFilter?.length) continue;
1968
2112
  for (const route of routes) {
1969
2113
  if (route.kind !== "openapi" || (route.basePath ?? route.path) !== sourceBasePath) continue;
1970
2114
  const key = `${route.locale ?? ""}:${targetBasePath}`;
1971
2115
  if (existingKeys.has(key)) continue;
1972
2116
  existingKeys.add(key);
1973
- additions.push({
1974
- ...route,
1975
- path: route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath,
2117
+ const path = route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath;
2118
+ additions.push(cloneOpenAPIRoute(route, {
2119
+ path,
1976
2120
  basePath: targetBasePath,
1977
- title: route.title,
1978
- virtualModuleId: virtualModuleIdFromPath(targetBasePath),
1979
- openapiTagFilter: tagFilter?.length ? tagFilter : void 0
1980
- });
2121
+ module: { virtualModuleId: virtualModuleIdFromPath(path) },
2122
+ openapi: {
2123
+ ...route.openapi,
2124
+ tagFilter: tagFilter?.length ? tagFilter : void 0
2125
+ }
2126
+ }));
1981
2127
  }
1982
2128
  }
1983
2129
  const nextRoutes = [...routes, ...additions];
1984
- return config.i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n)) : nextRoutes;
2130
+ if (!config.i18n) return nextRoutes;
2131
+ const routesWithAlternates = nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n));
2132
+ const bareAliases = [];
2133
+ const seenBare = new Set(routesWithAlternates.map((route) => route.path));
2134
+ for (const route of routesWithAlternates) {
2135
+ if (route.locale !== config.i18n.defaultLocale) continue;
2136
+ const basePath = route.basePath ?? route.path;
2137
+ if (basePath === route.path || seenBare.has(basePath)) continue;
2138
+ seenBare.add(basePath);
2139
+ bareAliases.push(cloneOpenAPIRoute(route, {
2140
+ path: basePath,
2141
+ module: { virtualModuleId: virtualModuleIdFromPath(basePath) },
2142
+ isBareAlias: true
2143
+ }));
2144
+ }
2145
+ return [...routesWithAlternates, ...bareAliases];
1985
2146
  }
1986
- /** Derive a stable, deduplicated key from an absolute spec file path. */
1987
- function specFileKeyFromPath(filePath, projectRoot) {
2147
+ /** Derive a stable, deduplicated id from an absolute spec file path. */
2148
+ function sourceSpecIdFromPath(filePath, projectRoot) {
1988
2149
  return relative(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1989
2150
  }
2151
+ function routeSpecModuleIdFromSourceSpecId(sourceSpecId, tagFilter) {
2152
+ if (!tagFilter?.length) return sourceSpecId;
2153
+ return `${sourceSpecId}_tags_${tagFilter.map((tag) => tag.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "tag").join("_")}`;
2154
+ }
2155
+ function openAPIRegistryKeys(route) {
2156
+ const keys = /* @__PURE__ */ new Set();
2157
+ const path = (route.path || "/").replace(/^\//, "");
2158
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
2159
+ if (route.locale) keys.add(`virtual:clarify-page/${route.locale}/${base}`);
2160
+ if (path) keys.add(`virtual:clarify-page/${path}`);
2161
+ else keys.add("virtual:clarify-page/index");
2162
+ if (!route.locale && base) keys.add(`virtual:clarify-page/${base}`);
2163
+ return [...keys];
2164
+ }
1990
2165
  function createOpenAPIPlugin() {
1991
- /** specs keyed by specFileKey (deduplicated per source file). */
2166
+ /** specs keyed by sourceSpecId (deduplicated per source file). */
1992
2167
  const specs = /* @__PURE__ */ new Map();
2168
+ const specModules = /* @__PURE__ */ new Map();
1993
2169
  return {
1994
2170
  name: "clarify:openapi",
1995
2171
  hooks: {
1996
- "routes:discover": (input) => ({
2172
+ "routes:discover": (input, ctx) => ({
1997
2173
  ...input,
1998
- routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
2174
+ routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.i18n)]
1999
2175
  }),
2000
2176
  "routes:discovered": async (routes, ctx) => {
2001
2177
  specs.clear();
2178
+ specModules.clear();
2002
2179
  const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
2003
2180
  const specByFilePath = /* @__PURE__ */ new Map();
2004
2181
  for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
2005
- const specFromCache = specByFilePath.get(route.filePath);
2182
+ const specFromCache = specByFilePath.get(route.source.filePath);
2006
2183
  const processor = createProjectContentProcessor(ctx.plugins, ctx);
2007
2184
  const result = specFromCache ? {
2008
2185
  ok: true,
2009
2186
  spec: specFromCache
2010
- } : await readOpenAPISpec(route.filePath, processor);
2187
+ } : await readOpenAPISpec(route.source.filePath, processor);
2011
2188
  if (!result.ok) {
2012
2189
  route.diagnostic = result.diagnostic;
2013
- route.title = route.title || "OpenAPI parse error";
2014
- route.sections = [];
2190
+ route.meta.title = route.meta.title || "OpenAPI parse error";
2191
+ route.meta.sections = [];
2015
2192
  continue;
2016
2193
  }
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, {
2194
+ const spec = normalizeOpenAPISpecSectionIds(result.spec);
2195
+ specByFilePath.set(route.source.filePath, spec);
2196
+ const sourceSpecId = sourceSpecIdFromPath(route.source.filePath, ctx.generateOptions.projectRoot);
2197
+ route.openapi = {
2198
+ ...route.openapi,
2199
+ sourceSpecId
2200
+ };
2201
+ specs.set(sourceSpecId, {
2022
2202
  spec,
2023
- filePath: route.filePath
2203
+ filePath: route.source.filePath
2024
2204
  });
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);
2205
+ route.meta.title = spec.info?.title ?? route.meta.title;
2206
+ route.meta.description = spec.info?.description ?? route.meta.description;
2207
+ route.meta.sections = extractOpenAPISections(spec, route.openapi?.tagFilter);
2208
+ const pageSpec = route.openapi?.tagFilter?.length ? filterSpecByTags(spec, route.openapi.tagFilter) : spec;
2209
+ const routeSpecModuleId = routeSpecModuleIdFromSourceSpecId(sourceSpecId, route.openapi?.tagFilter);
2210
+ route.openapi.routeSpecModuleId = routeSpecModuleId;
2211
+ specModules.set(routeSpecModuleId, pageSpec);
2212
+ route.source.content = JSON.stringify(pageSpec);
2030
2213
  }
2031
2214
  return nextRoutes;
2032
2215
  },
2033
2216
  "modules:before": (modules, ctx) => {
2034
2217
  const registryEntries = {};
2218
+ const serverRegistryEntries = {};
2035
2219
  for (const route of ctx.routes) {
2036
- if (route.kind !== "openapi" || !route.specFileKey) continue;
2037
- const spec = specs.get(route.specFileKey)?.spec;
2220
+ if (route.kind !== "openapi" || !route.openapi?.sourceSpecId) continue;
2221
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2222
+ const spec = specModules.get(routeSpecModuleId) ?? specs.get(route.openapi.sourceSpecId)?.spec;
2038
2223
  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;
2224
+ for (const registryKey of openAPIRegistryKeys(route)) {
2225
+ registryEntries[registryKey] = specVirtualModuleId(routeSpecModuleId);
2226
+ serverRegistryEntries[registryKey] = spec;
2227
+ }
2042
2228
  }
2043
2229
  modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
2044
- for (const [specKey, entry] of specs) modules.set(specVirtualModuleId(specKey), generateOpenAPISpecModule(entry.spec));
2230
+ modules.set(openApiServerRegistryModuleId, generateOpenAPIServerRegistryModule(serverRegistryEntries));
2231
+ for (const [routeSpecModuleId, spec] of specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2045
2232
  for (const route of ctx.routes) {
2046
2233
  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
- }));
2234
+ if (route.diagnostic) modules.set(route.module.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
2235
+ else if (route.openapi?.sourceSpecId) {
2236
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2237
+ if (!specModules.has(routeSpecModuleId)) continue;
2238
+ modules.set(route.module.virtualModuleId, generateOpenAPIPageModule({ specModuleId: specVirtualModuleId(routeSpecModuleId) }));
2055
2239
  }
2056
2240
  }
2057
2241
  return modules;
@@ -2119,11 +2303,11 @@ async function generatePagefindIndex(options, pagefind) {
2119
2303
  }
2120
2304
  function routeSearchContent(route) {
2121
2305
  return [
2122
- route.title,
2123
- route.description,
2124
- route.keywords?.join(" "),
2125
- route.sections?.map((section) => section.title).join(" "),
2126
- route.content
2306
+ route.meta.title,
2307
+ route.meta.description,
2308
+ route.meta.keywords?.join(" "),
2309
+ route.meta.sections?.map((section) => section.title).join(" "),
2310
+ route.source.content
2127
2311
  ].filter(Boolean).join("\n\n");
2128
2312
  }
2129
2313
  async function generateDevSearchIndex(ctx, root, pagefind) {
@@ -2145,7 +2329,7 @@ async function generateDevSearchIndex(ctx, root, pagefind) {
2145
2329
  url: route.path,
2146
2330
  content,
2147
2331
  language: toPagefindLanguage(route.locale ?? ctx.projectConfig.i18n?.defaultLocale),
2148
- meta: { title: route.title }
2332
+ meta: { title: route.meta.title }
2149
2333
  });
2150
2334
  assertNoPagefindErrors(`addCustomRecord ${route.path}`, result.errors);
2151
2335
  pageCount += 1;
@@ -2302,15 +2486,18 @@ function normalizeRepositoryUrl(repository) {
2302
2486
  function encodePath(path) {
2303
2487
  return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
2304
2488
  }
2305
- function createSourceUrl(filePath, contentRoot, source) {
2489
+ function createSourceEditUrl(filePath, contentRoot, source) {
2306
2490
  const repository = normalizeRepositoryUrl(source.repository);
2307
2491
  const branch = source.branch ?? "main";
2308
2492
  const sourcePath = [source.directory ? trimSlashes(source.directory) : "", relative(contentRoot, filePath).replaceAll("\\", "/")].filter(Boolean).join("/");
2309
2493
  return `${repository}/edit/${encodeURIComponent(branch)}/${encodePath(sourcePath)}`;
2310
2494
  }
2311
- function attachSourceUrls(routes, contentRoot, source) {
2495
+ function attachSourceEditUrls(routes, contentRoot, source) {
2312
2496
  if (!source) return;
2313
- for (const route of routes) route.sourceUrl = createSourceUrl(route.filePath, contentRoot, source);
2497
+ for (const route of routes) route.source = {
2498
+ ...route.source,
2499
+ sourceEditUrl: createSourceEditUrl(route.source.filePath, contentRoot, source)
2500
+ };
2314
2501
  }
2315
2502
  //#endregion
2316
2503
  //#region source/plugins/source-links/index.ts
@@ -2318,7 +2505,7 @@ function createSourceLinksPlugin() {
2318
2505
  return {
2319
2506
  name: "clarify:source-links",
2320
2507
  hooks: { "routes:discovered": (routes, ctx) => {
2321
- attachSourceUrls(routes, join(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
2508
+ attachSourceEditUrls(routes, join(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
2322
2509
  return routes;
2323
2510
  } }
2324
2511
  };
@@ -2475,9 +2662,9 @@ var CLARIFY_ENV_TEMPLATE = `// This file is generated by Clarify. Do not edit it
2475
2662
  // plugin and slot code, so the TypeScript language service can resolve them.
2476
2663
 
2477
2664
  declare module 'virtual:clarify/slot' {
2478
- import type { SlotContext } from '@clarify-labs/cli'
2665
+ import type { UISlotContext } from '@clarify-labs/cli'
2479
2666
 
2480
- export function useSlot(): SlotContext
2667
+ export function useSlot(): UISlotContext
2481
2668
  }
2482
2669
  `;
2483
2670
  function generateClarifyEnvDts(_plugins) {
@@ -2515,7 +2702,7 @@ function resolveProjectConfigFromContext(contextOrConfig) {
2515
2702
  }
2516
2703
  function routeTitle(contextOrConfig, route) {
2517
2704
  const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2518
- const title = route?.title?.trim();
2705
+ const title = route?.meta.title.trim();
2519
2706
  if (!title || title === projectConfig.title) return projectConfig.title;
2520
2707
  return `${title} - ${projectConfig.title}`;
2521
2708
  }
@@ -2563,8 +2750,8 @@ function injectSSRIntoTemplate(template, appHtml, contextOrConfig, route) {
2563
2750
  const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2564
2751
  let html = injectHtmlLocaleAttributes(template, contextOrConfig, route);
2565
2752
  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(", "));
2753
+ html = setNamedMeta(html, "description", route?.meta.description ?? projectConfig.description);
2754
+ html = setNamedMeta(html, "keywords", route?.meta.keywords?.join(", "));
2568
2755
  html = injectCanonicalUrl(html, contextOrConfig, route);
2569
2756
  const dataPagefindIgnore = route?.isBareAlias ? " data-pagefind-ignore" : "";
2570
2757
  html = html.replace(/<div id="root">([\s\S]*?)<\/div>/, `<div id="root"${dataPagefindIgnore}>${appHtml}</div>`);
@@ -2673,7 +2860,7 @@ function routeOutputFiles(outputDirectory, route) {
2673
2860
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
2674
2861
  import { routes, navigation } from 'virtual:clarify/routes/server';
2675
2862
  import { config } from 'virtual:clarify/config';
2676
- import { openApis } from 'virtual:clarify/openapi';
2863
+ import { openApiSpecs } from 'virtual:clarify/openapi/server';
2677
2864
  import { runtimeSlots } from 'virtual:clarify/slots';
2678
2865
 
2679
2866
  export async function render(url) {
@@ -2681,10 +2868,10 @@ export async function render(url) {
2681
2868
  // them synchronously (renderToString has no Suspense support).
2682
2869
  const entries = Object.values(runtimeSlots).flat()
2683
2870
  await Promise.all(entries.map(async (entry) => {
2684
- const mod = await entry.component()
2871
+ const mod = await entry.loadComponent()
2685
2872
  entry._resolved = mod.default
2686
2873
  }))
2687
- return renderToHTML({ config, routes, navigation, openApis, runtimeSlots, url, themeEditor: config.theme.editor });
2874
+ return renderToHTML({ config, routes, navigation, openApiSpecs, runtimeSlots, url, themeEditor: config.theme.editor });
2688
2875
  }`;
2689
2876
  function createTempEntryFile(content) {
2690
2877
  const entryPath = join(createClarifyTempDir("ssr"), "entry-server.ts");
@@ -2771,7 +2958,10 @@ var ClarifyContext = class {
2771
2958
  this.generateOptions = init.generateOptions;
2772
2959
  this.version = init.version;
2773
2960
  this.currentRoutes = init.routes ?? [];
2774
- this.currentNavigation = init.navigation ?? [];
2961
+ this.currentNavigation = init.navigation ?? {
2962
+ kind: "flat",
2963
+ nodes: []
2964
+ };
2775
2965
  this.currentPlugins = init.plugins ?? [];
2776
2966
  }
2777
2967
  get routes() {
@@ -2893,7 +3083,10 @@ var ClarifyEngine = class {
2893
3083
  generateOptions: placeholderOptions,
2894
3084
  version: cliPackageVersion,
2895
3085
  routes: [],
2896
- navigation: [],
3086
+ navigation: {
3087
+ kind: "flat",
3088
+ nodes: []
3089
+ },
2897
3090
  plugins: []
2898
3091
  });
2899
3092
  }
@@ -2996,9 +3189,9 @@ var ClarifyEngine = class {
2996
3189
  routes = await runPhase(plugins, "content:process", this.ctx, async () => {
2997
3190
  const pages = routes.map((route) => ({
2998
3191
  path: route.path,
2999
- filePath: route.filePath,
3000
- frontmatter: route.frontmatter ?? {},
3001
- content: route.content ?? ""
3192
+ filePath: route.source.filePath,
3193
+ frontmatter: route.source.frontmatter ?? {},
3194
+ content: route.source.content ?? ""
3002
3195
  }));
3003
3196
  const resolvedPages = await runHooks(plugins, "pages:resolved", pages, this.ctx);
3004
3197
  const pageByPath = new Map(resolvedPages.map((p) => [p.path, p]));
@@ -3007,13 +3200,22 @@ var ClarifyEngine = class {
3007
3200
  if (!page) return route;
3008
3201
  return {
3009
3202
  ...route,
3010
- frontmatter: page.frontmatter,
3011
- content: page.content
3203
+ source: {
3204
+ ...route.source,
3205
+ frontmatter: page.frontmatter,
3206
+ content: page.content
3207
+ }
3012
3208
  };
3013
3209
  });
3014
3210
  });
3015
3211
  routes = applyConfiguredPageRoutePaths(routes, tabs, i18n);
3016
- const navigation = tabs ? i18n ? buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) ?? {} : buildNavigationFromTabsConfig(routes, tabs) : buildNavigation(routes);
3212
+ const navigation = tabs ? i18n ? buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) ?? {
3213
+ kind: "localized-tabbed",
3214
+ locales: {}
3215
+ } : buildNavigationFromTabsConfig(routes, tabs) : {
3216
+ kind: "flat",
3217
+ nodes: buildNavigation(routes)
3218
+ };
3017
3219
  const resolved = await runHooks(plugins, "routes:resolved", {
3018
3220
  routes,
3019
3221
  navigation
@@ -3047,7 +3249,7 @@ var ClarifyEngine = class {
3047
3249
  writeClarifyEnvDts(this.root, this.plugins);
3048
3250
  }
3049
3251
  hasContentRouteForFile(filePath) {
3050
- return this.routes.some((route) => route.filePath === filePath);
3252
+ return this.routes.some((route) => route.source.filePath === filePath);
3051
3253
  }
3052
3254
  async collectBuildAssets() {
3053
3255
  if (!this.buildEnabled) return [];
@@ -3140,4 +3342,4 @@ function createClarifyEngine(options = {}) {
3140
3342
  //#endregion
3141
3343
  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
3344
 
3143
- //# sourceMappingURL=engine-3f5Tpa-6.js.map
3345
+ //# sourceMappingURL=engine-Ca_2MCj9.js.map