@clarify-labs/cli 0.10.2 → 0.11.0

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.
@@ -53,6 +53,24 @@ let _apidevtools_swagger_parser = require("@apidevtools/swagger-parser");
53
53
  _apidevtools_swagger_parser = __toESM(_apidevtools_swagger_parser, 1);
54
54
  let node_os = require("node:os");
55
55
  let node_module = require("node:module");
56
+ //#region source/parsers/content/diagnostic.ts
57
+ function displayPath(filePath, projectRoot) {
58
+ if (!filePath) return void 0;
59
+ if (!projectRoot) return filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "");
60
+ const normalized = (0, node_path.relative)(projectRoot, filePath);
61
+ return normalized && !normalized.startsWith("..") && !(0, node_path.isAbsolute)(normalized) ? normalized : filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "");
62
+ }
63
+ function createContentDiagnostic(options) {
64
+ const { error } = options;
65
+ return {
66
+ kind: options.kind,
67
+ title: options.title,
68
+ message: options.message,
69
+ filePath: displayPath(options.filePath, options.projectRoot),
70
+ details: error instanceof Error ? options.includeStack ? error.stack ?? error.message : error.message : String(error)
71
+ };
72
+ }
73
+ //#endregion
56
74
  //#region source/parsers/markdown/mdx.ts
57
75
  function getLanguage(className) {
58
76
  return (Array.isArray(className) ? className : typeof className === "string" ? className.split(/\s+/) : []).find((item) => typeof item === "string" && item.startsWith("language-"))?.replace(/^language-/, "") || "txt";
@@ -88,9 +106,18 @@ var rehypePlugins = [
88
106
  parseMetaString: _clarify_labs_renderer.parseCodeMeta
89
107
  }]
90
108
  ];
91
- function formatMdxDiagnostic(error) {
92
- if (error instanceof Error) return error.message;
93
- return String(error);
109
+ function createContentCompileDiagnostic(options) {
110
+ const { format, phase, error, filePath, projectRoot } = options;
111
+ const label = format === "mdx" ? "MDX" : "Markdown";
112
+ return createContentDiagnostic({
113
+ kind: format,
114
+ title: `${label} ${phase} error`,
115
+ message: phase === "syntax" ? "This page could not be compiled. Fix the MDX syntax or component usage above, then reload the route." : `This page could not be compiled. Fix the ${label} syntax or plugin error below, then reload the route.`,
116
+ error,
117
+ filePath,
118
+ projectRoot,
119
+ includeStack: phase === "compilation"
120
+ });
94
121
  }
95
122
  /**
96
123
  * Validate MDX content for diagnostics WITHOUT running the build-time rehype
@@ -121,13 +148,13 @@ async function compileMdxContent(content, filePath, projectRoot) {
121
148
  } catch (error) {
122
149
  return {
123
150
  ok: false,
124
- diagnostic: {
125
- kind: "mdx",
126
- title: "MDX syntax error",
127
- message: "This page could not be compiled. Fix the MDX syntax or component usage above, then reload the route.",
128
- filePath: filePath && projectRoot ? filePath.replace(projectRoot + "/", "").replace(/^\/+/, "") : filePath ? filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "") : void 0,
129
- details: formatMdxDiagnostic(error)
130
- }
151
+ diagnostic: createContentCompileDiagnostic({
152
+ format: "mdx",
153
+ phase: "syntax",
154
+ error,
155
+ filePath,
156
+ projectRoot
157
+ })
131
158
  };
132
159
  }
133
160
  }
@@ -383,10 +410,10 @@ function frontmatterKeywords(frontmatter) {
383
410
  const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
384
411
  return keywords.length > 0 ? keywords : void 0;
385
412
  }
386
- function withAlternates(route, routes, i18n) {
413
+ function withAlternates(route, routes, locales) {
387
414
  const basePath = route.basePath ?? route.path;
388
415
  const routeByLocaleAndBase = new Map(routes.filter((r) => !r.locale || r.path === `/${r.locale}` || r.path.startsWith(`/${r.locale}/`)).map((r) => [`${r.locale ?? ""}:${r.basePath ?? r.path}`, r]));
389
- const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
416
+ const alternates = Object.fromEntries(locales.locales.flatMap((locale) => {
390
417
  const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
391
418
  return alternate ? [[locale.code, alternate.path]] : [];
392
419
  }));
@@ -399,11 +426,11 @@ function withAlternates(route, routes, i18n) {
399
426
  * 为默认语言的 locale-prefixed 路由生成无前缀的"裸路径别名",方便不带语言
400
427
  * 前缀的 URL 也能访问。标记 `isBareAlias` 以便搜索索引生成时过滤重复索引。
401
428
  */
402
- function generateBareAliases(routes, i18n) {
429
+ function generateBareAliases(routes, locales) {
403
430
  const bareRoutes = [];
404
431
  const seenBare = new Set(routes.map((r) => r.path));
405
432
  for (const route of routes) {
406
- if (route.locale !== i18n.defaultLocale) continue;
433
+ if (route.locale !== locales.default) continue;
407
434
  const bp = route.basePath ?? route.path;
408
435
  if (bp === route.path || seenBare.has(bp)) continue;
409
436
  seenBare.add(bp);
@@ -415,42 +442,42 @@ function generateBareAliases(routes, i18n) {
415
442
  }
416
443
  return bareRoutes;
417
444
  }
418
- async function findLocalizedContentRoutes(contentRoot, i18n, options = {}) {
419
- if (!i18n) return findContentRoutes(contentRoot, contentRoot, options);
445
+ async function findLocalizedContentRoutes(contentRoot, locales, options = {}) {
446
+ if (!locales) return findContentRoutes(contentRoot, contentRoot, options);
420
447
  const localizedRoutes = [];
421
- for (const locale of i18n.locales) {
448
+ for (const locale of locales.locales) {
422
449
  const localeRoot = (0, node_path.join)(contentRoot, locale.code);
423
450
  const discovered = await findContentRoutes(localeRoot, localeRoot, options);
424
451
  for (const route of discovered) {
425
452
  const basePath = route.basePath ?? route.path;
426
453
  localizedRoutes.push({
427
454
  ...route,
428
- path: localizedRoutePath(basePath, locale.code, i18n),
455
+ path: localizedRoutePath(basePath, locale.code, locales),
429
456
  basePath,
430
457
  locale: locale.code,
431
458
  module: { virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.source.filePath) }
432
459
  });
433
460
  }
434
461
  }
435
- if (i18n.missing === "fallback") {
462
+ if (locales.missing === "fallback") {
436
463
  const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
437
- const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
464
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === locales.default);
438
465
  for (const sourceRoute of defaultRoutes) {
439
466
  const basePath = sourceRoute.basePath ?? sourceRoute.path;
440
- for (const locale of i18n.locales) {
467
+ for (const locale of locales.locales) {
441
468
  const key = `${locale.code}:${basePath}`;
442
469
  if (routeByLocaleAndBase.has(key)) continue;
443
470
  localizedRoutes.push({
444
471
  ...sourceRoute,
445
- path: localizedRoutePath(basePath, locale.code, i18n),
472
+ path: localizedRoutePath(basePath, locale.code, locales),
446
473
  locale: locale.code,
447
474
  isFallback: true
448
475
  });
449
476
  }
450
477
  }
451
478
  }
452
- const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
453
- return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, i18n)];
479
+ const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, locales));
480
+ return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, locales)];
454
481
  }
