@clarify-labs/cli 0.6.4 → 0.6.5

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.
@@ -8,10 +8,11 @@ import { toString } from "mdast-util-to-string";
8
8
  import shiki from "shiki";
9
9
  import { visit } from "unist-util-visit";
10
10
  import { markdownRemarkPlugins } from "@clarify-labs/renderer";
11
+ import { z } from "zod";
11
12
  import { remark } from "remark";
12
13
  import matter from "gray-matter";
13
14
  import SwaggerParser from "@apidevtools/swagger-parser";
14
- import { z } from "zod";
15
+ import { mkdir, writeFile } from "node:fs/promises";
15
16
  import { tmpdir } from "node:os";
16
17
  import { pathToFileURL } from "node:url";
17
18
  import { build, loadConfigFromFile } from "vite";
@@ -100,65 +101,520 @@ var rehypePlugins = [
100
101
  rehypeShiki
101
102
  ];
102
103
  //#endregion
103
- //#region source/parsers/frontmatter.ts
104
- /**
105
- * 在内容入口统一解析 MDX frontmatter,并返回可供后续流程复用的标准化正文。
106
- */
107
- function parseFrontmatter(content) {
108
- const parsed = matter(content);
104
+ //#region source/core/config-schema.ts
105
+ var clarifyLogoConfigSchema = z.union([z.string(), z.object({
106
+ light: z.string().optional(),
107
+ dark: z.string().optional()
108
+ })]);
109
+ var clarifyFaviconConfigSchema = z.union([z.string(), z.object({
110
+ light: z.string().optional(),
111
+ dark: z.string().optional()
112
+ })]);
113
+ var clarifyLocalizedTextSchema = z.union([z.string(), z.record(z.string(), z.string())]);
114
+ var clarifyLocaleConfigSchema = z.object({
115
+ code: z.string(),
116
+ label: z.string(),
117
+ dir: z.union([z.literal("ltr"), z.literal("rtl")]).optional()
118
+ });
119
+ var clarifyI18nConfigSchema = z.object({
120
+ defaultLocale: z.string().optional(),
121
+ missing: z.union([
122
+ z.literal("fallback"),
123
+ z.literal("404"),
124
+ z.literal("hide")
125
+ ]).optional(),
126
+ locales: z.array(clarifyLocaleConfigSchema).min(1)
127
+ }).superRefine((config, ctx) => {
128
+ const localeCodes = /* @__PURE__ */ new Set();
129
+ for (const [index, locale] of config.locales.entries()) {
130
+ if (localeCodes.has(locale.code)) ctx.addIssue({
131
+ code: z.ZodIssueCode.custom,
132
+ message: `Duplicate locale code "${locale.code}"`,
133
+ path: [
134
+ "locales",
135
+ index,
136
+ "code"
137
+ ]
138
+ });
139
+ localeCodes.add(locale.code);
140
+ }
141
+ if (config.defaultLocale && !localeCodes.has(config.defaultLocale)) ctx.addIssue({
142
+ code: z.ZodIssueCode.custom,
143
+ message: "defaultLocale must be one of i18n.locales",
144
+ path: ["defaultLocale"]
145
+ });
146
+ });
147
+ var clarifyNavbarLinkSchema = z.object({
148
+ label: clarifyLocalizedTextSchema,
149
+ href: z.string(),
150
+ external: z.boolean().optional()
151
+ });
152
+ var clarifyBannerConfigSchema = z.object({
153
+ content: clarifyLocalizedTextSchema,
154
+ dismissible: z.boolean().optional()
155
+ });
156
+ var clarifyFooterConfigSchema = z.object({
157
+ links: z.array(clarifyNavbarLinkSchema).optional(),
158
+ socials: z.record(z.string(), z.string()).optional(),
159
+ copyright: clarifyLocalizedTextSchema.optional()
160
+ });
161
+ var clarifySourceConfigSchema = z.object({
162
+ repository: z.string(),
163
+ branch: z.string().optional(),
164
+ directory: z.string().optional()
165
+ });
166
+ var clarifyPagesItemSchema = z.union([
167
+ z.string(),
168
+ z.object({
169
+ page: z.string(),
170
+ title: clarifyLocalizedTextSchema.optional(),
171
+ icon: z.string().optional(),
172
+ redirect: z.string().optional()
173
+ }),
174
+ z.object({
175
+ openapi: z.string(),
176
+ icon: z.string().optional(),
177
+ title: clarifyLocalizedTextSchema.optional(),
178
+ filter: z.object({ tags: z.array(z.string()).optional() }).optional()
179
+ })
180
+ ]);
181
+ var clarifyPagesGroupSchema = z.object({
182
+ group: clarifyLocalizedTextSchema,
183
+ icon: z.string().optional(),
184
+ pages: z.array(clarifyPagesItemSchema)
185
+ });
186
+ var clarifyPagesConfigSchema = z.union([z.array(clarifyPagesGroupSchema), z.literal("FileTree")]);
187
+ var clarifyTabItemSchema = z.object({
188
+ tab: clarifyLocalizedTextSchema,
189
+ icon: z.string().optional(),
190
+ pages: clarifyPagesConfigSchema.optional()
191
+ });
192
+ var clarifyTabsConfigSchema = z.array(clarifyTabItemSchema);
193
+ var clarifyThemePresetSchema = z.enum(["default", "base"]);
194
+ var clarifyThemeModeColorValueSchema = z.object({
195
+ light: z.string().optional(),
196
+ dark: z.string().optional()
197
+ });
198
+ var clarifyThemeColorValueSchema = z.union([z.string(), clarifyThemeModeColorValueSchema]);
199
+ var clarifyThemeColorTokensConfigSchema = z.object({
200
+ primary: clarifyThemeColorValueSchema.optional(),
201
+ accent: clarifyThemeColorValueSchema.optional(),
202
+ background: clarifyThemeColorValueSchema.optional(),
203
+ foreground: clarifyThemeColorValueSchema.optional(),
204
+ surface: clarifyThemeColorValueSchema.optional(),
205
+ muted: clarifyThemeColorValueSchema.optional(),
206
+ border: clarifyThemeColorValueSchema.optional(),
207
+ codeBackground: clarifyThemeColorValueSchema.optional()
208
+ });
209
+ var clarifyThemeRadiusTokensConfigSchema = z.object({
210
+ sm: z.string().optional(),
211
+ md: z.string().optional(),
212
+ lg: z.string().optional(),
213
+ xl: z.string().optional()
214
+ });
215
+ var clarifyThemeTokensConfigSchema = z.object({
216
+ colors: clarifyThemeColorTokensConfigSchema.optional(),
217
+ radius: clarifyThemeRadiusTokensConfigSchema.optional()
218
+ });
219
+ var clarifyThemeLayoutConfigSchema = z.object({ maxWidth: z.string().optional() });
220
+ var clarifyThemeConfigSchema = z.object({
221
+ preset: clarifyThemePresetSchema.optional(),
222
+ tokens: clarifyThemeTokensConfigSchema.optional(),
223
+ layout: clarifyThemeLayoutConfigSchema.optional(),
224
+ editor: z.boolean().optional()
225
+ });
226
+ var clarifyProjectConfigSchema = z.object({
227
+ title: z.string().optional(),
228
+ description: z.string().optional(),
229
+ siteUrl: z.string().optional(),
230
+ source: clarifySourceConfigSchema.optional(),
231
+ logo: clarifyLogoConfigSchema.optional(),
232
+ favicon: clarifyFaviconConfigSchema.optional(),
233
+ theme: clarifyThemeConfigSchema.optional(),
234
+ routePrefix: z.string().optional(),
235
+ navbar: z.object({ links: z.array(clarifyNavbarLinkSchema).optional() }).optional(),
236
+ banner: clarifyBannerConfigSchema.optional(),
237
+ footer: clarifyFooterConfigSchema.optional(),
238
+ i18n: clarifyI18nConfigSchema.optional(),
239
+ tabs: clarifyTabsConfigSchema.optional()
240
+ });
241
+ //#endregion
242
+ //#region source/core/theme.ts
243
+ var clarifyThemePresets = {
244
+ default: {
245
+ preset: "default",
246
+ tokens: {
247
+ colors: {
248
+ primary: "#047857",
249
+ accent: "#0D9488",
250
+ background: "#ffffff",
251
+ foreground: "#111827",
252
+ surface: "#ffffff",
253
+ muted: "#64748b",
254
+ border: "rgb(15 23 42 / 0.12)",
255
+ codeBackground: "#f6fbf8"
256
+ },
257
+ radius: {
258
+ sm: "6px",
259
+ md: "10px",
260
+ lg: "14px",
261
+ xl: "18px"
262
+ }
263
+ },
264
+ layout: { maxWidth: "82rem" },
265
+ editor: false
266
+ },
267
+ base: {
268
+ preset: "base",
269
+ tokens: {
270
+ colors: {
271
+ primary: "#18181b",
272
+ accent: "#52525b",
273
+ background: "#ffffff",
274
+ foreground: "#18181b",
275
+ surface: "#ffffff",
276
+ muted: "#71717a",
277
+ border: "rgb(24 24 27 / 0.12)",
278
+ codeBackground: "#f4f4f5"
279
+ },
280
+ radius: {
281
+ sm: "6px",
282
+ md: "8px",
283
+ lg: "12px",
284
+ xl: "16px"
285
+ }
286
+ },
287
+ layout: { maxWidth: "80rem" },
288
+ editor: false
289
+ }
290
+ };
291
+ function resolveThemeConfig(theme = {}) {
292
+ const preset = theme.preset ?? "default";
293
+ const presetTheme = clarifyThemePresets[preset];
109
294
  return {
110
- frontmatter: parsed.data,
111
- content: parsed.content.replace(/^\r?\n/, "")
295
+ preset,
296
+ tokens: {
297
+ colors: {
298
+ ...presetTheme.tokens.colors,
299
+ ...theme.tokens?.colors
300
+ },
301
+ radius: {
302
+ ...presetTheme.tokens.radius,
303
+ ...theme.tokens?.radius
304
+ }
305
+ },
306
+ layout: {
307
+ ...presetTheme.layout,
308
+ ...theme.layout
309
+ },
310
+ editor: theme.editor ?? presetTheme.editor
112
311
  };
113
312
  }
114
313
  //#endregion
115
- //#region source/parsers/routes.ts
116
- function kebabToTitle$1(str) {
117
- return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
118
- }
119
- function parseMdxTree(content) {
120
- return remark.parse(content);
121
- }
122
- /** 从内容中提取第一个 H1 标题 */
123
- function extractH1(content) {
124
- const tree = parseMdxTree(content);
125
- let title = "";
126
- visit(tree, "heading", (node) => {
127
- if (!title && node.depth === 1) title = toString(node);
128
- });
129
- return title;
314
+ //#region source/core/config.ts
315
+ function formatIssuePath(path) {
316
+ return path.reduce((result, segment) => {
317
+ if (typeof segment === "number") return `${result}[${segment}]`;
318
+ const key = String(segment);
319
+ return result ? `${result}.${key}` : key;
320
+ }, "");
130
321
  }
131
- /** 从 MDX/Markdown 内容中提取 H2/H3 章节 */
132
- function extractMdxSections(content) {
133
- const sections = [];
134
- const slugger = new GithubSlugger();
135
- visit(parseMdxTree(content), "heading", (node) => {
136
- if (node.depth !== 2 && node.depth !== 3) return;
137
- const title = toString(node);
138
- sections.push({
139
- id: slugger.slug(title),
140
- title,
141
- level: node.depth
142
- });
143
- });
144
- return sections;
322
+ function formatProjectConfigError(error) {
323
+ const issue = error.issues[0];
324
+ if (!issue) return "[clarify] config is invalid";
325
+ const path = formatIssuePath(issue.path);
326
+ if (!path) return `[clarify] config must contain an object: ${issue.message}`;
327
+ return `[clarify] config field "${path}" is invalid: ${issue.message}`;
145
328
  }
146
- function navigationSections(sections) {
147
- return sections.map((s) => ({
148
- id: s.id,
149
- title: s.title,
150
- badge: s.badge,
151
- tags: s.tags
152
- }));
329
+ function validateProjectConfig(value) {
330
+ const result = clarifyProjectConfigSchema.safeParse(value);
331
+ if (!result.success) throw new Error(formatProjectConfigError(result.error));
332
+ return result.data;
153
333
  }
154
- function normalizePath(path) {
155
- const clean = "/" + path.replace(/^\/+|\/+$/g, "");
156
- return clean === "/" ? "/" : clean.replace(/\/+/g, "/");
334
+ function resolveI18nConfig(i18n) {
335
+ if (!i18n) return void 0;
336
+ const firstLocale = i18n.locales[0]?.code;
337
+ const defaultLocale = i18n.defaultLocale ?? firstLocale;
338
+ if (!defaultLocale) return void 0;
339
+ return {
340
+ defaultLocale,
341
+ missing: i18n.missing ?? "fallback",
342
+ locales: i18n.locales
343
+ };
157
344
  }
158
- function basePathFromRef(ref) {
345
+ function resolveProjectConfig(config = {}) {
346
+ return {
347
+ title: config.title ?? "Clarify Docs",
348
+ description: config.description ?? "",
349
+ siteUrl: config.siteUrl,
350
+ source: config.source,
351
+ logo: config.logo,
352
+ favicon: config.favicon,
353
+ routePrefix: config.routePrefix ?? "/",
354
+ theme: resolveThemeConfig(config.theme),
355
+ navbar: config.navbar,
356
+ banner: config.banner,
357
+ footer: config.footer,
358
+ i18n: resolveI18nConfig(config.i18n),
359
+ tabs: config.tabs
360
+ };
361
+ }
362
+ //#endregion
363
+ //#region source/plugins/content-artifacts/artifacts.ts
364
+ var UTF8_SIGNATURE = "";
365
+ function normalizeBasePath(basePath) {
366
+ if (!basePath || basePath === "/") return "";
367
+ return "/" + basePath.replace(/^\/+|\/+$/g, "");
368
+ }
369
+ function containsNonAscii(content) {
370
+ return [...content].some((character) => character.codePointAt(0) > 127);
371
+ }
372
+ function withUtf8Signature(content) {
373
+ if (!containsNonAscii(content) || content.startsWith(UTF8_SIGNATURE)) return content;
374
+ return `${UTF8_SIGNATURE}${content}`;
375
+ }
376
+ function shouldUseUtf8Signature(route) {
377
+ if (route.kind === "mdx") return true;
378
+ return /\.ya?ml$/i.test(route.contentArtifactUrl ?? "");
379
+ }
380
+ function routeToMarkdownArtifactUrl(routePath) {
381
+ return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.md`;
382
+ }
383
+ function routeToOpenAPIArtifactUrl(routePath, filePath) {
384
+ const extension = extname(filePath).toLowerCase() || ".json";
385
+ return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi${extension}`;
386
+ }
387
+ function attachContentArtifactUrls(routes) {
388
+ for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path, route.filePath) : routeToMarkdownArtifactUrl(route.path);
389
+ }
390
+ function readRouteContent(route) {
391
+ if (route.content !== void 0) return route.content;
392
+ throw new Error(`Route content is missing from route context: ${route.filePath}`);
393
+ }
394
+ function readRouteArtifactContent(route) {
395
+ const content = readRouteContent(route);
396
+ return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
397
+ }
398
+ function writeContentArtifactFiles(routes, outputDirectory) {
399
+ for (const route of routes) {
400
+ if (!route.contentArtifactUrl) continue;
401
+ const outFile = join(outputDirectory, route.contentArtifactUrl.replace(/^\//, ""));
402
+ mkdirSync(dirname(outFile), { recursive: true });
403
+ writeFileSync(outFile, readRouteArtifactContent(route), "utf-8");
404
+ }
405
+ }
406
+ function createLlmsTxt(routes, projectConfig) {
407
+ const basePath = normalizeBasePath(projectConfig.routePrefix);
408
+ const lines = [`# ${projectConfig.title}`, ""];
409
+ if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
410
+ lines.push("## Docs");
411
+ for (const route of routes.filter((route) => route.kind === "mdx")) {
412
+ if (!route.contentArtifactUrl) continue;
413
+ lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
414
+ }
415
+ const openApiRoutes = routes.filter((route) => route.kind === "openapi");
416
+ if (openApiRoutes.length > 0) {
417
+ lines.push("", "## OpenAPI");
418
+ for (const route of openApiRoutes) {
419
+ if (!route.contentArtifactUrl) continue;
420
+ lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
421
+ }
422
+ }
423
+ return `${lines.join("\n")}\n`;
424
+ }
425
+ function createLlmsTxtArtifact(routes, projectConfig) {
426
+ return withUtf8Signature(createLlmsTxt(routes, projectConfig));
427
+ }
428
+ function writeLlmsTxt(routes, projectConfig, outputDirectory) {
429
+ mkdirSync(outputDirectory, { recursive: true });
430
+ writeFileSync(join(outputDirectory, "llms.txt"), createLlmsTxtArtifact(routes, projectConfig), "utf-8");
431
+ }
432
+ //#endregion
433
+ //#region source/plugins/content-artifacts/server.ts
434
+ function normalizeRoutePrefix$1(routePrefix) {
435
+ if (!routePrefix || routePrefix === "/") return "";
436
+ return `/${routePrefix.replace(/^\/+|\/+$/g, "")}`;
437
+ }
438
+ function resolveContentArtifactPath(url, projectConfig) {
439
+ const requestPath = url?.split("?")[0] ?? "";
440
+ const basePath = normalizeRoutePrefix$1(projectConfig.routePrefix);
441
+ return basePath && requestPath.startsWith(basePath) ? requestPath.slice(basePath.length) || "/" : requestPath;
442
+ }
443
+ function resolveContentArtifactType(route) {
444
+ if (route.kind === "openapi" && /\.ya?ml$/i.test(route.contentArtifactUrl ?? "")) return "text/yaml; charset=utf-8";
445
+ return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
446
+ }
447
+ function serveContentArtifacts(req, res, projectConfig, routes) {
448
+ const contentPath = resolveContentArtifactPath(req.url, projectConfig);
449
+ if (contentPath === "/llms.txt") {
450
+ res.statusCode = 200;
451
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
452
+ res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
453
+ return true;
454
+ }
455
+ const route = routes.find((route) => route.contentArtifactUrl === contentPath);
456
+ if (!route) return false;
457
+ res.statusCode = 200;
458
+ res.setHeader("Content-Type", resolveContentArtifactType(route));
459
+ res.end(readRouteArtifactContent(route), "utf8");
460
+ return true;
461
+ }
462
+ //#endregion
463
+ //#region source/plugins/content-artifacts/index.ts
464
+ function createContentArtifactsPlugin() {
465
+ return {
466
+ name: "clarify:content-artifacts",
467
+ hooks: {
468
+ "routes:resolved": (input) => {
469
+ attachContentArtifactUrls(input.routes);
470
+ return input;
471
+ },
472
+ "dev:configureServer": (server, ctx) => {
473
+ server.middlewares.use((req, res, next) => {
474
+ if (serveContentArtifacts(req, res, ctx.projectConfig, ctx.routes)) return;
475
+ next();
476
+ });
477
+ },
478
+ "build:done": (ctx) => {
479
+ const outputDirectory = ctx.generateOptions.outputDirectory;
480
+ if (!outputDirectory) return;
481
+ const outputRoot = join(ctx.generateOptions.projectRoot, outputDirectory);
482
+ writeContentArtifactFiles(ctx.routes, outputRoot);
483
+ writeLlmsTxt(ctx.routes, ctx.projectConfig, outputRoot);
484
+ }
485
+ }
486
+ };
487
+ }
488
+ //#endregion
489
+ //#region source/plugins/html-shell/index.ts
490
+ 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){}})();`;
491
+ var REL_ICON_RE = /<link\s+[^>]*rel=["'][^"']*\bicon\b[^"']*["'][^>]*>/i;
492
+ function inferIconType(href) {
493
+ const path = href.split(/[?#]/, 1)[0]?.toLowerCase() ?? href.toLowerCase();
494
+ if (path.endsWith(".svg")) return "image/svg+xml";
495
+ if (path.endsWith(".png")) return "image/png";
496
+ if (path.endsWith(".ico")) return "image/x-icon";
497
+ if (path.endsWith(".webp")) return "image/webp";
498
+ }
499
+ function createIconTag(href, media) {
500
+ return {
501
+ tag: "link",
502
+ attrs: {
503
+ rel: "icon",
504
+ ...inferIconType(href) ? { type: inferIconType(href) } : {},
505
+ href,
506
+ ...media ? { media } : {}
507
+ },
508
+ injectTo: "head"
509
+ };
510
+ }
511
+ function createIconTags(icon) {
512
+ if (!icon) return [];
513
+ if (typeof icon === "string") return [createIconTag(icon)];
514
+ const lightIcon = icon.light ?? icon.dark;
515
+ const darkIcon = icon.dark ?? icon.light;
516
+ if (!lightIcon || !darkIcon) return [];
517
+ if (lightIcon === darkIcon) return [createIconTag(lightIcon)];
518
+ return [createIconTag(lightIcon, "(prefers-color-scheme: light)"), createIconTag(darkIcon, "(prefers-color-scheme: dark)")];
519
+ }
520
+ function createHtmlShellPlugin() {
521
+ return {
522
+ name: "clarify:html-shell",
523
+ hooks: { "html:transform": (input, ctx) => ({
524
+ html: input.html,
525
+ tags: [
526
+ ...input.tags,
527
+ ...REL_ICON_RE.test(input.html) ? [] : createIconTags(ctx.projectConfig.favicon ?? ctx.projectConfig.logo),
528
+ {
529
+ tag: "script",
530
+ children: THEME_BOOTSTRAP_SCRIPT,
531
+ injectTo: "head-prepend"
532
+ },
533
+ {
534
+ tag: "script",
535
+ attrs: {
536
+ type: "module",
537
+ src: input.clientEntryId
538
+ },
539
+ injectTo: "body"
540
+ }
541
+ ],
542
+ clientEntryId: input.clientEntryId,
543
+ dev: input.dev
544
+ }) }
545
+ };
546
+ }
547
+ //#endregion
548
+ //#region source/parsers/frontmatter.ts
549
+ /**
550
+ * 在内容入口统一解析 MDX frontmatter,并返回可供后续流程复用的标准化正文。
551
+ */
552
+ function parseFrontmatter(content) {
553
+ const parsed = matter(content);
554
+ return {
555
+ frontmatter: parsed.data,
556
+ content: parsed.content.replace(/^\r?\n/, "")
557
+ };
558
+ }
559
+ //#endregion
560
+ //#region source/parsers/routes.ts
561
+ function kebabToTitle$1(str) {
562
+ return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
563
+ }
564
+ function parseMdxTree(content) {
565
+ return remark.parse(content);
566
+ }
567
+ /** 从内容中提取第一个 H1 标题 */
568
+ function extractH1(content) {
569
+ const tree = parseMdxTree(content);
570
+ let title = "";
571
+ visit(tree, "heading", (node) => {
572
+ if (!title && node.depth === 1) title = toString(node);
573
+ });
574
+ return title;
575
+ }
576
+ /** 从 MDX/Markdown 内容中提取 H2/H3 章节 */
577
+ function extractMdxSections(content) {
578
+ const sections = [];
579
+ const slugger = new GithubSlugger();
580
+ visit(parseMdxTree(content), "heading", (node) => {
581
+ if (node.depth !== 2 && node.depth !== 3) return;
582
+ const title = toString(node);
583
+ sections.push({
584
+ id: slugger.slug(title),
585
+ title,
586
+ level: node.depth
587
+ });
588
+ });
589
+ return sections;
590
+ }
591
+ function navigationSections(sections) {
592
+ return sections.map((s) => ({
593
+ id: s.id,
594
+ title: s.title,
595
+ badge: s.badge,
596
+ tags: s.tags
597
+ }));
598
+ }
599
+ function normalizePath(path) {
600
+ const clean = "/" + path.replace(/^\/+|\/+$/g, "");
601
+ return clean === "/" ? "/" : clean.replace(/\/+/g, "/");
602
+ }
603
+ function pathSegmentFromOpenAPITag(tag) {
604
+ return tag.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tag";
605
+ }
606
+ function openAPITagsPathSegment(tags) {
607
+ return tags.map(pathSegmentFromOpenAPITag).join("-");
608
+ }
609
+ function basePathFromRef(ref) {
159
610
  const withoutExtension = ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "");
160
611
  return normalizePath(withoutExtension === "index" ? "/" : withoutExtension.replace(/\/index$/, ""));
161
612
  }
613
+ function openAPIPagePathFromRef(ref, tagFilter) {
614
+ const basePath = basePathFromRef(ref);
615
+ if (!tagFilter?.length) return basePath;
616
+ return normalizePath(`${basePath}/${openAPITagsPathSegment(tagFilter)}`);
617
+ }
162
618
  function routePathFromRef(ref) {
163
619
  return basePathFromRef(ref);
164
620
  }
@@ -217,7 +673,7 @@ function frontmatterKeywords(frontmatter) {
217
673
  function buildNavigation(routes) {
218
674
  const root = [];
219
675
  for (const route of routes) {
220
- if (route.path === "/") continue;
676
+ if (route.path === "/" || (route.basePath ?? route.path) === "/404") continue;
221
677
  const parts = route.path.replace(/^\//, "").split("/");
222
678
  let current = root;
223
679
  for (let i = 0; i < parts.length; i++) {
@@ -244,6 +700,7 @@ function resolvePageItem(item) {
244
700
  if (typeof item === "string") return { pageRef: item };
245
701
  if ("openapi" in item) return {
246
702
  openapiRef: item.openapi,
703
+ openapiTagFilter: item.filter?.tags,
247
704
  title: item.title,
248
705
  icon: item.icon
249
706
  };
@@ -282,9 +739,9 @@ function buildNavigationFromConfig(routes, config) {
282
739
  const routeMap = new Map(routes.map((r) => [r.path, r]));
283
740
  return config.map((group) => {
284
741
  const children = group.pages.map((item) => {
285
- const { pageRef, openapiRef, redirect, title, icon } = resolvePageItem(item);
742
+ const { pageRef, openapiRef, openapiTagFilter, redirect, title, icon } = resolvePageItem(item);
286
743
  if (openapiRef) {
287
- const path = "/" + openapiRef.replace(/\.openapi\.(json|yaml|yml)$/, "");
744
+ const path = openAPIPagePathFromRef(openapiRef, openapiTagFilter);
288
745
  const route = routeMap.get(path);
289
746
  return {
290
747
  path,
@@ -326,9 +783,9 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
326
783
  const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
327
784
  return config.map((group) => {
328
785
  const children = group.pages.map((item) => {
329
- const { pageRef, openapiRef, redirect, title, icon } = resolvePageItem(item);
786
+ const { pageRef, openapiRef, openapiTagFilter, redirect, title, icon } = resolvePageItem(item);
330
787
  const ref = openapiRef ?? pageRef ?? "";
331
- const basePath = basePathFromRef(ref);
788
+ const basePath = openapiRef ? openAPIPagePathFromRef(openapiRef, openapiTagFilter) : basePathFromRef(ref);
332
789
  const route = routeMap.get(basePath);
333
790
  const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
334
791
  return {
@@ -365,190 +822,6 @@ function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
365
822
  return result;
366
823
  }
367
824
  //#endregion
368
- //#region source/plugins/content-artifacts/artifacts.ts
369
- var UTF8_SIGNATURE = "";
370
- function normalizeBasePath(basePath) {
371
- if (!basePath || basePath === "/") return "";
372
- return "/" + basePath.replace(/^\/+|\/+$/g, "");
373
- }
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) {
386
- return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.md`;
387
- }
388
- function routeToOpenAPIArtifactUrl(routePath, filePath) {
389
- const extension = extname(filePath).toLowerCase() || ".json";
390
- return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi${extension}`;
391
- }
392
- function attachContentArtifactUrls(routes) {
393
- for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path, route.filePath) : routeToMarkdownArtifactUrl(route.path);
394
- }
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}`);
398
- }
399
- function readRouteArtifactContent(route) {
400
- const content = readRouteContent(route);
401
- return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
402
- }
403
- function writeContentArtifactFiles(routes, outputDirectory) {
404
- for (const route of routes) {
405
- if (!route.contentArtifactUrl) continue;
406
- const outFile = join(outputDirectory, route.contentArtifactUrl.replace(/^\//, ""));
407
- mkdirSync(dirname(outFile), { recursive: true });
408
- writeFileSync(outFile, readRouteArtifactContent(route), "utf-8");
409
- }
410
- }
411
- function createLlmsTxt(routes, projectConfig) {
412
- const basePath = normalizeBasePath(projectConfig.routePrefix);
413
- const lines = [`# ${projectConfig.title}`, ""];
414
- if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
415
- lines.push("## Docs");
416
- for (const route of routes.filter((route) => route.kind === "mdx")) {
417
- if (!route.contentArtifactUrl) continue;
418
- lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
419
- }
420
- const openApiRoutes = routes.filter((route) => route.kind === "openapi");
421
- if (openApiRoutes.length > 0) {
422
- lines.push("", "## OpenAPI");
423
- for (const route of openApiRoutes) {
424
- if (!route.contentArtifactUrl) continue;
425
- lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
426
- }
427
- }
428
- return `${lines.join("\n")}\n`;
429
- }
430
- function createLlmsTxtArtifact(routes, projectConfig) {
431
- return withUtf8Signature(createLlmsTxt(routes, projectConfig));
432
- }
433
- function writeLlmsTxt(routes, projectConfig, outputDirectory) {
434
- mkdirSync(outputDirectory, { recursive: true });
435
- writeFileSync(join(outputDirectory, "llms.txt"), createLlmsTxtArtifact(routes, projectConfig), "utf-8");
436
- }
437
- //#endregion
438
- //#region source/plugins/content-artifacts/server.ts
439
- function normalizeRoutePrefix(routePrefix) {
440
- if (!routePrefix || routePrefix === "/") return "";
441
- return `/${routePrefix.replace(/^\/+|\/+$/g, "")}`;
442
- }
443
- function resolveContentArtifactPath(url, projectConfig) {
444
- const requestPath = url?.split("?")[0] ?? "";
445
- const basePath = normalizeRoutePrefix(projectConfig.routePrefix);
446
- return basePath && requestPath.startsWith(basePath) ? requestPath.slice(basePath.length) || "/" : requestPath;
447
- }
448
- function resolveContentArtifactType(route) {
449
- if (route.kind === "openapi" && /\.ya?ml$/i.test(route.contentArtifactUrl ?? "")) return "text/yaml; charset=utf-8";
450
- return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
451
- }
452
- function serveContentArtifacts(req, res, projectConfig, routes) {
453
- const contentPath = resolveContentArtifactPath(req.url, projectConfig);
454
- if (contentPath === "/llms.txt") {
455
- res.statusCode = 200;
456
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
457
- res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
458
- return true;
459
- }
460
- const route = routes.find((route) => route.contentArtifactUrl === contentPath);
461
- if (!route) return false;
462
- res.statusCode = 200;
463
- res.setHeader("Content-Type", resolveContentArtifactType(route));
464
- res.end(readRouteArtifactContent(route), "utf8");
465
- return true;
466
- }
467
- //#endregion
468
- //#region source/plugins/content-artifacts/index.ts
469
- function createContentArtifactsPlugin() {
470
- return {
471
- name: "clarify:content-artifacts",
472
- hooks: {
473
- "routes:resolved": (input) => {
474
- attachContentArtifactUrls(input.routes);
475
- return input;
476
- },
477
- "dev:configureServer": (server, ctx) => {
478
- server.middlewares.use((req, res, next) => {
479
- if (serveContentArtifacts(req, res, ctx.projectConfig, ctx.routes)) return;
480
- next();
481
- });
482
- },
483
- "build:done": (ctx) => {
484
- const outputDirectory = ctx.generateOptions.outputDirectory;
485
- if (!outputDirectory) return;
486
- writeContentArtifactFiles(ctx.routes, outputDirectory);
487
- writeLlmsTxt(ctx.routes, ctx.projectConfig, outputDirectory);
488
- }
489
- }
490
- };
491
- }
492
- //#endregion
493
- //#region source/plugins/html-shell/index.ts
494
- 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){}})();`;
495
- var REL_ICON_RE = /<link\s+[^>]*rel=["'][^"']*\bicon\b[^"']*["'][^>]*>/i;
496
- function inferIconType(href) {
497
- const path = href.split(/[?#]/, 1)[0]?.toLowerCase() ?? href.toLowerCase();
498
- if (path.endsWith(".svg")) return "image/svg+xml";
499
- if (path.endsWith(".png")) return "image/png";
500
- if (path.endsWith(".ico")) return "image/x-icon";
501
- if (path.endsWith(".webp")) return "image/webp";
502
- }
503
- function createIconTag(href, media) {
504
- return {
505
- tag: "link",
506
- attrs: {
507
- rel: "icon",
508
- ...inferIconType(href) ? { type: inferIconType(href) } : {},
509
- href,
510
- ...media ? { media } : {}
511
- },
512
- injectTo: "head"
513
- };
514
- }
515
- function createIconTags(icon) {
516
- if (!icon) return [];
517
- if (typeof icon === "string") return [createIconTag(icon)];
518
- const lightIcon = icon.light ?? icon.dark;
519
- const darkIcon = icon.dark ?? icon.light;
520
- if (!lightIcon || !darkIcon) return [];
521
- if (lightIcon === darkIcon) return [createIconTag(lightIcon)];
522
- return [createIconTag(lightIcon, "(prefers-color-scheme: light)"), createIconTag(darkIcon, "(prefers-color-scheme: dark)")];
523
- }
524
- function createHtmlShellPlugin() {
525
- return {
526
- name: "clarify:html-shell",
527
- hooks: { "html:transform": (input, ctx) => ({
528
- html: input.html,
529
- tags: [
530
- ...input.tags,
531
- ...REL_ICON_RE.test(input.html) ? [] : createIconTags(ctx.projectConfig.favicon ?? ctx.projectConfig.logo),
532
- {
533
- tag: "script",
534
- children: THEME_BOOTSTRAP_SCRIPT,
535
- injectTo: "head-prepend"
536
- },
537
- {
538
- tag: "script",
539
- attrs: {
540
- type: "module",
541
- src: input.clientEntryId
542
- },
543
- injectTo: "body"
544
- }
545
- ],
546
- clientEntryId: input.clientEntryId,
547
- dev: input.dev
548
- }) }
549
- };
550
- }
551
- //#endregion
552
825
  //#region source/plugins/openapi/parser.ts
553
826
  var OPENAPI_HTTP_METHODS = [
554
827
  "get",
@@ -591,21 +864,26 @@ function findOpenAPIRoutes(dir, base = dir) {
591
864
  }
592
865
  return routes;
593
866
  }
867
+ function operationMatchesTags(operationTags, filterTags) {
868
+ if (!filterTags?.length) return true;
869
+ return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
870
+ }
594
871
  /** Extract endpoint operations from an OpenAPI spec as page sections. */
595
- function extractOpenAPISections(spec) {
872
+ function extractOpenAPISections(spec, filterTags) {
596
873
  const sections = [];
597
874
  const paths = spec.paths ?? {};
598
875
  for (const [path, pathItem] of Object.entries(paths)) {
599
876
  if (!pathItem) continue;
600
877
  for (const method of OPENAPI_HTTP_METHODS) {
601
878
  const op = pathItem[method];
602
- if (!op) continue;
879
+ if (!op || !operationMatchesTags(op.tags, filterTags)) continue;
603
880
  const title = op.summary ?? `${method.toUpperCase()} ${path}`;
604
881
  sections.push({
605
882
  id: slug(`${method} ${path}`),
606
883
  title,
607
884
  level: 2,
608
- badge: method.toUpperCase()
885
+ badge: method.toUpperCase(),
886
+ tags: op.tags
609
887
  });
610
888
  }
611
889
  }
@@ -630,320 +908,315 @@ async function readOpenAPISpec(filePath) {
630
908
  }
631
909
  }
632
910
  //#endregion
633
- //#region source/plugins/openapi/virtual-modules.ts
634
- var openApiRegistryModuleId = "virtual:clarify-openapi-registry";
635
- function generateOpenAPIRegistryModule(openApis) {
636
- return `export const openApis = ${JSON.stringify(openApis)};`;
911
+ //#region source/core/virtual-modules.ts
912
+ var VIRTUAL_CONFIG = "virtual:clarify-config";
913
+ var VIRTUAL_ROUTES = "virtual:clarify-routes";
914
+ var VIRTUAL_SERVER_ROUTES = "virtual:clarify-routes/server";
915
+ var VIRTUAL_RUNTIME = "virtual:clarify-runtime";
916
+ var VIRTUAL_CLIENT_ENTRY = "virtual:clarify-entry-client";
917
+ var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify-entry-client";
918
+ function resolveVirtualId(id) {
919
+ return "\0" + id;
637
920
  }
638
- function generateOpenAPIModule(spec) {
639
- return `import { createElement } from 'react';
640
- import { OpenApiDocument } from '@clarify-labs/renderer';
641
- const spec = ${JSON.stringify(spec)};
642
- export default function OpenApiRoutePage() {
643
- return createElement(OpenApiDocument, { spec });
644
- }`;
921
+ function stripVirtualPrefix(id) {
922
+ return id.startsWith("\0") ? id.slice(1) : id;
645
923
  }
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
- );
665
- }`;
924
+ function generateConfigModule(projectConfig, buildOptions) {
925
+ return `export const config = ${JSON.stringify({
926
+ ...projectConfig,
927
+ ...buildOptions
928
+ })};`;
666
929
  }
667
- //#endregion
668
- //#region source/plugins/openapi/index.ts
669
- function createOpenAPIPlugin() {
670
- const specs = {};
671
- return {
672
- name: "clarify:openapi",
673
- hooks: {
674
- "routes:discover": (input) => ({
675
- ...input,
676
- routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
677
- }),
678
- "routes:discovered": async (routes) => {
679
- for (const key of Object.keys(specs)) delete specs[key];
680
- for (const route of routes.filter((route) => route.kind === "openapi")) {
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;
689
- specs[route.virtualModuleId] = spec;
690
- specs[`virtual:clarify-page/${route.path.replace(/^\//, "")}`] = spec;
691
- route.title = spec.info?.title ?? route.title;
692
- route.description = spec.info?.description ?? route.description;
693
- route.sections = extractOpenAPISections(spec);
694
- }
695
- return routes;
696
- },
697
- "modules:before": (modules, ctx) => {
698
- modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(specs));
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));
701
- return modules;
702
- }
703
- }
704
- };
930
+ function moduleSpecifier(value) {
931
+ return JSON.stringify(value);
705
932
  }
