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