@clarify-labs/cli 0.6.6 → 0.7.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.
@@ -40,9 +40,9 @@ let gray_matter = require("gray-matter");
40
40
  gray_matter = __toESM(gray_matter, 1);
41
41
  let _apidevtools_swagger_parser = require("@apidevtools/swagger-parser");
42
42
  _apidevtools_swagger_parser = __toESM(_apidevtools_swagger_parser, 1);
43
+ let node_os = require("node:os");
43
44
  let node_fs_promises = require("node:fs/promises");
44
45
  let zod = require("zod");
45
- let node_os = require("node:os");
46
46
  let node_url = require("node:url");
47
47
  let vite = require("vite");
48
48
  let node_module = require("node:module");
@@ -261,8 +261,80 @@ function createContentArtifactsPlugin() {
261
261
  };
262
262
  }
263
263
  //#endregion
264
+ //#region source/core/theme.ts
265
+ var clarifyThemePresets = {
266
+ default: {
267
+ preset: "default",
268
+ tokens: {
269
+ colors: {
270
+ primary: "#047857",
271
+ accent: "#0D9488",
272
+ background: "#ffffff",
273
+ foreground: "#111827",
274
+ surface: "#ffffff",
275
+ muted: "#64748b",
276
+ border: "rgb(15 23 42 / 0.12)",
277
+ codeBackground: "#f6fbf8"
278
+ },
279
+ radius: {
280
+ sm: "6px",
281
+ md: "10px",
282
+ lg: "14px",
283
+ xl: "18px"
284
+ }
285
+ },
286
+ layout: { maxWidth: "82rem" },
287
+ editor: false
288
+ },
289
+ base: {
290
+ preset: "base",
291
+ tokens: {
292
+ colors: {
293
+ primary: "#18181b",
294
+ accent: "#52525b",
295
+ background: "#ffffff",
296
+ foreground: "#18181b",
297
+ surface: "#ffffff",
298
+ muted: "#71717a",
299
+ border: "rgb(24 24 27 / 0.12)",
300
+ codeBackground: "#f4f4f5"
301
+ },
302
+ radius: {
303
+ sm: "6px",
304
+ md: "8px",
305
+ lg: "12px",
306
+ xl: "16px"
307
+ }
308
+ },
309
+ layout: { maxWidth: "80rem" },
310
+ editor: false
311
+ }
312
+ };
313
+ function resolveThemeConfig(theme = {}) {
314
+ const preset = theme.preset ?? "default";
315
+ const presetTheme = clarifyThemePresets[preset];
316
+ return {
317
+ preset,
318
+ tokens: {
319
+ colors: {
320
+ ...presetTheme.tokens.colors,
321
+ ...theme.tokens?.colors
322
+ },
323
+ radius: {
324
+ ...presetTheme.tokens.radius,
325
+ ...theme.tokens?.radius
326
+ }
327
+ },
328
+ layout: {
329
+ ...presetTheme.layout,
330
+ ...theme.layout
331
+ },
332
+ editor: theme.editor ?? presetTheme.editor
333
+ };
334
+ }
335
+ //#endregion
264
336
  //#region source/plugins/html-shell/index.ts