706
- //#endregion
707
- //#region source/core/config-schema.ts
708
- var clarifyLogoConfigSchema = z.union([z.string(), z.object({
709
- light: z.string().optional(),
710
- dark: z.string().optional()
711
- })]);
712
- var clarifyFaviconConfigSchema = z.union([z.string(), z.object({
713
- light: z.string().optional(),
714
- dark: z.string().optional()
715
- })]);
716
- var clarifyLocalizedTextSchema = z.union([z.string(), z.record(z.string(), z.string())]);
717
- var clarifyLocaleConfigSchema = z.object({
718
- code: z.string(),
719
- label: z.string(),
720
- dir: z.union([z.literal("ltr"), z.literal("rtl")]).optional()
721
- });
722
- var clarifyI18nConfigSchema = z.object({
723
- defaultLocale: z.string().optional(),
724
- missing: z.union([
725
- z.literal("fallback"),
726
- z.literal("404"),
727
- z.literal("hide")
728
- ]).optional(),
729
- locales: z.array(clarifyLocaleConfigSchema).min(1)
730
- }).superRefine((config, ctx) => {
731
- const localeCodes = /* @__PURE__ */ new Set();
732
- for (const [index, locale] of config.locales.entries()) {
733
- if (localeCodes.has(locale.code)) ctx.addIssue({
734
- code: z.ZodIssueCode.custom,
735
- message: `Duplicate locale code "${locale.code}"`,
736
- path: [
737
- "locales",
738
- index,
739
- "code"
740
- ]
741
- });
742
- localeCodes.add(locale.code);
743
- }
744
- if (config.defaultLocale && !localeCodes.has(config.defaultLocale)) ctx.addIssue({
745
- code: z.ZodIssueCode.custom,
746
- message: "defaultLocale must be one of i18n.locales",
747
- path: ["defaultLocale"]
748
- });
749
- });
750
- var clarifyNavbarLinkSchema = z.object({
751
- label: clarifyLocalizedTextSchema,
752
- href: z.string(),
753
- external: z.boolean().optional()
754
- });
755
- var clarifyBannerConfigSchema = z.object({
756
- content: clarifyLocalizedTextSchema,
757
- dismissible: z.boolean().optional()
758
- });
759
- var clarifyFooterConfigSchema = z.object({
760
- links: z.array(clarifyNavbarLinkSchema).optional(),
761
- socials: z.record(z.string(), z.string()).optional(),
762
- copyright: clarifyLocalizedTextSchema.optional()
763
- });
764
- var clarifyPagesItemSchema = z.union([
765
- z.string(),
766
- z.object({
767
- page: z.string(),
768
- title: clarifyLocalizedTextSchema.optional(),
769
- icon: z.string().optional(),
770
- redirect: z.string().optional()
771
- }),
772
- z.object({
773
- openapi: z.string(),
774
- icon: z.string().optional(),
775
- title: clarifyLocalizedTextSchema.optional()
776
- })
777
- ]);
778
- var clarifyPagesGroupSchema = z.object({
779
- group: clarifyLocalizedTextSchema,
780
- icon: z.string().optional(),
781
- pages: z.array(clarifyPagesItemSchema)
782
- });
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
- });
817
- var clarifyProjectConfigSchema = z.object({
818
- title: z.string().optional(),
819
- description: z.string().optional(),
820
- logo: clarifyLogoConfigSchema.optional(),
821
- favicon: clarifyFaviconConfigSchema.optional(),
822
- theme: clarifyThemeConfigSchema.optional(),
823
- routePrefix: z.string().optional(),
824
- navbar: z.object({ links: z.array(clarifyNavbarLinkSchema).optional() }).optional(),
825
- banner: clarifyBannerConfigSchema.optional(),
826
- footer: clarifyFooterConfigSchema.optional(),
827
- i18n: clarifyI18nConfigSchema.optional(),
828
- tabs: clarifyTabsConfigSchema.optional()
829
- });
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" }
933
+ function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode = "client") {
934
+ const imports = mode === "server" ? routes.map((r, i) => `import Page${i} from ${moduleSpecifier(r.virtualModuleId)};`).join("\n") : "";
935
+ const routesArray = routes.map((r, i) => {
936
+ const sections = r.sections && r.sections.length > 0 ? `, sections: ${JSON.stringify(r.sections.map((s) => ({
937
+ id: s.id,
938
+ title: s.title,
939
+ badge: s.badge,
940
+ tags: s.tags
941
+ })))}` : "";
942
+ const contentArtifactUrl = r.contentArtifactUrl ? `, contentArtifactUrl: ${JSON.stringify(r.contentArtifactUrl)}` : "";
943
+ const basePath = r.basePath ? `, basePath: ${JSON.stringify(r.basePath)}` : "";
944
+ const locale = r.locale ? `, locale: ${JSON.stringify(r.locale)}` : "";
945
+ const isFallback = r.isFallback ? ", isFallback: true" : "";
946
+ const alternates = r.alternates ? `, alternates: ${JSON.stringify(r.alternates)}` : "";
947
+ const description = r.description ? `, description: ${JSON.stringify(r.description)}` : "";
948
+ const keywords = r.keywords && r.keywords.length > 0 ? `, keywords: ${JSON.stringify(r.keywords)}` : "";
949
+ const sourceUrl = r.sourceUrl ? `, sourceUrl: ${JSON.stringify(r.sourceUrl)}` : "";
950
+ const component = mode === "server" ? `Page${i}` : `() => import(${moduleSpecifier(r.virtualModuleId)})`;
951
+ const lazy = mode === "client" ? ", lazy: true" : "";
952
+ 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}${sourceUrl} }`;
953
+ }).join(",\n");
954
+ const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
955
+ return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
956
+ }
957
+ function createRuntimeModule() {
958
+ return "export const openApis = {};";
959
+ }
960
+ function createClientEntryModule(options = {}) {
961
+ return `
962
+ import '@clarify-labs/renderer/style.css';
963
+ import { render } from '@clarify-labs/renderer/client';
964
+ import { routes, navigation } from '${VIRTUAL_ROUTES}';
965
+ import { config } from '${VIRTUAL_CONFIG}';
966
+ import { openApis } from '${VIRTUAL_RUNTIME}';
967
+ render({ config, routes, navigation, openApis, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
968
+ }
969
+ function buildVirtualModules(args) {
970
+ const modules = /* @__PURE__ */ new Map();
971
+ const clientEntryModule = createClientEntryModule({ themeEditor: args.themeEditor });
972
+ modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
973
+ modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
974
+ modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "server"));
975
+ modules.set(VIRTUAL_RUNTIME, createRuntimeModule());
976
+ modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
977
+ modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
978
+ for (const route of args.routes) modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
979
+ return modules;
980
+ }
981
+ //#endregion
982
+ //#region source/plugins/openapi/virtual-modules.ts
983
+ var openApiRegistryModuleId = VIRTUAL_RUNTIME;
984
+ function generateOpenAPIRegistryModule(openApis) {
985
+ return `export const openApis = ${JSON.stringify(openApis)};`;
986
+ }
987
+ function generateOpenAPIModule(spec, tagFilter) {
988
+ return `import { createElement } from 'react';
989
+ import { OpenApiDocument } from '@clarify-labs/renderer';
990
+ const spec = ${JSON.stringify(spec)};
991
+ const tagFilter = ${JSON.stringify(tagFilter)};
992
+ export default function OpenApiRoutePage() {
993
+ return createElement(OpenApiDocument, { spec, tagFilter });
994
+ }`;
995
+ }
996
+ function generateOpenAPIErrorModule(diagnostic) {
997
+ return `import { createElement } from 'react';
998
+ const diagnostic = ${JSON.stringify(diagnostic)};
999
+ export default function OpenApiErrorRoutePage() {
1000
+ return createElement('article', { className: 'flex h-full flex-col pt-16 pb-10' },
1001
+ createElement('div', { className: 'prose max-w-none flex-auto dark:prose-invert' },
1002
+ createElement('p', { className: 'mb-3 text-xs/6 font-medium tracking-widest text-red-500 uppercase dark:text-red-400' }, 'OpenAPI Error'),
1003
+ createElement('h1', null, diagnostic.title),
1004
+ createElement('p', { className: 'lead' }, diagnostic.message),
1005
+ 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' },
1006
+ createElement('div', { className: 'mb-1 font-semibold text-zinc-900 dark:text-white' }, 'File'),
1007
+ createElement('code', { className: 'break-all text-zinc-700 dark:text-zinc-300' }, diagnostic.filePath)
1008
+ ) : null,
1009
+ 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' },
1010
+ createElement('div', { className: 'mb-1 font-semibold' }, 'Why it happened'),
1011
+ createElement('pre', { className: 'whitespace-pre-wrap break-words text-xs' }, diagnostic.cause)
1012
+ ) : null
1013
+ )
1014
+ );
1015
+ }`;
1016
+ }
1017
+ //#endregion
1018
+ //#region source/plugins/openapi/index.ts
1019
+ function collectOpenAPIPageItems(config) {
1020
+ const items = [];
1021
+ function visitPages(pages) {
1022
+ if (!pages || pages === "FileTree") return;
1023
+ 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);
876
1024
  }
