@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.
@@ -22,6 +22,24 @@ import { fileURLToPath, pathToFileURL } from "node:url";
22
22
  import SwaggerParser from "@apidevtools/swagger-parser";
23
23
  import { tmpdir } from "node:os";
24
24
  import { createRequire } from "node:module";
25
+ //#region source/parsers/content/diagnostic.ts
26
+ function displayPath(filePath, projectRoot) {
27
+ if (!filePath) return void 0;
28
+ if (!projectRoot) return filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "");
29
+ const normalized = relative(projectRoot, filePath);
30
+ return normalized && !normalized.startsWith("..") && !isAbsolute(normalized) ? normalized : filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "");
31
+ }
32
+ function createContentDiagnostic(options) {
33
+ const { error } = options;
34
+ return {
35
+ kind: options.kind,
36
+ title: options.title,
37
+ message: options.message,
38
+ filePath: displayPath(options.filePath, options.projectRoot),
39
+ details: error instanceof Error ? options.includeStack ? error.stack ?? error.message : error.message : String(error)
40
+ };
41
+ }
42
+ //#endregion
25
43
  //#region source/parsers/markdown/mdx.ts
26
44
  function getLanguage(className) {
27
45
  return (Array.isArray(className) ? className : typeof className === "string" ? className.split(/\s+/) : []).find((item) => typeof item === "string" && item.startsWith("language-"))?.replace(/^language-/, "") || "txt";
@@ -57,9 +75,18 @@ var rehypePlugins = [
57
75
  parseMetaString: parseCodeMeta
58
76
  }]
59
77
  ];