455
482
  function buildNavigation(routes) {
456
483
  const root = [];
@@ -483,6 +510,7 @@ function routeIntentFromPagesItem(item) {
483
510
  kind: "page",
484
511
  ref: item
485
512
  };
513
+ if ("group" in item) throw new Error("Navigation groups do not resolve to route intents");
486
514
  if ("openapi" in item) return {
487
515
  kind: "openapi",
488
516
  ref: item.openapi,
@@ -510,19 +538,23 @@ function firstNavigationPath(nodes) {
510
538
  }
511
539
  function collectExplicitPagePathIntents(tabs) {
512
540
  const intents = [];
513
- for (const tab of tabs ?? []) {
514
- if (!tab.pages || tab.pages === "FileTree") continue;
515
- for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) {
541
+ const collectItems = (items) => {
542
+ for (const item of items) if (typeof item !== "string" && "group" in item) collectItems(item.pages);
543
+ else if (typeof item !== "string" && "page" in item && item.path) {
516
544
  const intent = routeIntentFromPagesItem(item);
517
545
  if (intent.kind === "page" && intent.path) intents.push({
518
546
  ...intent,
519
547
  path: intent.path
520
548
  });
521
549
  }
550
+ };
551
+ for (const tab of tabs ?? []) {
552
+ if (!tab.pages || tab.pages === "FileTree") continue;
553
+ for (const group of tab.pages) collectItems(group.pages);
522
554
  }
523
555
  return intents;
524
556
  }
525
- function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
557
+ function applyConfiguredPageRoutePaths(routes, tabs, locales) {
526
558
  const pageIntents = collectExplicitPagePathIntents(tabs);
527
559
  if (!pageIntents.length) return routes;
528
560
  const additions = [];
@@ -538,15 +570,15 @@ function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
538
570
  existingKeys.add(key);
539
571
  additions.push({
540
572
  ...route,
541
- path: route.locale && i18n ? localizedRoutePath(targetBasePath, route.locale, i18n) : targetBasePath,
573
+ path: route.locale && locales ? localizedRoutePath(targetBasePath, route.locale, locales) : targetBasePath,
542
574
  basePath: targetBasePath
543
575
  });
544
576
  }
545
577
  }
546
578
  const nextRoutes = [...routes, ...additions];
547
- const routesWithAlternates = i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, i18n)) : nextRoutes;
548
- if (!i18n) return routesWithAlternates;
549
- return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, i18n)];
579
+ const routesWithAlternates = locales ? nextRoutes.map((route) => withAlternates(route, nextRoutes, locales)) : nextRoutes;
580
+ if (!locales) return routesWithAlternates;
581
+ return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, locales)];
550
582
  }
551
583
  function buildNavigationFromPagesConfig(routes, config) {
552
584
  if (!config || config === "FileTree") return buildNavigation(routes);
@@ -569,28 +601,38 @@ function buildNavigationFromTabsConfig(routes, tabs) {
569
601
  }
570
602
  function buildNavigationFromConfig(routes, config) {
571
603
  const routeMap = new Map(routes.map((r) => [r.path, r]));
572
- return config.map((group) => {
573
- const children = group.pages.map((item) => {
574
- const intent = routeIntentFromPagesItem(item);
575
- if (intent.kind === "openapi") {
576
- const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
577
- const route = routeMap.get(path);
578
- return {
579
- path,
580
- title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
581
- icon: intent.icon,
582
- sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
583
- };
584
- }
585
- const path = routePathFromRef(intent.ref, intent.path);
604
+ const buildItem = (item) => {
605
+ if (typeof item !== "string" && "group" in item) {
606
+ const children = item.pages.map(buildItem);
607
+ return {
608
+ path: firstNavigationPath(children),
609
+ title: resolveLocalizedText(item.group, "", "") ?? "",
610
+ icon: item.icon,
611
+ children
612
+ };
613
+ }
614
+ const intent = routeIntentFromPagesItem(item);
615
+ if (intent.kind === "openapi") {
616
+ const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
586
617
  const route = routeMap.get(path);
587
618
  return {
588
- path: intent.redirect ? normalizePath(intent.redirect) : path,
619
+ path,
589
620
  title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
590
621
  icon: intent.icon,
591
622
  sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
592
623
  };
593
- });
624
+ }
625
+ const path = routePathFromRef(intent.ref, intent.path);
626
+ const route = routeMap.get(path);
627
+ return {
628
+ path: intent.redirect ? normalizePath(intent.redirect) : path,
629
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
630
+ icon: intent.icon,
631
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
632
+ };
633
+ };
634
+ return config.map((group) => {
635
+ const children = group.pages.map(buildItem);
594
636
  return {
595
637
  path: children[0]?.path ?? "/",
596
638
  title: resolveLocalizedText(group.group, "", "") ?? "",
@@ -599,51 +641,61 @@ function buildNavigationFromConfig(routes, config) {
599
641
  };
600
642
  });
601
643
  }
602
- function localizeNavigationPaths(nodes, locale, i18n) {
644
+ function localizeNavigationPaths(nodes, locale, locales) {
603
645
  return nodes.map((node) => ({
604
646
  ...node,
605
- path: localizedRoutePath(node.path, locale, i18n),
606
- children: node.children ? localizeNavigationPaths(node.children, locale, i18n) : void 0
647
+ path: localizedRoutePath(node.path, locale, locales),
648
+ children: node.children ? localizeNavigationPaths(node.children, locale, locales) : void 0
607
649
  }));
608
650
  }
609
- function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
651
+ function buildLocalizedNavigationForLocale(routes, config, locale, locales) {
610
652
  if (!config || config === "FileTree") return localizeNavigationPaths(buildNavigation(routes.map((route) => ({
611
653
  ...route,
612
654
  path: route.basePath ?? route.path
613
- }))), locale, i18n);
655
+ }))), locale, locales);
614
656
  const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
615
- return config.map((group) => {
616
- const children = group.pages.map((item) => {
617
- const intent = routeIntentFromPagesItem(item);
618
- const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
619
- const route = routeMap.get(basePath);
620
- const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
657
+ const buildItem = (item) => {
658
+ if (typeof item !== "string" && "group" in item) {
659
+ const children = item.pages.map(buildItem);
621
660
  return {
622
- path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, i18n) : void 0) ?? path,
623
- title: resolveLocalizedText(intent.title, locale, i18n.defaultLocale) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
624
- icon: intent.icon,
625
- sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
661
+ path: firstNavigationPath(children),
662
+ title: resolveLocalizedText(item.group, locale, locales.default) ?? "",
663
+ icon: item.icon,
664
+ children
626
665
  };
627
- });
666
+ }
667
+ const intent = routeIntentFromPagesItem(item);
668
+ const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
669
+ const route = routeMap.get(basePath);
670
+ const path = route?.path ?? localizedRoutePath(basePath, locale, locales);
671
+ return {
672
+ path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, locales) : void 0) ?? path,
673
+ title: resolveLocalizedText(intent.title, locale, locales.default) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
674
+ icon: intent.icon,
675
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
676
+ };
677
+ };
678
+ return config.map((group) => {
679
+ const children = group.pages.map(buildItem);
628
680
  return {
629
- path: children[0]?.path ?? localizedRoutePath("/", locale, i18n),
630
- title: resolveLocalizedText(group.group, locale, i18n.defaultLocale) ?? "",
681
+ path: children[0]?.path ?? localizedRoutePath("/", locale, locales),
682
+ title: resolveLocalizedText(group.group, locale, locales.default) ?? "",
631
683
  icon: group.icon,
632
684
  children
633
685
  };
634
686
  });
635
687
  }