265
- var THEME_BOOTSTRAP_SCRIPT = `(function(){try{var e=localStorage.getItem('clarify:theme');var t=e==='dark'||e==='light'||e==='system'?e:'system';var r=t==='system'?window.matchMedia('(prefers-color-scheme: dark)').matches:t==='dark';document.documentElement.classList.toggle('dark',r);document.documentElement.style.colorScheme=r?'dark':'light'}catch(e){}})();`;
337
+ var THEME_STORAGE_KEY = "clarify:theme";
266
338
  var REL_ICON_RE = /<link\s+[^>]*rel=["'][^"']*\bicon\b[^"']*["'][^>]*>/i;
267
339
  function inferIconType(href) {
268
340
  const path = href.split(/[?#]/, 1)[0]?.toLowerCase() ?? href.toLowerCase();
@@ -292,6 +364,62 @@ function createIconTags(icon) {
292
364
  if (lightIcon === darkIcon) return [createIconTag(lightIcon)];
293
365
  return [createIconTag(lightIcon, "(prefers-color-scheme: light)"), createIconTag(darkIcon, "(prefers-color-scheme: dark)")];
294
366
  }
367
+ function matchesPresetTheme(theme) {
368
+ const preset = clarifyThemePresets[theme.preset];
369
+ return JSON.stringify({
370
+ tokens: theme.tokens,
371
+ layout: theme.layout
372
+ }) === JSON.stringify({
373
+ tokens: preset.tokens,
374
+ layout: preset.layout
375
+ });
376
+ }
377
+ function effectiveTheme(theme, resolvedTheme) {
378
+ if (resolvedTheme !== "dark" || !matchesPresetTheme(theme)) return theme;
379
+ return {
380
+ ...theme,
381
+ tokens: {
382
+ ...theme.tokens,
383
+ colors: {
384
+ ...theme.tokens.colors,
385
+ background: "#09090b",
386
+ foreground: "#ffffff",
387
+ surface: "#18181b",
388
+ muted: "#a1a1aa",
389
+ border: "rgb(255 255 255 / 0.1)",
390
+ codeBackground: "#18181b"
391
+ }
392
+ }
393
+ };
394
+ }
395
+ function resolveThemeColorValue(value, resolvedTheme) {
396
+ return typeof value === "string" ? value : value[resolvedTheme] ?? value.light ?? value.dark ?? "";
397
+ }
398
+ function themeToCssVariables(theme, resolvedTheme) {
399
+ const resolved = effectiveTheme(theme, resolvedTheme);
400
+ const { colors } = resolved.tokens;
401
+ const { radius } = resolved.tokens;
402
+ return {
403
+ "--clarify-theme-tokens-colors-primary": resolveThemeColorValue(colors.primary, resolvedTheme),
404
+ "--clarify-theme-tokens-colors-accent": resolveThemeColorValue(colors.accent, resolvedTheme),
405
+ "--clarify-theme-tokens-colors-background": resolveThemeColorValue(colors.background, resolvedTheme),
406
+ "--clarify-theme-tokens-colors-foreground": resolveThemeColorValue(colors.foreground, resolvedTheme),
407
+ "--clarify-theme-tokens-colors-surface": resolveThemeColorValue(colors.surface, resolvedTheme),
408
+ "--clarify-theme-tokens-colors-muted": resolveThemeColorValue(colors.muted, resolvedTheme),
409
+ "--clarify-theme-tokens-colors-border": resolveThemeColorValue(colors.border, resolvedTheme),
410
+ "--clarify-theme-tokens-colors-code-background": resolveThemeColorValue(colors.codeBackground, resolvedTheme),
411
+ "--clarify-theme-tokens-radius-sm": radius.sm,
412
+ "--clarify-theme-tokens-radius-md": radius.md,
413
+ "--clarify-theme-tokens-radius-lg": radius.lg,
414
+ "--clarify-theme-tokens-radius-xl": radius.xl,
415
+ "--clarify-theme-layout-max-width": resolved.layout.maxWidth
416
+ };
417
+ }
418
+ function themeBootstrapScript(theme) {
419
+ const darkVariables = themeToCssVariables(theme, "dark");
420
+ const lightVariables = themeToCssVariables(theme, "light");
421
+ return `(function(){try{var e=localStorage.getItem(${JSON.stringify(THEME_STORAGE_KEY)});var t=e==='dark'||e==='light'||e==='system'?e:'system';var r=t==='system'?window.matchMedia('(prefers-color-scheme: dark)').matches:t==='dark';var v=r?${JSON.stringify(darkVariables)}:${JSON.stringify(lightVariables)};var s=document.documentElement.style;for(var k in v)s.setProperty(k,v[k]);document.documentElement.classList.toggle('dark',r);document.documentElement.style.colorScheme=r?'dark':'light'}catch(e){}})();`;
422
+ }
295
423
  function createHtmlShellPlugin() {
296
424
  return {
297
425
  name: "clarify:html-shell",
@@ -302,7 +430,7 @@ function createHtmlShellPlugin() {
302
430
  ...REL_ICON_RE.test(input.html) ? [] : createIconTags(ctx.projectConfig.favicon ?? ctx.projectConfig.logo),
303
431
  {
304
432
  tag: "script",
305
- children: THEME_BOOTSTRAP_SCRIPT,
433
+ children: themeBootstrapScript(ctx.projectConfig.theme),
306
434
  injectTo: "head-prepend"
307
435
  },
308
436
  {
@@ -385,13 +513,14 @@ function basePathFromRef(ref) {
385
513
  const withoutExtension = ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "");
386
514
  return normalizePath(withoutExtension === "index" ? "/" : withoutExtension.replace(/\/index$/, ""));
387
515
  }
388
- function openAPIPagePathFromRef(ref, tagFilter) {
516
+ function openAPIPagePathFromRef(ref, tagFilter, explicitPath) {
517
+ if (explicitPath) return normalizePath(explicitPath);
389
518
  const basePath = basePathFromRef(ref);
390
519
  if (!tagFilter?.length) return basePath;
391
520
  return normalizePath(`${basePath}/${openAPITagsPathSegment(tagFilter)}`);
392
521
  }
393
- function routePathFromRef(ref) {
394
- return basePathFromRef(ref);
522
+ function routePathFromRef(ref, explicitPath) {
523
+ return explicitPath ? normalizePath(explicitPath) : basePathFromRef(ref);
395
524
  }
396
525
  function virtualModuleIdFromRef(ref) {
397
526
  return "virtual:clarify-page/" + ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
@@ -445,6 +574,18 @@ function frontmatterKeywords(frontmatter) {
445
574
  const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
446
575
  return keywords.length > 0 ? keywords : void 0;
447
576
  }
577
+ function withAlternates$2(route, routes, i18n) {
578
+ const basePath = route.basePath ?? route.path;
579
+ const routeByLocaleAndBase = new Map(routes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
580
+ const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
581
+ const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
582
+ return alternate ? [[locale.code, alternate.path]] : [];
583
+ }));
584
+ return {
585
+ ...route,
586
+ alternates
587
+ };
588
+ }
448
589
  function buildNavigation(routes) {
449
590
  const root = [];
450
591
  for (const route of routes) {
@@ -476,11 +617,13 @@ function resolvePageItem(item) {
476
617
  if ("openapi" in item) return {
477
618
  openapiRef: item.openapi,
478
619
  openapiTagFilter: item.filter?.tags,
620
+ path: item.path,
479
621
  title: item.title,
480
622
  icon: item.icon
481
623
  };
482
624
  return {
483
625
  pageRef: item.page,
626
+ path: item.path,
484
627
  redirect: item.redirect,
485
628
  title: item.title,
486
629
  icon: item.icon
@@ -494,6 +637,41 @@ function firstNavigationPath(nodes) {
494
637
  }
495
638
  return "/";
496
639
  }
640
+ function collectExplicitPagePathItems(tabs) {
641
+ const items = [];
642
+ for (const tab of tabs ?? []) {
643
+ if (!tab.pages || tab.pages === "FileTree") continue;
644
+ for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) items.push({
645
+ pageRef: item.page,
646
+ path: item.path
647
+ });
648
+ }
649
+ return items;
650
+ }
651
+ function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
652
+ const pageItems = collectExplicitPagePathItems(tabs);
653
+ if (!pageItems.length) return routes;
654
+ const additions = [];
655
+ const existingKeys = new Set(routes.map((route) => `${route.locale ?? ""}:${route.basePath ?? route.path}`));
656
+ for (const item of pageItems) {
657
+ const sourceBasePath = routePathFromRef(item.pageRef);
658
+ const targetBasePath = routePathFromRef(item.pageRef, item.path);
659
+ if (sourceBasePath === targetBasePath) continue;
660
+ for (const route of routes) {
661
+ if (route.kind !== "mdx" || (route.basePath ?? route.path) !== sourceBasePath) continue;
662
+ const key = `${route.locale ?? ""}:${targetBasePath}`;
663
+ if (existingKeys.has(key)) continue;
664
+ existingKeys.add(key);
665
+ additions.push({
666
+ ...route,
667
+ path: route.locale && i18n ? localizedRoutePath(targetBasePath, route.locale, i18n) : targetBasePath,
668
+ basePath: targetBasePath
669
+ });
670
+ }
671
+ }
672
+ const nextRoutes = [...routes, ...additions];
673
+ return i18n ? nextRoutes.map((route) => withAlternates$2(route, nextRoutes, i18n)) : nextRoutes;
674
+ }
497
675
  function buildNavigationFromPagesConfig(routes, config) {
498
676
  if (!config || config === "FileTree") return buildNavigation(routes);
499
677
  return buildNavigationFromConfig(routes, config);
@@ -514,9 +692,9 @@ function buildNavigationFromConfig(routes, config) {
514
692
  const routeMap = new Map(routes.map((r) => [r.path, r]));
515
693
  return config.map((group) => {
516
694
  const children = group.pages.map((item) => {
517
- const { pageRef, openapiRef, openapiTagFilter, redirect, title, icon } = resolvePageItem(item);
695
+ const { pageRef, openapiRef, openapiTagFilter, path: explicitPath, redirect, title, icon } = resolvePageItem(item);
518
696
  if (openapiRef) {
519
- const path = openAPIPagePathFromRef(openapiRef, openapiTagFilter);
697
+ const path = openAPIPagePathFromRef(openapiRef, openapiTagFilter, explicitPath);
520
698
  const route = routeMap.get(path);
521
699
  return {
522
700
  path,
@@ -526,10 +704,10 @@ function buildNavigationFromConfig(routes, config) {
526
704
  };
527
705
  }
528
706
  const ref = pageRef ?? "";
529
- const path = ref === "index" ? "/" : "/" + ref;
707
+ const path = routePathFromRef(ref, explicitPath);
530
708
  const route = routeMap.get(path);
531
709
  return {
532
- path: redirect ? redirect : path,
710
+ path: redirect ? normalizePath(redirect) : path,
533
711
  title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle$1(path.split("/").pop() ?? ref),
534
712
  icon,
535
713
  sections: route?.sections ? navigationSections(route.sections) : void 0
@@ -558,13 +736,13 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
558
736
  const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
559
737
  return config.map((group) => {
560
738
  const children = group.pages.map((item) => {
561
- const { pageRef, openapiRef, openapiTagFilter, redirect, title, icon } = resolvePageItem(item);
739
+ const { pageRef, openapiRef, openapiTagFilter, path: explicitPath, redirect, title, icon } = resolvePageItem(item);
562
740
  const ref = openapiRef ?? pageRef ?? "";
563
- const basePath = openapiRef ? openAPIPagePathFromRef(openapiRef, openapiTagFilter) : basePathFromRef(ref);
741
+ const basePath = openapiRef ? openAPIPagePathFromRef(openapiRef, openapiTagFilter, explicitPath) : routePathFromRef(ref, explicitPath);
564
742
  const route = routeMap.get(basePath);
565
743
  const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
566
744
  return {
567
- path: (redirect ? localizedRoutePath(basePathFromRef(redirect), locale, i18n) : void 0) ?? path,
745
+ path: (redirect ? localizedRoutePath(routePathFromRef(redirect), locale, i18n) : void 0) ?? path,
568
746
  title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle$1(basePath.split("/").pop() ?? ref),
569
747
  icon,
570
748
  sections: route?.sections ? navigationSections(route.sections) : void 0
@@ -729,8 +907,10 @@ function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode =
729
907
  const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
730
908
  return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
731
909
  }
732
- function createRuntimeModule() {
733
- return "export const openApis = {};";
910
+ function createRuntimeModule(options = {}) {
911
+ return `${options.footerComponentSource === "path" && options.imports?.footerComponent ? `import FooterComponent from ${moduleSpecifier(options.imports.footerComponent)};` : ""}
912
+ export const openApis = {};
913
+ export const footerComponent = ${options.footerComponentSource === "path" ? "FooterComponent" : "undefined"};`;
734
914
  }
735
915
  function createClientEntryModule(options = {}) {
736
916
  return `
@@ -738,16 +918,22 @@ import '@clarify-labs/renderer/style.css';
738
918
  import { render } from '@clarify-labs/renderer/client';
739
919
  import { routes, navigation } from '${VIRTUAL_ROUTES}';
740
920
  import { config } from '${VIRTUAL_CONFIG}';
741
- import { openApis } from '${VIRTUAL_RUNTIME}';
742
- render({ config, routes, navigation, openApis, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
921
+ import { openApis, footerComponent } from '${VIRTUAL_RUNTIME}';
922
+ render({ config, routes, navigation, openApis, footerComponent, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
743
923
  }
744
924
  function buildVirtualModules(args) {
745
925
  const modules = /* @__PURE__ */ new Map();
746
926
  const clientEntryModule = createClientEntryModule({ themeEditor: args.themeEditor });
927
+ const footerComponent = args.projectConfig.footer;
928
+ const footerComponentSource = typeof footerComponent === "string" ? "path" : void 0;
929
+ const footerComponentImport = typeof footerComponent === "string" ? footerComponent : void 0;
747
930
  modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
748
931
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
749
932
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "server"));
750
- modules.set(VIRTUAL_RUNTIME, createRuntimeModule());
933
+ modules.set(VIRTUAL_RUNTIME, createRuntimeModule({
934
+ footerComponentSource,
935
+ imports: { footerComponent: footerComponentImport }
936
+ }));
751
937
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
752
938
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
753
939
  for (const route of args.routes) modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
@@ -756,8 +942,12 @@ function buildVirtualModules(args) {
756
942
  //#endregion
757
943
  //#region source/plugins/openapi/virtual-modules.ts
758
944
  var openApiRegistryModuleId = VIRTUAL_RUNTIME;
759
- function generateOpenAPIRegistryModule(openApis) {
760
- return `export const openApis = ${JSON.stringify(openApis)};`;
945
+ function generateOpenAPIRegistryModule(openApis, runtimeModule = "") {
946
+ const openApisExport = `export const openApis = ${JSON.stringify(openApis)};`;
947
+ const openApisPlaceholder = /export const openApis = \{\};?/;
948
+ if (!runtimeModule) return openApisExport;
949
+ if (openApisPlaceholder.test(runtimeModule)) return runtimeModule.replace(openApisPlaceholder, openApisExport);
950
+ return runtimeModule;
761
951
  }
762
952
  function generateOpenAPIModule(spec, tagFilter) {
763
953
  return `import { createElement } from 'react';
@@ -795,7 +985,7 @@ function collectOpenAPIPageItems(config) {
795
985
  const items = [];
796
986
  function visitPages(pages) {
797
987
  if (!pages || pages === "FileTree") return;
798
- for (const group of pages) for (const item of group.pages) if (typeof item !== "string" && "openapi" in item && item.filter?.tags?.length) items.push(item);
988
+ for (const group of pages) for (const item of group.pages) if (typeof item !== "string" && "openapi" in item && (item.path || item.filter?.tags?.length)) items.push(item);
799
989
  }
800
990
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
801
991
  return items;
@@ -812,28 +1002,31 @@ function withAlternates$1(route, routes, i18n) {
812
1002
  alternates
813
1003
  };
814
1004
  }
815
- function createTaggedOpenAPIRoutes(routes, config) {
1005
+ function virtualModuleIdFromPath(path) {
1006
+ return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
1007
+ }
1008
+ function createConfiguredOpenAPIRoutes(routes, config) {
816
1009
  const pageItems = collectOpenAPIPageItems(config);
817
1010
  if (!pageItems.length) return routes;
818
1011
  const additions = [];
819
1012
  const existingKeys = new Set(routes.map((route) => `${route.locale ?? ""}:${route.basePath ?? route.path}`));
820
1013
  for (const item of pageItems) {
821
1014
  const tagFilter = item.filter?.tags?.filter(Boolean);
822
- if (!tagFilter?.length) continue;
823
1015
  const sourceBasePath = openAPIPagePathFromRef(item.openapi);
824
- const filteredBasePath = openAPIPagePathFromRef(item.openapi, tagFilter);
1016
+ const targetBasePath = openAPIPagePathFromRef(item.openapi, tagFilter, item.path);
1017
+ if (sourceBasePath === targetBasePath && !tagFilter?.length) continue;
825
1018
  for (const route of routes) {
826
1019
  if (route.kind !== "openapi" || (route.basePath ?? route.path) !== sourceBasePath) continue;
827
- const key = `${route.locale ?? ""}:${filteredBasePath}`;
1020
+ const key = `${route.locale ?? ""}:${targetBasePath}`;
828
1021
  if (existingKeys.has(key)) continue;
829
1022
  existingKeys.add(key);
830
1023
  additions.push({
831
1024
  ...route,
832
- path: route.locale && config.i18n ? localizedRoutePath(filteredBasePath, route.locale, config.i18n) : filteredBasePath,
833
- basePath: filteredBasePath,
1025
+ path: route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath,
1026
+ basePath: targetBasePath,
834
1027
  title: route.title,
835
- virtualModuleId: `${route.virtualModuleId}/${openAPITagsPathSegment(tagFilter)}`,
836
- openapiTagFilter: tagFilter
1028
+ virtualModuleId: virtualModuleIdFromPath(targetBasePath),
1029
+ openapiTagFilter: tagFilter?.length ? tagFilter : void 0
837
1030
  });
838
1031
  }
839
1032
  }
@@ -851,7 +1044,7 @@ function createOpenAPIPlugin() {
851
1044
  }),
852
1045
  "routes:discovered": async (routes, ctx) => {
853
1046
  for (const key of Object.keys(specs)) delete specs[key];
854
- const nextRoutes = createTaggedOpenAPIRoutes(routes, ctx.projectConfig);
1047
+ const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
855
1048
  const specByFilePath = /* @__PURE__ */ new Map();
856
1049
  for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
857
1050
  const specFromCache = specByFilePath.get(route.filePath);
@@ -882,7 +1075,7 @@ function createOpenAPIPlugin() {
882
1075
  return nextRoutes;
883
1076
  },
884
1077
  "modules:before": (modules, ctx) => {
885
- modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(Object.fromEntries(Object.entries(specs).map(([moduleId, entry]) => [moduleId, entry.spec]))));
1078
+ modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(Object.fromEntries(Object.entries(specs).map(([moduleId, entry]) => [moduleId, entry.spec])), modules.get(openApiRegistryModuleId)));
886
1079
  for (const [moduleId, entry] of Object.entries(specs)) modules.set(moduleId, generateOpenAPIModule(entry.spec, entry.tagFilter));
887
1080
  for (const route of ctx.routes.filter((route) => route.kind === "openapi" && route.diagnostic)) if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
888
1081
  return modules;
@@ -891,6 +1084,180 @@ function createOpenAPIPlugin() {
891
1084
  };
892
1085
  }
893
1086
  //#endregion
1087
+ //#region source/core/search-language.ts
1088
+ function toPagefindLanguage(locale) {
1089
+ return locale?.trim().replace(/_/g, "-").split("-").filter(Boolean).join("-").toLowerCase() || "en";
1090
+ }
1091
+ //#endregion
1092
+ //#region source/core/temp-dir.ts
1093
+ function createClarifyTempDir(name) {
1094
+ return (0, node_fs.mkdtempSync)((0, node_path.join)((0, node_os.tmpdir)(), `clarify-${name}-`));
1095
+ }
1096
+ function removeClarifyTempDir(path) {
1097
+ if (!path) return;
1098
+ (0, node_fs.rmSync)(path, {
1099
+ recursive: true,
1100
+ force: true
1101
+ });
1102
+ }
1103
+ //#endregion
1104
+ //#region source/plugins/search-index/index.ts
1105
+ var pagefindContentTypes = {
1106
+ ".css": "text/css; charset=utf-8",
1107
+ ".js": "text/javascript; charset=utf-8",
1108
+ ".json": "application/json; charset=utf-8",
1109
+ ".wasm": "application/wasm"
1110
+ };
1111
+ function assertNoPagefindErrors(action, errors) {
1112
+ if (!errors?.length) return;
1113
+ throw new Error(`[clarify] Pagefind ${action} failed:\n${errors.join("\n")}`);
1114
+ }
1115
+ async function generatePagefindIndex(options) {
1116
+ const pagefind = await import("pagefind");
1117
+ const { errors, index } = await pagefind.createIndex({
1118
+ excludeSelectors: [
1119
+ "[data-pagefind-ignore]",
1120
+ "script",
1121
+ "style",
1122
+ "svg"
1123
+ ],
1124
+ includeCharacters: "._-/#:@",
1125
+ keepIndexUrl: false,
1126
+ verbose: false,
1127
+ writePlayground: false
1128
+ });
1129
+ assertNoPagefindErrors("createIndex", errors);
1130
+ if (!index) throw new Error("[clarify] Pagefind did not create an index.");
1131
+ try {
1132
+ const addResult = await index.addDirectory({
1133
+ path: options.sourceRoot ?? options.outputRoot,
1134
+ glob: "**/*.html"
1135
+ });
1136
+ assertNoPagefindErrors("addDirectory", addResult.errors);
1137
+ assertNoPagefindErrors("writeFiles", (await index.writeFiles({ outputPath: (0, node_path.join)(options.outputRoot, "pagefind") })).errors);
1138
+ return addResult.page_count;
1139
+ } finally {
1140
+ await index.deleteIndex();
1141
+ await pagefind.close();
1142
+ }
1143
+ }
1144
+ function routeSearchContent(route) {
1145
+ return [
1146
+ route.title,
1147
+ route.description,
1148
+ route.keywords?.join(" "),
1149
+ route.sections?.map((section) => section.title).join(" "),
1150
+ route.content
1151
+ ].filter(Boolean).join("\n\n");
1152
+ }
1153
+ async function generateDevSearchIndex(ctx, root) {
1154
+ const pagefind = await import("pagefind");
1155
+ const { errors, index } = await pagefind.createIndex({
1156
+ includeCharacters: "._-/#:@",
1157
+ keepIndexUrl: false,
1158
+ verbose: false,
1159
+ writePlayground: false
1160
+ });
1161
+ assertNoPagefindErrors("createIndex", errors);
1162
+ if (!index) throw new Error("[clarify] Pagefind did not create an index.");
1163
+ try {
1164
+ let pageCount = 0;
1165
+ for (const route of ctx.routes) {
1166
+ const content = routeSearchContent(route);
1167
+ if (!content.trim()) continue;
1168
+ const result = await index.addCustomRecord({
1169
+ url: route.path,
1170
+ content,
1171
+ language: toPagefindLanguage(route.locale ?? ctx.projectConfig.i18n?.defaultLocale),
1172
+ meta: { title: route.title }
1173
+ });
1174
+ assertNoPagefindErrors(`addCustomRecord ${route.path}`, result.errors);
1175
+ pageCount += 1;
1176
+ }
1177
+ assertNoPagefindErrors("writeFiles", (await index.writeFiles({ outputPath: (0, node_path.join)(root, "pagefind") })).errors);
1178
+ console.log(`[clarify] Dev search index generated for ${pageCount} pages.`);
1179
+ } finally {
1180
+ await index.deleteIndex();
1181
+ await pagefind.close();
1182
+ }
1183
+ }
1184
+ function normalizeDevPagefindPath(rawUrl, routePrefix) {
1185
+ const pathname = rawUrl?.split("?")[0];
1186
+ if (!pathname) return void 0;
1187
+ const normalizedPrefix = routePrefix && routePrefix !== "/" ? `/${routePrefix.replace(/^\/+|\/+$/g, "")}` : "";
1188
+ const pagefindPath = normalizedPrefix && pathname.startsWith(`${normalizedPrefix}/pagefind/`) ? pathname.slice(normalizedPrefix.length) : pathname;
1189
+ return pagefindPath.startsWith("/pagefind/") ? pagefindPath : void 0;
1190
+ }
1191
+ function serveDevPagefind(server, getRoot, ctx) {
1192
+ server.middlewares.use((req, res, next) => {
1193
+ const root = getRoot();
1194
+ const pagefindPath = normalizeDevPagefindPath(req.url, ctx.projectConfig.routePrefix);
1195
+ if (!root || !pagefindPath) {
1196
+ next();
1197
+ return;
1198
+ }
1199
+ const filePath = (0, node_path.join)(root, pagefindPath);
1200
+ const relativePath = (0, node_path.relative)(root, filePath);
1201
+ if (relativePath.startsWith("..") || relativePath === "" || !(0, node_fs.existsSync)(filePath)) {
1202
+ next();
1203
+ return;
1204
+ }
1205
+ const contentType = pagefindContentTypes[(0, node_path.extname)(filePath)] ?? "application/octet-stream";
1206
+ res.setHeader("Content-Type", contentType);
1207
+ (0, node_fs.createReadStream)(filePath).pipe(res);
1208
+ });
1209
+ }
1210
+ function createSearchIndexPlugin() {
1211
+ return {
1212
+ name: "clarify:search-index",
1213
+ hooks: {
1214
+ async "dev:configureServer"(server, ctx) {
1215
+ let currentRoot;
1216
+ const staleRoots = /* @__PURE__ */ new Set();
1217
+ const replaceDevIndex = async () => {
1218
+ const nextRoot = createClarifyTempDir("pagefind");
1219
+ await generateDevSearchIndex(ctx, nextRoot);
1220
+ const previousRoot = currentRoot;
1221
+ currentRoot = nextRoot;
1222
+ if (previousRoot) {
1223
+ staleRoots.add(previousRoot);
1224
+ setTimeout(() => {
1225
+ staleRoots.delete(previousRoot);
1226
+ removeClarifyTempDir(previousRoot);
1227
+ }, 3e4).unref?.();
1228
+ }
1229
+ };
1230
+ serveDevPagefind(server, () => currentRoot, ctx);
1231
+ await replaceDevIndex();
1232
+ let regenerateTimer;
1233
+ const regenerate = (filePath) => {
1234
+ if (!/\.(md|mdx|json|ya?ml)$/i.test(filePath)) return;
1235
+ if (regenerateTimer) clearTimeout(regenerateTimer);
1236
+ regenerateTimer = setTimeout(() => {
1237
+ replaceDevIndex().catch((err) => {
1238
+ console.error("[clarify] Failed to regenerate dev search index:", err);
1239
+ });
1240
+ }, 100);
1241
+ };
1242
+ server.watcher.on("add", regenerate);
1243
+ server.watcher.on("change", regenerate);
1244
+ server.watcher.on("unlink", regenerate);
1245
+ server.httpServer?.once("close", () => {
1246
+ if (regenerateTimer) clearTimeout(regenerateTimer);
1247
+ removeClarifyTempDir(currentRoot);
1248
+ for (const staleRoot of staleRoots) removeClarifyTempDir(staleRoot);
1249
+ });
1250
+ },
1251
+ async "build:done"(ctx) {
1252
+ const outputDirectory = ctx.generateOptions.outputDirectory;
1253
+ if (!outputDirectory) return;
1254
+ const pageCount = await generatePagefindIndex({ outputRoot: (0, node_path.resolve)(ctx.generateOptions.projectRoot, outputDirectory) });
1255
+ console.log(`[clarify] Search index generated for ${pageCount} pages.`);
1256
+ }
1257
+ }
1258
+ };
1259
+ }
1260
+ //#endregion
894
1261
  //#region source/plugins/seo/artifacts.ts
895
1262
  function trimTrailingSlash(value) {
896
1263
  return value.replace(/\/+$/, "");
@@ -986,7 +1353,8 @@ function createBuiltinPlugins(options = {}) {
986
1353
  createOpenAPIPlugin(),
987
1354
  createSourceLinksPlugin(),
988
1355
  createContentArtifactsPlugin(),
989
- createSeoPlugin()
1356
+ createSeoPlugin(),
1357
+ createSearchIndexPlugin()
990
1358
  ];
991
1359
  if (options.htmlShell ?? true) plugins.push(createHtmlShellPlugin());
992
1360
  return plugins;
@@ -1044,11 +1412,12 @@ var clarifyBannerConfigSchema = zod.z.object({
1044
1412
  content: clarifyLocalizedTextSchema,
1045
1413
  dismissible: zod.z.boolean().optional()
1046
1414
  });
1047
- var clarifyFooterConfigSchema = zod.z.object({
1415
+ var clarifyFooterLinksConfigSchema = zod.z.object({
1048
1416
  links: zod.z.array(clarifyNavbarLinkSchema).optional(),
1049
1417
  socials: zod.z.record(zod.z.string(), zod.z.string()).optional(),
1050
1418
  copyright: clarifyLocalizedTextSchema.optional()
1051
- });
1419
+ }).strict();
1420
+ var clarifyFooterConfigSchema = zod.z.union([clarifyFooterLinksConfigSchema, zod.z.string()]);
1052
1421
  var clarifySourceConfigSchema = zod.z.object({
1053
1422
  repository: zod.z.string(),
1054
1423
  branch: zod.z.string().optional(),
@@ -1058,12 +1427,14 @@ var clarifyPagesItemSchema = zod.z.union([
1058
1427
  zod.z.string(),
1059
1428
  zod.z.object({
1060
1429
  page: zod.z.string(),
1430
+ path: zod.z.string().optional(),
1061
1431
  title: clarifyLocalizedTextSchema.optional(),
1062
1432
  icon: zod.z.string().optional(),
1063
1433
  redirect: zod.z.string().optional()
1064
1434
  }),
1065
1435
  zod.z.object({
1066
1436
  openapi: zod.z.string(),
1437
+ path: zod.z.string().optional(),
1067
1438
  icon: zod.z.string().optional(),
1068
1439
  title: clarifyLocalizedTextSchema.optional(),
1069
1440
  filter: zod.z.object({ tags: zod.z.array(zod.z.string()).optional() }).optional()
@@ -1130,78 +1501,6 @@ var clarifyProjectConfigSchema = zod.z.object({
1130
1501
  tabs: clarifyTabsConfigSchema.optional()
1131
1502
  });
1132
1503
  //#endregion
1133
- //#region source/core/theme.ts
1134
- var clarifyThemePresets = {
1135
- default: {
1136
- preset: "default",
1137
- tokens: {
1138
- colors: {
1139
- primary: "#047857",
1140
- accent: "#0D9488",
1141
- background: "#ffffff",
1142
- foreground: "#111827",
1143
- surface: "#ffffff",
1144
- muted: "#64748b",
1145
- border: "rgb(15 23 42 / 0.12)",
1146
- codeBackground: "#f6fbf8"
1147
- },
1148
- radius: {
1149
- sm: "6px",
1150
- md: "10px",
1151
- lg: "14px",
1152
- xl: "18px"
1153
- }
1154
- },
1155
- layout: { maxWidth: "82rem" },
1156
- editor: false
1157
- },
1158
- base: {
1159
- preset: "base",
1160
- tokens: {
1161
- colors: {
1162
- primary: "#18181b",
1163
- accent: "#52525b",
1164
- background: "#ffffff",
1165
- foreground: "#18181b",
1166
- surface: "#ffffff",
1167
- muted: "#71717a",
1168
- border: "rgb(24 24 27 / 0.12)",
1169
- codeBackground: "#f4f4f5"
1170
- },
1171
- radius: {
1172
- sm: "6px",
1173
- md: "8px",
1174
- lg: "12px",
1175
- xl: "16px"
1176
- }
1177
- },
1178
- layout: { maxWidth: "80rem" },
1179
- editor: false
1180
- }
1181
- };
1182
- function resolveThemeConfig(theme = {}) {
1183
- const preset = theme.preset ?? "default";
1184
- const presetTheme = clarifyThemePresets[preset];
1185
- return {
1186
- preset,
1187
- tokens: {
1188
- colors: {
1189
- ...presetTheme.tokens.colors,
1190
- ...theme.tokens?.colors
1191
- },
1192
- radius: {
1193
- ...presetTheme.tokens.radius,
1194
- ...theme.tokens?.radius
1195
- }
1196
- },
1197
- layout: {
1198
- ...presetTheme.layout,
1199
- ...theme.layout
1200
- },
1201
- editor: theme.editor ?? presetTheme.editor
1202
- };
1203
- }
1204
- //#endregion
1205
1504
  //#region source/core/config.ts
1206
1505
  function formatIssuePath(path) {
1207
1506
  return path.reduce((result, segment) => {
@@ -1368,6 +1667,7 @@ async function resolveClarifySite(options = {}, resolveOptions = {}) {
1368
1667
  };
1369
1668
  let routes = await discoverRoutes(root, contentRoot, plugins, ctx);
1370
1669
  routes = await runHooks(plugins, "routes:discovered", routes, ctx);
1670
+ routes = applyConfiguredPageRoutePaths(routes, projectConfig.tabs, projectConfig.i18n);
1371
1671
  const defaultNavigation = projectConfig.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes);
1372
1672
  const resolved = await runHooks(plugins, "routes:resolved", {
1373
1673
  routes,
@@ -1494,13 +1794,13 @@ function routeOutputFiles(outputDirectory, route) {
1494
1794
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
1495
1795
  import { routes, navigation } from 'virtual:clarify-routes/server';
1496
1796
  import { config } from 'virtual:clarify-config';
1497
- import { openApis } from 'virtual:clarify-runtime';
1797
+ import { openApis, footerComponent } from 'virtual:clarify-runtime';
1498
1798
 
1499
1799
  export function render(url) {
1500
- return renderToHTML({ config, routes, navigation, openApis, url, themeEditor: config.theme.editor });
1800
+ return renderToHTML({ config, routes, navigation, openApis, footerComponent, url, themeEditor: config.theme.editor });
1501
1801
  }`;
1502
1802
  function createTempEntryFile(content) {
1503
- const entryPath = (0, node_path.join)((0, node_fs.mkdtempSync)((0, node_path.join)((0, node_os.tmpdir)(), "clarify-ssr-")), "entry-server.ts");
1803
+ const entryPath = (0, node_path.join)(createClarifyTempDir("ssr"), "entry-server.ts");
1504
1804
  (0, node_fs.writeFileSync)(entryPath, content, "utf-8");
1505
1805
  return entryPath;
1506
1806
  }
@@ -1540,6 +1840,7 @@ async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundle
1540
1840
  //#endregion
1541
1841
  //#region source/core/user-config.ts
1542
1842
  function defineConfig(config) {
1843
+ validateProjectConfig(config);
1543
1844
  return config;
1544
1845
  }
1545
1846
  var CONFIG_FILENAMES = [
@@ -1802,6 +2103,12 @@ Object.defineProperty(exports, "createClarifyRuntimeAliases", {
1802
2103
  return createClarifyRuntimeAliases;
1803
2104
  }
1804
2105
  });
2106
+ Object.defineProperty(exports, "createClarifyTempDir", {
2107
+ enumerable: true,
2108
+ get: function() {
2109
+ return createClarifyTempDir;
2110
+ }
2111
+ });
1805
2112
  Object.defineProperty(exports, "defineConfig", {
1806
2113
  enumerable: true,
1807
2114
  get: function() {
@@ -1821,4 +2128,4 @@ Object.defineProperty(exports, "resolveClarifySite", {
1821
2128
  }
1822
2129
  });
1823
2130
 
1824
- //# sourceMappingURL=plugin-GTN_SQ7d.cjs.map
2131
+ //# sourceMappingURL=plugin-dzXRzjdX.cjs.map