877
- };
878
- function resolveThemeConfig(theme = {}) {
879
- const preset = theme.preset ?? "default";
880
- const presetTheme = clarifyThemePresets[preset];
1025
+ for (const tab of config.tabs ?? []) visitPages(tab.pages);
1026
+ return items;
1027
+ }
1028
+ function withAlternates$1(route, routes, i18n) {
1029
+ const basePath = route.basePath ?? route.path;
1030
+ const routeByLocaleAndBase = new Map(routes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
1031
+ const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
1032
+ const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
1033
+ return alternate ? [[locale.code, alternate.path]] : [];
1034
+ }));
881
1035
  return {
882
- preset,
883
- tokens: {
884
- colors: {
885
- ...presetTheme.tokens.colors,
886
- ...theme.tokens?.colors
1036
+ ...route,
1037
+ alternates
1038
+ };
1039
+ }
1040
+ function createTaggedOpenAPIRoutes(routes, config) {
1041
+ const pageItems = collectOpenAPIPageItems(config);
1042
+ if (!pageItems.length) return routes;
1043
+ const additions = [];
1044
+ const existingKeys = new Set(routes.map((route) => `${route.locale ?? ""}:${route.basePath ?? route.path}`));
1045
+ for (const item of pageItems) {
1046
+ const tagFilter = item.filter?.tags?.filter(Boolean);
1047
+ if (!tagFilter?.length) continue;
1048
+ const sourceBasePath = openAPIPagePathFromRef(item.openapi);
1049
+ const filteredBasePath = openAPIPagePathFromRef(item.openapi, tagFilter);
1050
+ for (const route of routes) {
1051
+ if (route.kind !== "openapi" || (route.basePath ?? route.path) !== sourceBasePath) continue;
1052
+ const key = `${route.locale ?? ""}:${filteredBasePath}`;
1053
+ if (existingKeys.has(key)) continue;
1054
+ existingKeys.add(key);
1055
+ additions.push({
1056
+ ...route,
1057
+ path: route.locale && config.i18n ? localizedRoutePath(filteredBasePath, route.locale, config.i18n) : filteredBasePath,
1058
+ basePath: filteredBasePath,
1059
+ title: route.title,
1060
+ virtualModuleId: `${route.virtualModuleId}/${openAPITagsPathSegment(tagFilter)}`,
1061
+ openapiTagFilter: tagFilter
1062
+ });
1063
+ }
1064
+ }
1065
+ const nextRoutes = [...routes, ...additions];
1066
+ return config.i18n ? nextRoutes.map((route) => withAlternates$1(route, nextRoutes, config.i18n)) : nextRoutes;
1067
+ }
1068
+ function createOpenAPIPlugin() {
1069
+ const specs = {};
1070
+ return {
1071
+ name: "clarify:openapi",
1072
+ hooks: {
1073
+ "routes:discover": (input) => ({
1074
+ ...input,
1075
+ routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
1076
+ }),
1077
+ "routes:discovered": async (routes, ctx) => {
1078
+ for (const key of Object.keys(specs)) delete specs[key];
1079
+ const nextRoutes = createTaggedOpenAPIRoutes(routes, ctx.projectConfig);
1080
+ const specByFilePath = /* @__PURE__ */ new Map();
1081
+ for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
1082
+ const specFromCache = specByFilePath.get(route.filePath);
1083
+ const result = specFromCache ? {
1084
+ ok: true,
1085
+ spec: specFromCache
1086
+ } : await readOpenAPISpec(route.filePath);
1087
+ if (!result.ok) {
1088
+ route.diagnostic = result.diagnostic;
1089
+ route.title = route.title || "OpenAPI parse error";
1090
+ route.sections = [];
1091
+ continue;
1092
+ }
1093
+ const spec = result.spec;
1094
+ specByFilePath.set(route.filePath, spec);
1095
+ specs[route.virtualModuleId] = {
1096
+ spec,
1097
+ tagFilter: route.openapiTagFilter
1098
+ };
1099
+ specs[`virtual:clarify-page/${route.path.replace(/^\//, "")}`] = {
1100
+ spec,
1101
+ tagFilter: route.openapiTagFilter
1102
+ };
1103
+ route.title = spec.info?.title ?? route.title;
1104
+ route.description = spec.info?.description ?? route.description;
1105
+ route.sections = extractOpenAPISections(spec, route.openapiTagFilter);
1106
+ }
1107
+ return nextRoutes;
887
1108
  },
888
- radius: {
889
- ...presetTheme.tokens.radius,
890
- ...theme.tokens?.radius
1109
+ "modules:before": (modules, ctx) => {
1110
+ modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(Object.fromEntries(Object.entries(specs).map(([moduleId, entry]) => [moduleId, entry.spec]))));
1111
+ for (const [moduleId, entry] of Object.entries(specs)) modules.set(moduleId, generateOpenAPIModule(entry.spec, entry.tagFilter));
1112
+ for (const route of ctx.routes.filter((route) => route.kind === "openapi" && route.diagnostic)) if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
1113
+ return modules;
891
1114
  }
892
- },
893
- layout: {
894
- ...presetTheme.layout,
895
- ...theme.layout
896
1115
  }
897
1116
  };
898
1117
  }
