@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.
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
1
+ import { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import mdxPlugin from "@mdx-js/rollup";
4
4
  import tailwindcss from "@tailwindcss/vite";
@@ -11,9 +11,9 @@ import { markdownRemarkPlugins } from "@clarify-labs/renderer";
11
11
  import { remark } from "remark";
12
12
  import matter from "gray-matter";
13
13
  import SwaggerParser from "@apidevtools/swagger-parser";
14
+ import { tmpdir } from "node:os";
14
15
  import { mkdir, writeFile } from "node:fs/promises";
15
16
  import { z } from "zod";
16
- import { tmpdir } from "node:os";
17
17
  import { pathToFileURL } from "node:url";
18
18
  import { build, loadConfigFromFile } from "vite";
19
19
  import { createRequire } from "node:module";
@@ -232,8 +232,80 @@ function createContentArtifactsPlugin() {
232
232
  };
233
233
  }
234
234
  //#endregion
235
+ //#region source/core/theme.ts
236
+ var clarifyThemePresets = {
237
+ default: {
238
+ preset: "default",
239
+ tokens: {
240
+ colors: {
241
+ primary: "#047857",
242
+ accent: "#0D9488",
243
+ background: "#ffffff",
244
+ foreground: "#111827",
245
+ surface: "#ffffff",
246
+ muted: "#64748b",
247
+ border: "rgb(15 23 42 / 0.12)",
248
+ codeBackground: "#f6fbf8"
249
+ },
250
+ radius: {
251
+ sm: "6px",
252
+ md: "10px",
253
+ lg: "14px",
254
+ xl: "18px"
255
+ }
256
+ },
257
+ layout: { maxWidth: "82rem" },
258
+ editor: false
259
+ },
260
+ base: {
261
+ preset: "base",
262
+ tokens: {
263
+ colors: {
264
+ primary: "#18181b",
265
+ accent: "#52525b",
266
+ background: "#ffffff",
267
+ foreground: "#18181b",
268
+ surface: "#ffffff",
269
+ muted: "#71717a",
270
+ border: "rgb(24 24 27 / 0.12)",
271
+ codeBackground: "#f4f4f5"
272
+ },
273
+ radius: {
274
+ sm: "6px",
275
+ md: "8px",
276
+ lg: "12px",
277
+ xl: "16px"
278
+ }
279
+ },
280
+ layout: { maxWidth: "80rem" },
281
+ editor: false
282
+ }
283
+ };
284
+ function resolveThemeConfig(theme = {}) {
285
+ const preset = theme.preset ?? "default";
286
+ const presetTheme = clarifyThemePresets[preset];
287
+ return {
288
+ preset,
289
+ tokens: {
290
+ colors: {
291
+ ...presetTheme.tokens.colors,
292
+ ...theme.tokens?.colors
293
+ },
294
+ radius: {
295
+ ...presetTheme.tokens.radius,
296
+ ...theme.tokens?.radius
297
+ }
298
+ },
299
+ layout: {
300
+ ...presetTheme.layout,
301
+ ...theme.layout
302
+ },
303
+ editor: theme.editor ?? presetTheme.editor
304
+ };
305
+ }
306
+ //#endregion
235
307
  //#region source/plugins/html-shell/index.ts