60
- function formatMdxDiagnostic(error) {
61
- if (error instanceof Error) return error.message;
62
- return String(error);
78
+ function createContentCompileDiagnostic(options) {
79
+ const { format, phase, error, filePath, projectRoot } = options;
80
+ const label = format === "mdx" ? "MDX" : "Markdown";
81
+ return createContentDiagnostic({
82
+ kind: format,
83
+ title: `${label} ${phase} error`,
84
+ 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.`,
85
+ error,
86
+ filePath,
87
+ projectRoot,
88
+ includeStack: phase === "compilation"
89
+ });
63
90
  }
64
91
  /**
65
92
  * Validate MDX content for diagnostics WITHOUT running the build-time rehype
@@ -90,13 +117,13 @@ async function compileMdxContent(content, filePath, projectRoot) {
90
117
  } catch (error) {
91
118
  return {
92
119
  ok: false,
93
- diagnostic: {
94
- kind: "mdx",
95
- title: "MDX syntax error",
96
- message: "This page could not be compiled. Fix the MDX syntax or component usage above, then reload the route.",
97
- filePath: filePath && projectRoot ? filePath.replace(projectRoot + "/", "").replace(/^\/+/, "") : filePath ? filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "") : void 0,
98
- details: formatMdxDiagnostic(error)
99
- }
120
+ diagnostic: createContentCompileDiagnostic({
121
+ format: "mdx",
122
+ phase: "syntax",
123
+ error,
124
+ filePath,
125
+ projectRoot
126
+ })
100
127
  };
101
128
  }
102
129
  }
@@ -352,10 +379,10 @@ function frontmatterKeywords(frontmatter) {
352
379
  const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
353
380
  return keywords.length > 0 ? keywords : void 0;
354
381
  }
355
- function withAlternates(route, routes, i18n) {
382
+ function withAlternates(route, routes, locales) {
356
383
  const basePath = route.basePath ?? route.path;
357
384
  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]));
358
- const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
385
+ const alternates = Object.fromEntries(locales.locales.flatMap((locale) => {
359
386
  const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
360
387
  return alternate ? [[locale.code, alternate.path]] : [];
361
388
  }));
@@ -368,11 +395,11 @@ function withAlternates(route, routes, i18n) {
368
395
  * 为默认语言的 locale-prefixed 路由生成无前缀的"裸路径别名",方便不带语言
369
396
  * 前缀的 URL 也能访问。标记 `isBareAlias` 以便搜索索引生成时过滤重复索引。
370
397
  */
371
- function generateBareAliases(routes, i18n) {
398
+ function generateBareAliases(routes, locales) {
372
399
  const bareRoutes = [];
373
400
  const seenBare = new Set(routes.map((r) => r.path));
374
401
  for (const route of routes) {
375
- if (route.locale !== i18n.defaultLocale) continue;
402
+ if (route.locale !== locales.default) continue;
376
403
  const bp = route.basePath ?? route.path;
377
404
  if (bp === route.path || seenBare.has(bp)) continue;
378
405
  seenBare.add(bp);
@@ -384,42 +411,42 @@ function generateBareAliases(routes, i18n) {
384
411
  }
385
412
  return bareRoutes;
386
413
  }
387
- async function findLocalizedContentRoutes(contentRoot, i18n, options = {}) {
388
- if (!i18n) return findContentRoutes(contentRoot, contentRoot, options);
414
+ async function findLocalizedContentRoutes(contentRoot, locales, options = {}) {
415
+ if (!locales) return findContentRoutes(contentRoot, contentRoot, options);
389
416
  const localizedRoutes = [];
390
- for (const locale of i18n.locales) {
417
+ for (const locale of locales.locales) {
391
418
  const localeRoot = join(contentRoot, locale.code);
392
419
  const discovered = await findContentRoutes(localeRoot, localeRoot, options);
393
420
  for (const route of discovered) {
394
421
  const basePath = route.basePath ?? route.path;
395
422
  localizedRoutes.push({
396
423
  ...route,
397
- path: localizedRoutePath(basePath, locale.code, i18n),
424
+ path: localizedRoutePath(basePath, locale.code, locales),
398
425
  basePath,
399
426
  locale: locale.code,
400
427
  module: { virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.source.filePath) }
401
428
  });
402
429
  }
403
430
  }
404
- if (i18n.missing === "fallback") {
431
+ if (locales.missing === "fallback") {
405
432
  const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
406
- const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
433
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === locales.default);
407
434
  for (const sourceRoute of defaultRoutes) {
408
435
  const basePath = sourceRoute.basePath ?? sourceRoute.path;
409
- for (const locale of i18n.locales) {
436
+ for (const locale of locales.locales) {
410
437
  const key = `${locale.code}:${basePath}`;
411
438
  if (routeByLocaleAndBase.has(key)) continue;
412
439
  localizedRoutes.push({
413
440
  ...sourceRoute,
414
- path: localizedRoutePath(basePath, locale.code, i18n),
441
+ path: localizedRoutePath(basePath, locale.code, locales),
415
442
  locale: locale.code,
416
443
  isFallback: true
417
444
  });
418
445
  }
419
446
  }
420
447
  }
421
- const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
422
- return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, i18n)];
448
+ const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, locales));
449
+ return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, locales)];
423
450
  }
424
451
  function buildNavigation(routes) {
425
452
  const root = [];
@@ -452,6 +479,7 @@ function routeIntentFromPagesItem(item) {
452
479
  kind: "page",
453
480
  ref: item
454
481
  };
482
+ if ("group" in item) throw new Error("Navigation groups do not resolve to route intents");
455
483
  if ("openapi" in item) return {
456
484
  kind: "openapi",
457
485
  ref: item.openapi,
@@ -479,19 +507,23 @@ function firstNavigationPath(nodes) {
479
507
  }
480
508
  function collectExplicitPagePathIntents(tabs) {
481
509
  const intents = [];
482
- for (const tab of tabs ?? []) {
483
- if (!tab.pages || tab.pages === "FileTree") continue;
484
- for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) {
510
+ const collectItems = (items) => {
511
+ for (const item of items) if (typeof item !== "string" && "group" in item) collectItems(item.pages);
512
+ else if (typeof item !== "string" && "page" in item && item.path) {
485
513
  const intent = routeIntentFromPagesItem(item);
486
514
  if (intent.kind === "page" && intent.path) intents.push({
487
515
  ...intent,
488
516
  path: intent.path
489
517
  });
490
518
  }
519
+ };
520
+ for (const tab of tabs ?? []) {
521
+ if (!tab.pages || tab.pages === "FileTree") continue;
522
+ for (const group of tab.pages) collectItems(group.pages);
491
523
  }
492
524
  return intents;
493
525
  }
494
- function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
526
+ function applyConfiguredPageRoutePaths(routes, tabs, locales) {
495
527
  const pageIntents = collectExplicitPagePathIntents(tabs);
496
528
  if (!pageIntents.length) return routes;
497
529
  const additions = [];
@@ -507,15 +539,15 @@ function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
507
539
  existingKeys.add(key);
508
540
  additions.push({
509
541
  ...route,
510
- path: route.locale && i18n ? localizedRoutePath(targetBasePath, route.locale, i18n) : targetBasePath,
542
+ path: route.locale && locales ? localizedRoutePath(targetBasePath, route.locale, locales) : targetBasePath,
511
543
  basePath: targetBasePath
512
544
  });
513
545
  }
514
546
  }
515
547
  const nextRoutes = [...routes, ...additions];
516
- const routesWithAlternates = i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, i18n)) : nextRoutes;
517
- if (!i18n) return routesWithAlternates;
518
- return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, i18n)];
548
+ const routesWithAlternates = locales ? nextRoutes.map((route) => withAlternates(route, nextRoutes, locales)) : nextRoutes;
549
+ if (!locales) return routesWithAlternates;
550
+ return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, locales)];
519
551
  }
520
552
  function buildNavigationFromPagesConfig(routes, config) {
521
553
  if (!config || config === "FileTree") return buildNavigation(routes);
@@ -538,28 +570,38 @@ function buildNavigationFromTabsConfig(routes, tabs) {
538
570
  }
539
571
  function buildNavigationFromConfig(routes, config) {
540
572
  const routeMap = new Map(routes.map((r) => [r.path, r]));
541
- return config.map((group) => {
542
- const children = group.pages.map((item) => {
543
- const intent = routeIntentFromPagesItem(item);
544
- if (intent.kind === "openapi") {
545
- const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
546
- const route = routeMap.get(path);
547
- return {
548
- path,
549
- title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
550
- icon: intent.icon,
551
- sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
552
- };
553
- }
554
- const path = routePathFromRef(intent.ref, intent.path);
573
+ const buildItem = (item) => {
574
+ if (typeof item !== "string" && "group" in item) {
575
+ const children = item.pages.map(buildItem);
576
+ return {
577
+ path: firstNavigationPath(children),
578
+ title: resolveLocalizedText(item.group, "", "") ?? "",
579
+ icon: item.icon,
580
+ children
581
+ };
582
+ }
583
+ const intent = routeIntentFromPagesItem(item);
584
+ if (intent.kind === "openapi") {
585
+ const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
555
586
  const route = routeMap.get(path);
556
587
  return {
557
- path: intent.redirect ? normalizePath(intent.redirect) : path,
588
+ path,
558
589
  title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
559
590
  icon: intent.icon,
560
591
  sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
561
592
  };
562
- });
593
+ }
594
+ const path = routePathFromRef(intent.ref, intent.path);
595
+ const route = routeMap.get(path);
596
+ return {
597
+ path: intent.redirect ? normalizePath(intent.redirect) : path,
598
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
599
+ icon: intent.icon,
600
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
601
+ };
602
+ };
603
+ return config.map((group) => {
604
+ const children = group.pages.map(buildItem);
563
605
  return {
564
606
  path: children[0]?.path ?? "/",
565
607
  title: resolveLocalizedText(group.group, "", "") ?? "",
@@ -568,51 +610,61 @@ function buildNavigationFromConfig(routes, config) {
568
610
  };
569
611
  });
570
612
  }
571
- function localizeNavigationPaths(nodes, locale, i18n) {
613
+ function localizeNavigationPaths(nodes, locale, locales) {
572
614
  return nodes.map((node) => ({
573
615
  ...node,
574
- path: localizedRoutePath(node.path, locale, i18n),
575
- children: node.children ? localizeNavigationPaths(node.children, locale, i18n) : void 0
616
+ path: localizedRoutePath(node.path, locale, locales),
617
+ children: node.children ? localizeNavigationPaths(node.children, locale, locales) : void 0
576
618
  }));
577
619
  }
578
- function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
620
+ function buildLocalizedNavigationForLocale(routes, config, locale, locales) {
579
621
  if (!config || config === "FileTree") return localizeNavigationPaths(buildNavigation(routes.map((route) => ({
580
622
  ...route,
581
623
  path: route.basePath ?? route.path
582
- }))), locale, i18n);
624
+ }))), locale, locales);
583
625
  const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
584
- return config.map((group) => {
585
- const children = group.pages.map((item) => {
586
- const intent = routeIntentFromPagesItem(item);
587
- const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
588
- const route = routeMap.get(basePath);
589
- const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
626
+ const buildItem = (item) => {
627
+ if (typeof item !== "string" && "group" in item) {
628
+ const children = item.pages.map(buildItem);
590
629
  return {
591
- path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, i18n) : void 0) ?? path,
592
- title: resolveLocalizedText(intent.title, locale, i18n.defaultLocale) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
593
- icon: intent.icon,
594
- sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
630
+ path: firstNavigationPath(children),
631
+ title: resolveLocalizedText(item.group, locale, locales.default) ?? "",
632
+ icon: item.icon,
633
+ children
595
634
  };
596
- });
635
+ }
636
+ const intent = routeIntentFromPagesItem(item);
637
+ const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
638
+ const route = routeMap.get(basePath);
639
+ const path = route?.path ?? localizedRoutePath(basePath, locale, locales);
640
+ return {
641
+ path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, locales) : void 0) ?? path,
642
+ title: resolveLocalizedText(intent.title, locale, locales.default) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
643
+ icon: intent.icon,
644
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
645
+ };
646
+ };
647
+ return config.map((group) => {
648
+ const children = group.pages.map(buildItem);
597
649
  return {
598
- path: children[0]?.path ?? localizedRoutePath("/", locale, i18n),
599
- title: resolveLocalizedText(group.group, locale, i18n.defaultLocale) ?? "",
650
+ path: children[0]?.path ?? localizedRoutePath("/", locale, locales),
651
+ title: resolveLocalizedText(group.group, locale, locales.default) ?? "",
600
652
  icon: group.icon,
601
653
  children
602
654
  };
603
655
  });
604
656
  }
605
- function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
606
- if (!i18n) return void 0;
657
+ function buildLocalizedNavigationFromTabsConfig(routes, tabs, localesConfig) {
658
+ if (!localesConfig) return void 0;
607
659
  const locales = {};
608
- for (const locale of i18n.locales) {
660
+ for (const locale of localesConfig.locales) {
609
661
  const localeRoutes = routes.filter((route) => route.locale === locale.code);
610
662
  locales[locale.code] = { tabs: tabs.map((tab) => {
611
- const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code, i18n);
663
+ const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code, localesConfig);
612
664
  return {
613
665
  type: "tab",
614
666
  path: firstNavigationPath(children),
615
- title: resolveLocalizedText(tab.tab, locale.code, i18n.defaultLocale) ?? "",
667
+ title: resolveLocalizedText(tab.tab, locale.code, localesConfig.default) ?? "",
616
668
  icon: tab.icon,
617
669
  children
618
670
  };
@@ -679,7 +731,7 @@ function inferRouteForFile(filePath, contentRoot, defaultLocale) {
679
731
  const localeSegmentIndex = fileLocale ? segments.indexOf(fileLocale) : -1;
680
732
  const basePath = basePathFromRef(localeSegmentIndex >= 0 ? segments.slice(localeSegmentIndex + 1).join("/") : ref);
681
733
  const path = fileLocale && defaultLocale ? localizedRoutePath(basePath, fileLocale, {
682
- defaultLocale,
734
+ default: defaultLocale,
683
735
  missing: "fallback",
684
736
  locales: []
685
737
  }) : basePath;
@@ -732,12 +784,12 @@ async function handleDevRouteRequest(req, res, routes, context) {
732
784
  }
733
785
  const file = typeof body === "object" && body !== null ? body.file : void 0;
734
786
  if (typeof file === "string" && file.length > 0) {
735
- const route = resolveRouteForFile(file, routes, context.projectConfig.i18n?.defaultLocale);
787
+ const route = resolveRouteForFile(file, routes, context.projectConfig.locales?.default);
736
788
  if (route) {
737
789
  res.end(JSON.stringify(toDevRouteEntry(route)));
738
790
  return;
739
791
  }
740
- const inferred = inferRouteForFile(file, context.contentRoot, context.projectConfig.i18n?.defaultLocale);
792
+ const inferred = inferRouteForFile(file, context.contentRoot, context.projectConfig.locales?.default);
741
793
  res.end(JSON.stringify(inferred));
742
794
  return;
743
795
  }
@@ -763,8 +815,7 @@ function resolveThemeConfig(theme = {}) {
763
815
  layout: {
764
816
  ...presetTheme.layout,
765
817
  ...theme.layout
766
- },
767
- editor: theme.editor ?? presetTheme.editor
818
+ }
768
819
  };
769
820
  }
770
821
  //#endregion
@@ -772,26 +823,26 @@ function resolveThemeConfig(theme = {}) {
772
823
  var clarifyLogoConfigSchema = z.union([z.string(), z.object({
773
824
  light: z.string().optional(),
774
825
  dark: z.string().optional()
775
- })]);
826
+ }).strict()]);
776
827
  var clarifyFaviconConfigSchema = z.union([z.string(), z.object({
777
828
  light: z.string().optional(),
778
829
  dark: z.string().optional()
779
- })]);
830
+ }).strict()]);
780
831
  var clarifyLocalizedTextSchema = z.union([z.string(), z.record(z.string(), z.string())]);
781
832
  var clarifyLocaleConfigSchema = z.object({
782
833
  code: z.string(),
783
834
  label: z.string(),
784
835
  dir: z.union([z.literal("ltr"), z.literal("rtl")]).optional()
785
- });
786
- var clarifyI18nConfigSchema = z.object({
787
- defaultLocale: z.string().optional(),
836
+ }).strict();
837
+ var clarifyLocalesConfigSchema = z.object({
838
+ default: z.string().optional(),
788
839
  missing: z.union([
789
840
  z.literal("fallback"),
790
841
  z.literal("404"),
791
842
  z.literal("hide")
792
843
  ]).optional(),
793
844
  locales: z.array(clarifyLocaleConfigSchema).min(1)
794
- }).superRefine((config, ctx) => {
845
+ }).strict().superRefine((config, ctx) => {
795
846
  const localeCodes = /* @__PURE__ */ new Set();
796
847
  for (const [index, locale] of config.locales.entries()) {
797
848
  if (localeCodes.has(locale.code)) ctx.addIssue({
@@ -805,17 +856,17 @@ var clarifyI18nConfigSchema = z.object({
805
856
  });
806
857
  localeCodes.add(locale.code);
807
858
  }
808
- if (config.defaultLocale && !localeCodes.has(config.defaultLocale)) ctx.addIssue({
859
+ if (config.default && !localeCodes.has(config.default)) ctx.addIssue({
809
860
  code: z.ZodIssueCode.custom,
810
- message: "defaultLocale must be one of i18n.locales",
811
- path: ["defaultLocale"]
861
+ message: "default must be one of locales.locales",
862
+ path: ["default"]
812
863
  });
813
864
  });
814
865
  var clarifyNavbarLinkSchema = z.object({
815
866
  label: clarifyLocalizedTextSchema,
816
867
  href: z.string(),
817
868
  external: z.boolean().optional()
818
- });
869
+ }).strict();
819
870
  var clarifyBannerConfigOptionsSchema = z.object({
820
871
  content: clarifyLocalizedTextSchema,
821
872
  dismissible: z.boolean().optional(),
@@ -833,45 +884,46 @@ var clarifyVariableValueSchema = z.lazy(() => z.union([
833
884
  z.record(z.string(), clarifyVariableValueSchema)
834
885
  ]));
835
886
  var clarifyVariablesConfigSchema = z.record(z.string(), clarifyVariableValueSchema);
836
- var clarifySourceConfigSchema = z.object({
837
- repository: z.string(),
887
+ var clarifyRepositoryConfigSchema = z.object({
888
+ url: z.string().optional(),
838
889
  branch: z.string().optional(),
839
890
  directory: z.string().optional()
840
- });
841
- var clarifyPagesItemSchema = z.union([
891
+ }).strict();
892
+ var clarifyPagesItemSchema = z.lazy(() => z.union([
842
893
  z.string(),
894
+ clarifyPagesGroupSchema,
843
895
  z.object({
844
896
  page: z.string(),
845
897
  path: z.string().optional(),
846
898
  title: clarifyLocalizedTextSchema.optional(),
847
899
  icon: z.string().optional(),
848
900
  redirect: z.string().optional()
849
- }),
901
+ }).strict(),
850
902
  z.object({
851
903
  openapi: z.string(),
852
904
  path: z.string().optional(),
853
905
  icon: z.string().optional(),
854
906
  title: clarifyLocalizedTextSchema.optional(),
855
- filter: z.object({ tags: z.array(z.string()).optional() }).optional()
856
- })
857
- ]);
858
- var clarifyPagesGroupSchema = z.object({
907
+ filter: z.object({ tags: z.array(z.string()).optional() }).strict().optional()
908
+ }).strict()
909
+ ]));
910
+ var clarifyPagesGroupSchema = z.lazy(() => z.object({
859
911
  group: clarifyLocalizedTextSchema,
860
912
  icon: z.string().optional(),
861
913
  pages: z.array(clarifyPagesItemSchema)
862
- });
914
+ }).strict());
863
915
  var clarifyPagesConfigSchema = z.union([z.array(clarifyPagesGroupSchema), z.literal("FileTree")]);
864
916
  var clarifyTabItemSchema = z.object({
865
917
  tab: clarifyLocalizedTextSchema,
866
918
  icon: z.string().optional(),
867
919
  pages: clarifyPagesConfigSchema.optional()
868
- });
920
+ }).strict();
869
921
  var clarifyTabsConfigSchema = z.array(clarifyTabItemSchema);
870
922
  var clarifyThemePresetSchema = z.enum(["default", "base"]);
871
923
  var clarifyThemeModeColorValueSchema = z.object({
872
924
  light: z.string().optional(),
873
925
  dark: z.string().optional()
874
- });
926
+ }).strict();
875
927
  var clarifyThemeColorValueSchema = z.union([z.string(), clarifyThemeModeColorValueSchema]);
876
928
  var clarifyThemeColorTokensConfigSchema = z.object({
877
929
  primary: clarifyThemeColorValueSchema.optional(),
@@ -882,42 +934,65 @@ var clarifyThemeColorTokensConfigSchema = z.object({
882
934
  muted: clarifyThemeColorValueSchema.optional(),
883
935
  border: clarifyThemeColorValueSchema.optional(),
884
936
  codeBackground: clarifyThemeColorValueSchema.optional()
885
- });
937
+ }).strict();
886
938
  var clarifyThemeRadiusTokensConfigSchema = z.object({
887
939
  sm: z.string().optional(),
888
940
  md: z.string().optional(),
889
941
  lg: z.string().optional(),
890
942
  xl: z.string().optional()
891
- });
943
+ }).strict();
892
944
  var clarifyThemeTokensConfigSchema = z.object({
893
945
  colors: clarifyThemeColorTokensConfigSchema.optional(),
894
946
  radius: clarifyThemeRadiusTokensConfigSchema.optional()
895
- });
896
- var clarifyThemeLayoutConfigSchema = z.object({ maxWidth: z.string().optional() });
947
+ }).strict();
948
+ var clarifyThemeLayoutConfigSchema = z.object({ maxWidth: z.string().optional() }).strict();
897
949
  var clarifyThemeConfigSchema = z.object({
898
950
  preset: clarifyThemePresetSchema.optional(),
899
951
  tokens: clarifyThemeTokensConfigSchema.optional(),
900
- layout: clarifyThemeLayoutConfigSchema.optional(),
901
- editor: z.boolean().optional()
902
- });
952
+ layout: clarifyThemeLayoutConfigSchema.optional()
953
+ }).strict();
954
+ function featureSchema(shape, defaults) {
955
+ const objectSchema = z.object({
956
+ enabled: z.boolean().default(true),
957
+ ...shape
958
+ }).strict().default({
959
+ enabled: true,
960
+ ...defaults
961
+ });
962
+ return z.union([z.boolean(), objectSchema]).default({
963
+ enabled: true,
964
+ ...defaults
965
+ }).transform((value) => typeof value === "boolean" ? {
966
+ enabled: value,
967
+ ...defaults
968
+ } : value);
969
+ }
970
+ var clarifyFeaturesConfigSchema = z.object({
971
+ search: featureSchema({ provider: z.literal("pagefind").default("pagefind") }, { provider: "pagefind" }),
972
+ repository: featureSchema(clarifyRepositoryConfigSchema.shape, {}),
973
+ themeEditor: featureSchema({}, {}),
974
+ openapi: featureSchema({ playground: z.boolean().default(true) }, { playground: true })
975
+ }).strict();
903
976
  var clarifyProjectConfigSchema = z.object({
904
977
  title: z.string().optional(),
905
978
  description: z.string().optional(),
906
979
  siteUrl: z.string().optional(),
907
- source: clarifySourceConfigSchema.optional(),
980
+ routePrefix: z.string().default("/"),
981
+ assetPrefix: z.string().optional(),
908
982
  logo: clarifyLogoConfigSchema.optional(),
909
983
  homeUrl: z.string().optional(),
910
984
  favicon: clarifyFaviconConfigSchema.optional(),
911
985
  theme: clarifyThemeConfigSchema.optional(),
912
- routePrefix: z.string().optional(),
913
- assetPrefix: z.string().optional(),
914
- navbar: z.object({ links: z.array(clarifyNavbarLinkSchema).optional() }).optional(),
986
+ navigation: z.object({
987
+ links: z.array(clarifyNavbarLinkSchema).optional(),
988
+ tabs: clarifyTabsConfigSchema.optional()
989
+ }).strict().optional(),
915
990
  banner: clarifyBannerConfigOptionsSchema.optional(),
916
991
  footer: clarifyFooterConfigSchema.optional(),
992
+ locales: clarifyLocalesConfigSchema.optional(),
917
993
  variables: clarifyVariablesConfigSchema.optional(),
918
- i18n: clarifyI18nConfigSchema.optional(),
919
- tabs: clarifyTabsConfigSchema.optional()
920
- });
994
+ features: clarifyFeaturesConfigSchema.optional()
995
+ }).strict();
921
996
  //#endregion
922
997
  //#region source/core/config/config.ts
923
998
  function formatIssuePath(path) {
@@ -939,17 +1014,20 @@ function validateProjectConfig(value) {
939
1014
  if (!result.success) throw new Error(formatProjectConfigError(result.error));
940
1015
  return result.data;
941
1016
  }
942
- function resolveI18nConfig(i18n) {
943
- if (!i18n) return void 0;
944
- const firstLocale = i18n.locales[0]?.code;
945
- const defaultLocale = i18n.defaultLocale ?? firstLocale;
1017
+ function resolveLocalesConfig(locales) {
1018
+ if (!locales) return void 0;
1019
+ const firstLocale = locales.locales[0]?.code;
1020
+ const defaultLocale = locales.default ?? firstLocale;
946
1021
  if (!defaultLocale) return void 0;
947
1022
  return {
948
- defaultLocale,
949
- missing: i18n.missing ?? "fallback",
950
- locales: i18n.locales
1023
+ default: defaultLocale,
1024
+ missing: locales.missing ?? "fallback",
1025
+ locales: locales.locales
951
1026
  };
952
1027
  }
1028
+ function resolveFeaturesConfig(features = {}) {
1029
+ return clarifyFeaturesConfigSchema.parse(features);
1030
+ }
953
1031
  function resolveRoutePrefix(routePrefix) {
954
1032
  const trimmed = routePrefix?.trim();
955
1033
  if (!trimmed || trimmed === "/") return "/";
@@ -970,26 +1048,24 @@ function resolveProjectConfig(config = {}) {
970
1048
  title: config.title ?? "Clarify Docs",
971
1049
  description: config.description ?? "",
972
1050
  siteUrl: config.siteUrl,
973
- source: config.source,
1051
+ routePrefix,
1052
+ assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
974
1053
  logo: config.logo,
975
1054
  homeUrl: config.homeUrl,
976
1055
  favicon: config.favicon,
977
- routePrefix,
978
- assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
979
1056
  theme: resolveThemeConfig(config.theme),
980
- navbar: config.navbar,
1057
+ navigation: config.navigation,
981
1058
  banner: config.banner,
982
1059
  footer: config.footer,
1060
+ locales: resolveLocalesConfig(config.locales),
983
1061
  variables: config.variables ?? {},
984
- i18n: resolveI18nConfig(config.i18n),
985
- tabs: config.tabs
1062
+ features: resolveFeaturesConfig(config.features)
986
1063
  };
987
1064
  }
988
1065
  //#endregion
989
1066
  //#region source/core/config/user-config.ts
990
1067
  function defineConfig(config) {
991
- validateProjectConfig(config);
992
- return config;
1068
+ return validateClarifyConfig(config);
993
1069
  }
994
1070
  /**
995
1071
  * Config filenames Clarify CLI looks for at the project root, in resolution
@@ -1013,18 +1089,23 @@ function assertConfigObject(config, configFile) {
1013
1089
  }
1014
1090
  function loadJsonConfig(configFile) {
1015
1091
  const config = assertConfigObject(JSON.parse(readFileSync(configFile, "utf-8")), configFile);
1016
- const projectConfig = validateProjectConfig(config);
1017
- const ssg = config.ssg && typeof config.ssg === "object" && !Array.isArray(config.ssg) ? config.ssg : void 0;
1018
- return {
1092
+ if ("plugins" in config) throw new Error("[clarify] clarify.json does not support plugins; use clarify.ts or clarify.js");
1093
+ return validateClarifyConfig(config);
1094
+ }
1095
+ function validateClarifyConfig(config) {
1096
+ const { plugins, ...projectConfigInput } = config;
1097
+ if (plugins !== void 0 && !Array.isArray(plugins)) throw new Error("[clarify] config field \"plugins\" is invalid: Expected an array");
1098
+ const projectConfig = validateProjectConfig(projectConfigInput);
1099
+ return plugins ? {
1019
1100
  ...projectConfig,
1020
- ...ssg ? { ssg } : {}
1021
- };
1101
+ plugins
1102
+ } : projectConfig;
1022
1103
  }
1023
1104
  async function loadClarifyConfig(root, env) {
1024
1105
  const configFile = findClarifyConfigFile(root);
1025
1106
  if (!configFile) return {};
1026
1107
  if (configFile.endsWith(".json")) return loadJsonConfig(configFile);
1027
- return assertConfigObject((await loadConfigFromFile(env, configFile, root, "silent"))?.config, configFile);
1108
+ return validateClarifyConfig(assertConfigObject((await loadConfigFromFile(env, configFile, root, "silent"))?.config, configFile));
1028
1109
  }
1029
1110
  /**
1030
1111
  * File extensions Clarify treats as content files. Kept in sync with the
@@ -1050,9 +1131,9 @@ function buildProjectInfo(context) {
1050
1131
  contentRoot: (contentRoot.startsWith(projectRoot) ? contentRoot.slice(projectRoot.length).replace(/^[/\\]+/, "") : contentRoot) || "source",
1051
1132
  projectRoot
1052
1133
  };
1053
- if (projectConfig.i18n) response.i18n = {
1054
- defaultLocale: projectConfig.i18n.defaultLocale,
1055
- locales: projectConfig.i18n.locales.map((l) => l.code)
1134
+ if (projectConfig.locales) response.locales = {
1135
+ default: projectConfig.locales.default,
1136
+ locales: projectConfig.locales.locales.map((l) => l.code)
1056
1137
  };
1057
1138
  return response;
1058
1139
  }
@@ -1112,10 +1193,23 @@ function resolveVirtualModuleId(id, modules, routes) {
1112
1193
  if (route) return resolveVirtualId(route.module.virtualModuleId);
1113
1194
  return null;
1114
1195
  }
1115
- function generateConfigModule(projectConfig, buildOptions, version) {
1196
+ function generateConfigModule(projectConfig, version) {
1116
1197
  const runtimeConfig = {
1117
- ...projectConfig,
1118
- ...buildOptions
1198
+ title: projectConfig.title,
1199
+ description: projectConfig.description,
1200
+ siteUrl: projectConfig.siteUrl,
1201
+ routePrefix: projectConfig.routePrefix,
1202
+ assetPrefix: projectConfig.assetPrefix,
1203
+ logo: projectConfig.logo,
1204
+ homeUrl: projectConfig.homeUrl,
1205
+ favicon: projectConfig.favicon,
1206
+ theme: projectConfig.theme,
1207
+ navigation: projectConfig.navigation,
1208
+ banner: projectConfig.banner,
1209
+ footer: projectConfig.footer,
1210
+ locales: projectConfig.locales,
1211
+ variables: projectConfig.variables,
1212
+ features: projectConfig.features
1119
1213
  };
1120
1214
  if (version) runtimeConfig.version = version;
1121
1215
  return `export const config = ${JSON.stringify(runtimeConfig)};`;
@@ -1207,16 +1301,10 @@ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
1207
1301
  }
1208
1302
  return `export const runtimeSlots = {\n${[...grouped.entries()].map(([name, list]) => ` ${JSON.stringify(name)}: [\n ${list.join(",\n ")}\n ]`).join(",\n")}\n};\n`;
1209
1303
  }
1210
- function generateMdxErrorModule(diagnostic) {
1304
+ function generateContentDiagnosticModule(diagnostic) {
1211
1305
  return `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';
1212
1306
 
1213
- export const contentDiagnostic = ${JSON.stringify({
1214
- kind: diagnostic.kind ?? "mdx",
1215
- title: diagnostic.title,
1216
- message: diagnostic.message,
1217
- filePath: diagnostic.filePath,
1218
- details: diagnostic.details
1219
- })};
1307
+ export const contentDiagnostic = ${JSON.stringify(diagnostic)};
1220
1308
 
1221
1309
  export default createContentDiagnosticComponent(contentDiagnostic);
1222
1310
  `;
@@ -1225,7 +1313,7 @@ function buildVirtualModules(args) {
1225
1313
  const modules = /* @__PURE__ */ new Map();
1226
1314
  const clientEntryModule = createClientEntryModule({ themeEditor: args.themeEditor });
1227
1315
  const allPlugins = [...args.plugins ?? []];
1228
- modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions, args.version));
1316
+ modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.version));
1229
1317
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, "client"));
1230
1318
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, "server"));
1231
1319
  modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
@@ -1234,7 +1322,7 @@ function buildVirtualModules(args) {
1234
1322
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1235
1323
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1236
1324
  for (const route of args.routes) {
1237
- const moduleContent = route.diagnostic ? generateMdxErrorModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1325
+ const moduleContent = route.diagnostic ? generateContentDiagnosticModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1238
1326
  modules.set(route.module.virtualModuleId, moduleContent);
1239
1327
  }
1240
1328
  return modules;
@@ -1285,26 +1373,42 @@ function createNormalizedContentPlugin(engine) {
1285
1373
  }
1286
1374
  };
1287
1375
  }
1288
- function createMdxPlugin() {
1289
- return mdxPlugin({
1290
- include: ["**/*.mdx"],
1291
- format: "mdx",
1292
- jsxImportSource: "react",
1293
- providerImportSource: "@clarify-labs/renderer",
1294
- remarkPlugins,
1295
- rehypePlugins
1296
- });
1376
+ function createContentCompileTransform(transform, format, projectRoot, errorPolicy = () => "diagnostic") {
1377
+ return async function contentCompileTransform(code, id) {
1378
+ try {
1379
+ return await transform.call(this, code, id);
1380
+ } catch (error) {
1381
+ if (errorPolicy() === "throw") throw error;
1382
+ return {
1383
+ code: generateContentDiagnosticModule(createContentCompileDiagnostic({
1384
+ format,
1385
+ phase: "compilation",
1386
+ error,
1387
+ filePath: id.replace(/\?.*$/, ""),
1388
+ projectRoot
1389
+ })),
1390
+ map: null
1391
+ };
1392
+ }
1393
+ };
1297
1394
  }
1298
- function createMarkdownPlugin() {
1299
- return mdxPlugin({
1300
- include: ["**/*.md"],
1301
- format: "md",
1395
+ function createContentCompilerPlugin(format, projectRoot, errorPolicy) {
1396
+ const markdown = format === "markdown";
1397
+ const plugin = mdxPlugin({
1398
+ include: [markdown ? "**/*.md" : "**/*.mdx"],
1399
+ format: markdown ? "md" : "mdx",
1302
1400
  jsxImportSource: "react",
1303
1401
  providerImportSource: "@clarify-labs/renderer",
1304
1402
  remarkPlugins,
1305
- remarkRehypeOptions: { allowDangerousHtml: true },
1306
- rehypePlugins: [rehypeRaw, ...rehypePlugins]
1403
+ ...markdown ? { remarkRehypeOptions: { allowDangerousHtml: true } } : {},
1404
+ rehypePlugins: markdown ? [rehypeRaw, ...rehypePlugins] : rehypePlugins
1307
1405
  });
1406
+ const transform = typeof plugin.transform === "function" ? plugin.transform : plugin.transform?.handler;
1407
+ if (!transform) return plugin;
1408
+ return {
1409
+ ...plugin,
1410
+ transform: createContentCompileTransform(transform, format, projectRoot, errorPolicy)
1411
+ };
1308
1412
  }
1309
1413
  function createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx) {
1310
1414
  let viteConfig;
@@ -1426,13 +1530,22 @@ function createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown,
1426
1530
  * site itself - it only bridges Vite lifecycle hooks to the engine.
1427
1531
  */
1428
1532
  function createViteAdapter(engine) {
1533
+ let contentCompileErrorPolicy = "diagnostic";
1534
+ const errorPolicy = () => contentCompileErrorPolicy;
1429
1535
  const normalizedContentPlugin = createNormalizedContentPlugin(engine);
1430
- const markdown = createMarkdownPlugin();
1431
- const mdx = createMdxPlugin();
1536
+ const markdown = createContentCompilerPlugin("markdown", engine.root, errorPolicy);
1537
+ const mdx = createContentCompilerPlugin("mdx", engine.root, errorPolicy);
1538
+ const compilePolicyPlugin = {
1539
+ name: "clarify:content-compile-policy",
1540
+ configResolved(config) {
1541
+ contentCompileErrorPolicy = config.command === "build" ? "throw" : "diagnostic";
1542
+ }
1543
+ };
1432
1544
  const tailwind = tailwindcss();
1433
1545
  return [
1434
1546
  ...react(),
1435
1547
  ...Array.isArray(tailwind) ? tailwind : [tailwind],
1548
+ compilePolicyPlugin,
1436
1549
  normalizedContentPlugin,
1437
1550
  createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx),
1438
1551
  markdown,
@@ -1441,7 +1554,7 @@ function createViteAdapter(engine) {
1441
1554
  }
1442
1555
  var cliPackageVersion = {
1443
1556
  name: "@clarify-labs/cli",
1444
- version: "0.10.2",
1557
+ version: "0.11.0",
1445
1558
  license: "AGPL-3.0-only",
1446
1559
  type: "module",
1447
1560
  repository: {
@@ -1521,8 +1634,7 @@ function resolveBuildOptions(options = {}) {
1521
1634
  return {
1522
1635
  projectRoot: options.projectRoot ?? process.cwd(),
1523
1636
  rootDirectory: options.rootDirectory ?? "source",
1524
- outputDirectory: options.outputDirectory,
1525
- ssg: { failOnError: options.ssg?.failOnError ?? true }
1637
+ outputDirectory: options.outputDirectory
1526
1638
  };
1527
1639
  }
1528
1640
  //#endregion
@@ -1580,7 +1692,7 @@ function llmsTxtListItem(route, basePath) {
1580
1692
  return description ? `- [${route.meta.title}](${basePath}${contentArtifactUrl}): ${description}` : `- [${route.meta.title}](${basePath}${contentArtifactUrl})`;
1581
1693
  }
1582
1694
  function llmsTxtLocaleLabel(locale, projectConfig) {
1583
- return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
1695
+ return projectConfig.locales?.locales.find((item) => item.code === locale)?.label ?? locale;
1584
1696
  }
1585
1697
  function groupRoutesByLocale(routes) {
1586
1698
  const groups = /* @__PURE__ */ new Map();
@@ -1592,7 +1704,7 @@ function groupRoutesByLocale(routes) {
1592
1704
  }
1593
1705
  function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
1594
1706
  const routesBySource = /* @__PURE__ */ new Map();
1595
- const defaultLocale = projectConfig.i18n?.defaultLocale;
1707
+ const defaultLocale = projectConfig.locales?.default;
1596
1708
  for (const route of routes) {
1597
1709
  const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.artifacts?.contentArtifactUrl ?? route.path}`;
1598
1710
  const previousRoute = routesBySource.get(sourceKey);
@@ -1794,9 +1906,6 @@ var OPENAPI_HTTP_METHODS = [
1794
1906
  "trace"
1795
1907
  ];
1796
1908
  var CLARIFY_OPENAPI_SECTION_ID_EXTENSION = "x-clarify-section-id";
1797
- function errorMessage(error) {
1798
- return error instanceof Error ? error.message : String(error);
1799
- }
1800
1909
  function isSameOpenAPIFile(url, filePath) {
1801
1910
  try {
1802
1911
  return resolve(fileURLToPath(url)) === resolve(filePath);
@@ -1911,7 +2020,7 @@ function extractOpenAPISections(spec, filterTags) {
1911
2020
  }
1912
2021
  return sections;
1913
2022
  }
1914
- async function readOpenAPISpec(filePath, contentProcessor) {
2023
+ async function readOpenAPISpec(filePath, contentProcessor, projectRoot) {
1915
2024
  try {
1916
2025
  if (!contentProcessor) return {
1917
2026
  ok: true,
@@ -1934,13 +2043,14 @@ async function readOpenAPISpec(filePath, contentProcessor) {
1934
2043
  } catch (error) {
1935
2044
  return {
1936
2045
  ok: false,
1937
- diagnostic: {
2046
+ diagnostic: createContentDiagnostic({
1938
2047
  kind: "openapi",
1939
2048
  title: "OpenAPI spec parse failed",
1940
- message: `Clarify could not parse ${filePath}.`,
2049
+ message: "Clarify could not parse this OpenAPI specification.",
2050
+ error,
1941
2051
  filePath,
1942
- details: errorMessage(error)
1943
- }
2052
+ projectRoot
2053
+ })
1944
2054
  };
1945
2055
  }
1946
2056
  }
@@ -1971,32 +2081,25 @@ export const routeData = {};
1971
2081
  export default createOpenApiRouteComponent({ ...routeData, spec });
1972
2082
  `;
1973
2083
  }
1974
- function generateOpenAPIErrorModule(diagnostic) {
1975
- return `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';
1976
-
1977
- export const contentDiagnostic = ${JSON.stringify({
1978
- kind: diagnostic.kind ?? "openapi",
1979
- title: diagnostic.title,
1980
- message: diagnostic.message,
1981
- filePath: diagnostic.filePath,
1982
- details: diagnostic.details
1983
- })};
1984
-
1985
- export default createContentDiagnosticComponent(contentDiagnostic);
1986
- `;
1987
- }
1988
2084
  //#endregion
1989
2085
  //#region source/plugins/openapi/index.ts
1990
2086
  function collectOpenAPIPageIntents(config) {
1991
2087
  const intents = [];
1992
- function visitPages(pages) {
1993
- if (!pages || pages === "FileTree") return;
1994
- for (const group of pages) for (const item of group.pages) {
2088
+ function visitItems(items) {
2089
+ for (const item of items) {
2090
+ if (typeof item !== "string" && "group" in item) {
2091
+ visitItems(item.pages);
2092
+ continue;
2093
+ }
1995
2094
  const intent = routeIntentFromPagesItem(item);
1996
2095
  if (intent.kind === "openapi" && (intent.path || intent.tagFilter?.length)) intents.push(intent);
1997
2096
  }
1998
2097
  }
1999
- for (const tab of config.tabs ?? []) visitPages(tab.pages);
2098
+ function visitPages(pages) {
2099
+ if (!pages || pages === "FileTree") return;
2100
+ for (const group of pages) visitItems(group.pages);
2101
+ }
2102
+ for (const tab of config.navigation?.tabs ?? []) visitPages(tab.pages);
2000
2103
  return intents;
2001
2104
  }
2002
2105
  function virtualModuleIdFromPath(path) {
@@ -2019,14 +2122,14 @@ function cloneOpenAPIRoute(route, overrides = {}) {
2019
2122
  ...overrides
2020
2123
  };
2021
2124
  }
2022
- function findOpenAPIRoutesForContentRoot(contentRoot, i18n) {
2023
- if (!i18n) return findOpenAPIRoutes(contentRoot);
2125
+ function findOpenAPIRoutesForContentRoot(contentRoot, locales) {
2126
+ if (!locales) return findOpenAPIRoutes(contentRoot);
2024
2127
  const localizedRoutes = [];
2025
- for (const locale of i18n.locales) {
2128
+ for (const locale of locales.locales) {
2026
2129
  const localeRoot = join(contentRoot, locale.code);
2027
2130
  for (const route of findOpenAPIRoutes(localeRoot, localeRoot)) {
2028
2131
  const basePath = route.basePath ?? route.path;
2029
- const path = localizedRoutePath(basePath, locale.code, i18n);
2132
+ const path = localizedRoutePath(basePath, locale.code, locales);
2030
2133
  localizedRoutes.push({
2031
2134
  ...route,
2032
2135
  path,
@@ -2036,15 +2139,15 @@ function findOpenAPIRoutesForContentRoot(contentRoot, i18n) {
2036
2139
  });
2037
2140
  }
2038
2141
  }
2039
- if (i18n.missing === "fallback") {
2142
+ if (locales.missing === "fallback") {
2040
2143
  const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
2041
- const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
2144
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === locales.default);
2042
2145
  for (const sourceRoute of defaultRoutes) {
2043
2146
  const basePath = sourceRoute.basePath ?? sourceRoute.path;
2044
- for (const locale of i18n.locales) {
2147
+ for (const locale of locales.locales) {
2045
2148
  const key = `${locale.code}:${basePath}`;
2046
2149
  if (routeByLocaleAndBase.has(key)) continue;
2047
- const path = localizedRoutePath(basePath, locale.code, i18n);
2150
+ const path = localizedRoutePath(basePath, locale.code, locales);
2048
2151
  localizedRoutes.push(cloneOpenAPIRoute(sourceRoute, {
2049
2152
  path,
2050
2153
  locale: locale.code,
@@ -2071,7 +2174,7 @@ function createConfiguredOpenAPIRoutes(routes, config) {
2071
2174
  const key = `${route.locale ?? ""}:${targetBasePath}`;
2072
2175
  if (existingKeys.has(key)) continue;
2073
2176
  existingKeys.add(key);
2074
- const path = route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath;
2177
+ const path = route.locale && config.locales ? localizedRoutePath(targetBasePath, route.locale, config.locales) : targetBasePath;
2075
2178
  additions.push(cloneOpenAPIRoute(route, {
2076
2179
  path,
2077
2180
  basePath: targetBasePath,
@@ -2084,12 +2187,12 @@ function createConfiguredOpenAPIRoutes(routes, config) {
2084
2187
  }
2085
2188
  }
2086
2189
  const nextRoutes = [...routes, ...additions];
2087
- if (!config.i18n) return nextRoutes;
2088
- const routesWithAlternates = nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n));
2190
+ if (!config.locales) return nextRoutes;
2191
+ const routesWithAlternates = nextRoutes.map((route) => withAlternates(route, nextRoutes, config.locales));
2089
2192
  const bareAliases = [];
2090
2193
  const seenBare = new Set(routesWithAlternates.map((route) => route.path));
2091
2194
  for (const route of routesWithAlternates) {
2092
- if (route.locale !== config.i18n.defaultLocale) continue;
2195
+ if (route.locale !== config.locales.default) continue;
2093
2196
  const basePath = route.basePath ?? route.path;
2094
2197
  if (basePath === route.path || seenBare.has(basePath)) continue;
2095
2198
  seenBare.add(basePath);
@@ -2128,7 +2231,7 @@ function createOpenAPIPlugin() {
2128
2231
  hooks: {
2129
2232
  "routes:discover": (input, ctx) => ({
2130
2233
  ...input,
2131
- routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.i18n)]
2234
+ routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.locales)]
2132
2235
  }),
2133
2236
  "routes:discovered": async (routes, ctx) => {
2134
2237
  specs.clear();
@@ -2141,7 +2244,7 @@ function createOpenAPIPlugin() {
2141
2244
  const result = specFromCache ? {
2142
2245
  ok: true,
2143
2246
  spec: specFromCache
2144
- } : await readOpenAPISpec(route.source.filePath, processor);
2247
+ } : await readOpenAPISpec(route.source.filePath, processor, ctx.generateOptions.projectRoot);
2145
2248
  if (!result.ok) {
2146
2249
  route.diagnostic = result.diagnostic;
2147
2250
  route.meta.title = route.meta.title || "OpenAPI parse error";
@@ -2188,7 +2291,7 @@ function createOpenAPIPlugin() {
2188
2291
  for (const [routeSpecModuleId, spec] of specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2189
2292
  for (const route of ctx.routes) {
2190
2293
  if (route.kind !== "openapi") continue;
2191
- if (route.diagnostic) modules.set(route.module.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
2294
+ if (route.diagnostic) modules.set(route.module.virtualModuleId, generateContentDiagnosticModule(route.diagnostic));
2192
2295
  else if (route.openapi?.sourceSpecId) {
2193
2296
  const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2194
2297
  if (!specModules.has(routeSpecModuleId)) continue;
@@ -2285,7 +2388,7 @@ async function generateDevSearchIndex(ctx, root, pagefind) {
2285
2388
  const result = await index.addCustomRecord({
2286
2389
  url: route.path,
2287
2390
  content,
2288
- language: toPagefindLanguage(route.locale ?? ctx.projectConfig.i18n?.defaultLocale),
2391
+ language: toPagefindLanguage(route.locale ?? ctx.projectConfig.locales?.default),
2289
2392
  meta: { title: route.meta.title }
2290
2393
  });
2291
2394
  assertNoPagefindErrors(`addCustomRecord ${route.path}`, result.errors);
@@ -2328,6 +2431,7 @@ function createSearchIndexPlugin() {
2328
2431
  name: "clarify:search-index",
2329
2432
  hooks: {
2330
2433
  async "dev:configureServer"(server, ctx) {
2434
+ if (!ctx.projectConfig.features.search.enabled) return;
2331
2435
  const pagefind = await import("pagefind");
2332
2436
  let currentRoot;
2333
2437
  const staleRoots = /* @__PURE__ */ new Set();
@@ -2367,6 +2471,7 @@ function createSearchIndexPlugin() {
2367
2471
  });
2368
2472
  },
2369
2473
  async "build:done"(ctx) {
2474
+ if (!ctx.projectConfig.features.search.enabled) return;
2370
2475
  const outputDirectory = ctx.generateOptions.outputDirectory;
2371
2476
  if (!outputDirectory) return;
2372
2477
  const pageCount = await generatePagefindIndex({ outputRoot: resolve(ctx.generateOptions.projectRoot, outputDirectory) });
@@ -2419,16 +2524,18 @@ function createSeoPlugin() {
2419
2524
  return {
2420
2525
  name: "clarify:seo",
2421
2526
  hooks: { "build:assets"(ctx) {
2527
+ const assets = [];
2422
2528
  const sitemap = createSitemapXml(ctx.routes, ctx.projectConfig);
2423
2529
  const robots = createRobotsTxt(ctx.projectConfig);
2424
- if (!sitemap || !robots) return [];
2425
- return [{
2530
+ if (sitemap) assets.push({
2426
2531
  fileName: "sitemap.xml",
2427
2532
  source: sitemap
2428
- }, {
2533
+ });
2534
+ if (robots) assets.push({
2429
2535
  fileName: "robots.txt",
2430
2536
  source: robots
2431
- }];
2537
+ });
2538
+ return assets;
2432
2539
  } }
2433
2540
  };
2434
2541
  }
@@ -2444,7 +2551,8 @@ function encodePath(path) {
2444
2551
  return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
2445
2552
  }
2446
2553
  function createSourceEditUrl(filePath, contentRoot, source) {
2447
- const repository = normalizeRepositoryUrl(source.repository);
2554
+ if (!source.url) return "";
2555
+ const repository = normalizeRepositoryUrl(source.url);
2448
2556
  const branch = source.branch ?? "main";
2449
2557
  const sourcePath = [source.directory ? trimSlashes(source.directory) : "", relative(contentRoot, filePath).replaceAll("\\", "/")].filter(Boolean).join("/");
2450
2558
  return `${repository}/edit/${encodeURIComponent(branch)}/${encodePath(sourcePath)}`;
@@ -2462,7 +2570,8 @@ function createSourceLinksPlugin() {
2462
2570
  return {
2463
2571
  name: "clarify:source-links",
2464
2572
  hooks: { "routes:discovered": (routes, ctx) => {
2465
- attachSourceEditUrls(routes, join(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
2573
+ if (!ctx.projectConfig.features.repository.enabled) return routes;
2574
+ attachSourceEditUrls(routes, join(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.features.repository);
2466
2575
  return routes;
2467
2576
  } }
2468
2577
  };
@@ -2577,18 +2686,18 @@ async function resolveProjectContext(options = {}, env = {
2577
2686
  }) {
2578
2687
  const projectRoot = resolve(options.projectRoot ?? process.cwd());
2579
2688
  const initialBuildOptions = resolveBuildOptions(options);
2580
- const contentRoot = join(projectRoot, initialBuildOptions.rootDirectory);
2581
2689
  const configFilePath = findClarifyConfigFile(projectRoot);
2582
2690
  const config = configFilePath && existsSync(configFilePath) ? await loadClarifyConfig(projectRoot, env) : {};
2583
2691
  const mergedOptions = {
2584
2692
  ...config,
2585
2693
  ...options,
2586
2694
  projectRoot,
2587
- rootDirectory: initialBuildOptions.rootDirectory,
2588
- outputDirectory: initialBuildOptions.outputDirectory
2695
+ rootDirectory: options.rootDirectory ?? initialBuildOptions.rootDirectory,
2696
+ outputDirectory: options.outputDirectory ?? initialBuildOptions.outputDirectory
2589
2697
  };
2590
2698
  const resolvedProjectConfig = resolveProjectConfig(mergedOptions);
2591
2699
  const resolvedBuildOptions = resolveBuildOptions(mergedOptions);
2700
+ const contentRoot = join(projectRoot, resolvedBuildOptions.rootDirectory);
2592
2701
  return {
2593
2702
  projectRoot,
2594
2703
  contentRoot,
@@ -2678,9 +2787,9 @@ function canonicalUrl(contextOrConfig, route) {
2678
2787
  /** Inject `lang` (and optional `dir`) onto the `<html>` tag for the route locale. */
2679
2788
  function injectHtmlLocaleAttributes(html, contextOrConfig, route) {
2680
2789
  const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2681
- const localeCode = route?.locale ?? projectConfig.i18n?.defaultLocale;
2790
+ const localeCode = route?.locale ?? projectConfig.locales?.default;
2682
2791
  if (!localeCode) return html;
2683
- const dir = (projectConfig.i18n?.locales.find((locale) => locale.code === localeCode))?.dir;
2792
+ const dir = (projectConfig.locales?.locales.find((locale) => locale.code === localeCode))?.dir;
2684
2793
  return html.replace(/<html\b([^>]*)>/i, (_match, attributes) => {
2685
2794
  let nextAttributes = attributes.replace(/\s+lang=("[^"]*"|'[^']*'|[^\s>]*)/i, "").replace(/\s+dir=("[^"]*"|'[^']*'|[^\s>]*)/i, "");
2686
2795
  nextAttributes = `${nextAttributes} lang="${escapeHtml(localeCode)}"`;
@@ -2858,7 +2967,7 @@ async function buildSSRBundle(root, ssrEntry, ssrOutDir, plugins) {
2858
2967
  }
2859
2968
  });
2860
2969
  }
2861
- async function renderSSGRoutes(routes, contextOrConfig, outputDirectory, ssrBundlePath, failOnError = true) {
2970
+ async function renderSSGRoutes(routes, contextOrConfig, outputDirectory, ssrBundlePath) {
2862
2971
  const { render } = await import(pathToFileURL(ssrBundlePath).href);
2863
2972
  const template = readIndexHtml(outputDirectory);
2864
2973
  if (!template) throw new Error(`[clarify] index.html not found in outputDirectory "${outputDirectory}". Make sure Vite build produces it.`);
@@ -2870,7 +2979,7 @@ async function renderSSGRoutes(routes, contextOrConfig, outputDirectory, ssrBund
2870
2979
  }
2871
2980
  } catch (err) {
2872
2981
  console.error(`[clarify] Failed to render route "${route.path}":`, err);
2873
- if (failOnError) throw err;
2982
+ throw err;
2874
2983
  }
2875
2984
  }
2876
2985
  //#endregion
@@ -2947,10 +3056,10 @@ var ClarifyContext = class {
2947
3056
  this.currentPlugins = plugins;
2948
3057
  }
2949
3058
  get isI18n() {
2950
- return Boolean(this.projectConfig.i18n);
3059
+ return Boolean(this.projectConfig.locales);
2951
3060
  }
2952
3061
  get defaultLocale() {
2953
- return this.projectConfig.i18n?.defaultLocale;
3062
+ return this.projectConfig.locales?.default;
2954
3063
  }
2955
3064
  get(key) {
2956
3065
  return this.values.get(key);
@@ -3020,6 +3129,12 @@ function loadVirtualModule(id, modules) {
3020
3129
  const bareId = stripVirtualPrefix(id);
3021
3130
  return modules.get(bareId) ?? modules.get(id) ?? null;
3022
3131
  }
3132
+ function assertNoContentDiagnostics(routes) {
3133
+ const failures = routes.filter((route) => route.diagnostic);
3134
+ if (failures.length === 0) return;
3135
+ const summary = failures.map((route) => `- ${route.path}: ${route.diagnostic?.title ?? "Content error"}${route.diagnostic?.filePath ? ` (${route.diagnostic.filePath})` : ""}`).join("\n");
3136
+ throw new Error(`[clarify] Content diagnostics prevented the build:\n${summary}`);
3137
+ }
3023
3138
  var ClarifyEngine = class {
3024
3139
  options;
3025
3140
  root;
@@ -3138,10 +3253,10 @@ var ClarifyEngine = class {
3138
3253
  async discoverSite() {
3139
3254
  const plugins = this.ctx.plugins;
3140
3255
  const contentRoot = this.ctx.contentRoot;
3141
- const { i18n, tabs } = this.ctx.projectConfig;
3256
+ const { locales, navigation: navigationConfig } = this.ctx.projectConfig;
3142
3257
  let routes = await runPhase(plugins, "site:discover", this.ctx, async () => {
3143
3258
  const options = { contentProcessor: createProjectContentProcessor(plugins, this.ctx) };
3144
- const discovered = i18n ? await findLocalizedContentRoutes(contentRoot, i18n, options) : await findContentRoutes(contentRoot, contentRoot, options);
3259
+ const discovered = locales ? await findLocalizedContentRoutes(contentRoot, locales, options) : await findContentRoutes(contentRoot, contentRoot, options);
3145
3260
  const result = await runHooks(plugins, "routes:discover", {
3146
3261
  contentRoot,
3147
3262
  routes: discovered
@@ -3170,8 +3285,9 @@ var ClarifyEngine = class {
3170
3285
  };
3171
3286
  });
3172
3287
  });
3173
- routes = applyConfiguredPageRoutePaths(routes, tabs, i18n);
3174
- const navigation = tabs ? i18n ? buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) ?? {
3288
+ const tabs = navigationConfig?.tabs;
3289
+ routes = applyConfiguredPageRoutePaths(routes, tabs, locales);
3290
+ const navigation = tabs ? locales ? buildLocalizedNavigationFromTabsConfig(routes, tabs, locales) ?? {
3175
3291
  kind: "localized-tabbed",
3176
3292
  locales: {}
3177
3293
  } : buildNavigationFromTabsConfig(routes, tabs) : {
@@ -3201,7 +3317,7 @@ var ClarifyEngine = class {
3201
3317
  routes: this.routes,
3202
3318
  navigation: this.navigation,
3203
3319
  plugins: this.plugins,
3204
- themeEditor: this.runtime.command === "serve" || this.runtime.themeEditor === true || this.projectConfig.theme.editor,
3320
+ themeEditor: this.runtime.command === "serve" || this.runtime.themeEditor === true || this.projectConfig.features.themeEditor.enabled,
3205
3321
  version: this.ctx.version
3206
3322
  });
3207
3323
  this.virtualModules = await runHooks(this.plugins, "modules:before", this.virtualModules, this.ctx);
@@ -3244,6 +3360,7 @@ var ClarifyEngine = class {
3244
3360
  await runPhase(this.plugins, "ssg", this.ctx, async () => {
3245
3361
  if (process.env.SKIP_CLARIFY_SSG) return;
3246
3362
  if (!await runInterceptHooks(this.plugins, "ssg:shouldRun", this.ctx)) return;
3363
+ assertNoContentDiagnostics(this.routes);
3247
3364
  const outputDir = this.runtime.outputDirectory ?? this.generateOptions.outputDirectory;
3248
3365
  if (!outputDir) throw new Error("[clarify] outputDirectory is required before SSG runs");
3249
3366
  const ssrOutputDir = join(outputDir, ".ssr");
@@ -3257,10 +3374,10 @@ var ClarifyEngine = class {
3257
3374
  plugins: this.runtime.ssrPlugins ?? []
3258
3375
  });
3259
3376
  const ssrBundlePath = join(ssrOutputDir, "entry-server.js");
3260
- await renderSSGRoutes(this.routes, this.runtimeContext(), outputDir, ssrBundlePath, this.generateOptions.ssg.failOnError);
3377
+ await renderSSGRoutes(this.routes, this.runtimeContext(), outputDir, ssrBundlePath);
3261
3378
  } catch (err) {
3262
3379
  console.error("[clarify] SSG failed:", err);
3263
- if (this.generateOptions.ssg.failOnError) throw err;
3380
+ throw err;
3264
3381
  } finally {
3265
3382
  if (tempEntryPath) try {
3266
3383
  rmSync(tempEntryPath, { force: true });
@@ -3304,4 +3421,4 @@ function createClarifyEngine(options = {}) {
3304
3421
  //#endregion
3305
3422
  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 };
3306
3423
 
3307
- //# sourceMappingURL=engine-CSSL_gGd.js.map
3424
+ //# sourceMappingURL=engine-DfF5RJjC.js.map