899
1118
  //#endregion
900
- //#region source/core/config.ts
901
- function formatIssuePath(path) {
902
- return path.reduce((result, segment) => {
903
- if (typeof segment === "number") return `${result}[${segment}]`;
904
- const key = String(segment);
905
- return result ? `${result}.${key}` : key;
906
- }, "");
1119
+ //#region source/plugins/seo/artifacts.ts
1120
+ function trimTrailingSlash(value) {
1121
+ return value.replace(/\/+$/, "");
907
1122
  }
908
- function formatProjectConfigError(error) {
909
- const issue = error.issues[0];
910
- if (!issue) return "[clarify] config is invalid";
911
- const path = formatIssuePath(issue.path);
912
- if (!path) return `[clarify] config must contain an object: ${issue.message}`;
913
- return `[clarify] config field "${path}" is invalid: ${issue.message}`;
1123
+ function normalizeRoutePrefix(routePrefix) {
1124
+ if (!routePrefix || routePrefix === "/") return "";
1125
+ return `/${routePrefix.replace(/^\/+|\/+$/g, "")}`;
914
1126
  }
915
- function validateProjectConfig(value) {
916
- const result = clarifyProjectConfigSchema.safeParse(value);
917
- if (!result.success) throw new Error(formatProjectConfigError(result.error));
918
- return result.data;
1127
+ function routeUrl(siteUrl, routePrefix, routePath) {
1128
+ const base = trimTrailingSlash(siteUrl);
1129
+ return `${base}${normalizeRoutePrefix(routePrefix)}${routePath === "/" ? "" : routePath.replace(/^\/+/, "/")}` || base;
919
1130
  }
920
- function resolveI18nConfig(i18n) {
921
- if (!i18n) return void 0;
922
- const firstLocale = i18n.locales[0]?.code;
923
- const defaultLocale = i18n.defaultLocale ?? firstLocale;
924
- if (!defaultLocale) return void 0;
1131
+ function escapeXml(value) {
1132
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
1133
+ }
1134
+ function publicRoutes(routes) {
1135
+ return routes.filter((route) => route.path !== "/404");
1136
+ }
1137
+ function createSitemapXml(routes, config) {
1138
+ if (!config.siteUrl) return void 0;
1139
+ return [
1140
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1141
+ "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">",
1142
+ ...publicRoutes(routes).map((route) => routeUrl(config.siteUrl, config.routePrefix, route.path)).filter((url, index, list) => list.indexOf(url) === index).sort().map((url) => ` <url><loc>${escapeXml(url)}</loc></url>`),
1143
+ "</urlset>",
1144
+ ""
1145
+ ].join("\n");
1146
+ }
1147
+ function createRobotsTxt(config) {
1148
+ if (!config.siteUrl) return void 0;
1149
+ return [
1150
+ "User-agent: *",
1151
+ "Allow: /",
1152
+ `Sitemap: ${routeUrl(config.siteUrl, config.routePrefix, "/sitemap.xml")}`,
1153
+ ""
1154
+ ].join("\n");
1155
+ }
1156
+ async function writeSeoFiles(outputRoot, routes, config) {
1157
+ const sitemap = createSitemapXml(routes, config);
1158
+ const robots = createRobotsTxt(config);
1159
+ if (!sitemap || !robots) return;
1160
+ await mkdir(outputRoot, { recursive: true });
1161
+ await Promise.all([writeFile(join(outputRoot, "sitemap.xml"), sitemap, "utf-8"), writeFile(join(outputRoot, "robots.txt"), robots, "utf-8")]);
1162
+ }
1163
+ //#endregion
1164
+ //#region source/plugins/seo/index.ts
1165
+ function createSeoPlugin() {
925
1166
  return {
926
- defaultLocale,
927
- missing: i18n.missing ?? "fallback",
928
- locales: i18n.locales
1167
+ name: "clarify:seo",
1168
+ hooks: { async "build:done"(ctx) {
1169
+ const outputDirectory = ctx.generateOptions.outputDirectory;
1170
+ if (!outputDirectory) return;
1171
+ await writeSeoFiles(join(ctx.generateOptions.projectRoot, outputDirectory), ctx.routes, ctx.projectConfig);
1172
+ } }
929
1173
  };
930
1174
  }
931
- function resolveProjectConfig(config = {}) {
1175
+ //#endregion
1176
+ //#region source/plugins/source-links/source-links.ts
1177
+ function trimSlashes(value) {
1178
+ return value.replace(/^\/+|\/+$/g, "");
1179
+ }
1180
+ function normalizeRepositoryUrl(repository) {
1181
+ return repository.replace(/\.git$/, "").replace(/\/+$/, "");
1182
+ }
1183
+ function encodePath(path) {
1184
+ return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
1185
+ }
1186
+ function createSourceUrl(filePath, contentRoot, source) {
1187
+ const repository = normalizeRepositoryUrl(source.repository);
1188
+ const branch = source.branch ?? "main";
1189
+ const sourcePath = [source.directory ? trimSlashes(source.directory) : "", relative(contentRoot, filePath).replaceAll("\\", "/")].filter(Boolean).join("/");
1190
+ return `${repository}/edit/${encodeURIComponent(branch)}/${encodePath(sourcePath)}`;
1191
+ }
1192
+ function attachSourceUrls(routes, contentRoot, source) {
1193
+ if (!source) return;
1194
+ for (const route of routes) route.sourceUrl = createSourceUrl(route.filePath, contentRoot, source);
1195
+ }
1196
+ //#endregion
1197
+ //#region source/plugins/source-links/index.ts
1198
+ function createSourceLinksPlugin() {
932
1199
  return {
933
- title: config.title ?? "Clarify Docs",
934
- description: config.description ?? "",
935
- logo: config.logo,
936
- favicon: config.favicon,
937
- routePrefix: config.routePrefix ?? "/",
938
- theme: resolveThemeConfig(config.theme),
939
- navbar: config.navbar,
940
- banner: config.banner,
941
- footer: config.footer,
942
- i18n: resolveI18nConfig(config.i18n),
943
- tabs: config.tabs
1200
+ name: "clarify:source-links",
1201
+ hooks: { "routes:discovered": (routes, ctx) => {
1202
+ attachSourceUrls(routes, join(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
1203
+ return routes;
1204
+ } }
944
1205
  };
945
1206
  }
946
1207
  //#endregion
1208
+ //#region source/core/builtin.ts
1209
+ function createBuiltinPlugins(options = {}) {
1210
+ const plugins = [
1211
+ createOpenAPIPlugin(),
1212
+ createSourceLinksPlugin(),
1213
+ createContentArtifactsPlugin(),
1214
+ createSeoPlugin()
1215
+ ];
1216
+ if (options.htmlShell ?? true) plugins.push(createHtmlShellPlugin());
1217
+ return plugins;
1218
+ }
1219
+ //#endregion
947
1220
  //#region source/core/hooks.ts
948
1221
  async function runHooks(plugins, hookName, input, ctx) {
949
1222
  let result = input;
@@ -984,12 +1257,104 @@ async function runBuildDoneHooks(plugins, ctx) {
984
1257
  //#region source/core/options.ts
985
1258
  function resolveBuildOptions(options = {}) {
986
1259
  return {
1260
+ projectRoot: options.projectRoot ?? process.cwd(),
987
1261
  rootDirectory: options.rootDirectory ?? "source",
988
1262
  outputDirectory: options.outputDirectory,
989
1263
  ssg: { failOnError: options.ssg?.failOnError ?? true }
990
1264
  };
991
1265
  }
992
1266
  //#endregion
1267
+ //#region source/core/site.ts
1268
+ function withAlternates(route, routeList, i18n) {
1269
+ const basePath = route.basePath ?? route.path;
1270
+ const routeByLocaleAndBase = new Map(routeList.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
1271
+ const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
1272
+ const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
1273
+ return alternate ? [[locale.code, alternate.path]] : [];
1274
+ }));
1275
+ return {
1276
+ ...route,
1277
+ alternates
1278
+ };
1279
+ }
1280
+ async function discoverRoutesForRoot(routeRoot, locale, plugins, ctx) {
1281
+ return (await runHooks(plugins, "routes:discover", {
1282
+ contentRoot: routeRoot,
1283
+ locale,
1284
+ routes: findContentRoutes(routeRoot)
1285
+ }, ctx)).routes;
1286
+ }
1287
+ async function discoverRoutes(root, contentRoot, plugins, ctx) {
1288
+ const i18n = ctx.projectConfig.i18n;
1289
+ if (!i18n) return discoverRoutesForRoot(contentRoot, void 0, plugins, ctx);
1290
+ const localizedRoutes = [];
1291
+ for (const locale of i18n.locales) {
1292
+ const discovered = await discoverRoutesForRoot(join(contentRoot, locale.code), locale.code, plugins, ctx);
1293
+ for (const route of discovered) {
1294
+ const basePath = route.basePath ?? route.path;
1295
+ localizedRoutes.push({
1296
+ ...route,
1297
+ path: localizedRoutePath(basePath, locale.code, i18n),
1298
+ basePath,
1299
+ locale: locale.code,
1300
+ virtualModuleId: virtualModuleIdFromRef(relative(contentRoot, route.filePath))
1301
+ });
1302
+ }
1303
+ }
1304
+ if (i18n.missing === "fallback") {
1305
+ const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
1306
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
1307
+ for (const sourceRoute of defaultRoutes) {
1308
+ const basePath = sourceRoute.basePath ?? sourceRoute.path;
1309
+ for (const locale of i18n.locales) {
1310
+ const key = `${locale.code}:${basePath}`;
1311
+ if (routeByLocaleAndBase.has(key)) continue;
1312
+ localizedRoutes.push({
1313
+ ...sourceRoute,
1314
+ path: localizedRoutePath(basePath, locale.code, i18n),
1315
+ locale: locale.code,
1316
+ isFallback: true
1317
+ });
1318
+ }
1319
+ }
1320
+ }
1321
+ return localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
1322
+ }
1323
+ async function resolveClarifySite(options = {}, resolveOptions = {}) {
1324
+ const root = options.projectRoot ?? process.cwd();
1325
+ const projectConfig = resolveProjectConfig(options);
1326
+ const generateOptions = resolveBuildOptions(options);
1327
+ const contentRoot = join(root, generateOptions.rootDirectory);
1328
+ const plugins = [...createBuiltinPlugins({ htmlShell: resolveOptions.includeHtmlShellPlugin }), ...options.plugins ?? []];
1329
+ const ctx = {
1330
+ projectConfig,
1331
+ generateOptions,
1332
+ routes: [],
1333
+ navigation: []
1334
+ };
1335
+ let routes = await discoverRoutes(root, contentRoot, plugins, ctx);
1336
+ routes = await runHooks(plugins, "routes:discovered", routes, ctx);
1337
+ const defaultNavigation = projectConfig.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes);
1338
+ const resolved = await runHooks(plugins, "routes:resolved", {
1339
+ routes,
1340
+ navigation: defaultNavigation
1341
+ }, ctx);
1342
+ routes = resolved.routes;
1343
+ const navigation = resolved.navigation;
1344
+ ctx.routes = routes;
1345
+ ctx.navigation = navigation;
1346
+ return {
1347
+ root,
1348
+ contentRoot,
1349
+ projectConfig,
1350
+ generateOptions,
1351
+ plugins,
1352
+ ctx,
1353
+ routes,
1354
+ navigation
1355
+ };
1356
+ }
1357
+ //#endregion
993
1358
  //#region source/core/runtime-deps.ts
994
1359
  var requireFromCli = createRequire(import.meta.url);
995
1360
  function resolveCliDependency(id) {
@@ -1084,13 +1449,21 @@ function injectSSRIntoTemplate(template, appHtml, projectConfig, route) {
1084
1449
  html = html.replace(/<div id="root">[\s\S]*?<\/div>/, `<div id="root">${appHtml}</div>`);
1085
1450
  return html;
1086
1451
  }
1452
+ function isNotFoundRoute(route) {
1453
+ return (route.basePath ?? route.path) === "/404";
1454
+ }
1455
+ function routeOutputFiles(outputDirectory, route) {
1456
+ const files = [join(outputDirectory, route.path, "index.html")];
1457
+ if (isNotFoundRoute(route) && (!route.locale || route.path === "/404")) files.push(join(outputDirectory, "404.html"));
1458
+ return files;
1459
+ }
1087
1460
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
1088
1461
  import { routes, navigation } from 'virtual:clarify-routes/server';
1089
1462
  import { config } from 'virtual:clarify-config';
1090
- import { openApis } from 'virtual:clarify-openapi-registry';
1463
+ import { openApis } from 'virtual:clarify-runtime';
1091
1464
 
1092
1465
  export function render(url) {
1093
- return renderToHTML({ config, routes, navigation, openApis, url });
1466
+ return renderToHTML({ config, routes, navigation, openApis, url, themeEditor: config.theme.editor });
1094
1467
  }`;
1095
1468
  function createTempEntryFile(content) {
1096
1469
  const entryPath = join(mkdtempSync(join(tmpdir(), "clarify-ssr-")), "entry-server.ts");
@@ -1121,9 +1494,10 @@ async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundle
1121
1494
  if (!template) throw new Error(`[clarify] index.html not found in outputDirectory "${outputDirectory}". Make sure Vite build produces it.`);
1122
1495
  for (const route of routes) try {
1123
1496
  const finalHtml = injectSSRIntoTemplate(template, render(route.path), projectConfig, route);
1124
- const outFile = join(outputDirectory, route.path, "index.html");
1125
- mkdirSync(dirname(outFile), { recursive: true });
1126
- writeFileSync(outFile, finalHtml, "utf-8");
1497
+ for (const outFile of routeOutputFiles(outputDirectory, route)) {
1498
+ mkdirSync(dirname(outFile), { recursive: true });
1499
+ writeFileSync(outFile, finalHtml, "utf-8");
1500
+ }
1127
1501
  } catch (err) {
1128
1502
  console.error(`[clarify] Failed to render route "${route.path}":`, err);
1129
1503
  if (failOnError) throw err;
@@ -1165,75 +1539,6 @@ async function loadClarifyConfig(root, env) {
1165
1539
  return assertConfigObject((await loadConfigFromFile(env, configFile, root, "silent"))?.config, configFile);
1166
1540
  }
1167
1541
  //#endregion
1168
- //#region source/core/virtual-modules.ts
1169
- var VIRTUAL_CONFIG = "virtual:clarify-config";
1170
- var VIRTUAL_ROUTES = "virtual:clarify-routes";
1171
- var VIRTUAL_SERVER_ROUTES = "virtual:clarify-routes/server";
1172
- var VIRTUAL_OPENAPI_REGISTRY = openApiRegistryModuleId;
1173
- var VIRTUAL_CLIENT_ENTRY = "virtual:clarify-entry-client";
1174
- var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify-entry-client";
1175
- function resolveVirtualId(id) {
1176
- return "\0" + id;
1177
- }
1178
- function stripVirtualPrefix(id) {
1179
- return id.startsWith("\0") ? id.slice(1) : id;
1180
- }
1181
- function generateConfigModule(projectConfig, buildOptions) {
1182
- return `export const config = ${JSON.stringify({
1183
- ...projectConfig,
1184
- ...buildOptions
1185
- })};`;
1186
- }
1187
- function moduleSpecifier(value) {
1188
- return JSON.stringify(value);
1189
- }
1190
- function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode = "client") {
1191
- const imports = mode === "server" ? routes.map((r, i) => `import Page${i} from ${moduleSpecifier(r.virtualModuleId)};`).join("\n") : "";
1192
- const routesArray = routes.map((r, i) => {
1193
- const sections = r.sections && r.sections.length > 0 ? `, sections: ${JSON.stringify(r.sections.map((s) => ({
1194
- id: s.id,
1195
- title: s.title,
1196
- badge: s.badge,
1197
- tags: s.tags
1198
- })))}` : "";
1199
- const contentArtifactUrl = r.contentArtifactUrl ? `, contentArtifactUrl: ${JSON.stringify(r.contentArtifactUrl)}` : "";
1200
- const basePath = r.basePath ? `, basePath: ${JSON.stringify(r.basePath)}` : "";
1201
- const locale = r.locale ? `, locale: ${JSON.stringify(r.locale)}` : "";
1202
- const isFallback = r.isFallback ? ", isFallback: true" : "";
1203
- const alternates = r.alternates ? `, alternates: ${JSON.stringify(r.alternates)}` : "";
1204
- const description = r.description ? `, description: ${JSON.stringify(r.description)}` : "";
1205
- const keywords = r.keywords && r.keywords.length > 0 ? `, keywords: ${JSON.stringify(r.keywords)}` : "";
1206
- const component = mode === "server" ? `Page${i}` : `() => import(${moduleSpecifier(r.virtualModuleId)})`;
1207
- const lazy = mode === "client" ? ", lazy: true" : "";
1208
- 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} }`;
1209
- }).join(",\n");
1210
- const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
1211
- return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
1212
- }
1213
- function createClientEntryModule() {
1214
- return `
1215
- import '@clarify-labs/renderer/style.css';
1216
- import { render } from '@clarify-labs/renderer/client';
1217
- import { routes, navigation } from '${VIRTUAL_ROUTES}';
1218
- import { config } from '${VIRTUAL_CONFIG}';
1219
- import { openApis } from '${VIRTUAL_OPENAPI_REGISTRY}';
1220
- render({ config, routes, navigation, openApis });`;
1221
- }
1222
- function buildVirtualModules(args) {
1223
- const modules = /* @__PURE__ */ new Map();
1224
- modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
1225
- modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
1226
- modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "server"));
1227
- modules.set(VIRTUAL_OPENAPI_REGISTRY, "export const openApis = {};");
1228
- modules.set(VIRTUAL_CLIENT_ENTRY, createClientEntryModule());
1229
- modules.set(RESOLVED_CLIENT_ENTRY, createClientEntryModule());
1230
- for (const route of args.routes) {
1231
- if (route.kind === "openapi") continue;
1232
- modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
1233
- }
1234
- return modules;
1235
- }
1236
- //#endregion
1237
1542
  //#region source/core/plugin.ts
1238
1543
  function loadVirtualModule(id, modules) {
1239
1544
  const bareId = stripVirtualPrefix(id);
@@ -1246,12 +1551,7 @@ function clarifyPlugin(options = {}) {
1246
1551
  const contentRoot = join(root, generateOptions.rootDirectory);
1247
1552
  const configFilePath = findClarifyConfigFile(root);
1248
1553
  let routes = [];
1249
- const clarifyPlugins = [
1250
- createOpenAPIPlugin(),
1251
- createContentArtifactsPlugin(),
1252
- createHtmlShellPlugin(),
1253
- ...options.plugins ?? []
1254
- ];
1554
+ let clarifyPlugins = [...createBuiltinPlugins(), ...options.plugins ?? []];
1255
1555
  const ctx = {
1256
1556
  projectConfig,
1257
1557
  generateOptions,
@@ -1276,71 +1576,15 @@ function clarifyPlugin(options = {}) {
1276
1576
  ctx.projectConfig = projectConfig;
1277
1577
  ctx.generateOptions = generateOptions;
1278
1578
  }
1279
- function withAlternates(route, routeList, i18n) {
1280
- const basePath = route.basePath ?? route.path;
1281
- const routeByLocaleAndBase = new Map(routeList.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
1282
- const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
1283
- const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
1284
- return alternate ? [[locale.code, alternate.path]] : [];
1285
- }));
1286
- return {
1287
- ...route,
1288
- alternates
1289
- };
1290
- }
1291
- async function discoverRoutesForRoot(routeRoot, locale) {
1292
- return (await runHooks(clarifyPlugins, "routes:discover", {
1293
- contentRoot: routeRoot,
1294
- locale,
1295
- routes: findContentRoutes(routeRoot)
1296
- }, ctx)).routes;
1297
- }
1298
- async function discoverRoutes() {
1299
- const i18n = projectConfig.i18n;
1300
- if (!i18n) return discoverRoutesForRoot(contentRoot);
1301
- const localizedRoutes = [];
1302
- for (const locale of i18n.locales) {
1303
- const discovered = await discoverRoutesForRoot(join(contentRoot, locale.code), locale.code);
1304
- for (const route of discovered) {
1305
- const basePath = route.basePath ?? route.path;
1306
- localizedRoutes.push({
1307
- ...route,
1308
- path: localizedRoutePath(basePath, locale.code, i18n),
1309
- basePath,
1310
- locale: locale.code,
1311
- virtualModuleId: virtualModuleIdFromRef(relative(contentRoot, route.filePath))
1312
- });
1313
- }
1314
- }
1315
- if (i18n.missing === "fallback") {
1316
- const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
1317
- const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
1318
- for (const sourceRoute of defaultRoutes) {
1319
- const basePath = sourceRoute.basePath ?? sourceRoute.path;
1320
- for (const locale of i18n.locales) {
1321
- const key = `${locale.code}:${basePath}`;
1322
- if (routeByLocaleAndBase.has(key)) continue;
1323
- localizedRoutes.push({
1324
- ...sourceRoute,
1325
- path: localizedRoutePath(basePath, locale.code, i18n),
1326
- locale: locale.code,
1327
- isFallback: true
1328
- });
1329
- }
1330
- }
1331
- }
1332
- return localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
1333
- }
1334
1579
  async function resolveRoutesAndSpecs() {
1335
- routes = await discoverRoutes();
1336
- routes = await runHooks(clarifyPlugins, "routes:discovered", routes, ctx);
1337
- const defaultNavigation = projectConfig.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes);
1338
- const resolved = await runHooks(clarifyPlugins, "routes:resolved", {
1339
- routes,
1340
- navigation: defaultNavigation
1341
- }, ctx);
1342
- routes = resolved.routes;
1343
- resolvedNavigation = resolved.navigation;
1580
+ const site = await resolveClarifySite(options);
1581
+ projectConfig = site.projectConfig;
1582
+ generateOptions = site.generateOptions;
1583
+ routes = site.routes;
1584
+ resolvedNavigation = site.navigation;
1585
+ clarifyPlugins = site.plugins;
1586
+ ctx.projectConfig = projectConfig;
1587
+ ctx.generateOptions = generateOptions;
1344
1588
  ctx.routes = routes;
1345
1589
  ctx.navigation = resolvedNavigation;
1346
1590
  }
@@ -1349,7 +1593,8 @@ function clarifyPlugin(options = {}) {
1349
1593
  projectConfig,
1350
1594
  generateOptions,
1351
1595
  routes,
1352
- navigation: resolvedNavigation
1596
+ navigation: resolvedNavigation,
1597
+ themeEditor: viteConfig.command === "serve" || projectConfig.theme.editor
1353
1598
  });
1354
1599
  virtualModules = await runHooks(clarifyPlugins, "modules:before", virtualModules, ctx);
1355
1600
  }