236
- 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){}})();`;
308
+ var THEME_STORAGE_KEY = "clarify:theme";
237
309
  var REL_ICON_RE = /<link\s+[^>]*rel=["'][^"']*\bicon\b[^"']*["'][^>]*>/i;
238
310
  function inferIconType(href) {
239
311
  const path = href.split(/[?#]/, 1)[0]?.toLowerCase() ?? href.toLowerCase();
@@ -263,6 +335,62 @@ function createIconTags(icon) {
263
335
  if (lightIcon === darkIcon) return [createIconTag(lightIcon)];
264
336
  return [createIconTag(lightIcon, "(prefers-color-scheme: light)"), createIconTag(darkIcon, "(prefers-color-scheme: dark)")];
265
337
  }
338
+ function matchesPresetTheme(theme) {
339
+ const preset = clarifyThemePresets[theme.preset];
340
+ return JSON.stringify({
341
+ tokens: theme.tokens,
342
+ layout: theme.layout
343
+ }) === JSON.stringify({
344
+ tokens: preset.tokens,
345
+ layout: preset.layout
346
+ });
347
+ }
348
+ function effectiveTheme(theme, resolvedTheme) {
349
+ if (resolvedTheme !== "dark" || !matchesPresetTheme(theme)) return theme;
350
+ return {
351
+ ...theme,
352
+ tokens: {
353
+ ...theme.tokens,
354
+ colors: {
355
+ ...theme.tokens.colors,
356
+ background: "#09090b",
357
+ foreground: "#ffffff",
358
+ surface: "#18181b",
359
+ muted: "#a1a1aa",
360
+ border: "rgb(255 255 255 / 0.1)",
361
+ codeBackground: "#18181b"
362
+ }
363
+ }
364
+ };
365
+ }
366
+ function resolveThemeColorValue(value, resolvedTheme) {
367
+ return typeof value === "string" ? value : value[resolvedTheme] ?? value.light ?? value.dark ?? "";
368
+ }
369
+ function themeToCssVariables(theme, resolvedTheme) {
370
+ const resolved = effectiveTheme(theme, resolvedTheme);
371
+ const { colors } = resolved.tokens;
372
+ const { radius } = resolved.tokens;
373
+ return {
374
+ "--clarify-theme-tokens-colors-primary": resolveThemeColorValue(colors.primary, resolvedTheme),
375
+ "--clarify-theme-tokens-colors-accent": resolveThemeColorValue(colors.accent, resolvedTheme),
376
+ "--clarify-theme-tokens-colors-background": resolveThemeColorValue(colors.background, resolvedTheme),
377
+ "--clarify-theme-tokens-colors-foreground": resolveThemeColorValue(colors.foreground, resolvedTheme),
378
+ "--clarify-theme-tokens-colors-surface": resolveThemeColorValue(colors.surface, resolvedTheme),
379
+ "--clarify-theme-tokens-colors-muted": resolveThemeColorValue(colors.muted, resolvedTheme),
380
+ "--clarify-theme-tokens-colors-border": resolveThemeColorValue(colors.border, resolvedTheme),
381
+ "--clarify-theme-tokens-colors-code-background": resolveThemeColorValue(colors.codeBackground, resolvedTheme),
382
+ "--clarify-theme-tokens-radius-sm": radius.sm,
383
+ "--clarify-theme-tokens-radius-md": radius.md,
384
+ "--clarify-theme-tokens-radius-lg": radius.lg,
385
+ "--clarify-theme-tokens-radius-xl": radius.xl,
386
+ "--clarify-theme-layout-max-width": resolved.layout.maxWidth
387
+ };
388
+ }
389
+ function themeBootstrapScript(theme) {
390
+ const darkVariables = themeToCssVariables(theme, "dark");
391
+ const lightVariables = themeToCssVariables(theme, "light");
392
+ 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){}})();`;
393
+ }
266
394
  function createHtmlShellPlugin() {
267
395
  return {
268
396
  name: "clarify:html-shell",
@@ -273,7 +401,7 @@ function createHtmlShellPlugin() {
273
401
  ...REL_ICON_RE.test(input.html) ? [] : createIconTags(ctx.projectConfig.favicon ?? ctx.projectConfig.logo),
274
402
  {
275
403
  tag: "script",
276
- children: THEME_BOOTSTRAP_SCRIPT,
404
+ children: themeBootstrapScript(ctx.projectConfig.theme),
277
405
  injectTo: "head-prepend"
278
406
  },
279
407
  {
@@ -356,13 +484,14 @@ function basePathFromRef(ref) {
356
484
  const withoutExtension = ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "");
357
485
  return normalizePath(withoutExtension === "index" ? "/" : withoutExtension.replace(/\/index$/, ""));
358
486
  }
359
- function openAPIPagePathFromRef(ref, tagFilter) {
487
+ function openAPIPagePathFromRef(ref, tagFilter, explicitPath) {
488
+ if (explicitPath) return normalizePath(explicitPath);
360
489
  const basePath = basePathFromRef(ref);
361
490
  if (!tagFilter?.length) return basePath;
362
491
  return normalizePath(`${basePath}/${openAPITagsPathSegment(tagFilter)}`);
363
492
  }
364
- function routePathFromRef(ref) {
365
- return basePathFromRef(ref);
493
+ function routePathFromRef(ref, explicitPath) {
494
+ return explicitPath ? normalizePath(explicitPath) : basePathFromRef(ref);
366
495
  }
367
496
  function virtualModuleIdFromRef(ref) {
368
497
  return "virtual:clarify-page/" + ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
@@ -416,6 +545,18 @@ function frontmatterKeywords(frontmatter) {
416
545
  const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
417
546
  return keywords.length > 0 ? keywords : void 0;
418
547
  }
548
+ function withAlternates$2(route, routes, i18n) {
549
+ const basePath = route.basePath ?? route.path;
550
+ const routeByLocaleAndBase = new Map(routes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
551
+ const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
552
+ const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
553
+ return alternate ? [[locale.code, alternate.path]] : [];
554
+ }));
555
+ return {
556
+ ...route,
557
+ alternates
558
+ };
559
+ }
419
560
  function buildNavigation(routes) {
420
561
  const root = [];
421
562
  for (const route of routes) {
@@ -447,11 +588,13 @@ function resolvePageItem(item) {
447
588
  if ("openapi" in item) return {
448
589
  openapiRef: item.openapi,
449
590
  openapiTagFilter: item.filter?.tags,
591
+ path: item.path,
450
592
  title: item.title,
451
593
  icon: item.icon
452
594
  };
453
595
  return {
454
596
  pageRef: item.page,
597
+ path: item.path,
455
598
  redirect: item.redirect,
456
599
  title: item.title,
457
600
  icon: item.icon
@@ -465,6 +608,41 @@ function firstNavigationPath(nodes) {
465
608
  }
466
609
  return "/";
467
610
  }
611
+ function collectExplicitPagePathItems(tabs) {
612
+ const items = [];
613
+ for (const tab of tabs ?? []) {
614
+ if (!tab.pages || tab.pages === "FileTree") continue;
615
+ for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) items.push({
616
+ pageRef: item.page,
617
+ path: item.path
618
+ });
619
+ }
620
+ return items;
621
+ }
622
+ function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
623
+ const pageItems = collectExplicitPagePathItems(tabs);
624
+ if (!pageItems.length) return routes;
625
+ const additions = [];
626
+ const existingKeys = new Set(routes.map((route) => `${route.locale ?? ""}:${route.basePath ?? route.path}`));
627
+ for (const item of pageItems) {
628
+ const sourceBasePath = routePathFromRef(item.pageRef);
629
+ const targetBasePath = routePathFromRef(item.pageRef, item.path);
630
+ if (sourceBasePath === targetBasePath) continue;
631
+ for (const route of routes) {
632
+ if (route.kind !== "mdx" || (route.basePath ?? route.path) !== sourceBasePath) continue;
633
+ const key = `${route.locale ?? ""}:${targetBasePath}`;
634
+ if (existingKeys.has(key)) continue;
635
+ existingKeys.add(key);
636
+ additions.push({
637
+ ...route,
638
+ path: route.locale && i18n ? localizedRoutePath(targetBasePath, route.locale, i18n) : targetBasePath,
639
+ basePath: targetBasePath
640
+ });
641
+ }
642
+ }
643
+ const nextRoutes = [...routes, ...additions];
644
+ return i18n ? nextRoutes.map((route) => withAlternates$2(route, nextRoutes, i18n)) : nextRoutes;
645
+ }
468
646
  function buildNavigationFromPagesConfig(routes, config) {
469
647
  if (!config || config === "FileTree") return buildNavigation(routes);
470
648
  return buildNavigationFromConfig(routes, config);
@@ -485,9 +663,9 @@ function buildNavigationFromConfig(routes, config) {
485
663
  const routeMap = new Map(routes.map((r) => [r.path, r]));
486
664
  return config.map((group) => {
487
665
  const children = group.pages.map((item) => {
488
- const { pageRef, openapiRef, openapiTagFilter, redirect, title, icon } = resolvePageItem(item);
666
+ const { pageRef, openapiRef, openapiTagFilter, path: explicitPath, redirect, title, icon } = resolvePageItem(item);
489
667
  if (openapiRef) {
490
- const path = openAPIPagePathFromRef(openapiRef, openapiTagFilter);
668
+ const path = openAPIPagePathFromRef(openapiRef, openapiTagFilter, explicitPath);
491
669
  const route = routeMap.get(path);
492
670
  return {
493
671
  path,
@@ -497,10 +675,10 @@ function buildNavigationFromConfig(routes, config) {
497
675
  };
498
676
  }
499
677
  const ref = pageRef ?? "";
500
- const path = ref === "index" ? "/" : "/" + ref;
678
+ const path = routePathFromRef(ref, explicitPath);
501
679
  const route = routeMap.get(path);
502
680
  return {
503
- path: redirect ? redirect : path,
681
+ path: redirect ? normalizePath(redirect) : path,
504
682
  title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle$1(path.split("/").pop() ?? ref),
505
683
  icon,
506
684
  sections: route?.sections ? navigationSections(route.sections) : void 0
@@ -529,13 +707,13 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
529
707
  const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
530
708
  return config.map((group) => {
531
709
  const children = group.pages.map((item) => {
532
- const { pageRef, openapiRef, openapiTagFilter, redirect, title, icon } = resolvePageItem(item);
710
+ const { pageRef, openapiRef, openapiTagFilter, path: explicitPath, redirect, title, icon } = resolvePageItem(item);
533
711
  const ref = openapiRef ?? pageRef ?? "";
534
- const basePath = openapiRef ? openAPIPagePathFromRef(openapiRef, openapiTagFilter) : basePathFromRef(ref);
712
+ const basePath = openapiRef ? openAPIPagePathFromRef(openapiRef, openapiTagFilter, explicitPath) : routePathFromRef(ref, explicitPath);
535
713
  const route = routeMap.get(basePath);
536
714
  const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
537
715
  return {
538
- path: (redirect ? localizedRoutePath(basePathFromRef(redirect), locale, i18n) : void 0) ?? path,
716
+ path: (redirect ? localizedRoutePath(routePathFromRef(redirect), locale, i18n) : void 0) ?? path,
539
717
  title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle$1(basePath.split("/").pop() ?? ref),
540
718
  icon,
541
719
  sections: route?.sections ? navigationSections(route.sections) : void 0
@@ -700,8 +878,10 @@ function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode =
700
878
  const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
701
879
  return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
702
880
  }
703
- function createRuntimeModule() {
704
- return "export const openApis = {};";
881
+ function createRuntimeModule(options = {}) {
882
+ return `${options.footerComponentSource === "path" && options.imports?.footerComponent ? `import FooterComponent from ${moduleSpecifier(options.imports.footerComponent)};` : ""}
883
+ export const openApis = {};
884
+ export const footerComponent = ${options.footerComponentSource === "path" ? "FooterComponent" : "undefined"};`;
705
885
  }
706
886
  function createClientEntryModule(options = {}) {
707
887
  return `
@@ -709,16 +889,22 @@ import '@clarify-labs/renderer/style.css';
709
889
  import { render } from '@clarify-labs/renderer/client';
710
890
  import { routes, navigation } from '${VIRTUAL_ROUTES}';
711
891
  import { config } from '${VIRTUAL_CONFIG}';
712
- import { openApis } from '${VIRTUAL_RUNTIME}';
713
- render({ config, routes, navigation, openApis, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
892
+ import { openApis, footerComponent } from '${VIRTUAL_RUNTIME}';
893
+ render({ config, routes, navigation, openApis, footerComponent, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
714
894
  }
715
895
  function buildVirtualModules(args) {
716
896
  const modules = /* @__PURE__ */ new Map();
717
897
  const clientEntryModule = createClientEntryModule({ themeEditor: args.themeEditor });
898
+ const footerComponent = args.projectConfig.footer;
899
+ const footerComponentSource = typeof footerComponent === "string" ? "path" : void 0;
900
+ const footerComponentImport = typeof footerComponent === "string" ? footerComponent : void 0;
718
901
  modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
719
902
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
720
903
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "server"));
721
- modules.set(VIRTUAL_RUNTIME, createRuntimeModule());
904
+ modules.set(VIRTUAL_RUNTIME, createRuntimeModule({
905
+ footerComponentSource,
906
+ imports: { footerComponent: footerComponentImport }
907
+ }));
722
908
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
723
909
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
724
910
  for (const route of args.routes) modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
@@ -727,8 +913,12 @@ function buildVirtualModules(args) {
727
913
  //#endregion
728
914
  //#region source/plugins/openapi/virtual-modules.ts
729
915
  var openApiRegistryModuleId = VIRTUAL_RUNTIME;
730
- function generateOpenAPIRegistryModule(openApis) {
731
- return `export const openApis = ${JSON.stringify(openApis)};`;
916
+ function generateOpenAPIRegistryModule(openApis, runtimeModule = "") {
917
+ const openApisExport = `export const openApis = ${JSON.stringify(openApis)};`;
918
+ const openApisPlaceholder = /export const openApis = \{\};?/;
919
+ if (!runtimeModule) return openApisExport;
920
+ if (openApisPlaceholder.test(runtimeModule)) return runtimeModule.replace(openApisPlaceholder, openApisExport);
921
+ return runtimeModule;
732
922
  }
733
923
  function generateOpenAPIModule(spec, tagFilter) {
734
924
  return `import { createElement } from 'react';
@@ -766,7 +956,7 @@ function collectOpenAPIPageItems(config) {
766
956
  const items = [];
767
957
  function visitPages(pages) {
768
958
  if (!pages || pages === "FileTree") return;
769
- 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);
959
+ 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);
770
960
  }
771
961
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
772
962
  return items;
@@ -783,28 +973,31 @@ function withAlternates$1(route, routes, i18n) {
783
973
  alternates
784
974
  };
785
975
  }
786
- function createTaggedOpenAPIRoutes(routes, config) {
976
+ function virtualModuleIdFromPath(path) {
977
+ return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
978
+ }
979
+ function createConfiguredOpenAPIRoutes(routes, config) {
787
980
  const pageItems = collectOpenAPIPageItems(config);
788
981
  if (!pageItems.length) return routes;
789
982
  const additions = [];
790
983
  const existingKeys = new Set(routes.map((route) => `${route.locale ?? ""}:${route.basePath ?? route.path}`));
791
984
  for (const item of pageItems) {
792
985
  const tagFilter = item.filter?.tags?.filter(Boolean);
793
- if (!tagFilter?.length) continue;
794
986
  const sourceBasePath = openAPIPagePathFromRef(item.openapi);
795
- const filteredBasePath = openAPIPagePathFromRef(item.openapi, tagFilter);
987
+ const targetBasePath = openAPIPagePathFromRef(item.openapi, tagFilter, item.path);
988
+ if (sourceBasePath === targetBasePath && !tagFilter?.length) continue;
796
989
  for (const route of routes) {
797
990
  if (route.kind !== "openapi" || (route.basePath ?? route.path) !== sourceBasePath) continue;
798
- const key = `${route.locale ?? ""}:${filteredBasePath}`;
991
+ const key = `${route.locale ?? ""}:${targetBasePath}`;
799
992
  if (existingKeys.has(key)) continue;
800
993
  existingKeys.add(key);
801
994
  additions.push({
802
995
  ...route,
803
- path: route.locale && config.i18n ? localizedRoutePath(filteredBasePath, route.locale, config.i18n) : filteredBasePath,
804
- basePath: filteredBasePath,
996
+ path: route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath,
997
+ basePath: targetBasePath,
805
998
  title: route.title,
806
- virtualModuleId: `${route.virtualModuleId}/${openAPITagsPathSegment(tagFilter)}`,
807
- openapiTagFilter: tagFilter
999
+ virtualModuleId: virtualModuleIdFromPath(targetBasePath),
1000
+ openapiTagFilter: tagFilter?.length ? tagFilter : void 0
808
1001
  });
809
1002
  }
810
1003
  }
@@ -822,7 +1015,7 @@ function createOpenAPIPlugin() {
822
1015
  }),
823
1016
  "routes:discovered": async (routes, ctx) => {
824
1017
  for (const key of Object.keys(specs)) delete specs[key];
825
- const nextRoutes = createTaggedOpenAPIRoutes(routes, ctx.projectConfig);
1018
+ const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
826
1019
  const specByFilePath = /* @__PURE__ */ new Map();
827
1020
  for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
828
1021
  const specFromCache = specByFilePath.get(route.filePath);
@@ -853,7 +1046,7 @@ function createOpenAPIPlugin() {
853
1046
  return nextRoutes;
854
1047
  },
855
1048
  "modules:before": (modules, ctx) => {
856
- modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(Object.fromEntries(Object.entries(specs).map(([moduleId, entry]) => [moduleId, entry.spec]))));
1049
+ modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(Object.fromEntries(Object.entries(specs).map(([moduleId, entry]) => [moduleId, entry.spec])), modules.get(openApiRegistryModuleId)));
857
1050
  for (const [moduleId, entry] of Object.entries(specs)) modules.set(moduleId, generateOpenAPIModule(entry.spec, entry.tagFilter));
858
1051
  for (const route of ctx.routes.filter((route) => route.kind === "openapi" && route.diagnostic)) if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
859
1052
  return modules;
@@ -862,6 +1055,180 @@ function createOpenAPIPlugin() {
862
1055
  };
863
1056
  }
864
1057
  //#endregion
1058
+ //#region source/core/search-language.ts
1059
+ function toPagefindLanguage(locale) {
1060
+ return locale?.trim().replace(/_/g, "-").split("-").filter(Boolean).join("-").toLowerCase() || "en";
1061
+ }
1062
+ //#endregion
1063
+ //#region source/core/temp-dir.ts
1064
+ function createClarifyTempDir(name) {
1065
+ return mkdtempSync(join(tmpdir(), `clarify-${name}-`));
1066
+ }
1067
+ function removeClarifyTempDir(path) {
1068
+ if (!path) return;
1069
+ rmSync(path, {
1070
+ recursive: true,
1071
+ force: true
1072
+ });
1073
+ }
1074
+ //#endregion
1075
+ //#region source/plugins/search-index/index.ts
1076
+ var pagefindContentTypes = {
1077
+ ".css": "text/css; charset=utf-8",
1078
+ ".js": "text/javascript; charset=utf-8",
1079
+ ".json": "application/json; charset=utf-8",
1080
+ ".wasm": "application/wasm"
1081
+ };
1082
+ function assertNoPagefindErrors(action, errors) {
1083
+ if (!errors?.length) return;
1084
+ throw new Error(`[clarify] Pagefind ${action} failed:\n${errors.join("\n")}`);
1085
+ }
1086
+ async function generatePagefindIndex(options) {
1087
+ const pagefind = await import("pagefind");
1088
+ const { errors, index } = await pagefind.createIndex({
1089
+ excludeSelectors: [
1090
+ "[data-pagefind-ignore]",
1091
+ "script",
1092
+ "style",
1093
+ "svg"
1094
+ ],
1095
+ includeCharacters: "._-/#:@",
1096
+ keepIndexUrl: false,
1097
+ verbose: false,
1098
+ writePlayground: false
1099
+ });
1100
+ assertNoPagefindErrors("createIndex", errors);
1101
+ if (!index) throw new Error("[clarify] Pagefind did not create an index.");
1102
+ try {
1103
+ const addResult = await index.addDirectory({
1104
+ path: options.sourceRoot ?? options.outputRoot,
1105
+ glob: "**/*.html"
1106
+ });
1107
+ assertNoPagefindErrors("addDirectory", addResult.errors);
1108
+ assertNoPagefindErrors("writeFiles", (await index.writeFiles({ outputPath: join(options.outputRoot, "pagefind") })).errors);
1109
+ return addResult.page_count;
1110
+ } finally {
1111
+ await index.deleteIndex();
1112
+ await pagefind.close();
1113
+ }
1114
+ }
1115
+ function routeSearchContent(route) {
1116
+ return [
1117
+ route.title,
1118
+ route.description,
1119
+ route.keywords?.join(" "),
1120
+ route.sections?.map((section) => section.title).join(" "),
1121
+ route.content
1122
+ ].filter(Boolean).join("\n\n");
1123
+ }
1124
+ async function generateDevSearchIndex(ctx, root) {
1125
+ const pagefind = await import("pagefind");
1126
+ const { errors, index } = await pagefind.createIndex({
1127
+ includeCharacters: "._-/#:@",
1128
+ keepIndexUrl: false,
1129
+ verbose: false,
1130
+ writePlayground: false
1131
+ });
1132
+ assertNoPagefindErrors("createIndex", errors);
1133
+ if (!index) throw new Error("[clarify] Pagefind did not create an index.");
1134
+ try {
1135
+ let pageCount = 0;
1136
+ for (const route of ctx.routes) {
1137
+ const content = routeSearchContent(route);
1138
+ if (!content.trim()) continue;
1139
+ const result = await index.addCustomRecord({
1140
+ url: route.path,
1141
+ content,
1142
+ language: toPagefindLanguage(route.locale ?? ctx.projectConfig.i18n?.defaultLocale),
1143
+ meta: { title: route.title }
1144
+ });
1145
+ assertNoPagefindErrors(`addCustomRecord ${route.path}`, result.errors);
1146
+ pageCount += 1;
1147
+ }
1148
+ assertNoPagefindErrors("writeFiles", (await index.writeFiles({ outputPath: join(root, "pagefind") })).errors);
1149
+ console.log(`[clarify] Dev search index generated for ${pageCount} pages.`);
1150
+ } finally {
1151
+ await index.deleteIndex();
1152
+ await pagefind.close();
1153
+ }
1154
+ }
1155
+ function normalizeDevPagefindPath(rawUrl, routePrefix) {
1156
+ const pathname = rawUrl?.split("?")[0];
1157
+ if (!pathname) return void 0;
1158
+ const normalizedPrefix = routePrefix && routePrefix !== "/" ? `/${routePrefix.replace(/^\/+|\/+$/g, "")}` : "";
1159
+ const pagefindPath = normalizedPrefix && pathname.startsWith(`${normalizedPrefix}/pagefind/`) ? pathname.slice(normalizedPrefix.length) : pathname;
1160
+ return pagefindPath.startsWith("/pagefind/") ? pagefindPath : void 0;
1161
+ }
1162
+ function serveDevPagefind(server, getRoot, ctx) {
1163
+ server.middlewares.use((req, res, next) => {
1164
+ const root = getRoot();
1165
+ const pagefindPath = normalizeDevPagefindPath(req.url, ctx.projectConfig.routePrefix);
1166
+ if (!root || !pagefindPath) {
1167
+ next();
1168
+ return;
1169
+ }
1170
+ const filePath = join(root, pagefindPath);
1171
+ const relativePath = relative(root, filePath);
1172
+ if (relativePath.startsWith("..") || relativePath === "" || !existsSync(filePath)) {
1173
+ next();
1174
+ return;
1175
+ }
1176
+ const contentType = pagefindContentTypes[extname(filePath)] ?? "application/octet-stream";
1177
+ res.setHeader("Content-Type", contentType);
1178
+ createReadStream(filePath).pipe(res);
1179
+ });
1180
+ }
1181
+ function createSearchIndexPlugin() {
1182
+ return {
1183
+ name: "clarify:search-index",
1184
+ hooks: {
1185
+ async "dev:configureServer"(server, ctx) {
1186
+ let currentRoot;
1187
+ const staleRoots = /* @__PURE__ */ new Set();
1188
+ const replaceDevIndex = async () => {
1189
+ const nextRoot = createClarifyTempDir("pagefind");
1190
+ await generateDevSearchIndex(ctx, nextRoot);
1191
+ const previousRoot = currentRoot;
1192
+ currentRoot = nextRoot;
1193
+ if (previousRoot) {
1194
+ staleRoots.add(previousRoot);
1195
+ setTimeout(() => {
1196
+ staleRoots.delete(previousRoot);
1197
+ removeClarifyTempDir(previousRoot);
1198
+ }, 3e4).unref?.();
1199
+ }
1200
+ };
1201
+ serveDevPagefind(server, () => currentRoot, ctx);
1202
+ await replaceDevIndex();
1203
+ let regenerateTimer;
1204
+ const regenerate = (filePath) => {
1205
+ if (!/\.(md|mdx|json|ya?ml)$/i.test(filePath)) return;
1206
+ if (regenerateTimer) clearTimeout(regenerateTimer);
1207
+ regenerateTimer = setTimeout(() => {
1208
+ replaceDevIndex().catch((err) => {
1209
+ console.error("[clarify] Failed to regenerate dev search index:", err);
1210
+ });
1211
+ }, 100);
1212
+ };
1213
+ server.watcher.on("add", regenerate);
1214
+ server.watcher.on("change", regenerate);
1215
+ server.watcher.on("unlink", regenerate);
1216
+ server.httpServer?.once("close", () => {
1217
+ if (regenerateTimer) clearTimeout(regenerateTimer);
1218
+ removeClarifyTempDir(currentRoot);
1219
+ for (const staleRoot of staleRoots) removeClarifyTempDir(staleRoot);
1220
+ });
1221
+ },
1222
+ async "build:done"(ctx) {
1223
+ const outputDirectory = ctx.generateOptions.outputDirectory;
1224
+ if (!outputDirectory) return;
1225
+ const pageCount = await generatePagefindIndex({ outputRoot: resolve(ctx.generateOptions.projectRoot, outputDirectory) });
1226
+ console.log(`[clarify] Search index generated for ${pageCount} pages.`);
1227
+ }
1228
+ }
1229
+ };
1230
+ }
1231
+ //#endregion
865
1232
  //#region source/plugins/seo/artifacts.ts
866
1233
  function trimTrailingSlash(value) {
867
1234
  return value.replace(/\/+$/, "");
@@ -957,7 +1324,8 @@ function createBuiltinPlugins(options = {}) {
957
1324
  createOpenAPIPlugin(),
958
1325
  createSourceLinksPlugin(),
959
1326
  createContentArtifactsPlugin(),
960
- createSeoPlugin()
1327
+ createSeoPlugin(),
1328
+ createSearchIndexPlugin()
961
1329
  ];
962
1330
  if (options.htmlShell ?? true) plugins.push(createHtmlShellPlugin());
963
1331
  return plugins;
@@ -1015,11 +1383,12 @@ var clarifyBannerConfigSchema = z.object({
1015
1383
  content: clarifyLocalizedTextSchema,
1016
1384
  dismissible: z.boolean().optional()
1017
1385
  });
1018
- var clarifyFooterConfigSchema = z.object({
1386
+ var clarifyFooterLinksConfigSchema = z.object({
1019
1387
  links: z.array(clarifyNavbarLinkSchema).optional(),
1020
1388
  socials: z.record(z.string(), z.string()).optional(),
1021
1389
  copyright: clarifyLocalizedTextSchema.optional()
1022
- });
1390
+ }).strict();
1391
+ var clarifyFooterConfigSchema = z.union([clarifyFooterLinksConfigSchema, z.string()]);
1023
1392
  var clarifySourceConfigSchema = z.object({
1024
1393
  repository: z.string(),
1025
1394
  branch: z.string().optional(),
@@ -1029,12 +1398,14 @@ var clarifyPagesItemSchema = z.union([
1029
1398
  z.string(),
1030
1399
  z.object({
1031
1400
  page: z.string(),
1401
+ path: z.string().optional(),
1032
1402
  title: clarifyLocalizedTextSchema.optional(),
1033
1403
  icon: z.string().optional(),
1034
1404
  redirect: z.string().optional()
1035
1405
  }),
1036
1406
  z.object({
1037
1407
  openapi: z.string(),
1408
+ path: z.string().optional(),
1038
1409
  icon: z.string().optional(),
1039
1410
  title: clarifyLocalizedTextSchema.optional(),
1040
1411
  filter: z.object({ tags: z.array(z.string()).optional() }).optional()
@@ -1101,78 +1472,6 @@ var clarifyProjectConfigSchema = z.object({
1101
1472
  tabs: clarifyTabsConfigSchema.optional()
1102
1473
  });
1103
1474
  //#endregion
1104
- //#region source/core/theme.ts
1105
- var clarifyThemePresets = {
1106
- default: {
1107
- preset: "default",
1108
- tokens: {
1109
- colors: {
1110
- primary: "#047857",
1111
- accent: "#0D9488",
1112
- background: "#ffffff",
1113
- foreground: "#111827",
1114
- surface: "#ffffff",
1115
- muted: "#64748b",
1116
- border: "rgb(15 23 42 / 0.12)",
1117
- codeBackground: "#f6fbf8"
1118
- },
1119
- radius: {
1120
- sm: "6px",
1121
- md: "10px",
1122
- lg: "14px",
1123
- xl: "18px"
1124
- }
1125
- },
1126
- layout: { maxWidth: "82rem" },
1127
- editor: false
1128
- },
1129
- base: {
1130
- preset: "base",
1131
- tokens: {
1132
- colors: {
1133
- primary: "#18181b",
1134
- accent: "#52525b",
1135
- background: "#ffffff",
1136
- foreground: "#18181b",
1137
- surface: "#ffffff",
1138
- muted: "#71717a",
1139
- border: "rgb(24 24 27 / 0.12)",
1140
- codeBackground: "#f4f4f5"
1141
- },
1142
- radius: {
1143
- sm: "6px",
1144
- md: "8px",
1145
- lg: "12px",
1146
- xl: "16px"
1147
- }
1148
- },
1149
- layout: { maxWidth: "80rem" },
1150
- editor: false
1151
- }
1152
- };
1153
- function resolveThemeConfig(theme = {}) {
1154
- const preset = theme.preset ?? "default";
1155
- const presetTheme = clarifyThemePresets[preset];
1156
- return {
1157
- preset,
1158
- tokens: {
1159
- colors: {
1160
- ...presetTheme.tokens.colors,
1161
- ...theme.tokens?.colors
1162
- },
1163
- radius: {
1164
- ...presetTheme.tokens.radius,
1165
- ...theme.tokens?.radius
1166
- }
1167
- },
1168
- layout: {
1169
- ...presetTheme.layout,
1170
- ...theme.layout
1171
- },
1172
- editor: theme.editor ?? presetTheme.editor
1173
- };
1174
- }
1175
- //#endregion
1176
1475
  //#region source/core/config.ts
1177
1476
  function formatIssuePath(path) {
1178
1477
  return path.reduce((result, segment) => {
@@ -1339,6 +1638,7 @@ async function resolveClarifySite(options = {}, resolveOptions = {}) {
1339
1638
  };
1340
1639
  let routes = await discoverRoutes(root, contentRoot, plugins, ctx);
1341
1640
  routes = await runHooks(plugins, "routes:discovered", routes, ctx);
1641
+ routes = applyConfiguredPageRoutePaths(routes, projectConfig.tabs, projectConfig.i18n);
1342
1642
  const defaultNavigation = projectConfig.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes);
1343
1643
  const resolved = await runHooks(plugins, "routes:resolved", {
1344
1644
  routes,
@@ -1465,13 +1765,13 @@ function routeOutputFiles(outputDirectory, route) {
1465
1765
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
1466
1766
  import { routes, navigation } from 'virtual:clarify-routes/server';
1467
1767
  import { config } from 'virtual:clarify-config';
1468
- import { openApis } from 'virtual:clarify-runtime';
1768
+ import { openApis, footerComponent } from 'virtual:clarify-runtime';
1469
1769
 
1470
1770
  export function render(url) {
1471
- return renderToHTML({ config, routes, navigation, openApis, url, themeEditor: config.theme.editor });
1771
+ return renderToHTML({ config, routes, navigation, openApis, footerComponent, url, themeEditor: config.theme.editor });
1472
1772
  }`;
1473
1773
  function createTempEntryFile(content) {
1474
- const entryPath = join(mkdtempSync(join(tmpdir(), "clarify-ssr-")), "entry-server.ts");
1774
+ const entryPath = join(createClarifyTempDir("ssr"), "entry-server.ts");
1475
1775
  writeFileSync(entryPath, content, "utf-8");
1476
1776
  return entryPath;
1477
1777
  }
@@ -1511,6 +1811,7 @@ async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundle
1511
1811
  //#endregion
1512
1812
  //#region source/core/user-config.ts
1513
1813
  function defineConfig(config) {
1814
+ validateProjectConfig(config);
1514
1815
  return config;
1515
1816
  }
1516
1817
  var CONFIG_FILENAMES = [
@@ -1761,6 +2062,6 @@ function clarifyPlugin(options = {}) {
1761
2062
  ].flat().filter(Boolean);
1762
2063
  }
1763
2064
  //#endregion
1764
- export { resolveClarifySite as a, createClarifyRuntimeAliases as i, defineConfig as n, loadClarifyConfig as r, clarifyPlugin as t };
2065
+ export { resolveClarifySite as a, createClarifyRuntimeAliases as i, defineConfig as n, createClarifyTempDir as o, loadClarifyConfig as r, clarifyPlugin as t };
1765
2066
 
1766
- //# sourceMappingURL=plugin-DC2f5YAH.js.map
2067
+ //# sourceMappingURL=plugin-BNzlJ4o7.js.map