636
- function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
637
- if (!i18n) return void 0;
688
+ function buildLocalizedNavigationFromTabsConfig(routes, tabs, localesConfig) {
689
+ if (!localesConfig) return void 0;
638
690
  const locales = {};
639
- for (const locale of i18n.locales) {
691
+ for (const locale of localesConfig.locales) {
640
692
  const localeRoutes = routes.filter((route) => route.locale === locale.code);
641
693
  locales[locale.code] = { tabs: tabs.map((tab) => {
642
- const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code, i18n);
694
+ const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code, localesConfig);
643
695
  return {
644
696
  type: "tab",
645
697
  path: firstNavigationPath(children),
646
- title: resolveLocalizedText(tab.tab, locale.code, i18n.defaultLocale) ?? "",
698
+ title: resolveLocalizedText(tab.tab, locale.code, localesConfig.default) ?? "",
647
699
  icon: tab.icon,
648
700
  children
649
701
  };
@@ -710,7 +762,7 @@ function inferRouteForFile(filePath, contentRoot, defaultLocale) {
710
762
  const localeSegmentIndex = fileLocale ? segments.indexOf(fileLocale) : -1;
711
763
  const basePath = basePathFromRef(localeSegmentIndex >= 0 ? segments.slice(localeSegmentIndex + 1).join("/") : ref);
712
764
  const path = fileLocale && defaultLocale ? localizedRoutePath(basePath, fileLocale, {
713
- defaultLocale,
765
+ default: defaultLocale,
714
766
  missing: "fallback",
715
767
  locales: []
716
768
  }) : basePath;
@@ -763,12 +815,12 @@ async function handleDevRouteRequest(req, res, routes, context) {
763
815
  }
764
816
  const file = typeof body === "object" && body !== null ? body.file : void 0;
765
817
  if (typeof file === "string" && file.length > 0) {
766
- const route = resolveRouteForFile(file, routes, context.projectConfig.i18n?.defaultLocale);
818
+ const route = resolveRouteForFile(file, routes, context.projectConfig.locales?.default);
767
819
  if (route) {
768
820
  res.end(JSON.stringify(toDevRouteEntry(route)));
769
821
  return;
770
822
  }
771
- const inferred = inferRouteForFile(file, context.contentRoot, context.projectConfig.i18n?.defaultLocale);
823
+ const inferred = inferRouteForFile(file, context.contentRoot, context.projectConfig.locales?.default);
772
824
  res.end(JSON.stringify(inferred));
773
825
  return;
774
826
  }
@@ -794,8 +846,7 @@ function resolveThemeConfig(theme = {}) {
794
846
  layout: {
795
847
  ...presetTheme.layout,
796
848
  ...theme.layout
797
- },
798
- editor: theme.editor ?? presetTheme.editor
849
+ }
799
850
  };
800
851
  }
801
852
  //#endregion
@@ -803,26 +854,26 @@ function resolveThemeConfig(theme = {}) {
803
854
  var clarifyLogoConfigSchema = zod.z.union([zod.z.string(), zod.z.object({
804
855
  light: zod.z.string().optional(),
805
856
  dark: zod.z.string().optional()
806
- })]);
857
+ }).strict()]);
807
858
  var clarifyFaviconConfigSchema = zod.z.union([zod.z.string(), zod.z.object({
808
859
  light: zod.z.string().optional(),
809
860
  dark: zod.z.string().optional()
810
- })]);
861
+ }).strict()]);
811
862
  var clarifyLocalizedTextSchema = zod.z.union([zod.z.string(), zod.z.record(zod.z.string(), zod.z.string())]);
812
863
  var clarifyLocaleConfigSchema = zod.z.object({
813
864
  code: zod.z.string(),
814
865
  label: zod.z.string(),
815
866
  dir: zod.z.union([zod.z.literal("ltr"), zod.z.literal("rtl")]).optional()
816
- });
817
- var clarifyI18nConfigSchema = zod.z.object({
818
- defaultLocale: zod.z.string().optional(),
867
+ }).strict();
868
+ var clarifyLocalesConfigSchema = zod.z.object({
869
+ default: zod.z.string().optional(),
819
870
  missing: zod.z.union([
820
871
  zod.z.literal("fallback"),
821
872
  zod.z.literal("404"),
822
873
  zod.z.literal("hide")
823
874
  ]).optional(),
824
875
  locales: zod.z.array(clarifyLocaleConfigSchema).min(1)
825
- }).superRefine((config, ctx) => {
876
+ }).strict().superRefine((config, ctx) => {
826
877
  const localeCodes = /* @__PURE__ */ new Set();
827
878
  for (const [index, locale] of config.locales.entries()) {
828
879
  if (localeCodes.has(locale.code)) ctx.addIssue({
@@ -836,17 +887,17 @@ var clarifyI18nConfigSchema = zod.z.object({
836
887
  });
837
888
  localeCodes.add(locale.code);
838
889
  }
839
- if (config.defaultLocale && !localeCodes.has(config.defaultLocale)) ctx.addIssue({
890
+ if (config.default && !localeCodes.has(config.default)) ctx.addIssue({
840
891
  code: zod.z.ZodIssueCode.custom,
841
- message: "defaultLocale must be one of i18n.locales",
842
- path: ["defaultLocale"]
892
+ message: "default must be one of locales.locales",
893
+ path: ["default"]
843
894
  });
844
895
  });
845
896
  var clarifyNavbarLinkSchema = zod.z.object({
846
897
  label: clarifyLocalizedTextSchema,
847
898
  href: zod.z.string(),
848
899
  external: zod.z.boolean().optional()
849
- });
900
+ }).strict();
850
901
  var clarifyBannerConfigOptionsSchema = zod.z.object({
851
902
  content: clarifyLocalizedTextSchema,
852
903
  dismissible: zod.z.boolean().optional(),
@@ -864,45 +915,46 @@ var clarifyVariableValueSchema = zod.z.lazy(() => zod.z.union([
864
915
  zod.z.record(zod.z.string(), clarifyVariableValueSchema)
865
916
  ]));
866
917
  var clarifyVariablesConfigSchema = zod.z.record(zod.z.string(), clarifyVariableValueSchema);
867
- var clarifySourceConfigSchema = zod.z.object({
868
- repository: zod.z.string(),
918
+ var clarifyRepositoryConfigSchema = zod.z.object({
919
+ url: zod.z.string().optional(),
869
920
  branch: zod.z.string().optional(),
870
921
  directory: zod.z.string().optional()
871
- });
872
- var clarifyPagesItemSchema = zod.z.union([
922
+ }).strict();
923
+ var clarifyPagesItemSchema = zod.z.lazy(() => zod.z.union([
873
924
  zod.z.string(),
925
+ clarifyPagesGroupSchema,
874
926
  zod.z.object({
875
927
  page: zod.z.string(),
876
928
  path: zod.z.string().optional(),
877
929
  title: clarifyLocalizedTextSchema.optional(),
878
930
  icon: zod.z.string().optional(),
879
931
  redirect: zod.z.string().optional()
880
- }),
932
+ }).strict(),
881
933
  zod.z.object({
882
934
  openapi: zod.z.string(),
883
935
  path: zod.z.string().optional(),
884
936
  icon: zod.z.string().optional(),
885
937
  title: clarifyLocalizedTextSchema.optional(),
886
- filter: zod.z.object({ tags: zod.z.array(zod.z.string()).optional() }).optional()
887
- })
888
- ]);
889
- var clarifyPagesGroupSchema = zod.z.object({
938
+ filter: zod.z.object({ tags: zod.z.array(zod.z.string()).optional() }).strict().optional()
939
+ }).strict()
940
+ ]));
941
+ var clarifyPagesGroupSchema = zod.z.lazy(() => zod.z.object({
890
942
  group: clarifyLocalizedTextSchema,
891
943
  icon: zod.z.string().optional(),
892
944
  pages: zod.z.array(clarifyPagesItemSchema)
893
- });
945
+ }).strict());
894
946
  var clarifyPagesConfigSchema = zod.z.union([zod.z.array(clarifyPagesGroupSchema), zod.z.literal("FileTree")]);
895
947
  var clarifyTabItemSchema = zod.z.object({
896
948
  tab: clarifyLocalizedTextSchema,
897
949
  icon: zod.z.string().optional(),
898
950
  pages: clarifyPagesConfigSchema.optional()
899
- });
951
+ }).strict();
900
952
  var clarifyTabsConfigSchema = zod.z.array(clarifyTabItemSchema);
901
953
  var clarifyThemePresetSchema = zod.z.enum(["default", "base"]);
902
954
  var clarifyThemeModeColorValueSchema = zod.z.object({
903
955
  light: zod.z.string().optional(),
904
956
  dark: zod.z.string().optional()
905
- });
957
+ }).strict();
906
958
  var clarifyThemeColorValueSchema = zod.z.union([zod.z.string(), clarifyThemeModeColorValueSchema]);