@@ -1429,8 +1674,9 @@ function clarifyPlugin(options = {}) {
1429
1674
  if (id === "virtual:clarify-config" || id === resolveVirtualId("virtual:clarify-config")) return resolveVirtualId(VIRTUAL_CONFIG);
1430
1675
  if (id === "virtual:clarify-routes" || id === resolveVirtualId("virtual:clarify-routes")) return resolveVirtualId(VIRTUAL_ROUTES);
1431
1676
  if (id === "virtual:clarify-routes/server" || id === resolveVirtualId("virtual:clarify-routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
1432
- if (id === VIRTUAL_OPENAPI_REGISTRY || id === resolveVirtualId(VIRTUAL_OPENAPI_REGISTRY)) return resolveVirtualId(VIRTUAL_OPENAPI_REGISTRY);
1433
- const route = routes.find((r) => r.virtualModuleId === id || r.virtualModuleId === stripVirtualPrefix(id));
1677
+ const moduleId = stripVirtualPrefix(id);
1678
+ if (virtualModules.has(moduleId)) return resolveVirtualId(moduleId);
1679
+ const route = routes.find((r) => r.virtualModuleId === id || r.virtualModuleId === moduleId);
1434
1680
  if (route) return resolveVirtualId(route.virtualModuleId);
1435
1681
  return null;
1436
1682
  },
@@ -1452,10 +1698,11 @@ function clarifyPlugin(options = {}) {
1452
1698
  transformIndexHtml: {
1453
1699
  order: "pre",
1454
1700
  async handler(html, transformCtx) {
1701
+ const clientEntryId = transformCtx.server ? `/@id/${VIRTUAL_CLIENT_ENTRY}` : VIRTUAL_CLIENT_ENTRY;
1455
1702
  const result = await runHooks(clarifyPlugins, "html:transform", {
1456
1703
  html,
1457
1704
  tags: [],
1458
- clientEntryId: transformCtx.server ? `/@id/${VIRTUAL_CLIENT_ENTRY}` : VIRTUAL_CLIENT_ENTRY,
1705
+ clientEntryId,
1459
1706
  dev: Boolean(transformCtx.server)
1460
1707
  }, ctx);
1461
1708
  return {
@@ -1483,7 +1730,7 @@ function clarifyPlugin(options = {}) {
1483
1730
  if (id === "virtual:clarify-config") return id;
1484
1731
  if (id === "virtual:clarify-routes") return id;
1485
1732
  if (id === "virtual:clarify-routes/server") return id;
1486
- if (id === VIRTUAL_OPENAPI_REGISTRY) return id;
1733
+ if (virtualModules.has(stripVirtualPrefix(id))) return stripVirtualPrefix(id);
1487
1734
  if (routes.find((r) => r.virtualModuleId === id)) return id;
1488
1735
  return null;
1489
1736
  },
@@ -1509,6 +1756,6 @@ function clarifyPlugin(options = {}) {
1509
1756
  ].flat().filter(Boolean);
1510
1757
  }
1511
1758
  //#endregion
1512
- export { createClarifyRuntimeAliases as i, defineConfig as n, loadClarifyConfig as r, clarifyPlugin as t };
1759
+ export { resolveClarifySite as a, createClarifyRuntimeAliases as i, defineConfig as n, loadClarifyConfig as r, clarifyPlugin as t };
1513
1760
 
1514
- //# sourceMappingURL=plugin-DChFdTyI.js.map
1761
+ //# sourceMappingURL=plugin-BgE7WVoS.js.map