@clarify-labs/cli 0.2.0 → 0.5.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.
@@ -31,11 +31,10 @@ _vitejs_plugin_react = __toESM(_vitejs_plugin_react, 1);
31
31
  let github_slugger = require("github-slugger");
32
32
  github_slugger = __toESM(github_slugger, 1);
33
33
  let mdast_util_to_string = require("mdast-util-to-string");
34
- let remark_gfm = require("remark-gfm");
35
- remark_gfm = __toESM(remark_gfm, 1);
36
34
  let shiki = require("shiki");
37
35
  shiki = __toESM(shiki, 1);
38
36
  let unist_util_visit = require("unist-util-visit");
37
+ let _clarify_labs_renderer = require("@clarify-labs/renderer");
39
38
  let remark = require("remark");
40
39
  let gray_matter = require("gray-matter");
41
40
  gray_matter = __toESM(gray_matter, 1);
@@ -123,7 +122,7 @@ function rehypeSlugSections() {
123
122
  });
124
123
  };
125
124
  }
126
- var remarkPlugins = [remark_gfm.default];
125
+ var remarkPlugins = _clarify_labs_renderer.markdownRemarkPlugins;
127
126
  var rehypePlugins = [
128
127
  rehypeSlugSections,
129
128
  rehypeParseCodeBlocks,
@@ -132,11 +131,14 @@ var rehypePlugins = [
132
131
  //#endregion
133
132
  //#region source/parsers/frontmatter.ts
134
133
  /**
135
- * 从 MDX 文件内容中提取 frontmatter。
136
- * 使用 gray-matter 支持 YAML frontmatter 的标准语法和类型解析。
134
+ * 在内容入口统一解析 MDX frontmatter,并返回可供后续流程复用的标准化正文。
137
135
  */
138
- function extractFrontmatter(content) {
139
- return (0, gray_matter.default)(content).data;
136
+ function parseFrontmatter(content) {
137
+ const parsed = (0, gray_matter.default)(content);
138
+ return {
139
+ frontmatter: parsed.data,
140
+ content: parsed.content.replace(/^\r?\n/, "")
141
+ };
140
142
  }
141
143
  //#endregion
142
144
  //#region source/parsers/routes.ts
@@ -213,8 +215,7 @@ function findContentRoutes(dir, base = dir) {
213
215
  const relativePath = (0, node_path.relative)(base, fullPath);
214
216
  const pathParts = relativePath.replace(/\.mdx?$/, "").split("/");
215
217
  const cleanPath = ("/" + pathParts.map((p) => p === "index" ? "" : p).filter(Boolean).join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
216
- const content = (0, node_fs.readFileSync)(fullPath, "utf-8");
217
- const frontmatter = extractFrontmatter(content);
218
+ const { frontmatter, content } = parseFrontmatter((0, node_fs.readFileSync)(fullPath, "utf-8"));
218
219
  let title = typeof frontmatter.title === "string" ? frontmatter.title : "";
219
220
  if (!title) {
220
221
  const lastPart = pathParts[pathParts.length - 1] ?? "";
@@ -226,13 +227,22 @@ function findContentRoutes(dir, base = dir) {
226
227
  filePath: fullPath,
227
228
  virtualModuleId: "virtual:clarify-page/" + relativePath.replace(/\.mdx?$/, "").replace(/\/+/g, "/"),
228
229
  title,
230
+ description: typeof frontmatter.description === "string" ? frontmatter.description : void 0,
231
+ keywords: frontmatterKeywords(frontmatter),
229
232
  kind: "mdx",
233
+ frontmatter,
234
+ content,
230
235
  sections: extractMdxSections(content)
231
236
  });
232
237
  }
233
238
  }
234
239
  return routes;
235
240
  }
241
+ function frontmatterKeywords(frontmatter) {
242
+ const value = frontmatter.keywords ?? frontmatter.keyword ?? frontmatter.tags;
243
+ const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
244
+ return keywords.length > 0 ? keywords : void 0;
245
+ }
236
246
  function buildNavigation(routes) {
237
247
  const root = [];
238
248
  for (const route of routes) {
@@ -273,6 +283,30 @@ function resolvePageItem(item) {
273
283
  icon: item.icon
274
284
  };
275
285
  }
286
+ function firstNavigationPath(nodes) {
287
+ for (const node of nodes) {
288
+ if (node.path) return node.path;
289
+ const childPath = node.children ? firstNavigationPath(node.children) : void 0;
290
+ if (childPath) return childPath;
291
+ }
292
+ return "/";
293
+ }
294
+ function buildNavigationFromPagesConfig(routes, config) {
295
+ if (!config || config === "FileTree") return buildNavigation(routes);
296
+ return buildNavigationFromConfig(routes, config);
297
+ }
298
+ function buildNavigationFromTabsConfig(routes, tabs) {
299
+ return { tabs: tabs.map((tab) => {
300
+ const children = buildNavigationFromPagesConfig(routes, tab.pages);
301
+ return {
302
+ type: "tab",
303
+ path: firstNavigationPath(children),
304
+ title: resolveLocalizedText(tab.tab, "", "") ?? "",
305
+ icon: tab.icon,
306
+ children
307
+ };
308
+ }) };
309
+ }
276
310
  function buildNavigationFromConfig(routes, config) {
277
311
  const routeMap = new Map(routes.map((r) => [r.path, r]));
278
312
  return config.map((group) => {
@@ -313,69 +347,94 @@ function localizeNavigationPaths(nodes, locale, i18n) {
313
347
  children: node.children ? localizeNavigationPaths(node.children, locale, i18n) : void 0
314
348
  }));
315
349
  }
316
- function buildLocalizedNavigation(routes, config, i18n) {
350
+ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
351
+ if (!config || config === "FileTree") return localizeNavigationPaths(buildNavigation(routes.map((route) => ({
352
+ ...route,
353
+ path: route.basePath ?? route.path
354
+ }))), locale, i18n);
355
+ const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
356
+ return config.map((group) => {
357
+ const children = group.pages.map((item) => {
358
+ const { pageRef, openapiRef, redirect, title, icon } = resolvePageItem(item);
359
+ const ref = openapiRef ?? pageRef ?? "";
360
+ const basePath = basePathFromRef(ref);
361
+ const route = routeMap.get(basePath);
362
+ const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
363
+ return {
364
+ path: (redirect ? localizedRoutePath(basePathFromRef(redirect), locale, i18n) : void 0) ?? path,
365
+ title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle$1(basePath.split("/").pop() ?? ref),
366
+ icon,
367
+ sections: route?.sections ? navigationSections(route.sections) : void 0
368
+ };
369
+ });
370
+ return {
371
+ path: children[0]?.path ?? localizedRoutePath("/", locale, i18n),
372
+ title: resolveLocalizedText(group.group, locale, i18n.defaultLocale) ?? "",
373
+ icon: group.icon,
374
+ children
375
+ };
376
+ });
377
+ }
378
+ function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
317
379
  if (!i18n) return void 0;
318
380
  const result = {};
319
381
  for (const locale of i18n.locales) {
320
382
  const localeRoutes = routes.filter((route) => route.locale === locale.code);
321
- if (!config || config === "FileTree") {
322
- const baseNavigation = buildNavigation(localeRoutes.map((route) => ({
323
- ...route,
324
- path: route.basePath ?? route.path
325
- })));
326
- result[locale.code] = localizeNavigationPaths(baseNavigation, locale.code, i18n);
327
- continue;
328
- }
329
- const routeMap = new Map(localeRoutes.map((route) => [route.basePath ?? route.path, route]));
330
- result[locale.code] = config.map((group) => {
331
- const children = group.pages.map((item) => {
332
- const { pageRef, openapiRef, redirect, title, icon } = resolvePageItem(item);
333
- const ref = openapiRef ?? pageRef ?? "";
334
- const basePath = basePathFromRef(ref);
335
- const route = routeMap.get(basePath);
336
- const path = route?.path ?? localizedRoutePath(basePath, locale.code, i18n);
337
- return {
338
- path: (redirect ? localizedRoutePath(basePathFromRef(redirect), locale.code, i18n) : void 0) ?? path,
339
- title: resolveLocalizedText(title, locale.code, i18n.defaultLocale) ?? route?.title ?? kebabToTitle$1(basePath.split("/").pop() ?? ref),
340
- icon,
341
- sections: route?.sections ? navigationSections(route.sections) : void 0
342
- };
343
- });
383
+ result[locale.code] = { tabs: tabs.map((tab) => {
384
+ const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code, i18n);
344
385
  return {
345
- path: children[0]?.path ?? localizedRoutePath("/", locale.code, i18n),
346
- title: resolveLocalizedText(group.group, locale.code, i18n.defaultLocale) ?? "",
347
- icon: group.icon,
386
+ type: "tab",
387
+ path: firstNavigationPath(children),
388
+ title: resolveLocalizedText(tab.tab, locale.code, i18n.defaultLocale) ?? "",
389
+ icon: tab.icon,
348
390
  children
349
391
  };
350
- });
392
+ }) };
351
393
  }
352
394
  return result;
353
395
  }
354
396
  //#endregion
355
- //#region source/plugins/content-artifacts/raw-content.ts
397
+ //#region source/plugins/content-artifacts/artifacts.ts
398
+ var UTF8_SIGNATURE = "";
356
399
  function normalizeBasePath(basePath) {
357
400
  if (!basePath || basePath === "/") return "";
358
401
  return "/" + basePath.replace(/^\/+|\/+$/g, "");
359
402
  }
360
- function routeToMarkdownUrl(routePath) {
403
+ function containsNonAscii(content) {
404
+ return [...content].some((character) => character.codePointAt(0) > 127);
405
+ }
406
+ function withUtf8Signature(content) {
407
+ if (!containsNonAscii(content) || content.startsWith(UTF8_SIGNATURE)) return content;
408
+ return `${UTF8_SIGNATURE}${content}`;
409
+ }
410
+ function shouldUseUtf8Signature(route) {
411
+ if (route.kind === "mdx") return true;
412
+ return /\.ya?ml$/i.test(route.contentArtifactUrl ?? "");
413
+ }
414
+ function routeToMarkdownArtifactUrl(routePath) {
361
415
  return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.md`;
362
416
  }
363
- function routeToOpenAPIUrl(routePath, filePath) {
417
+ function routeToOpenAPIArtifactUrl(routePath, filePath) {
364
418
  const extension = (0, node_path.extname)(filePath).toLowerCase() || ".json";
365
419
  return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi${extension}`;
366
420
  }
367
- function enrichRoutesWithRawContent(routes) {
368
- for (const route of routes) route.rawContentUrl = route.kind === "openapi" ? routeToOpenAPIUrl(route.path, route.filePath) : routeToMarkdownUrl(route.path);
421
+ function attachContentArtifactUrls(routes) {
422
+ for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path, route.filePath) : routeToMarkdownArtifactUrl(route.path);
423
+ }
424
+ function readRouteContent(route) {
425
+ if (route.content !== void 0) return route.content;
426
+ throw new Error(`Route content is missing from route context: ${route.filePath}`);
369
427
  }
370
- function readRawContent(route) {
371
- return (0, node_fs.readFileSync)(route.filePath, "utf-8");
428
+ function readRouteArtifactContent(route) {
429
+ const content = readRouteContent(route);
430
+ return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
372
431
  }
373
- function writeRawContentFiles(routes, outputDirectory) {
432
+ function writeContentArtifactFiles(routes, outputDirectory) {
374
433
  for (const route of routes) {
375
- if (!route.rawContentUrl) continue;
376
- const outFile = (0, node_path.join)(outputDirectory, route.rawContentUrl.replace(/^\//, ""));
434
+ if (!route.contentArtifactUrl) continue;
435
+ const outFile = (0, node_path.join)(outputDirectory, route.contentArtifactUrl.replace(/^\//, ""));
377
436
  (0, node_fs.mkdirSync)((0, node_path.dirname)(outFile), { recursive: true });
378
- (0, node_fs.writeFileSync)(outFile, readRawContent(route), "utf-8");
437
+ (0, node_fs.writeFileSync)(outFile, readRouteArtifactContent(route), "utf-8");
379
438
  }
380
439
  }
381
440
  function createLlmsTxt(routes, projectConfig) {
@@ -384,22 +443,25 @@ function createLlmsTxt(routes, projectConfig) {
384
443
  if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
385
444
  lines.push("## Docs");
386
445
  for (const route of routes.filter((route) => route.kind === "mdx")) {
387
- if (!route.rawContentUrl) continue;
388
- lines.push(`- [${route.title}](${basePath}${route.rawContentUrl})`);
446
+ if (!route.contentArtifactUrl) continue;
447
+ lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
389
448
  }
390
449
  const openApiRoutes = routes.filter((route) => route.kind === "openapi");
391
450
  if (openApiRoutes.length > 0) {
392
451
  lines.push("", "## OpenAPI");
393
452
  for (const route of openApiRoutes) {
394
- if (!route.rawContentUrl) continue;
395
- lines.push(`- [${route.title}](${basePath}${route.rawContentUrl})`);
453
+ if (!route.contentArtifactUrl) continue;
454
+ lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
396
455
  }
397
456
  }
398
457
  return `${lines.join("\n")}\n`;
399
458
  }
459
+ function createLlmsTxtArtifact(routes, projectConfig) {
460
+ return withUtf8Signature(createLlmsTxt(routes, projectConfig));
461
+ }
400
462
  function writeLlmsTxt(routes, projectConfig, outputDirectory) {
401
463
  (0, node_fs.mkdirSync)(outputDirectory, { recursive: true });
402
- (0, node_fs.writeFileSync)((0, node_path.join)(outputDirectory, "llms.txt"), createLlmsTxt(routes, projectConfig), "utf-8");
464
+ (0, node_fs.writeFileSync)((0, node_path.join)(outputDirectory, "llms.txt"), createLlmsTxtArtifact(routes, projectConfig), "utf-8");
403
465
  }
404
466
  //#endregion
405
467
  //#region source/plugins/content-artifacts/server.ts
@@ -412,8 +474,8 @@ function resolveContentArtifactPath(url, projectConfig) {
412
474
  const basePath = normalizeRoutePrefix(projectConfig.routePrefix);
413
475
  return basePath && requestPath.startsWith(basePath) ? requestPath.slice(basePath.length) || "/" : requestPath;
414
476
  }
415
- function resolveRawContentType(route) {
416
- if (route.kind === "openapi" && /\.ya?ml$/i.test(route.rawContentUrl ?? "")) return "text/yaml; charset=utf-8";
477
+ function resolveContentArtifactType(route) {
478
+ if (route.kind === "openapi" && /\.ya?ml$/i.test(route.contentArtifactUrl ?? "")) return "text/yaml; charset=utf-8";
417
479
  return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
418
480
  }
419
481
  function serveContentArtifacts(req, res, projectConfig, routes) {
@@ -421,14 +483,14 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
421
483
  if (contentPath === "/llms.txt") {
422
484
  res.statusCode = 200;
423
485
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
424
- res.end(createLlmsTxt(routes, projectConfig));
486
+ res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
425
487
  return true;
426
488
  }
427
- const route = routes.find((route) => route.rawContentUrl === contentPath);
489
+ const route = routes.find((route) => route.contentArtifactUrl === contentPath);
428
490
  if (!route) return false;
429
491
  res.statusCode = 200;
430
- res.setHeader("Content-Type", resolveRawContentType(route));
431
- res.end(readRawContent(route));
492
+ res.setHeader("Content-Type", resolveContentArtifactType(route));
493
+ res.end(readRouteArtifactContent(route), "utf8");
432
494
  return true;
433
495
  }
434
496
  //#endregion
@@ -438,7 +500,7 @@ function createContentArtifactsPlugin() {
438
500
  name: "clarify:content-artifacts",
439
501
  hooks: {
440
502
  "routes:resolved": (input) => {
441
- enrichRoutesWithRawContent(input.routes);
503
+ attachContentArtifactUrls(input.routes);
442
504
  return input;
443
505
  },
444
506
  "dev:configureServer": (server, ctx) => {
@@ -450,7 +512,7 @@ function createContentArtifactsPlugin() {
450
512
  "build:done": (ctx) => {
451
513
  const outputDirectory = ctx.generateOptions.outputDirectory;
452
514
  if (!outputDirectory) return;
453
- writeRawContentFiles(ctx.routes, outputDirectory);
515
+ writeContentArtifactFiles(ctx.routes, outputDirectory);
454
516
  writeLlmsTxt(ctx.routes, ctx.projectConfig, outputDirectory);
455
517
  }
456
518
  }
@@ -527,6 +589,9 @@ var OPENAPI_HTTP_METHODS = [
527
589
  "patch",
528
590
  "trace"
529
591
  ];
592
+ function errorMessage(error) {
593
+ return error instanceof Error ? error.message : String(error);
594
+ }
530
595
  function kebabToTitle(str) {
531
596
  return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
532
597
  }
@@ -549,7 +614,8 @@ function findOpenAPIRoutes(dir, base = dir) {
549
614
  filePath: fullPath,
550
615
  virtualModuleId: virtualModuleIdFromRef(ref),
551
616
  title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
552
- kind: "openapi"
617
+ kind: "openapi",
618
+ content: (0, node_fs.readFileSync)(fullPath, "utf-8")
553
619
  });
554
620
  }
555
621
  return routes;
@@ -576,9 +642,20 @@ function extractOpenAPISections(spec) {
576
642
  }
577
643
  async function readOpenAPISpec(filePath) {
578
644
  try {
579
- return await _apidevtools_swagger_parser.default.dereference(filePath);
580
- } catch {
581
- return null;
645
+ return {
646
+ ok: true,
647
+ spec: await _apidevtools_swagger_parser.default.dereference(filePath)
648
+ };
649
+ } catch (error) {
650
+ return {
651
+ ok: false,
652
+ diagnostic: {
653
+ title: "OpenAPI spec parse failed",
654
+ message: `Clarify could not parse ${filePath}.`,
655
+ filePath,
656
+ cause: errorMessage(error)
657
+ }
658
+ };
582
659
  }
583
660
  }
584
661
  //#endregion
@@ -589,10 +666,31 @@ function generateOpenAPIRegistryModule(openApis) {
589
666
  }
590
667
  function generateOpenAPIModule(spec) {
591
668
  return `import { createElement } from 'react';
592
- import { OpenApiPage } from '@clarify-labs/renderer';
669
+ import { OpenApiDocument } from '@clarify-labs/renderer';
593
670
  const spec = ${JSON.stringify(spec)};
594
671
  export default function OpenApiRoutePage() {
595
- return createElement(OpenApiPage, { spec });
672
+ return createElement(OpenApiDocument, { spec });
673
+ }`;
674
+ }
675
+ function generateOpenAPIErrorModule(diagnostic) {
676
+ return `import { createElement } from 'react';
677
+ const diagnostic = ${JSON.stringify(diagnostic)};
678
+ export default function OpenApiErrorRoutePage() {
679
+ return createElement('article', { className: 'flex h-full flex-col pt-16 pb-10' },
680
+ createElement('div', { className: 'prose max-w-none flex-auto dark:prose-invert' },
681
+ createElement('p', { className: 'mb-3 text-xs/6 font-medium tracking-widest text-red-500 uppercase dark:text-red-400' }, 'OpenAPI Error'),
682
+ createElement('h1', null, diagnostic.title),
683
+ createElement('p', { className: 'lead' }, diagnostic.message),
684
+ diagnostic.filePath ? createElement('div', { className: 'not-prose mt-6 rounded-lg border border-zinc-200 bg-zinc-50 p-4 text-sm dark:border-white/10 dark:bg-white/5' },
685
+ createElement('div', { className: 'mb-1 font-semibold text-zinc-900 dark:text-white' }, 'File'),
686
+ createElement('code', { className: 'break-all text-zinc-700 dark:text-zinc-300' }, diagnostic.filePath)
687
+ ) : null,
688
+ diagnostic.cause ? createElement('div', { className: 'not-prose mt-4 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800 dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-200' },
689
+ createElement('div', { className: 'mb-1 font-semibold' }, 'Why it happened'),
690
+ createElement('pre', { className: 'whitespace-pre-wrap break-words text-xs' }, diagnostic.cause)
691
+ ) : null
692
+ )
693
+ );
596
694
  }`;
597
695
  }
598
696
  //#endregion
@@ -609,18 +707,26 @@ function createOpenAPIPlugin() {
609
707
  "routes:discovered": async (routes) => {
610
708
  for (const key of Object.keys(specs)) delete specs[key];
611
709
  for (const route of routes.filter((route) => route.kind === "openapi")) {
612
- const spec = await readOpenAPISpec(route.filePath);
613
- if (!spec) throw new Error(`Failed to parse OpenAPI spec: ${route.filePath}`);
710
+ const result = await readOpenAPISpec(route.filePath);
711
+ if (!result.ok) {
712
+ route.diagnostic = result.diagnostic;
713
+ route.title = route.title || "OpenAPI parse error";
714
+ route.sections = [];
715
+ continue;
716
+ }
717
+ const spec = result.spec;
614
718
  specs[route.virtualModuleId] = spec;
615
719
  specs[`virtual:clarify-page/${route.path.replace(/^\//, "")}`] = spec;
616
720
  route.title = spec.info?.title ?? route.title;
721
+ route.description = spec.info?.description ?? route.description;
617
722
  route.sections = extractOpenAPISections(spec);
618
723
  }
619
724
  return routes;
620
725
  },
621
- "modules:before": (modules) => {
726
+ "modules:before": (modules, ctx) => {
622
727
  modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(specs));
623
728
  for (const [moduleId, spec] of Object.entries(specs)) modules.set(moduleId, generateOpenAPIModule(spec));
729
+ for (const route of ctx.routes.filter((route) => route.kind === "openapi" && route.diagnostic)) if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
624
730
  return modules;
625
731
  }
626
732
  }
@@ -704,20 +810,122 @@ var clarifyPagesGroupSchema = zod.z.object({
704
810
  pages: zod.z.array(clarifyPagesItemSchema)
705
811
  });
706
812
  var clarifyPagesConfigSchema = zod.z.union([zod.z.array(clarifyPagesGroupSchema), zod.z.literal("FileTree")]);
813
+ var clarifyTabItemSchema = zod.z.object({
814
+ tab: clarifyLocalizedTextSchema,
815
+ icon: zod.z.string().optional(),
816
+ pages: clarifyPagesConfigSchema.optional()
817
+ });
818
+ var clarifyTabsConfigSchema = zod.z.array(clarifyTabItemSchema);
819
+ var clarifyThemePresetSchema = zod.z.enum(["default", "base"]);
820
+ var clarifyThemeColorTokensConfigSchema = zod.z.object({
821
+ primary: zod.z.string().optional(),
822
+ accent: zod.z.string().optional(),
823
+ background: zod.z.string().optional(),
824
+ foreground: zod.z.string().optional(),
825
+ surface: zod.z.string().optional(),
826
+ muted: zod.z.string().optional(),
827
+ border: zod.z.string().optional(),
828
+ codeBackground: zod.z.string().optional()
829
+ });
830
+ var clarifyThemeRadiusTokensConfigSchema = zod.z.object({
831
+ sm: zod.z.string().optional(),
832
+ md: zod.z.string().optional(),
833
+ lg: zod.z.string().optional(),
834
+ xl: zod.z.string().optional()
835
+ });
836
+ var clarifyThemeTokensConfigSchema = zod.z.object({
837
+ colors: clarifyThemeColorTokensConfigSchema.optional(),
838
+ radius: clarifyThemeRadiusTokensConfigSchema.optional()
839
+ });
840
+ var clarifyThemeLayoutConfigSchema = zod.z.object({ maxWidth: zod.z.string().optional() });
841
+ var clarifyThemeConfigSchema = zod.z.object({
842
+ preset: clarifyThemePresetSchema.optional(),
843
+ tokens: clarifyThemeTokensConfigSchema.optional(),
844
+ layout: clarifyThemeLayoutConfigSchema.optional()
845
+ });
707
846
  var clarifyProjectConfigSchema = zod.z.object({
708
847
  title: zod.z.string().optional(),
709
848
  description: zod.z.string().optional(),
710
849
  logo: clarifyLogoConfigSchema.optional(),
711
850
  favicon: clarifyFaviconConfigSchema.optional(),
712
- theme: zod.z.object({ primary: zod.z.string().optional() }).optional(),
851
+ theme: clarifyThemeConfigSchema.optional(),
713
852
  routePrefix: zod.z.string().optional(),
714
853
  navbar: zod.z.object({ links: zod.z.array(clarifyNavbarLinkSchema).optional() }).optional(),
715
854
  banner: clarifyBannerConfigSchema.optional(),
716
855
  footer: clarifyFooterConfigSchema.optional(),
717
856
  i18n: clarifyI18nConfigSchema.optional(),
718
- pages: clarifyPagesConfigSchema.optional()
857
+ tabs: clarifyTabsConfigSchema.optional()
719
858
  });
720
859
  //#endregion
860
+ //#region source/core/theme.ts
861
+ var clarifyThemePresets = {
862
+ default: {
863
+ preset: "default",
864
+ tokens: {
865
+ colors: {
866
+ primary: "#00D492",
867
+ accent: "#00F6C9",
868
+ background: "#ffffff",
869
+ foreground: "#111827",
870
+ surface: "#ffffff",
871
+ muted: "#64748b",
872
+ border: "rgb(15 23 42 / 0.12)",
873
+ codeBackground: "#f7fefb"
874
+ },
875
+ radius: {
876
+ sm: "6px",
877
+ md: "10px",
878
+ lg: "14px",
879
+ xl: "18px"
880
+ }
881
+ },
882
+ layout: { maxWidth: "82rem" }
883
+ },
884
+ base: {
885
+ preset: "base",
886
+ tokens: {
887
+ colors: {
888
+ primary: "#18181b",
889
+ accent: "#52525b",
890
+ background: "#ffffff",
891
+ foreground: "#18181b",
892
+ surface: "#ffffff",
893
+ muted: "#71717a",
894
+ border: "rgb(24 24 27 / 0.12)",
895
+ codeBackground: "#f4f4f5"
896
+ },
897
+ radius: {
898
+ sm: "6px",
899
+ md: "8px",
900
+ lg: "12px",
901
+ xl: "16px"
902
+ }
903
+ },
904
+ layout: { maxWidth: "80rem" }
905
+ }
906
+ };
907
+ function resolveThemeConfig(theme = {}) {
908
+ const preset = theme.preset ?? "default";
909
+ const presetTheme = clarifyThemePresets[preset];
910
+ return {
911
+ preset,
912
+ tokens: {
913
+ colors: {
914
+ ...presetTheme.tokens.colors,
915
+ ...theme.tokens?.colors
916
+ },
917
+ radius: {
918
+ ...presetTheme.tokens.radius,
919
+ ...theme.tokens?.radius
920
+ }
921
+ },
922
+ layout: {
923
+ ...presetTheme.layout,
924
+ ...theme.layout
925
+ }
926
+ };
927
+ }
928
+ //#endregion
721
929
  //#region source/core/config.ts
722
930
  function formatIssuePath(path) {
723
931
  return path.reduce((result, segment) => {
@@ -756,12 +964,12 @@ function resolveProjectConfig(config = {}) {
756
964
  logo: config.logo,
757
965
  favicon: config.favicon,
758
966
  routePrefix: config.routePrefix ?? "/",
759
- theme: config.theme ?? {},
967
+ theme: resolveThemeConfig(config.theme),
760
968
  navbar: config.navbar,
761
969
  banner: config.banner,
762
970
  footer: config.footer,
763
971
  i18n: resolveI18nConfig(config.i18n),
764
- pages: config.pages
972
+ tabs: config.tabs
765
973
  };
766
974
  }
767
975
  //#endregion
@@ -901,18 +1109,28 @@ function injectHtmlLocaleAttributes(html, projectConfig, route) {
901
1109
  return `<html${nextAttributes}>`;
902
1110
  });
903
1111
  }
1112
+ function routeTitle(projectConfig, route) {
1113
+ const title = route?.title?.trim();
1114
+ if (!title || title === projectConfig.title) return projectConfig.title;
1115
+ return `${title} - ${projectConfig.title}`;
1116
+ }
1117
+ function setNamedMeta(html, name, content) {
1118
+ const metaPattern = new RegExp(`<meta\\b(?=[^>]*\\bname=["']${name}["'])[^>]*>\\n?`, "i");
1119
+ if (!content) return html.replace(metaPattern, "");
1120
+ const meta = `<meta name="${name}" content="${escapeHtml(content)}" />`;
1121
+ if (metaPattern.test(html)) return html.replace(metaPattern, meta);
1122
+ return html.replace("</head>", ` ${meta}\n </head>`);
1123
+ }
904
1124
  function injectSSRIntoTemplate(template, appHtml, projectConfig, route) {
905
1125
  let html = injectHtmlLocaleAttributes(template, projectConfig, route);
906
- html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(projectConfig.title)}</title>`);
907
- if (projectConfig.description && !html.includes("name=\"description\"")) {
908
- const descriptionMeta = `<meta name="description" content="${escapeHtml(projectConfig.description)}" />`;
909
- html = html.replace("</head>", ` ${descriptionMeta}\n </head>`);
910
- }
1126
+ html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(routeTitle(projectConfig, route))}</title>`);
1127
+ html = setNamedMeta(html, "description", route?.description ?? projectConfig.description);
1128
+ html = setNamedMeta(html, "keywords", route?.keywords?.join(", "));
911
1129
  html = html.replace(/<div id="root">[\s\S]*?<\/div>/, `<div id="root">${appHtml}</div>`);
912
1130
  return html;
913
1131
  }
914
1132
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
915
- import { routes, navigation } from 'virtual:clarify-routes';
1133
+ import { routes, navigation } from 'virtual:clarify-routes/server';
916
1134
  import { config } from 'virtual:clarify-config';
917
1135
  import { openApis } from 'virtual:clarify-openapi-registry';
918
1136
 
@@ -934,7 +1152,10 @@ async function buildSSRBundle(root, ssrEntry, ssrOutDir, plugins) {
934
1152
  ssr: ssrEntry,
935
1153
  outDir: ssrOutDir,
936
1154
  emptyOutDir: true,
937
- rollupOptions: { input: ssrEntry }
1155
+ rolldownOptions: {
1156
+ input: ssrEntry,
1157
+ checks: { pluginTimings: false }
1158
+ }
938
1159
  }
939
1160
  });
940
1161
  }
@@ -991,6 +1212,7 @@ async function loadClarifyConfig(root, env) {
991
1212
  //#region source/core/virtual-modules.ts
992
1213
  var VIRTUAL_CONFIG = "virtual:clarify-config";
993
1214
  var VIRTUAL_ROUTES = "virtual:clarify-routes";
1215
+ var VIRTUAL_SERVER_ROUTES = "virtual:clarify-routes/server";
994
1216
  var VIRTUAL_OPENAPI_REGISTRY = openApiRegistryModuleId;
995
1217
  var VIRTUAL_CLIENT_ENTRY = "virtual:clarify-entry-client";
996
1218
  var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify-entry-client";
@@ -1006,8 +1228,11 @@ function generateConfigModule(projectConfig, buildOptions) {
1006
1228
  ...buildOptions
1007
1229
  })};`;
1008
1230
  }
1009
- function generateRoutesModule(routes, pagesConfig, resolvedNavigation, projectConfig) {
1010
- const imports = routes.map((r, i) => `import Page${i} from '${r.virtualModuleId}';`).join("\n");
1231
+ function moduleSpecifier(value) {
1232
+ return JSON.stringify(value);
1233
+ }
1234
+ function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode = "client") {
1235
+ const imports = mode === "server" ? routes.map((r, i) => `import Page${i} from ${moduleSpecifier(r.virtualModuleId)};`).join("\n") : "";
1011
1236
  const routesArray = routes.map((r, i) => {
1012
1237
  const sections = r.sections && r.sections.length > 0 ? `, sections: ${JSON.stringify(r.sections.map((s) => ({
1013
1238
  id: s.id,
@@ -1015,14 +1240,18 @@ function generateRoutesModule(routes, pagesConfig, resolvedNavigation, projectCo
1015
1240
  badge: s.badge,
1016
1241
  tags: s.tags
1017
1242
  })))}` : "";
1018
- const rawContentUrl = r.rawContentUrl ? `, rawContentUrl: ${JSON.stringify(r.rawContentUrl)}` : "";
1243
+ const contentArtifactUrl = r.contentArtifactUrl ? `, contentArtifactUrl: ${JSON.stringify(r.contentArtifactUrl)}` : "";
1019
1244
  const basePath = r.basePath ? `, basePath: ${JSON.stringify(r.basePath)}` : "";
1020
1245
  const locale = r.locale ? `, locale: ${JSON.stringify(r.locale)}` : "";
1021
1246
  const isFallback = r.isFallback ? ", isFallback: true" : "";
1022
1247
  const alternates = r.alternates ? `, alternates: ${JSON.stringify(r.alternates)}` : "";
1023
- return ` { path: ${JSON.stringify(r.path)}, title: ${JSON.stringify(r.title)}, component: Page${i}, kind: '${r.kind}'${basePath}${locale}${isFallback}${alternates}${sections}${rawContentUrl} }`;
1248
+ const description = r.description ? `, description: ${JSON.stringify(r.description)}` : "";
1249
+ const keywords = r.keywords && r.keywords.length > 0 ? `, keywords: ${JSON.stringify(r.keywords)}` : "";
1250
+ const component = mode === "server" ? `Page${i}` : `() => import(${moduleSpecifier(r.virtualModuleId)})`;
1251
+ const lazy = mode === "client" ? ", lazy: true" : "";
1252
+ return ` { path: ${JSON.stringify(r.path)}, title: ${JSON.stringify(r.title)}, component: ${component}${lazy}, kind: ${JSON.stringify(r.kind)}${basePath}${locale}${isFallback}${alternates}${description}${keywords}${sections}${contentArtifactUrl} }`;
1024
1253
  }).join(",\n");
1025
- const navigation = resolvedNavigation ?? (projectConfig?.i18n ? buildLocalizedNavigation(routes, pagesConfig, projectConfig.i18n) ?? {} : pagesConfig && pagesConfig !== "FileTree" ? buildNavigationFromConfig(routes, pagesConfig) : buildNavigation(routes));
1254
+ const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
1026
1255
  return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
1027
1256
  }
1028
1257
  function createClientEntryModule() {
@@ -1037,13 +1266,14 @@ render({ config, routes, navigation, openApis });`;
1037
1266
  function buildVirtualModules(args) {
1038
1267
  const modules = /* @__PURE__ */ new Map();
1039
1268
  modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
1040
- modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.projectConfig.pages, args.navigation, args.projectConfig));
1269
+ modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
1270
+ modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "server"));
1041
1271
  modules.set(VIRTUAL_OPENAPI_REGISTRY, "export const openApis = {};");
1042
1272
  modules.set(VIRTUAL_CLIENT_ENTRY, createClientEntryModule());
1043
1273
  modules.set(RESOLVED_CLIENT_ENTRY, createClientEntryModule());
1044
1274
  for (const route of args.routes) {
1045
1275
  if (route.kind === "openapi") continue;
1046
- modules.set(route.virtualModuleId, `export { default } from '${route.filePath}';`);
1276
+ modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
1047
1277
  }
1048
1278
  return modules;
1049
1279
  }
@@ -1148,7 +1378,7 @@ function clarifyPlugin(options = {}) {
1148
1378
  async function resolveRoutesAndSpecs() {
1149
1379
  routes = await discoverRoutes();
1150
1380
  routes = await runHooks(clarifyPlugins, "routes:discovered", routes, ctx);
1151
- const defaultNavigation = projectConfig.i18n ? buildLocalizedNavigation(routes, projectConfig.pages, projectConfig.i18n) ?? {} : projectConfig.pages && projectConfig.pages !== "FileTree" ? buildNavigationFromConfig(routes, projectConfig.pages) : buildNavigation(routes);
1381
+ const defaultNavigation = projectConfig.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes);
1152
1382
  const resolved = await runHooks(clarifyPlugins, "routes:resolved", {
1153
1383
  routes,
1154
1384
  navigation: defaultNavigation
@@ -1182,6 +1412,20 @@ function clarifyPlugin(options = {}) {
1182
1412
  function hasContentRouteForFile(filePath) {
1183
1413
  return routes.some((route) => route.filePath === filePath);
1184
1414
  }
1415
+ const normalizedMdxContentPlugin = {
1416
+ name: "clarify:normalized-mdx-content",
1417
+ enforce: "pre",
1418
+ transform(_code, id) {
1419
+ if (!/\.mdx?(?:\?|$)/.test(id)) return null;
1420
+ const filePath = id.replace(/\?.*$/, "");
1421
+ const route = routes.find((route) => route.kind === "mdx" && route.filePath === filePath);
1422
+ if (!route || route.content === void 0) return null;
1423
+ return {
1424
+ code: route.content,
1425
+ map: null
1426
+ };
1427
+ }
1428
+ };
1185
1429
  const mdx = (0, _mdx_js_rollup.default)({
1186
1430
  include: ["**/*.{md,mdx}"],
1187
1431
  jsxImportSource: "react",
@@ -1192,6 +1436,7 @@ function clarifyPlugin(options = {}) {
1192
1436
  return [
1193
1437
  (0, _vitejs_plugin_react.default)(),
1194
1438
  (0, _tailwindcss_vite.default)(),
1439
+ normalizedMdxContentPlugin,
1195
1440
  {
1196
1441
  name: "clarify:core",
1197
1442
  async config() {
@@ -1227,6 +1472,7 @@ function clarifyPlugin(options = {}) {
1227
1472
  if (id === "virtual:clarify-entry-client" || id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1228
1473
  if (id === "virtual:clarify-config" || id === resolveVirtualId("virtual:clarify-config")) return resolveVirtualId(VIRTUAL_CONFIG);
1229
1474
  if (id === "virtual:clarify-routes" || id === resolveVirtualId("virtual:clarify-routes")) return resolveVirtualId(VIRTUAL_ROUTES);
1475
+ if (id === "virtual:clarify-routes/server" || id === resolveVirtualId("virtual:clarify-routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
1230
1476
  if (id === VIRTUAL_OPENAPI_REGISTRY || id === resolveVirtualId(VIRTUAL_OPENAPI_REGISTRY)) return resolveVirtualId(VIRTUAL_OPENAPI_REGISTRY);
1231
1477
  const route = routes.find((r) => r.virtualModuleId === id || r.virtualModuleId === stripVirtualPrefix(id));
1232
1478
  if (route) return resolveVirtualId(route.virtualModuleId);
@@ -1272,19 +1518,24 @@ function clarifyPlugin(options = {}) {
1272
1518
  let tempEntryPath;
1273
1519
  try {
1274
1520
  tempEntryPath = createTempEntryFile(SSR_ENTRY_CODE);
1275
- await buildSSRBundle(root, tempEntryPath, ssrOutputDir, [{
1276
- name: "clarify:virtual-ssg",
1277
- resolveId(id) {
1278
- if (id === "virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1279
- if (id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1280
- if (id === "virtual:clarify-config") return id;
1281
- if (id === "virtual:clarify-routes") return id;
1282
- if (id === VIRTUAL_OPENAPI_REGISTRY) return id;
1283
- if (routes.find((r) => r.virtualModuleId === id)) return id;
1284
- return null;
1521
+ await buildSSRBundle(root, tempEntryPath, ssrOutputDir, [
1522
+ {
1523
+ name: "clarify:virtual-ssg",
1524
+ resolveId(id) {
1525
+ if (id === "virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1526
+ if (id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1527
+ if (id === "virtual:clarify-config") return id;
1528
+ if (id === "virtual:clarify-routes") return id;
1529
+ if (id === "virtual:clarify-routes/server") return id;
1530
+ if (id === VIRTUAL_OPENAPI_REGISTRY) return id;
1531
+ if (routes.find((r) => r.virtualModuleId === id)) return id;
1532
+ return null;
1533
+ },
1534
+ load: (id) => loadVirtualModule(id, virtualModules)
1285
1535
  },
1286
- load: (id) => loadVirtualModule(id, virtualModules)
1287
- }, mdx]);
1536
+ normalizedMdxContentPlugin,
1537
+ mdx
1538
+ ]);
1288
1539
  const ssrBundlePath = (0, node_path.join)(ssrOutputDir, "entry-server.js");
1289
1540
  await renderSSGRoutes(routes, projectConfig, outputDir, ssrBundlePath, generateOptions.ssg.failOnError);
1290
1541
  } catch (err) {
@@ -1327,4 +1578,4 @@ Object.defineProperty(exports, "loadClarifyConfig", {
1327
1578
  }
1328
1579
  });
1329
1580
 
1330
- //# sourceMappingURL=plugin-CEyCiD5r.cjs.map
1581
+ //# sourceMappingURL=plugin-DnD0L83F.cjs.map