907
959
  var clarifyThemeColorTokensConfigSchema = zod.z.object({
908
960
  primary: clarifyThemeColorValueSchema.optional(),
@@ -913,42 +965,65 @@ var clarifyThemeColorTokensConfigSchema = zod.z.object({
913
965
  muted: clarifyThemeColorValueSchema.optional(),
914
966
  border: clarifyThemeColorValueSchema.optional(),
915
967
  codeBackground: clarifyThemeColorValueSchema.optional()
916
- });
968
+ }).strict();
917
969
  var clarifyThemeRadiusTokensConfigSchema = zod.z.object({
918
970
  sm: zod.z.string().optional(),
919
971
  md: zod.z.string().optional(),
920
972
  lg: zod.z.string().optional(),
921
973
  xl: zod.z.string().optional()
922
- });
974
+ }).strict();
923
975
  var clarifyThemeTokensConfigSchema = zod.z.object({
924
976
  colors: clarifyThemeColorTokensConfigSchema.optional(),
925
977
  radius: clarifyThemeRadiusTokensConfigSchema.optional()
926
- });
927
- var clarifyThemeLayoutConfigSchema = zod.z.object({ maxWidth: zod.z.string().optional() });
978
+ }).strict();
979
+ var clarifyThemeLayoutConfigSchema = zod.z.object({ maxWidth: zod.z.string().optional() }).strict();
928
980
  var clarifyThemeConfigSchema = zod.z.object({
929
981
  preset: clarifyThemePresetSchema.optional(),
930
982
  tokens: clarifyThemeTokensConfigSchema.optional(),
931
- layout: clarifyThemeLayoutConfigSchema.optional(),
932
- editor: zod.z.boolean().optional()
933
- });
983
+ layout: clarifyThemeLayoutConfigSchema.optional()
984
+ }).strict();
985
+ function featureSchema(shape, defaults) {
986
+ const objectSchema = zod.z.object({
987
+ enabled: zod.z.boolean().default(true),
988
+ ...shape
989
+ }).strict().default({
990
+ enabled: true,
991
+ ...defaults
992
+ });
993
+ return zod.z.union([zod.z.boolean(), objectSchema]).default({
994
+ enabled: true,
995
+ ...defaults
996
+ }).transform((value) => typeof value === "boolean" ? {
997
+ enabled: value,
998
+ ...defaults
999
+ } : value);
1000
+ }
1001
+ var clarifyFeaturesConfigSchema = zod.z.object({
1002
+ search: featureSchema({ provider: zod.z.literal("pagefind").default("pagefind") }, { provider: "pagefind" }),
1003
+ repository: featureSchema(clarifyRepositoryConfigSchema.shape, {}),
1004
+ themeEditor: featureSchema({}, {}),
1005
+ openapi: featureSchema({ playground: zod.z.boolean().default(true) }, { playground: true })
1006
+ }).strict();
934
1007
  var clarifyProjectConfigSchema = zod.z.object({
935
1008
  title: zod.z.string().optional(),
936
1009
  description: zod.z.string().optional(),
937
1010
  siteUrl: zod.z.string().optional(),
938
- source: clarifySourceConfigSchema.optional(),
1011
+ routePrefix: zod.z.string().default("/"),
1012
+ assetPrefix: zod.z.string().optional(),
939
1013
  logo: clarifyLogoConfigSchema.optional(),
940
1014
  homeUrl: zod.z.string().optional(),
941
1015
  favicon: clarifyFaviconConfigSchema.optional(),
942
1016
  theme: clarifyThemeConfigSchema.optional(),
943
- routePrefix: zod.z.string().optional(),
944
- assetPrefix: zod.z.string().optional(),
945
- navbar: zod.z.object({ links: zod.z.array(clarifyNavbarLinkSchema).optional() }).optional(),
1017
+ navigation: zod.z.object({
1018
+ links: zod.z.array(clarifyNavbarLinkSchema).optional(),
1019
+ tabs: clarifyTabsConfigSchema.optional()
1020
+ }).strict().optional(),
946
1021
  banner: clarifyBannerConfigOptionsSchema.optional(),
947
1022
  footer: clarifyFooterConfigSchema.optional(),
1023
+ locales: clarifyLocalesConfigSchema.optional(),
948
1024
  variables: clarifyVariablesConfigSchema.optional(),
949
- i18n: clarifyI18nConfigSchema.optional(),
950
- tabs: clarifyTabsConfigSchema.optional()
951
- });
1025
+ features: clarifyFeaturesConfigSchema.optional()
1026
+ }).strict();
952
1027
  //#endregion
953
1028
  //#region source/core/config/config.ts
954
1029
  function formatIssuePath(path) {
@@ -970,17 +1045,20 @@ function validateProjectConfig(value) {
970
1045
  if (!result.success) throw new Error(formatProjectConfigError(result.error));
971
1046
  return result.data;
972
1047
  }
973
- function resolveI18nConfig(i18n) {
974
- if (!i18n) return void 0;
975
- const firstLocale = i18n.locales[0]?.code;
976
- const defaultLocale = i18n.defaultLocale ?? firstLocale;
1048
+ function resolveLocalesConfig(locales) {
1049
+ if (!locales) return void 0;
1050
+ const firstLocale = locales.locales[0]?.code;
1051
+ const defaultLocale = locales.default ?? firstLocale;
977
1052
  if (!defaultLocale) return void 0;
978
1053
  return {
979
- defaultLocale,
980
- missing: i18n.missing ?? "fallback",
981
- locales: i18n.locales
1054
+ default: defaultLocale,
1055
+ missing: locales.missing ?? "fallback",
1056
+ locales: locales.locales
982
1057
  };
983
1058
  }
1059
+ function resolveFeaturesConfig(features = {}) {
1060
+ return clarifyFeaturesConfigSchema.parse(features);
1061
+ }
984
1062
  function resolveRoutePrefix(routePrefix) {
985
1063
  const trimmed = routePrefix?.trim();
986
1064
  if (!trimmed || trimmed === "/") return "/";
@@ -1001,26 +1079,24 @@ function resolveProjectConfig(config = {}) {
1001
1079
  title: config.title ?? "Clarify Docs",
1002
1080
  description: config.description ?? "",
1003
1081
  siteUrl: config.siteUrl,
1004
- source: config.source,
1082
+ routePrefix,
1083
+ assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
1005
1084
  logo: config.logo,
1006
1085
  homeUrl: config.homeUrl,
1007
1086
  favicon: config.favicon,
1008
- routePrefix,
1009
- assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
1010
1087
  theme: resolveThemeConfig(config.theme),
1011
- navbar: config.navbar,
1088
+ navigation: config.navigation,
1012
1089
  banner: config.banner,
1013
1090
  footer: config.footer,
1091
+ locales: resolveLocalesConfig(config.locales),
1014
1092
  variables: config.variables ?? {},
1015
- i18n: resolveI18nConfig(config.i18n),
1016
- tabs: config.tabs
1093
+ features: resolveFeaturesConfig(config.features)
1017
1094
  };
1018
1095
  }
1019
1096
  //#endregion
1020
1097
  //#region source/core/config/user-config.ts
1021
1098
  function defineConfig(config) {
1022
- validateProjectConfig(config);
1023
- return config;
1099
+ return validateClarifyConfig(config);
1024
1100
  }
1025
1101
  /**
1026
1102
  * Config filenames Clarify CLI looks for at the project root, in resolution
@@ -1044,18 +1120,23 @@ function assertConfigObject(config, configFile) {
1044
1120
  }
1045
1121
  function loadJsonConfig(configFile) {
1046
1122
  const config = assertConfigObject(JSON.parse((0, node_fs.readFileSync)(configFile, "utf-8")), configFile);
1047
- const projectConfig = validateProjectConfig(config);
1048
- const ssg = config.ssg && typeof config.ssg === "object" && !Array.isArray(config.ssg) ? config.ssg : void 0;
1049
- return {
1123
+ if ("plugins" in config) throw new Error("[clarify] clarify.json does not support plugins; use clarify.ts or clarify.js");
1124
+ return validateClarifyConfig(config);
1125
+ }
1126
+ function validateClarifyConfig(config) {
1127
+ const { plugins, ...projectConfigInput } = config;
1128
+ if (plugins !== void 0 && !Array.isArray(plugins)) throw new Error("[clarify] config field \"plugins\" is invalid: Expected an array");
1129
+ const projectConfig = validateProjectConfig(projectConfigInput);
1130
+ return plugins ? {
1050
1131
  ...projectConfig,
1051
- ...ssg ? { ssg } : {}
1052
- };
1132
+ plugins
1133
+ } : projectConfig;
1053
1134
  }
1054
1135
  async function loadClarifyConfig(root, env) {
1055
1136
  const configFile = findClarifyConfigFile(root);
1056
1137
  if (!configFile) return {};
1057
1138
  if (configFile.endsWith(".json")) return loadJsonConfig(configFile);
1058
- return assertConfigObject((await (0, vite.loadConfigFromFile)(env, configFile, root, "silent"))?.config, configFile);
1139
+ return validateClarifyConfig(assertConfigObject((await (0, vite.loadConfigFromFile)(env, configFile, root, "silent"))?.config, configFile));
1059
1140
  }
1060
1141
  /**
1061
1142
  * File extensions Clarify treats as content files. Kept in sync with the
@@ -1081,9 +1162,9 @@ function buildProjectInfo(context) {
1081
1162
  contentRoot: (contentRoot.startsWith(projectRoot) ? contentRoot.slice(projectRoot.length).replace(/^[/\\]+/, "") : contentRoot) || "source",
1082
1163
  projectRoot
1083
1164
  };
1084
- if (projectConfig.i18n) response.i18n = {
1085
- defaultLocale: projectConfig.i18n.defaultLocale,
1086
- locales: projectConfig.i18n.locales.map((l) => l.code)
1165
+ if (projectConfig.locales) response.locales = {
1166
+ default: projectConfig.locales.default,
1167
+ locales: projectConfig.locales.locales.map((l) => l.code)
1087
1168
  };
1088
1169
  return response;
1089
1170
  }
@@ -1143,10 +1224,23 @@ function resolveVirtualModuleId(id, modules, routes) {
1143
1224
  if (route) return resolveVirtualId(route.module.virtualModuleId);
1144
1225
  return null;
1145
1226
  }
1146
- function generateConfigModule(projectConfig, buildOptions, version) {
1227
+ function generateConfigModule(projectConfig, version) {
1147
1228
  const runtimeConfig = {
1148
- ...projectConfig,
1149
- ...buildOptions
1229
+ title: projectConfig.title,
1230
+ description: projectConfig.description,
1231
+ siteUrl: projectConfig.siteUrl,
1232
+ routePrefix: projectConfig.routePrefix,
1233
+ assetPrefix: projectConfig.assetPrefix,
1234
+ logo: projectConfig.logo,
1235
+ homeUrl: projectConfig.homeUrl,
1236
+ favicon: projectConfig.favicon,
1237
+ theme: projectConfig.theme,
1238
+ navigation: projectConfig.navigation,
1239
+ banner: projectConfig.banner,
1240
+ footer: projectConfig.footer,
1241
+ locales: projectConfig.locales,
1242
+ variables: projectConfig.variables,
1243
+ features: projectConfig.features
1150
1244
  };
1151
1245
  if (version) runtimeConfig.version = version;
1152
1246
  return `export const config = ${JSON.stringify(runtimeConfig)};`;
@@ -1238,16 +1332,10 @@ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
1238
1332
  }
1239
1333
  return `export const runtimeSlots = {\n${[...grouped.entries()].map(([name, list]) => ` ${JSON.stringify(name)}: [\n ${list.join(",\n ")}\n ]`).join(",\n")}\n};\n`;
1240
1334
  }
1241
- function generateMdxErrorModule(diagnostic) {
1335
+ function generateContentDiagnosticModule(diagnostic) {
1242
1336
  return `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';
1243
1337
 
1244
- export const contentDiagnostic = ${JSON.stringify({
1245
- kind: diagnostic.kind ?? "mdx",
1246
- title: diagnostic.title,
1247
- message: diagnostic.message,
1248
- filePath: diagnostic.filePath,
1249
- details: diagnostic.details
1250
- })};
1338
+ export const contentDiagnostic = ${JSON.stringify(diagnostic)};
1251
1339
 
1252
1340
  export default createContentDiagnosticComponent(contentDiagnostic);
1253
1341
  `;
@@ -1256,7 +1344,7 @@ function buildVirtualModules(args) {
1256
1344
  const modules = /* @__PURE__ */ new Map();
1257
1345
  const clientEntryModule = createClientEntryModule({ themeEditor: args.themeEditor });
1258
1346
  const allPlugins = [...args.plugins ?? []];
1259
- modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions, args.version));
1347
+ modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.version));
1260
1348
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, "client"));
1261
1349
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, "server"));
1262
1350
  modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
