@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.
@@ -5,9 +5,9 @@ import tailwindcss from "@tailwindcss/vite";
5
5
  import react from "@vitejs/plugin-react";
6
6
  import GithubSlugger, { slug } from "github-slugger";
7
7
  import { toString } from "mdast-util-to-string";
8
- import remarkGfm from "remark-gfm";
9
8
  import shiki from "shiki";
10
9
  import { visit } from "unist-util-visit";
10
+ 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";
@@ -93,7 +93,7 @@ function rehypeSlugSections() {
93
93
  });
94
94
  };
95
95
  }
96
- var remarkPlugins = [remarkGfm];
96
+ var remarkPlugins = markdownRemarkPlugins;
97
97
  var rehypePlugins = [
98
98
  rehypeSlugSections,
99
99
  rehypeParseCodeBlocks,
@@ -102,11 +102,14 @@ var rehypePlugins = [
102
102
  //#endregion
103
103
  //#region source/parsers/frontmatter.ts
104
104
  /**
105
- * 从 MDX 文件内容中提取 frontmatter。
106
- * 使用 gray-matter 支持 YAML frontmatter 的标准语法和类型解析。
105
+ * 在内容入口统一解析 MDX frontmatter,并返回可供后续流程复用的标准化正文。
107
106
  */
108
- function extractFrontmatter(content) {
109
- return matter(content).data;
107
+ function parseFrontmatter(content) {
108
+ const parsed = matter(content);
109
+ return {
110
+ frontmatter: parsed.data,
111
+ content: parsed.content.replace(/^\r?\n/, "")
112
+ };
110
113
  }
111
114
  //#endregion
112
115
  //#region source/parsers/routes.ts
@@ -183,8 +186,7 @@ function findContentRoutes(dir, base = dir) {
183
186
  const relativePath = relative(base, fullPath);
184
187
  const pathParts = relativePath.replace(/\.mdx?$/, "").split("/");
185
188
  const cleanPath = ("/" + pathParts.map((p) => p === "index" ? "" : p).filter(Boolean).join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
186
- const content = readFileSync(fullPath, "utf-8");
187
- const frontmatter = extractFrontmatter(content);
189
+ const { frontmatter, content } = parseFrontmatter(readFileSync(fullPath, "utf-8"));
188
190
  let title = typeof frontmatter.title === "string" ? frontmatter.title : "";
189
191
  if (!title) {
190
192
  const lastPart = pathParts[pathParts.length - 1] ?? "";
@@ -196,13 +198,22 @@ function findContentRoutes(dir, base = dir) {
196
198
  filePath: fullPath,
197
199
  virtualModuleId: "virtual:clarify-page/" + relativePath.replace(/\.mdx?$/, "").replace(/\/+/g, "/"),
198
200
  title,
201
+ description: typeof frontmatter.description === "string" ? frontmatter.description : void 0,
202
+ keywords: frontmatterKeywords(frontmatter),
199
203
  kind: "mdx",
204
+ frontmatter,
205
+ content,
200
206
  sections: extractMdxSections(content)
201
207
  });
202
208
  }
203
209
  }
204
210
  return routes;
205
211
  }
212
+ function frontmatterKeywords(frontmatter) {
213
+ const value = frontmatter.keywords ?? frontmatter.keyword ?? frontmatter.tags;
214
+ const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
215
+ return keywords.length > 0 ? keywords : void 0;
216
+ }
206
217
  function buildNavigation(routes) {
207
218
  const root = [];
208
219
  for (const route of routes) {
@@ -243,6 +254,30 @@ function resolvePageItem(item) {
243
254
  icon: item.icon
244
255
  };
245
256
  }
257
+ function firstNavigationPath(nodes) {
258
+ for (const node of nodes) {
259
+ if (node.path) return node.path;
260
+ const childPath = node.children ? firstNavigationPath(node.children) : void 0;
261
+ if (childPath) return childPath;
262
+ }
263
+ return "/";
264
+ }
265
+ function buildNavigationFromPagesConfig(routes, config) {
266
+ if (!config || config === "FileTree") return buildNavigation(routes);
267
+ return buildNavigationFromConfig(routes, config);
268
+ }
269
+ function buildNavigationFromTabsConfig(routes, tabs) {
270
+ return { tabs: tabs.map((tab) => {
271
+ const children = buildNavigationFromPagesConfig(routes, tab.pages);
272
+ return {
273
+ type: "tab",
274
+ path: firstNavigationPath(children),
275
+ title: resolveLocalizedText(tab.tab, "", "") ?? "",
276
+ icon: tab.icon,
277
+ children
278
+ };
279
+ }) };
280
+ }
246
281
  function buildNavigationFromConfig(routes, config) {
247
282
  const routeMap = new Map(routes.map((r) => [r.path, r]));
248
283
  return config.map((group) => {
@@ -283,69 +318,94 @@ function localizeNavigationPaths(nodes, locale, i18n) {
283
318
  children: node.children ? localizeNavigationPaths(node.children, locale, i18n) : void 0
284
319
  }));
285
320
  }
286
- function buildLocalizedNavigation(routes, config, i18n) {
321
+ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
322
+ if (!config || config === "FileTree") return localizeNavigationPaths(buildNavigation(routes.map((route) => ({
323
+ ...route,
324
+ path: route.basePath ?? route.path
325
+ }))), locale, i18n);
326
+ const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
327
+ return config.map((group) => {
328
+ const children = group.pages.map((item) => {
329
+ const { pageRef, openapiRef, redirect, title, icon } = resolvePageItem(item);
330
+ const ref = openapiRef ?? pageRef ?? "";
331
+ const basePath = basePathFromRef(ref);
332
+ const route = routeMap.get(basePath);
333
+ const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
334
+ return {
335
+ path: (redirect ? localizedRoutePath(basePathFromRef(redirect), locale, i18n) : void 0) ?? path,
336
+ title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle$1(basePath.split("/").pop() ?? ref),
337
+ icon,
338
+ sections: route?.sections ? navigationSections(route.sections) : void 0
339
+ };
340
+ });
341
+ return {
342
+ path: children[0]?.path ?? localizedRoutePath("/", locale, i18n),
343
+ title: resolveLocalizedText(group.group, locale, i18n.defaultLocale) ?? "",
344
+ icon: group.icon,
345
+ children
346
+ };
347
+ });
348
+ }
349
+ function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
287
350
  if (!i18n) return void 0;
288
351
  const result = {};
289
352
  for (const locale of i18n.locales) {
290
353
  const localeRoutes = routes.filter((route) => route.locale === locale.code);
291
- if (!config || config === "FileTree") {
292
- const baseNavigation = buildNavigation(localeRoutes.map((route) => ({
293
- ...route,
294
- path: route.basePath ?? route.path
295
- })));
296
- result[locale.code] = localizeNavigationPaths(baseNavigation, locale.code, i18n);
297
- continue;
298
- }
299
- const routeMap = new Map(localeRoutes.map((route) => [route.basePath ?? route.path, route]));
300
- result[locale.code] = config.map((group) => {
301
- const children = group.pages.map((item) => {
302
- const { pageRef, openapiRef, redirect, title, icon } = resolvePageItem(item);
303
- const ref = openapiRef ?? pageRef ?? "";
304
- const basePath = basePathFromRef(ref);
305
- const route = routeMap.get(basePath);
306
- const path = route?.path ?? localizedRoutePath(basePath, locale.code, i18n);
307
- return {
308
- path: (redirect ? localizedRoutePath(basePathFromRef(redirect), locale.code, i18n) : void 0) ?? path,
309
- title: resolveLocalizedText(title, locale.code, i18n.defaultLocale) ?? route?.title ?? kebabToTitle$1(basePath.split("/").pop() ?? ref),
310
- icon,
311
- sections: route?.sections ? navigationSections(route.sections) : void 0
312
- };
313
- });
354
+ result[locale.code] = { tabs: tabs.map((tab) => {
355
+ const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code, i18n);
314
356
  return {
315
- path: children[0]?.path ?? localizedRoutePath("/", locale.code, i18n),
316
- title: resolveLocalizedText(group.group, locale.code, i18n.defaultLocale) ?? "",
317
- icon: group.icon,
357
+ type: "tab",
358
+ path: firstNavigationPath(children),
359
+ title: resolveLocalizedText(tab.tab, locale.code, i18n.defaultLocale) ?? "",
360
+ icon: tab.icon,
318
361
  children
319
362
  };
320
- });
363
+ }) };
321
364
  }
322
365
  return result;
323
366
  }
324
367
  //#endregion
325
- //#region source/plugins/content-artifacts/raw-content.ts
368
+ //#region source/plugins/content-artifacts/artifacts.ts
369
+ var UTF8_SIGNATURE = "";
326
370
  function normalizeBasePath(basePath) {
327
371
  if (!basePath || basePath === "/") return "";
328
372
  return "/" + basePath.replace(/^\/+|\/+$/g, "");
329
373
  }
330
- function routeToMarkdownUrl(routePath) {
374
+ function containsNonAscii(content) {
375
+ return [...content].some((character) => character.codePointAt(0) > 127);
376
+ }
377
+ function withUtf8Signature(content) {
378
+ if (!containsNonAscii(content) || content.startsWith(UTF8_SIGNATURE)) return content;
379
+ return `${UTF8_SIGNATURE}${content}`;
380
+ }
381
+ function shouldUseUtf8Signature(route) {
382
+ if (route.kind === "mdx") return true;
383
+ return /\.ya?ml$/i.test(route.contentArtifactUrl ?? "");
384
+ }
385
+ function routeToMarkdownArtifactUrl(routePath) {
331
386
  return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.md`;
332
387
  }
333
- function routeToOpenAPIUrl(routePath, filePath) {
388
+ function routeToOpenAPIArtifactUrl(routePath, filePath) {
334
389
  const extension = extname(filePath).toLowerCase() || ".json";
335
390
  return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi${extension}`;
336
391
  }
337
- function enrichRoutesWithRawContent(routes) {
338
- for (const route of routes) route.rawContentUrl = route.kind === "openapi" ? routeToOpenAPIUrl(route.path, route.filePath) : routeToMarkdownUrl(route.path);
392
+ function attachContentArtifactUrls(routes) {
393
+ for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path, route.filePath) : routeToMarkdownArtifactUrl(route.path);
339
394
  }
340
- function readRawContent(route) {
341
- return readFileSync(route.filePath, "utf-8");
395
+ function readRouteContent(route) {
396
+ if (route.content !== void 0) return route.content;
397
+ throw new Error(`Route content is missing from route context: ${route.filePath}`);
342
398
  }
343
- function writeRawContentFiles(routes, outputDirectory) {
399
+ function readRouteArtifactContent(route) {
400
+ const content = readRouteContent(route);
401
+ return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
402
+ }
403
+ function writeContentArtifactFiles(routes, outputDirectory) {
344
404
  for (const route of routes) {
345
- if (!route.rawContentUrl) continue;
346
- const outFile = join(outputDirectory, route.rawContentUrl.replace(/^\//, ""));
405
+ if (!route.contentArtifactUrl) continue;
406
+ const outFile = join(outputDirectory, route.contentArtifactUrl.replace(/^\//, ""));
347
407
  mkdirSync(dirname(outFile), { recursive: true });
348
- writeFileSync(outFile, readRawContent(route), "utf-8");
408
+ writeFileSync(outFile, readRouteArtifactContent(route), "utf-8");
349
409
  }
350
410
  }
351
411
  function createLlmsTxt(routes, projectConfig) {
@@ -354,22 +414,25 @@ function createLlmsTxt(routes, projectConfig) {
354
414
  if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
355
415
  lines.push("## Docs");
356
416
  for (const route of routes.filter((route) => route.kind === "mdx")) {
357
- if (!route.rawContentUrl) continue;
358
- lines.push(`- [${route.title}](${basePath}${route.rawContentUrl})`);
417
+ if (!route.contentArtifactUrl) continue;
418
+ lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
359
419
  }
360
420
  const openApiRoutes = routes.filter((route) => route.kind === "openapi");
361
421
  if (openApiRoutes.length > 0) {
362
422
  lines.push("", "## OpenAPI");
363
423
  for (const route of openApiRoutes) {
364
- if (!route.rawContentUrl) continue;
365
- lines.push(`- [${route.title}](${basePath}${route.rawContentUrl})`);
424
+ if (!route.contentArtifactUrl) continue;
425
+ lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
366
426
  }
367
427
  }
368
428
  return `${lines.join("\n")}\n`;
369
429
  }
430
+ function createLlmsTxtArtifact(routes, projectConfig) {
431
+ return withUtf8Signature(createLlmsTxt(routes, projectConfig));
432
+ }
370
433
  function writeLlmsTxt(routes, projectConfig, outputDirectory) {
371
434
  mkdirSync(outputDirectory, { recursive: true });
372
- writeFileSync(join(outputDirectory, "llms.txt"), createLlmsTxt(routes, projectConfig), "utf-8");
435
+ writeFileSync(join(outputDirectory, "llms.txt"), createLlmsTxtArtifact(routes, projectConfig), "utf-8");
373
436
  }
374
437
  //#endregion
375
438
  //#region source/plugins/content-artifacts/server.ts
@@ -382,8 +445,8 @@ function resolveContentArtifactPath(url, projectConfig) {
382
445
  const basePath = normalizeRoutePrefix(projectConfig.routePrefix);
383
446
  return basePath && requestPath.startsWith(basePath) ? requestPath.slice(basePath.length) || "/" : requestPath;
384
447
  }
385
- function resolveRawContentType(route) {
386
- if (route.kind === "openapi" && /\.ya?ml$/i.test(route.rawContentUrl ?? "")) return "text/yaml; charset=utf-8";
448
+ function resolveContentArtifactType(route) {
449
+ if (route.kind === "openapi" && /\.ya?ml$/i.test(route.contentArtifactUrl ?? "")) return "text/yaml; charset=utf-8";
387
450
  return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
388
451
  }
389
452
  function serveContentArtifacts(req, res, projectConfig, routes) {
@@ -391,14 +454,14 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
391
454
  if (contentPath === "/llms.txt") {
392
455
  res.statusCode = 200;
393
456
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
394
- res.end(createLlmsTxt(routes, projectConfig));
457
+ res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
395
458
  return true;
396
459
  }
397
- const route = routes.find((route) => route.rawContentUrl === contentPath);
460
+ const route = routes.find((route) => route.contentArtifactUrl === contentPath);
398
461
  if (!route) return false;
399
462
  res.statusCode = 200;
400
- res.setHeader("Content-Type", resolveRawContentType(route));
401
- res.end(readRawContent(route));
463
+ res.setHeader("Content-Type", resolveContentArtifactType(route));
464
+ res.end(readRouteArtifactContent(route), "utf8");
402
465
  return true;
403
466
  }
404
467
  //#endregion
@@ -408,7 +471,7 @@ function createContentArtifactsPlugin() {
408
471
  name: "clarify:content-artifacts",
409
472
  hooks: {
410
473
  "routes:resolved": (input) => {
411
- enrichRoutesWithRawContent(input.routes);
474
+ attachContentArtifactUrls(input.routes);
412
475
  return input;
413
476
  },
414
477
  "dev:configureServer": (server, ctx) => {
@@ -420,7 +483,7 @@ function createContentArtifactsPlugin() {
420
483
  "build:done": (ctx) => {
421
484
  const outputDirectory = ctx.generateOptions.outputDirectory;
422
485
  if (!outputDirectory) return;
423
- writeRawContentFiles(ctx.routes, outputDirectory);
486
+ writeContentArtifactFiles(ctx.routes, outputDirectory);
424
487
  writeLlmsTxt(ctx.routes, ctx.projectConfig, outputDirectory);
425
488
  }
426
489
  }
@@ -497,6 +560,9 @@ var OPENAPI_HTTP_METHODS = [
497
560
  "patch",
498
561
  "trace"
499
562
  ];
563
+ function errorMessage(error) {
564
+ return error instanceof Error ? error.message : String(error);
565
+ }
500
566
  function kebabToTitle(str) {
501
567
  return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
502
568
  }
@@ -519,7 +585,8 @@ function findOpenAPIRoutes(dir, base = dir) {
519
585
  filePath: fullPath,
520
586
  virtualModuleId: virtualModuleIdFromRef(ref),
521
587
  title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
522
- kind: "openapi"
588
+ kind: "openapi",
589
+ content: readFileSync(fullPath, "utf-8")
523
590
  });
524
591
  }
525
592
  return routes;
@@ -546,9 +613,20 @@ function extractOpenAPISections(spec) {
546
613
  }
547
614
  async function readOpenAPISpec(filePath) {
548
615
  try {
549
- return await SwaggerParser.dereference(filePath);
550
- } catch {
551
- return null;
616
+ return {
617
+ ok: true,
618
+ spec: await SwaggerParser.dereference(filePath)
619
+ };
620
+ } catch (error) {
621
+ return {
622
+ ok: false,
623
+ diagnostic: {
624
+ title: "OpenAPI spec parse failed",
625
+ message: `Clarify could not parse ${filePath}.`,
626
+ filePath,
627
+ cause: errorMessage(error)
628
+ }
629
+ };
552
630
  }
553
631
  }
554
632
  //#endregion
@@ -559,10 +637,31 @@ function generateOpenAPIRegistryModule(openApis) {
559
637
  }
560
638
  function generateOpenAPIModule(spec) {
561
639
  return `import { createElement } from 'react';
562
- import { OpenApiPage } from '@clarify-labs/renderer';
640
+ import { OpenApiDocument } from '@clarify-labs/renderer';
563
641
  const spec = ${JSON.stringify(spec)};
564
642
  export default function OpenApiRoutePage() {
565
- return createElement(OpenApiPage, { spec });
643
+ return createElement(OpenApiDocument, { spec });
644
+ }`;
645
+ }
646
+ function generateOpenAPIErrorModule(diagnostic) {
647
+ return `import { createElement } from 'react';
648
+ const diagnostic = ${JSON.stringify(diagnostic)};
649
+ export default function OpenApiErrorRoutePage() {
650
+ return createElement('article', { className: 'flex h-full flex-col pt-16 pb-10' },
651
+ createElement('div', { className: 'prose max-w-none flex-auto dark:prose-invert' },
652
+ createElement('p', { className: 'mb-3 text-xs/6 font-medium tracking-widest text-red-500 uppercase dark:text-red-400' }, 'OpenAPI Error'),
653
+ createElement('h1', null, diagnostic.title),
654
+ createElement('p', { className: 'lead' }, diagnostic.message),
655
+ 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' },
656
+ createElement('div', { className: 'mb-1 font-semibold text-zinc-900 dark:text-white' }, 'File'),
657
+ createElement('code', { className: 'break-all text-zinc-700 dark:text-zinc-300' }, diagnostic.filePath)
658
+ ) : null,
659
+ 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' },
660
+ createElement('div', { className: 'mb-1 font-semibold' }, 'Why it happened'),
661
+ createElement('pre', { className: 'whitespace-pre-wrap break-words text-xs' }, diagnostic.cause)
662
+ ) : null
663
+ )
664
+ );
566
665
  }`;
567
666
  }
568
667
  //#endregion
@@ -579,18 +678,26 @@ function createOpenAPIPlugin() {
579
678
  "routes:discovered": async (routes) => {
580
679
  for (const key of Object.keys(specs)) delete specs[key];
581
680
  for (const route of routes.filter((route) => route.kind === "openapi")) {
582
- const spec = await readOpenAPISpec(route.filePath);
583
- if (!spec) throw new Error(`Failed to parse OpenAPI spec: ${route.filePath}`);
681
+ const result = await readOpenAPISpec(route.filePath);
682
+ if (!result.ok) {
683
+ route.diagnostic = result.diagnostic;
684
+ route.title = route.title || "OpenAPI parse error";
685
+ route.sections = [];
686
+ continue;
687
+ }
688
+ const spec = result.spec;
584
689
  specs[route.virtualModuleId] = spec;
585
690
  specs[`virtual:clarify-page/${route.path.replace(/^\//, "")}`] = spec;
586
691
  route.title = spec.info?.title ?? route.title;
692
+ route.description = spec.info?.description ?? route.description;
587
693
  route.sections = extractOpenAPISections(spec);
588
694
  }
589
695
  return routes;
590
696
  },
591
- "modules:before": (modules) => {
697
+ "modules:before": (modules, ctx) => {
592
698
  modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(specs));
593
699
  for (const [moduleId, spec] of Object.entries(specs)) modules.set(moduleId, generateOpenAPIModule(spec));
700
+ for (const route of ctx.routes.filter((route) => route.kind === "openapi" && route.diagnostic)) if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
594
701
  return modules;
595
702
  }
596
703
  }
@@ -674,20 +781,122 @@ var clarifyPagesGroupSchema = z.object({
674
781
  pages: z.array(clarifyPagesItemSchema)
675
782
  });
676
783
  var clarifyPagesConfigSchema = z.union([z.array(clarifyPagesGroupSchema), z.literal("FileTree")]);
784
+ var clarifyTabItemSchema = z.object({
785
+ tab: clarifyLocalizedTextSchema,
786
+ icon: z.string().optional(),
787
+ pages: clarifyPagesConfigSchema.optional()
788
+ });
789
+ var clarifyTabsConfigSchema = z.array(clarifyTabItemSchema);
790
+ var clarifyThemePresetSchema = z.enum(["default", "base"]);
791
+ var clarifyThemeColorTokensConfigSchema = z.object({
792
+ primary: z.string().optional(),
793
+ accent: z.string().optional(),
794
+ background: z.string().optional(),
795
+ foreground: z.string().optional(),
796
+ surface: z.string().optional(),
797
+ muted: z.string().optional(),
798
+ border: z.string().optional(),
799
+ codeBackground: z.string().optional()
800
+ });
801
+ var clarifyThemeRadiusTokensConfigSchema = z.object({
802
+ sm: z.string().optional(),
803
+ md: z.string().optional(),
804
+ lg: z.string().optional(),
805
+ xl: z.string().optional()
806
+ });
807
+ var clarifyThemeTokensConfigSchema = z.object({
808
+ colors: clarifyThemeColorTokensConfigSchema.optional(),
809
+ radius: clarifyThemeRadiusTokensConfigSchema.optional()
810
+ });
811
+ var clarifyThemeLayoutConfigSchema = z.object({ maxWidth: z.string().optional() });
812
+ var clarifyThemeConfigSchema = z.object({
813
+ preset: clarifyThemePresetSchema.optional(),
814
+ tokens: clarifyThemeTokensConfigSchema.optional(),
815
+ layout: clarifyThemeLayoutConfigSchema.optional()
816
+ });
677
817
  var clarifyProjectConfigSchema = z.object({
678
818
  title: z.string().optional(),
679
819
  description: z.string().optional(),
680
820
  logo: clarifyLogoConfigSchema.optional(),
681
821
  favicon: clarifyFaviconConfigSchema.optional(),
682
- theme: z.object({ primary: z.string().optional() }).optional(),
822
+ theme: clarifyThemeConfigSchema.optional(),
683
823
  routePrefix: z.string().optional(),
684
824
  navbar: z.object({ links: z.array(clarifyNavbarLinkSchema).optional() }).optional(),
685
825
  banner: clarifyBannerConfigSchema.optional(),
686
826
  footer: clarifyFooterConfigSchema.optional(),
687
827
  i18n: clarifyI18nConfigSchema.optional(),
688
- pages: clarifyPagesConfigSchema.optional()
828
+ tabs: clarifyTabsConfigSchema.optional()
689
829
  });
690
830
  //#endregion
831
+ //#region source/core/theme.ts
832
+ var clarifyThemePresets = {
833
+ default: {
834
+ preset: "default",
835
+ tokens: {
836
+ colors: {
837
+ primary: "#00D492",
838
+ accent: "#00F6C9",
839
+ background: "#ffffff",
840
+ foreground: "#111827",
841
+ surface: "#ffffff",
842
+ muted: "#64748b",
843
+ border: "rgb(15 23 42 / 0.12)",
844
+ codeBackground: "#f7fefb"
845
+ },
846
+ radius: {
847
+ sm: "6px",
848
+ md: "10px",
849
+ lg: "14px",
850
+ xl: "18px"
851
+ }
852
+ },
853
+ layout: { maxWidth: "82rem" }
854
+ },
855
+ base: {
856
+ preset: "base",
857
+ tokens: {
858
+ colors: {
859
+ primary: "#18181b",
860
+ accent: "#52525b",
861
+ background: "#ffffff",
862
+ foreground: "#18181b",
863
+ surface: "#ffffff",
864
+ muted: "#71717a",
865
+ border: "rgb(24 24 27 / 0.12)",
866
+ codeBackground: "#f4f4f5"
867
+ },
868
+ radius: {
869
+ sm: "6px",
870
+ md: "8px",
871
+ lg: "12px",
872
+ xl: "16px"
873
+ }
874
+ },
875
+ layout: { maxWidth: "80rem" }
876
+ }
877
+ };
878
+ function resolveThemeConfig(theme = {}) {
879
+ const preset = theme.preset ?? "default";
880
+ const presetTheme = clarifyThemePresets[preset];
881
+ return {
882
+ preset,
883
+ tokens: {
884
+ colors: {
885
+ ...presetTheme.tokens.colors,
886
+ ...theme.tokens?.colors
887
+ },
888
+ radius: {
889
+ ...presetTheme.tokens.radius,
890
+ ...theme.tokens?.radius
891
+ }
892
+ },
893
+ layout: {
894
+ ...presetTheme.layout,
895
+ ...theme.layout
896
+ }
897
+ };
898
+ }
899
+ //#endregion
691
900
  //#region source/core/config.ts
692
901
  function formatIssuePath(path) {
693
902
  return path.reduce((result, segment) => {
@@ -726,12 +935,12 @@ function resolveProjectConfig(config = {}) {
726
935
  logo: config.logo,
727
936
  favicon: config.favicon,
728
937
  routePrefix: config.routePrefix ?? "/",
729
- theme: config.theme ?? {},
938
+ theme: resolveThemeConfig(config.theme),
730
939
  navbar: config.navbar,
731
940
  banner: config.banner,
732
941
  footer: config.footer,
733
942
  i18n: resolveI18nConfig(config.i18n),
734
- pages: config.pages
943
+ tabs: config.tabs
735
944
  };
736
945
  }
737
946
  //#endregion
@@ -871,18 +1080,28 @@ function injectHtmlLocaleAttributes(html, projectConfig, route) {
871
1080
  return `<html${nextAttributes}>`;
872
1081
  });
873
1082
  }
1083
+ function routeTitle(projectConfig, route) {
1084
+ const title = route?.title?.trim();
1085
+ if (!title || title === projectConfig.title) return projectConfig.title;
1086
+ return `${title} - ${projectConfig.title}`;
1087
+ }
1088
+ function setNamedMeta(html, name, content) {
1089
+ const metaPattern = new RegExp(`<meta\\b(?=[^>]*\\bname=["']${name}["'])[^>]*>\\n?`, "i");
1090
+ if (!content) return html.replace(metaPattern, "");
1091
+ const meta = `<meta name="${name}" content="${escapeHtml(content)}" />`;
1092
+ if (metaPattern.test(html)) return html.replace(metaPattern, meta);
1093
+ return html.replace("</head>", ` ${meta}\n </head>`);
1094
+ }
874
1095
  function injectSSRIntoTemplate(template, appHtml, projectConfig, route) {
875
1096
  let html = injectHtmlLocaleAttributes(template, projectConfig, route);
876
- html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(projectConfig.title)}</title>`);
877
- if (projectConfig.description && !html.includes("name=\"description\"")) {
878
- const descriptionMeta = `<meta name="description" content="${escapeHtml(projectConfig.description)}" />`;
879
- html = html.replace("</head>", ` ${descriptionMeta}\n </head>`);
880
- }
1097
+ html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(routeTitle(projectConfig, route))}</title>`);
1098
+ html = setNamedMeta(html, "description", route?.description ?? projectConfig.description);
1099
+ html = setNamedMeta(html, "keywords", route?.keywords?.join(", "));
881
1100
  html = html.replace(/<div id="root">[\s\S]*?<\/div>/, `<div id="root">${appHtml}</div>`);
882
1101
  return html;
883
1102
  }
884
1103
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
885
- import { routes, navigation } from 'virtual:clarify-routes';
1104
+ import { routes, navigation } from 'virtual:clarify-routes/server';
886
1105
  import { config } from 'virtual:clarify-config';
887
1106
  import { openApis } from 'virtual:clarify-openapi-registry';
888
1107
 
@@ -904,7 +1123,10 @@ async function buildSSRBundle(root, ssrEntry, ssrOutDir, plugins) {
904
1123
  ssr: ssrEntry,
905
1124
  outDir: ssrOutDir,
906
1125
  emptyOutDir: true,
907
- rollupOptions: { input: ssrEntry }
1126
+ rolldownOptions: {
1127
+ input: ssrEntry,
1128
+ checks: { pluginTimings: false }
1129
+ }
908
1130
  }
909
1131
  });
910
1132
  }
@@ -961,6 +1183,7 @@ async function loadClarifyConfig(root, env) {
961
1183
  //#region source/core/virtual-modules.ts
962
1184
  var VIRTUAL_CONFIG = "virtual:clarify-config";
963
1185
  var VIRTUAL_ROUTES = "virtual:clarify-routes";
1186
+ var VIRTUAL_SERVER_ROUTES = "virtual:clarify-routes/server";
964
1187
  var VIRTUAL_OPENAPI_REGISTRY = openApiRegistryModuleId;
965
1188
  var VIRTUAL_CLIENT_ENTRY = "virtual:clarify-entry-client";
966
1189
  var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify-entry-client";
@@ -976,8 +1199,11 @@ function generateConfigModule(projectConfig, buildOptions) {
976
1199
  ...buildOptions
977
1200
  })};`;
978
1201
  }
979
- function generateRoutesModule(routes, pagesConfig, resolvedNavigation, projectConfig) {
980
- const imports = routes.map((r, i) => `import Page${i} from '${r.virtualModuleId}';`).join("\n");
1202
+ function moduleSpecifier(value) {
1203
+ return JSON.stringify(value);
1204
+ }
1205
+ function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode = "client") {
1206
+ const imports = mode === "server" ? routes.map((r, i) => `import Page${i} from ${moduleSpecifier(r.virtualModuleId)};`).join("\n") : "";
981
1207
  const routesArray = routes.map((r, i) => {
982
1208
  const sections = r.sections && r.sections.length > 0 ? `, sections: ${JSON.stringify(r.sections.map((s) => ({
983
1209
  id: s.id,
@@ -985,14 +1211,18 @@ function generateRoutesModule(routes, pagesConfig, resolvedNavigation, projectCo
985
1211
  badge: s.badge,
986
1212
  tags: s.tags
987
1213
  })))}` : "";
988
- const rawContentUrl = r.rawContentUrl ? `, rawContentUrl: ${JSON.stringify(r.rawContentUrl)}` : "";
1214
+ const contentArtifactUrl = r.contentArtifactUrl ? `, contentArtifactUrl: ${JSON.stringify(r.contentArtifactUrl)}` : "";
989
1215
  const basePath = r.basePath ? `, basePath: ${JSON.stringify(r.basePath)}` : "";
990
1216
  const locale = r.locale ? `, locale: ${JSON.stringify(r.locale)}` : "";
991
1217
  const isFallback = r.isFallback ? ", isFallback: true" : "";
992
1218
  const alternates = r.alternates ? `, alternates: ${JSON.stringify(r.alternates)}` : "";
993
- return ` { path: ${JSON.stringify(r.path)}, title: ${JSON.stringify(r.title)}, component: Page${i}, kind: '${r.kind}'${basePath}${locale}${isFallback}${alternates}${sections}${rawContentUrl} }`;
1219
+ const description = r.description ? `, description: ${JSON.stringify(r.description)}` : "";
1220
+ const keywords = r.keywords && r.keywords.length > 0 ? `, keywords: ${JSON.stringify(r.keywords)}` : "";
1221
+ const component = mode === "server" ? `Page${i}` : `() => import(${moduleSpecifier(r.virtualModuleId)})`;
1222
+ const lazy = mode === "client" ? ", lazy: true" : "";
1223
+ 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} }`;
994
1224
  }).join(",\n");
995
- const navigation = resolvedNavigation ?? (projectConfig?.i18n ? buildLocalizedNavigation(routes, pagesConfig, projectConfig.i18n) ?? {} : pagesConfig && pagesConfig !== "FileTree" ? buildNavigationFromConfig(routes, pagesConfig) : buildNavigation(routes));
1225
+ const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
996
1226
  return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
997
1227
  }
998
1228
  function createClientEntryModule() {
@@ -1007,13 +1237,14 @@ render({ config, routes, navigation, openApis });`;
1007
1237
  function buildVirtualModules(args) {
1008
1238
  const modules = /* @__PURE__ */ new Map();
1009
1239
  modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
1010
- modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.projectConfig.pages, args.navigation, args.projectConfig));
1240
+ modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
1241
+ modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "server"));
1011
1242
  modules.set(VIRTUAL_OPENAPI_REGISTRY, "export const openApis = {};");
1012
1243
  modules.set(VIRTUAL_CLIENT_ENTRY, createClientEntryModule());
1013
1244
  modules.set(RESOLVED_CLIENT_ENTRY, createClientEntryModule());
1014
1245
  for (const route of args.routes) {
1015
1246
  if (route.kind === "openapi") continue;
1016
- modules.set(route.virtualModuleId, `export { default } from '${route.filePath}';`);
1247
+ modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
1017
1248
  }
1018
1249
  return modules;
1019
1250
  }
@@ -1118,7 +1349,7 @@ function clarifyPlugin(options = {}) {
1118
1349
  async function resolveRoutesAndSpecs() {
1119
1350
  routes = await discoverRoutes();
1120
1351
  routes = await runHooks(clarifyPlugins, "routes:discovered", routes, ctx);
1121
- const defaultNavigation = projectConfig.i18n ? buildLocalizedNavigation(routes, projectConfig.pages, projectConfig.i18n) ?? {} : projectConfig.pages && projectConfig.pages !== "FileTree" ? buildNavigationFromConfig(routes, projectConfig.pages) : buildNavigation(routes);
1352
+ const defaultNavigation = projectConfig.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes);
1122
1353
  const resolved = await runHooks(clarifyPlugins, "routes:resolved", {
1123
1354
  routes,
1124
1355
  navigation: defaultNavigation
@@ -1152,6 +1383,20 @@ function clarifyPlugin(options = {}) {
1152
1383
  function hasContentRouteForFile(filePath) {
1153
1384
  return routes.some((route) => route.filePath === filePath);
1154
1385
  }
1386
+ const normalizedMdxContentPlugin = {
1387
+ name: "clarify:normalized-mdx-content",
1388
+ enforce: "pre",
1389
+ transform(_code, id) {
1390
+ if (!/\.mdx?(?:\?|$)/.test(id)) return null;
1391
+ const filePath = id.replace(/\?.*$/, "");
1392
+ const route = routes.find((route) => route.kind === "mdx" && route.filePath === filePath);
1393
+ if (!route || route.content === void 0) return null;
1394
+ return {
1395
+ code: route.content,
1396
+ map: null
1397
+ };
1398
+ }
1399
+ };
1155
1400
  const mdx = mdxPlugin({
1156
1401
  include: ["**/*.{md,mdx}"],
1157
1402
  jsxImportSource: "react",
@@ -1162,6 +1407,7 @@ function clarifyPlugin(options = {}) {
1162
1407
  return [
1163
1408
  react(),
1164
1409
  tailwindcss(),
1410
+ normalizedMdxContentPlugin,
1165
1411
  {
1166
1412
  name: "clarify:core",
1167
1413
  async config() {
@@ -1197,6 +1443,7 @@ function clarifyPlugin(options = {}) {
1197
1443
  if (id === "virtual:clarify-entry-client" || id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1198
1444
  if (id === "virtual:clarify-config" || id === resolveVirtualId("virtual:clarify-config")) return resolveVirtualId(VIRTUAL_CONFIG);
1199
1445
  if (id === "virtual:clarify-routes" || id === resolveVirtualId("virtual:clarify-routes")) return resolveVirtualId(VIRTUAL_ROUTES);
1446
+ if (id === "virtual:clarify-routes/server" || id === resolveVirtualId("virtual:clarify-routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
1200
1447
  if (id === VIRTUAL_OPENAPI_REGISTRY || id === resolveVirtualId(VIRTUAL_OPENAPI_REGISTRY)) return resolveVirtualId(VIRTUAL_OPENAPI_REGISTRY);
1201
1448
  const route = routes.find((r) => r.virtualModuleId === id || r.virtualModuleId === stripVirtualPrefix(id));
1202
1449
  if (route) return resolveVirtualId(route.virtualModuleId);
@@ -1242,19 +1489,24 @@ function clarifyPlugin(options = {}) {
1242
1489
  let tempEntryPath;
1243
1490
  try {
1244
1491
  tempEntryPath = createTempEntryFile(SSR_ENTRY_CODE);
1245
- await buildSSRBundle(root, tempEntryPath, ssrOutputDir, [{
1246
- name: "clarify:virtual-ssg",
1247
- resolveId(id) {
1248
- if (id === "virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1249
- if (id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1250
- if (id === "virtual:clarify-config") return id;
1251
- if (id === "virtual:clarify-routes") return id;
1252
- if (id === VIRTUAL_OPENAPI_REGISTRY) return id;
1253
- if (routes.find((r) => r.virtualModuleId === id)) return id;
1254
- return null;
1492
+ await buildSSRBundle(root, tempEntryPath, ssrOutputDir, [
1493
+ {
1494
+ name: "clarify:virtual-ssg",
1495
+ resolveId(id) {
1496
+ if (id === "virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1497
+ if (id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1498
+ if (id === "virtual:clarify-config") return id;
1499
+ if (id === "virtual:clarify-routes") return id;
1500
+ if (id === "virtual:clarify-routes/server") return id;
1501
+ if (id === VIRTUAL_OPENAPI_REGISTRY) return id;
1502
+ if (routes.find((r) => r.virtualModuleId === id)) return id;
1503
+ return null;
1504
+ },
1505
+ load: (id) => loadVirtualModule(id, virtualModules)
1255
1506
  },
1256
- load: (id) => loadVirtualModule(id, virtualModules)
1257
- }, mdx]);
1507
+ normalizedMdxContentPlugin,
1508
+ mdx
1509
+ ]);
1258
1510
  const ssrBundlePath = join(ssrOutputDir, "entry-server.js");
1259
1511
  await renderSSGRoutes(routes, projectConfig, outputDir, ssrBundlePath, generateOptions.ssg.failOnError);
1260
1512
  } catch (err) {
@@ -1274,4 +1526,4 @@ function clarifyPlugin(options = {}) {
1274
1526
  //#endregion
1275
1527
  export { createClarifyRuntimeAliases as i, defineConfig as n, loadClarifyConfig as r, clarifyPlugin as t };
1276
1528
 
1277
- //# sourceMappingURL=plugin-BUWojlPv.js.map
1529
+ //# sourceMappingURL=plugin-PEEknlAO.js.map