@@ -1265,7 +1353,7 @@ function buildVirtualModules(args) {
1265
1353
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1266
1354
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1267
1355
  for (const route of args.routes) {
1268
- const moduleContent = route.diagnostic ? generateMdxErrorModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1356
+ const moduleContent = route.diagnostic ? generateContentDiagnosticModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1269
1357
  modules.set(route.module.virtualModuleId, moduleContent);
1270
1358
  }
1271
1359
  return modules;
@@ -1316,26 +1404,42 @@ function createNormalizedContentPlugin(engine) {
1316
1404
  }
1317
1405
  };
1318
1406
  }
1319
- function createMdxPlugin() {
1320
- return (0, _mdx_js_rollup.default)({
1321
- include: ["**/*.mdx"],
1322
- format: "mdx",
1323
- jsxImportSource: "react",
1324
- providerImportSource: "@clarify-labs/renderer",
1325
- remarkPlugins,
1326
- rehypePlugins
1327
- });
1407
+ function createContentCompileTransform(transform, format, projectRoot, errorPolicy = () => "diagnostic") {
1408
+ return async function contentCompileTransform(code, id) {
1409
+ try {
1410
+ return await transform.call(this, code, id);
1411
+ } catch (error) {
1412
+ if (errorPolicy() === "throw") throw error;
1413
+ return {
1414
+ code: generateContentDiagnosticModule(createContentCompileDiagnostic({
1415
+ format,
1416
+ phase: "compilation",
1417
+ error,
1418
+ filePath: id.replace(/\?.*$/, ""),
1419
+ projectRoot
1420
+ })),
1421
+ map: null
1422
+ };
1423
+ }
1424
+ };
1328
1425
  }
1329
- function createMarkdownPlugin() {
1330
- return (0, _mdx_js_rollup.default)({
1331
- include: ["**/*.md"],
1332
- format: "md",
1426
+ function createContentCompilerPlugin(format, projectRoot, errorPolicy) {
1427
+ const markdown = format === "markdown";
1428
+ const plugin = (0, _mdx_js_rollup.default)({
1429
+ include: [markdown ? "**/*.md" : "**/*.mdx"],
1430
+ format: markdown ? "md" : "mdx",
1333
1431
  jsxImportSource: "react",
1334
1432
  providerImportSource: "@clarify-labs/renderer",
1335
1433
  remarkPlugins,
1336
- remarkRehypeOptions: { allowDangerousHtml: true },
1337
- rehypePlugins: [rehype_raw.default, ...rehypePlugins]
1434
+ ...markdown ? { remarkRehypeOptions: { allowDangerousHtml: true } } : {},
1435
+ rehypePlugins: markdown ? [rehype_raw.default, ...rehypePlugins] : rehypePlugins
1338
1436
  });
1437
+ const transform = typeof plugin.transform === "function" ? plugin.transform : plugin.transform?.handler;
1438
+ if (!transform) return plugin;
1439
+ return {
1440
+ ...plugin,
1441
+ transform: createContentCompileTransform(transform, format, projectRoot, errorPolicy)
1442
+ };
1339
1443
  }
1340
1444
  function createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx) {
1341
1445
  let viteConfig;
@@ -1457,13 +1561,22 @@ function createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown,
1457
1561
  * site itself - it only bridges Vite lifecycle hooks to the engine.
1458
1562
  */
1459
1563
  function createViteAdapter(engine) {
1564
+ let contentCompileErrorPolicy = "diagnostic";
1565
+ const errorPolicy = () => contentCompileErrorPolicy;
1460
1566
  const normalizedContentPlugin = createNormalizedContentPlugin(engine);
1461
- const markdown = createMarkdownPlugin();
1462
- const mdx = createMdxPlugin();
1567
+ const markdown = createContentCompilerPlugin("markdown", engine.root, errorPolicy);
1568
+ const mdx = createContentCompilerPlugin("mdx", engine.root, errorPolicy);
1569
+ const compilePolicyPlugin = {
1570
+ name: "clarify:content-compile-policy",
1571
+ configResolved(config) {
1572
+ contentCompileErrorPolicy = config.command === "build" ? "throw" : "diagnostic";
1573
+ }
1574
+ };
1463
1575
  const tailwind = (0, _tailwindcss_vite.default)();
1464
1576
  return [
1465
1577
  ...(0, _vitejs_plugin_react.default)(),
1466
1578
  ...Array.isArray(tailwind) ? tailwind : [tailwind],
1579
+ compilePolicyPlugin,
1467
1580
  normalizedContentPlugin,
1468
1581
  createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx),
1469
1582
  markdown,
@@ -1472,7 +1585,7 @@ function createViteAdapter(engine) {
1472
1585
  }
1473
1586
  var cliPackageVersion = {
1474
1587
  name: "@clarify-labs/cli",
1475
- version: "0.10.2",
1588
+ version: "0.11.0",
1476
1589
  license: "AGPL-3.0-only",
1477
1590
  type: "module",
1478
1591
  repository: {
@@ -1552,8 +1665,7 @@ function resolveBuildOptions(options = {}) {
1552
1665
  return {
1553
1666
  projectRoot: options.projectRoot ?? process.cwd(),
1554
1667
  rootDirectory: options.rootDirectory ?? "source",
1555
- outputDirectory: options.outputDirectory,
1556
- ssg: { failOnError: options.ssg?.failOnError ?? true }
1668
+ outputDirectory: options.outputDirectory
1557
1669
  };
1558
1670
  }
1559
1671
  //#endregion
@@ -1611,7 +1723,7 @@ function llmsTxtListItem(route, basePath) {
1611
1723
  return description ? `- [${route.meta.title}](${basePath}${contentArtifactUrl}): ${description}` : `- [${route.meta.title}](${basePath}${contentArtifactUrl})`;
1612
1724
  }
1613
1725
  function llmsTxtLocaleLabel(locale, projectConfig) {
1614
- return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
1726
+ return projectConfig.locales?.locales.find((item) => item.code === locale)?.label ?? locale;
1615
1727
  }
1616
1728
  function groupRoutesByLocale(routes) {
1617
1729
  const groups = /* @__PURE__ */ new Map();
@@ -1623,7 +1735,7 @@ function groupRoutesByLocale(routes) {
1623
1735
  }
1624
1736
  function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
1625
1737
  const routesBySource = /* @__PURE__ */ new Map();
1626
- const defaultLocale = projectConfig.i18n?.defaultLocale;
1738
+ const defaultLocale = projectConfig.locales?.default;
1627
1739
  for (const route of routes) {
1628
1740
  const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.artifacts?.contentArtifactUrl ?? route.path}`;
1629
1741
  const previousRoute = routesBySource.get(sourceKey);
@@ -1825,9 +1937,6 @@ var OPENAPI_HTTP_METHODS = [
1825
1937
  "trace"
1826
1938
  ];
1827
1939
  var CLARIFY_OPENAPI_SECTION_ID_EXTENSION = "x-clarify-section-id";
1828
- function errorMessage(error) {
1829
- return error instanceof Error ? error.message : String(error);
1830
- }
1831
1940
  function isSameOpenAPIFile(url, filePath) {
1832
1941
  try {
1833
1942
  return (0, node_path.resolve)((0, node_url.fileURLToPath)(url)) === (0, node_path.resolve)(filePath);
@@ -1942,7 +2051,7 @@ function extractOpenAPISections(spec, filterTags) {
1942
2051
  }
1943
2052
  return sections;
1944
2053
  }
1945
- async function readOpenAPISpec(filePath, contentProcessor) {
2054
+ async function readOpenAPISpec(filePath, contentProcessor, projectRoot) {
1946
2055
  try {
1947
2056
  if (!contentProcessor) return {
1948
2057
  ok: true,
@@ -1965,13 +2074,14 @@ async function readOpenAPISpec(filePath, contentProcessor) {
1965
2074
  } catch (error) {
1966
2075
  return {
1967
2076
  ok: false,
1968
- diagnostic: {
2077
+ diagnostic: createContentDiagnostic({
1969
2078
  kind: "openapi",
1970
2079
  title: "OpenAPI spec parse failed",
1971
- message: `Clarify could not parse ${filePath}.`,
2080
+ message: "Clarify could not parse this OpenAPI specification.",
2081
+ error,
1972
2082
  filePath,
1973
- details: errorMessage(error)
1974
- }
2083
+ projectRoot
2084
+ })
1975
2085
  };
1976
2086
  }
1977
2087
  }
@@ -2002,32 +2112,25 @@ export const routeData = {};
2002
2112
  export default createOpenApiRouteComponent({ ...routeData, spec });
2003
2113
  `;
2004
2114
  }
2005
- function generateOpenAPIErrorModule(diagnostic) {
2006
- return `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';
2007
-
2008
- export const contentDiagnostic = ${JSON.stringify({
2009
- kind: diagnostic.kind ?? "openapi",
2010
- title: diagnostic.title,
2011
- message: diagnostic.message,
2012
- filePath: diagnostic.filePath,
2013
- details: diagnostic.details
2014
- })};
2015
-
2016
- export default createContentDiagnosticComponent(contentDiagnostic);
2017
- `;
2018
- }
2019
2115
  //#endregion
2020
2116
  //#region source/plugins/openapi/index.ts
2021
2117
  function collectOpenAPIPageIntents(config) {
2022
2118
  const intents = [];
2023
- function visitPages(pages) {
2024
- if (!pages || pages === "FileTree") return;
2025
- for (const group of pages) for (const item of group.pages) {
2119
+ function visitItems(items) {
2120
+ for (const item of items) {
2121
+ if (typeof item !== "string" && "group" in item) {
2122
+ visitItems(item.pages);
2123
+ continue;
2124
+ }
2026
2125
  const intent = routeIntentFromPagesItem(item);
2027
2126
  if (intent.kind === "openapi" && (intent.path || intent.tagFilter?.length)) intents.push(intent);
2028
2127
  }
2029
2128
  }
2030
- for (const tab of config.tabs ?? []) visitPages(tab.pages);
2129
+ function visitPages(pages) {
2130
+ if (!pages || pages === "FileTree") return;
2131
+ for (const group of pages) visitItems(group.pages);
2132
+ }
2133
+ for (const tab of config.navigation?.tabs ?? []) visitPages(tab.pages);
2031
2134
  return intents;
2032
2135
  }
2033
2136
  function virtualModuleIdFromPath(path) {
@@ -2050,14 +2153,14 @@ function cloneOpenAPIRoute(route, overrides = {}) {
2050
2153
  ...overrides
2051
2154
  };
2052
2155
  }
2053
- function findOpenAPIRoutesForContentRoot(contentRoot, i18n) {
2054
- if (!i18n) return findOpenAPIRoutes(contentRoot);
2156
+ function findOpenAPIRoutesForContentRoot(contentRoot, locales) {
2157
+ if (!locales) return findOpenAPIRoutes(contentRoot);
2055
2158
  const localizedRoutes = [];
2056
- for (const locale of i18n.locales) {
2159
+ for (const locale of locales.locales) {
2057
2160
  const localeRoot = (0, node_path.join)(contentRoot, locale.code);
2058
2161
  for (const route of findOpenAPIRoutes(localeRoot, localeRoot)) {
2059
2162
  const basePath = route.basePath ?? route.path;
2060
- const path = localizedRoutePath(basePath, locale.code, i18n);
2163
+ const path = localizedRoutePath(basePath, locale.code, locales);
2061
2164
  localizedRoutes.push({
2062
2165
  ...route,
2063
2166
  path,
@@ -2067,15 +2170,15 @@ function findOpenAPIRoutesForContentRoot(contentRoot, i18n) {
2067
2170
  });
2068
2171
  }
2069
2172
  }
2070
- if (i18n.missing === "fallback") {
2173
+ if (locales.missing === "fallback") {
2071
2174
  const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
2072
- const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
2175
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === locales.default);
2073
2176
  for (const sourceRoute of defaultRoutes) {
2074
2177
  const basePath = sourceRoute.basePath ?? sourceRoute.path;
2075
- for (const locale of i18n.locales) {
2178
+ for (const locale of locales.locales) {
2076
2179
  const key = `${locale.code}:${basePath}`;
2077
2180
  if (routeByLocaleAndBase.has(key)) continue;
2078
- const path = localizedRoutePath(basePath, locale.code, i18n);
2181
+ const path = localizedRoutePath(basePath, locale.code, locales);
2079
2182
  localizedRoutes.push(cloneOpenAPIRoute(sourceRoute, {
2080
2183
  path,
2081
2184
  locale: locale.code,
@@ -2102,7 +2205,7 @@ function createConfiguredOpenAPIRoutes(routes, config) {
2102
2205
  const key = `${route.locale ?? ""}:${targetBasePath}`;
2103
2206
  if (existingKeys.has(key)) continue;
2104
2207
  existingKeys.add(key);
2105
- const path = route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath;
2208
+ const path = route.locale && config.locales ? localizedRoutePath(targetBasePath, route.locale, config.locales) : targetBasePath;
2106
2209
  additions.push(cloneOpenAPIRoute(route, {
2107
2210
  path,
2108
2211
  basePath: targetBasePath,
@@ -2115,12 +2218,12 @@ function createConfiguredOpenAPIRoutes(routes, config) {
2115
2218
  }
2116
2219
  }
2117
2220
  const nextRoutes = [...routes, ...additions];
2118
- if (!config.i18n) return nextRoutes;
2119
- const routesWithAlternates = nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n));
2221
+ if (!config.locales) return nextRoutes;
2222
+ const routesWithAlternates = nextRoutes.map((route) => withAlternates(route, nextRoutes, config.locales));
2120
2223
  const bareAliases = [];
2121
2224
  const seenBare = new Set(routesWithAlternates.map((route) => route.path));
2122
2225
  for (const route of routesWithAlternates) {
2123
- if (route.locale !== config.i18n.defaultLocale) continue;
2226
+ if (route.locale !== config.locales.default) continue;
2124
2227
  const basePath = route.basePath ?? route.path;
2125
2228
  if (basePath === route.path || seenBare.has(basePath)) continue;
2126
2229
  seenBare.add(basePath);
@@ -2159,7 +2262,7 @@ function createOpenAPIPlugin() {
2159
2262
  hooks: {
2160
2263
  "routes:discover": (input, ctx) => ({
2161
2264
  ...input,
2162
- routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.i18n)]
2265
+ routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.locales)]
2163
2266
  }),
2164
2267
  "routes:discovered": async (routes, ctx) => {
2165
2268
  specs.clear();
@@ -2172,7 +2275,7 @@ function createOpenAPIPlugin() {
2172
2275
  const result = specFromCache ? {
2173
2276
  ok: true,
2174
2277
  spec: specFromCache
2175
- } : await readOpenAPISpec(route.source.filePath, processor);
2278
+ } : await readOpenAPISpec(route.source.filePath, processor, ctx.generateOptions.projectRoot);
2176
2279
  if (!result.ok) {
2177
2280
  route.diagnostic = result.diagnostic;
2178
2281
  route.meta.title = route.meta.title || "OpenAPI parse error";
@@ -2219,7 +2322,7 @@ function createOpenAPIPlugin() {
2219
2322
  for (const [routeSpecModuleId, spec] of specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2220
2323
  for (const route of ctx.routes) {
2221
2324
  if (route.kind !== "openapi") continue;
2222
- if (route.diagnostic) modules.set(route.module.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
2325
+ if (route.diagnostic) modules.set(route.module.virtualModuleId, generateContentDiagnosticModule(route.diagnostic));
2223
2326
  else if (route.openapi?.sourceSpecId) {
2224
2327
  const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2225
2328
  if (!specModules.has(routeSpecModuleId)) continue;
@@ -2316,7 +2419,7 @@ async function generateDevSearchIndex(ctx, root, pagefind) {
2316
2419
  const result = await index.addCustomRecord({
2317
2420
  url: route.path,
2318
2421
  content,
2319
- language: toPagefindLanguage(route.locale ?? ctx.projectConfig.i18n?.defaultLocale),
2422
+ language: toPagefindLanguage(route.locale ?? ctx.projectConfig.locales?.default),
2320
2423
  meta: { title: route.meta.title }
2321
2424
  });
2322
2425
  assertNoPagefindErrors(`addCustomRecord ${route.path}`, result.errors);
@@ -2359,6 +2462,7 @@ function createSearchIndexPlugin() {
2359
2462
  name: "clarify:search-index",
2360
2463
  hooks: {
2361
2464
  async "dev:configureServer"(server, ctx) {
2465
+ if (!ctx.projectConfig.features.search.enabled) return;
2362
2466
  const pagefind = await import("pagefind");
2363
2467
  let currentRoot;
2364
2468
  const staleRoots = /* @__PURE__ */ new Set();
@@ -2398,6 +2502,7 @@ function createSearchIndexPlugin() {
2398
2502
  });
2399
2503
  },
2400
2504
  async "build:done"(ctx) {
2505
+ if (!ctx.projectConfig.features.search.enabled) return;
2401
2506
  const outputDirectory = ctx.generateOptions.outputDirectory;
2402
2507
  if (!outputDirectory) return;
2403
2508
  const pageCount = await generatePagefindIndex({ outputRoot: (0, node_path.resolve)(ctx.generateOptions.projectRoot, outputDirectory) });
@@ -2450,16 +2555,18 @@ function createSeoPlugin() {
2450
2555
  return {
2451
2556
  name: "clarify:seo",
2452
2557
  hooks: { "build:assets"(ctx) {
2558
+ const assets = [];
2453
2559
  const sitemap = createSitemapXml(ctx.routes, ctx.projectConfig);
2454
2560
  const robots = createRobotsTxt(ctx.projectConfig);
2455
- if (!sitemap || !robots) return [];
2456
- return [{
2561
+ if (sitemap) assets.push({
2457
2562
  fileName: "sitemap.xml",
2458
2563
  source: sitemap
2459
- }, {
2564
+ });
2565
+ if (robots) assets.push({
2460
2566
  fileName: "robots.txt",
2461
2567
  source: robots
2462
- }];
2568
+ });
2569
+ return assets;
2463
2570
  } }
2464
2571
  };
2465
2572
  }
@@ -2475,7 +2582,8 @@ function encodePath(path) {
2475
2582
  return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
2476
2583
  }
2477
2584
  function createSourceEditUrl(filePath, contentRoot, source) {
2478
- const repository = normalizeRepositoryUrl(source.repository);
2585
+ if (!source.url) return "";
2586
+ const repository = normalizeRepositoryUrl(source.url);
2479
2587
  const branch = source.branch ?? "main";
2480
2588
  const sourcePath = [source.directory ? trimSlashes(source.directory) : "", (0, node_path.relative)(contentRoot, filePath).replaceAll("\\", "/")].filter(Boolean).join("/");
2481
2589
  return `${repository}/edit/${encodeURIComponent(branch)}/${encodePath(sourcePath)}`;
@@ -2493,7 +2601,8 @@ function createSourceLinksPlugin() {
2493
2601
  return {
2494
2602
  name: "clarify:source-links",
2495
2603
  hooks: { "routes:discovered": (routes, ctx) => {
2496
- attachSourceEditUrls(routes, (0, node_path.join)(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
2604
+ if (!ctx.projectConfig.features.repository.enabled) return routes;
2605
+ attachSourceEditUrls(routes, (0, node_path.join)(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.features.repository);
2497
2606
  return routes;
2498
2607
  } }
2499
2608
  };
@@ -2608,18 +2717,18 @@ async function resolveProjectContext(options = {}, env = {
2608
2717
  }) {
2609
2718
  const projectRoot = (0, node_path.resolve)(options.projectRoot ?? process.cwd());
2610
2719
  const initialBuildOptions = resolveBuildOptions(options);
2611
- const contentRoot = (0, node_path.join)(projectRoot, initialBuildOptions.rootDirectory);
2612
2720
  const configFilePath = findClarifyConfigFile(projectRoot);
2613
2721
  const config = configFilePath && (0, node_fs.existsSync)(configFilePath) ? await loadClarifyConfig(projectRoot, env) : {};
2614
2722
  const mergedOptions = {
2615
2723
  ...config,
2616
2724
  ...options,
2617
2725
  projectRoot,
2618
- rootDirectory: initialBuildOptions.rootDirectory,
2619
- outputDirectory: initialBuildOptions.outputDirectory
2726
+ rootDirectory: options.rootDirectory ?? initialBuildOptions.rootDirectory,
2727
+ outputDirectory: options.outputDirectory ?? initialBuildOptions.outputDirectory
2620
2728
  };
2621
2729
  const resolvedProjectConfig = resolveProjectConfig(mergedOptions);
2622
2730
  const resolvedBuildOptions = resolveBuildOptions(mergedOptions);
2731
+ const contentRoot = (0, node_path.join)(projectRoot, resolvedBuildOptions.rootDirectory);
2623
2732
  return {
2624
2733
  projectRoot,
2625
2734
  contentRoot,
@@ -2709,9 +2818,9 @@ function canonicalUrl(contextOrConfig, route) {
2709
2818
  /** Inject `lang` (and optional `dir`) onto the `<html>` tag for the route locale. */
2710
2819
  function injectHtmlLocaleAttributes(html, contextOrConfig, route) {
2711
2820
  const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2712
- const localeCode = route?.locale ?? projectConfig.i18n?.defaultLocale;
2821
+ const localeCode = route?.locale ?? projectConfig.locales?.default;
2713
2822
  if (!localeCode) return html;
2714
- const dir = (projectConfig.i18n?.locales.find((locale) => locale.code === localeCode))?.dir;
2823
+ const dir = (projectConfig.locales?.locales.find((locale) => locale.code === localeCode))?.dir;
2715
2824
  return html.replace(/<html\b([^>]*)>/i, (_match, attributes) => {
2716
2825
  let nextAttributes = attributes.replace(/\s+lang=("[^"]*"|'[^']*'|[^\s>]*)/i, "").replace(/\s+dir=("[^"]*"|'[^']*'|[^\s>]*)/i, "");
2717
2826
  nextAttributes = `${nextAttributes} lang="${escapeHtml(localeCode)}"`;
@@ -2889,7 +2998,7 @@ async function buildSSRBundle(root, ssrEntry, ssrOutDir, plugins) {
2889
2998
  }
2890
2999
  });
2891
3000
  }
2892
- async function renderSSGRoutes(routes, contextOrConfig, outputDirectory, ssrBundlePath, failOnError = true) {
3001
+ async function renderSSGRoutes(routes, contextOrConfig, outputDirectory, ssrBundlePath) {
2893
3002
  const { render } = await import((0, node_url.pathToFileURL)(ssrBundlePath).href);
2894
3003
  const template = readIndexHtml(outputDirectory);
2895
3004
  if (!template) throw new Error(`[clarify] index.html not found in outputDirectory "${outputDirectory}". Make sure Vite build produces it.`);
@@ -2901,7 +3010,7 @@ async function renderSSGRoutes(routes, contextOrConfig, outputDirectory, ssrBund
2901
3010
  }
2902
3011
  } catch (err) {
2903
3012
  console.error(`[clarify] Failed to render route "${route.path}":`, err);
2904
- if (failOnError) throw err;
3013
+ throw err;
2905
3014
  }
2906
3015
  }
2907
3016
  //#endregion
@@ -2978,10 +3087,10 @@ var ClarifyContext = class {
2978
3087
  this.currentPlugins = plugins;
2979
3088
  }
2980
3089
  get isI18n() {
2981
- return Boolean(this.projectConfig.i18n);
3090
+ return Boolean(this.projectConfig.locales);
2982
3091
  }
2983
3092
  get defaultLocale() {
2984
- return this.projectConfig.i18n?.defaultLocale;
3093
+ return this.projectConfig.locales?.default;
2985
3094
  }
2986
3095
  get(key) {
2987
3096
  return this.values.get(key);
@@ -3051,6 +3160,12 @@ function loadVirtualModule(id, modules) {
3051
3160
  const bareId = stripVirtualPrefix(id);
3052
3161
  return modules.get(bareId) ?? modules.get(id) ?? null;
3053
3162
  }
3163
+ function assertNoContentDiagnostics(routes) {
3164
+ const failures = routes.filter((route) => route.diagnostic);
3165
+ if (failures.length === 0) return;
3166
+ const summary = failures.map((route) => `- ${route.path}: ${route.diagnostic?.title ?? "Content error"}${route.diagnostic?.filePath ? ` (${route.diagnostic.filePath})` : ""}`).join("\n");
3167
+ throw new Error(`[clarify] Content diagnostics prevented the build:\n${summary}`);
3168
+ }
3054
3169
  var ClarifyEngine = class {
3055
3170
  options;
3056
3171
  root;
@@ -3169,10 +3284,10 @@ var ClarifyEngine = class {
3169
3284
  async discoverSite() {
3170
3285
  const plugins = this.ctx.plugins;
3171
3286
  const contentRoot = this.ctx.contentRoot;
3172
- const { i18n, tabs } = this.ctx.projectConfig;
3287
+ const { locales, navigation: navigationConfig } = this.ctx.projectConfig;
3173
3288
  let routes = await runPhase(plugins, "site:discover", this.ctx, async () => {
3174
3289
  const options = { contentProcessor: createProjectContentProcessor(plugins, this.ctx) };
3175
- const discovered = i18n ? await findLocalizedContentRoutes(contentRoot, i18n, options) : await findContentRoutes(contentRoot, contentRoot, options);
3290
+ const discovered = locales ? await findLocalizedContentRoutes(contentRoot, locales, options) : await findContentRoutes(contentRoot, contentRoot, options);
3176
3291
  const result = await runHooks(plugins, "routes:discover", {
3177
3292
  contentRoot,
3178
3293
  routes: discovered
@@ -3201,8 +3316,9 @@ var ClarifyEngine = class {
3201
3316
  };
3202
3317
  });
3203
3318
  });
3204
- routes = applyConfiguredPageRoutePaths(routes, tabs, i18n);
3205
- const navigation = tabs ? i18n ? buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) ?? {
3319
+ const tabs = navigationConfig?.tabs;
3320
+ routes = applyConfiguredPageRoutePaths(routes, tabs, locales);
3321
+ const navigation = tabs ? locales ? buildLocalizedNavigationFromTabsConfig(routes, tabs, locales) ?? {
3206
3322
  kind: "localized-tabbed",
3207
3323
  locales: {}
3208
3324
  } : buildNavigationFromTabsConfig(routes, tabs) : {
@@ -3232,7 +3348,7 @@ var ClarifyEngine = class {
3232
3348
  routes: this.routes,
3233
3349
  navigation: this.navigation,
3234
3350
  plugins: this.plugins,
3235
- themeEditor: this.runtime.command === "serve" || this.runtime.themeEditor === true || this.projectConfig.theme.editor,
3351
+ themeEditor: this.runtime.command === "serve" || this.runtime.themeEditor === true || this.projectConfig.features.themeEditor.enabled,
3236
3352
  version: this.ctx.version
3237
3353
  });
3238
3354
  this.virtualModules = await runHooks(this.plugins, "modules:before", this.virtualModules, this.ctx);
@@ -3275,6 +3391,7 @@ var ClarifyEngine = class {
3275
3391
  await runPhase(this.plugins, "ssg", this.ctx, async () => {
3276
3392
  if (process.env.SKIP_CLARIFY_SSG) return;
3277
3393
  if (!await runInterceptHooks(this.plugins, "ssg:shouldRun", this.ctx)) return;
3394
+ assertNoContentDiagnostics(this.routes);
3278
3395
  const outputDir = this.runtime.outputDirectory ?? this.generateOptions.outputDirectory;
3279
3396
  if (!outputDir) throw new Error("[clarify] outputDirectory is required before SSG runs");
3280
3397
  const ssrOutputDir = (0, node_path.join)(outputDir, ".ssr");
@@ -3288,10 +3405,10 @@ var ClarifyEngine = class {
3288
3405
  plugins: this.runtime.ssrPlugins ?? []
3289
3406
  });
3290
3407
  const ssrBundlePath = (0, node_path.join)(ssrOutputDir, "entry-server.js");
3291
- await renderSSGRoutes(this.routes, this.runtimeContext(), outputDir, ssrBundlePath, this.generateOptions.ssg.failOnError);
3408
+ await renderSSGRoutes(this.routes, this.runtimeContext(), outputDir, ssrBundlePath);
3292
3409
  } catch (err) {
3293
3410
  console.error("[clarify] SSG failed:", err);
3294
- if (this.generateOptions.ssg.failOnError) throw err;
3411
+ throw err;
3295
3412
  } finally {
3296
3413
  if (tempEntryPath) try {
3297
3414
  (0, node_fs.rmSync)(tempEntryPath, { force: true });
@@ -3382,4 +3499,4 @@ Object.defineProperty(exports, "defineConfig", {
3382
3499
  }
3383
3500
  });
3384
3501
 
3385
- //# sourceMappingURL=engine-qafLfnXQ.cjs.map
3502
+ //# sourceMappingURL=engine-BfkoyVNo.cjs.map