@clarify-labs/cli 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/output/cli/spawn.d.ts +1 -1
  2. package/output/cli.cjs +60 -128
  3. package/output/cli.cjs.map +1 -1
  4. package/output/cli.js +56 -124
  5. package/output/cli.js.map +1 -1
  6. package/output/core/adapters.d.ts +11 -0
  7. package/output/core/{config-schema.d.ts → config/config-schema.d.ts} +1 -10
  8. package/output/core/{config.d.ts → config/config.d.ts} +1 -1
  9. package/output/core/{options.d.ts → config/options.d.ts} +1 -1
  10. package/output/core/{user-config.d.ts → config/user-config.d.ts} +1 -1
  11. package/output/core/engine/context.d.ts +51 -0
  12. package/output/core/engine/engine.d.ts +68 -0
  13. package/output/core/engine/phases.d.ts +10 -0
  14. package/output/core/engine/types.d.ts +26 -0
  15. package/output/core/{builtin.d.ts → plugin/builtin.d.ts} +1 -1
  16. package/output/core/{hooks.d.ts → plugin/hooks.d.ts} +2 -1
  17. package/output/core/plugin/manager.d.ts +9 -0
  18. package/output/core/project/project-context.d.ts +16 -0
  19. package/output/core/{project-info.d.ts → project/project-info.d.ts} +3 -3
  20. package/output/core/{env-types.d.ts → runtime/env-types.d.ts} +1 -1
  21. package/output/core/runtime/html-template.d.ts +13 -0
  22. package/output/core/{ssg.d.ts → runtime/ssg.d.ts} +3 -3
  23. package/output/core/runtime/startup.d.ts +8 -0
  24. package/output/core/{virtual-modules.d.ts → runtime/virtual-modules.d.ts} +16 -13
  25. package/output/core/{vite-config.d.ts → runtime/vite-config.d.ts} +2 -1
  26. package/output/{plugin-poS3N83A.js → engine-3f5Tpa-6.js} +2403 -1804
  27. package/output/engine-3f5Tpa-6.js.map +1 -0
  28. package/output/{plugin-BUFe4vEb.cjs → engine-I04EZzjg.cjs} +2425 -1815
  29. package/output/engine-I04EZzjg.cjs.map +1 -0
  30. package/output/index.cjs +5 -3
  31. package/output/index.d.ts +6 -4
  32. package/output/index.js +2 -2
  33. package/output/parsers/content/content.d.ts +20 -0
  34. package/output/parsers/markdown/mdx.d.ts +37 -0
  35. package/output/{core → parsers/router}/dev-routes.d.ts +2 -2
  36. package/output/parsers/{routes.d.ts → routes/routes.d.ts} +2 -2
  37. package/output/parsers/{routes.test-utils.d.ts → routes/routes.test-utils.d.ts} +1 -1
  38. package/output/plugins/openapi/parser.d.ts +1 -1
  39. package/output/plugins/openapi/virtual-modules.d.ts +1 -4
  40. package/output/plugins/variables/index.d.ts +2 -1
  41. package/output/types.d.ts +40 -5
  42. package/package.json +12 -12
  43. package/output/core/content.d.ts +0 -5
  44. package/output/core/plugin.d.ts +0 -3
  45. package/output/core/site.d.ts +0 -17
  46. package/output/parsers/content.d.ts +0 -11
  47. package/output/parsers/mdx.d.ts +0 -13
  48. package/output/parsers/variables.d.ts +0 -2
  49. package/output/plugin-BUFe4vEb.cjs.map +0 -1
  50. package/output/plugin-poS3N83A.js.map +0 -1
  51. /package/output/core/{temp-dir.d.ts → project/temp-dir.d.ts} +0 -0
  52. /package/output/core/{log.d.ts → runtime/log.d.ts} +0 -0
  53. /package/output/core/{runtime-deps.d.ts → runtime/runtime-deps.d.ts} +0 -0
  54. /package/output/parsers/{frontmatter.d.ts → markdown/frontmatter.d.ts} +0 -0
  55. /package/output/{core → parsers/markdown}/utils.d.ts +0 -0
  56. /package/output/{core → parsers}/theme.d.ts +0 -0
  57. /package/output/{core → plugins/search-index}/search-language.d.ts +0 -0
@@ -1,28 +1,30 @@
1
- import { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
1
  import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
3
2
  import mdxPlugin from "@mdx-js/rollup";
4
3
  import tailwindcss from "@tailwindcss/vite";
5
4
  import react from "@vitejs/plugin-react";
5
+ import { compile } from "@mdx-js/mdx";
6
6
  import GithubSlugger, { slug } from "github-slugger";
7
7
  import { toString } from "mdast-util-to-string";
8
- import shiki from "shiki";
8
+ import { bundledLanguages, createCssVariablesTheme, createHighlighter } from "shiki";
9
9
  import { visit } from "unist-util-visit";
10
10
  import { markdownRemarkPlugins, themeBootstrapScript, themePresets } from "@clarify-labs/renderer";
11
- import { stringify } from "yaml";
12
- import matter from "gray-matter";
11
+ import { readFile, readdir, stat } from "node:fs/promises";
13
12
  import { remark } from "remark";
13
+ import matter from "gray-matter";
14
+ import { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
15
+ import { build, loadConfigFromFile } from "vite";
16
+ import { z } from "zod";
17
+ import { stringify } from "yaml";
14
18
  import { fileURLToPath, pathToFileURL } from "node:url";
15
19
  import SwaggerParser from "@apidevtools/swagger-parser";
16
20
  import { tmpdir } from "node:os";
17
- import { z } from "zod";
18
- import { build, loadConfigFromFile } from "vite";
19
21
  import { createRequire } from "node:module";
20
- //#region source/core/utils.ts
22
+ //#region source/parsers/markdown/utils.ts
21
23
  function escapeHtml(text) {
22
24
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
23
25
  }
24
26
  //#endregion
25
- //#region source/parsers/mdx.ts
27
+ //#region source/parsers/markdown/mdx.ts
26
28
  function getLanguage(className) {
27
29
  return (Array.isArray(className) ? className : typeof className === "string" ? className.split(/\s+/) : []).find((item) => typeof item === "string" && item.startsWith("language-"))?.replace(/^language-/, "") || "txt";
28
30
  }
@@ -34,11 +36,21 @@ function getTextContent(node) {
34
36
  function renderPlainCode(code) {
35
37
  return code.split("\n").map((line) => `<span>${escapeHtml(line)}</span>`).join("\n");
36
38
  }
39
+ function isBundledLanguage(language) {
40
+ return language in bundledLanguages;
41
+ }
37
42
  var highlighter;
43
+ var cssVariablesTheme = createCssVariablesTheme();
38
44
  async function getHighlighter() {
39
- highlighter = highlighter ?? await shiki.getHighlighter({ theme: "css-variables" });
45
+ highlighter = highlighter ?? await createHighlighter({
46
+ themes: [cssVariablesTheme],
47
+ langs: []
48
+ });
40
49
  return highlighter;
41
50
  }
51
+ function renderHighlightedCode(html) {
52
+ return (/<code[^>]*>([\s\S]*?)<\/code>/.exec(html)?.[1] ?? html).split("\n").map((line) => line.trimStart().startsWith("<span") ? line : `<span>${line}</span>`).join("\n");
53
+ }
42
54
  function rehypeParseCodeBlocks() {
43
55
  return (tree) => {
44
56
  visit(tree, "element", (node, _nodeIndex, parentNode) => {
@@ -59,6 +71,15 @@ function rehypeParseCodeBlocks() {
59
71
  function rehypeShiki() {
60
72
  return async (tree) => {
61
73
  const shikiHighlighter = await getHighlighter();
74
+ const languages = /* @__PURE__ */ new Set();
75
+ visit(tree, "element", (node) => {
76
+ if (node.tagName !== "pre") return;
77
+ const codeNode = node.children?.[0];
78
+ if (codeNode?.tagName !== "code") return;
79
+ const language = typeof node.properties?.language === "string" ? node.properties.language : getLanguage(codeNode.properties?.className);
80
+ if (language && language !== "txt" && isBundledLanguage(language)) languages.add(language);
81
+ });
82
+ if (languages.size > 0) await shikiHighlighter.loadLanguage(...languages);
62
83
  visit(tree, "element", (node) => {
63
84
  if (node.tagName !== "pre") return;
64
85
  const codeNode = node.children?.[0];
@@ -72,12 +93,10 @@ function rehypeShiki() {
72
93
  codeNode.properties.language = language;
73
94
  let highlighted = renderPlainCode(code);
74
95
  if (language && language !== "txt") try {
75
- const tokens = shikiHighlighter.codeToThemedTokens(code, language);
76
- highlighted = shiki.renderToHtml(tokens, { elements: {
77
- pre: ({ children }) => children,
78
- code: ({ children }) => children,
79
- line: ({ children }) => `<span>${children}</span>`
80
- } });
96
+ highlighted = renderHighlightedCode(shikiHighlighter.codeToHtml(code, {
97
+ lang: language,
98
+ theme: cssVariablesTheme
99
+ }));
81
100
  } catch {
82
101
  highlighted = renderPlainCode(code);
83
102
  }
@@ -105,271 +124,116 @@ var rehypePlugins = [
105
124
  rehypeParseCodeBlocks,
106
125
  rehypeShiki
107
126
  ];
108
- //#endregion
109
- //#region source/plugins/content-artifacts/artifacts.ts
110
- var UTF8_SIGNATURE = "";
111
- function normalizeBasePath(basePath) {
112
- if (!basePath || basePath === "/") return "";
113
- return "/" + basePath.replace(/^\/+|\/+$/g, "");
114
- }
115
- function containsNonAscii(content) {
116
- return [...content].some((character) => character.codePointAt(0) > 127);
117
- }
118
- function withUtf8Signature(content) {
119
- if (!containsNonAscii(content) || content.startsWith(UTF8_SIGNATURE)) return content;
120
- return `${UTF8_SIGNATURE}${content}`;
121
- }
122
- function shouldUseUtf8Signature(route) {
123
- if (route.kind === "mdx") return true;
124
- return false;
125
- }
126
- function routeToMarkdownArtifactUrl(routePath) {
127
- return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.md`;
128
- }
129
- function routeToOpenAPIArtifactUrl(routePath) {
130
- return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi.json`;
131
- }
132
- function attachContentArtifactUrls(routes) {
133
- for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path);
134
- }
135
- function readRouteContent(route) {
136
- if (route.content !== void 0) return route.content;
137
- throw new Error(`Route content is missing from route context: ${route.filePath}`);
138
- }
139
- function readRouteArtifactContent(route) {
140
- const content = readRouteContent(route);
141
- return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
142
- }
143
- function isLlmsTxtRoute(route) {
144
- return !route.path.split("/").includes("404");
145
- }
146
- function llmsTxtDescription(route) {
147
- if (route.description) return route.description;
148
- const sections = route.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
149
- if (sections?.length) return `Covers ${sections.join(", ")}.`;
150
- if (route.keywords?.length) return `Related topics: ${route.keywords.join(", ")}.`;
151
- if (route.kind === "openapi") return "OpenAPI artifact for machine-readable API reference data.";
152
- }
153
- function llmsTxtListItem(route, basePath) {
154
- if (!route.contentArtifactUrl) return void 0;
155
- const description = llmsTxtDescription(route);
156
- return description ? `- [${route.title}](${basePath}${route.contentArtifactUrl}): ${description}` : `- [${route.title}](${basePath}${route.contentArtifactUrl})`;
157
- }
158
- function llmsTxtLocaleLabel(locale, projectConfig) {
159
- return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
127
+ function formatMdxDiagnostic(error) {
128
+ if (error instanceof Error) return error.message;
129
+ return String(error);
160
130
  }
161
- function groupRoutesByLocale(routes) {
162
- const groups = /* @__PURE__ */ new Map();
163
- for (const route of routes) {
164
- const key = route.locale ?? "default";
165
- groups.set(key, [...groups.get(key) ?? [], route]);
131
+ /**
132
+ * Validate MDX content for diagnostics WITHOUT running the build-time rehype
133
+ * pipeline (Shiki highlighting, slug injection, code-block parsing).
134
+ *
135
+ * `rehypePlugins` includes `rehypeShiki`, which loads grammar/WASM assets and
136
+ * is expensive. The real highlighting happens at Vite build time via the
137
+ * `@mdx-js/rollup` plugin (see `createMdxPlugin` in `core/adapters.ts`), which
138
+ * runs the full remark + rehype pipeline. Re-running it here, during route
139
+ * discovery (Phase 3), would compile every `.mdx` file twice and invoke Shiki
140
+ * once per file per `engine.refresh()` — see ARCHITECTURE.md §2.2 which states
141
+ * "不应在 core 中执行代码高亮".
142
+ *
143
+ * The remark pipeline still runs (shared with the Vite plugin via
144
+ * `remarkPlugins`), so MDX/JSX syntax errors — the only class of error this
145
+ * diagnostic is meant to surface ahead of the Vite build — are caught. The
146
+ * output is discarded; only the thrown error is used.
147
+ */
148
+ async function compileMdxContent(content, filePath, projectRoot) {
149
+ try {
150
+ await compile(content, {
151
+ jsx: true,
152
+ providerImportSource: "@clarify-labs/renderer",
153
+ remarkPlugins
154
+ });
155
+ return { ok: true };
156
+ } catch (error) {
157
+ return {
158
+ ok: false,
159
+ diagnostic: {
160
+ kind: "mdx",
161
+ title: "MDX syntax error",
162
+ message: "This page could not be compiled. Fix the MDX syntax or component usage above, then reload the route.",
163
+ filePath: filePath && projectRoot ? filePath.replace(projectRoot + "/", "").replace(/^\/+/, "") : filePath ? filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "") : void 0,
164
+ details: formatMdxDiagnostic(error)
165
+ }
166
+ };
166
167
  }
167
- return groups;
168
168
  }
169
- function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
170
- const routesBySource = /* @__PURE__ */ new Map();
171
- const defaultLocale = projectConfig.i18n?.defaultLocale;
172
- for (const route of routes) {
173
- const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.contentArtifactUrl ?? route.path}`;
174
- const previousRoute = routesBySource.get(sourceKey);
175
- if (!previousRoute || previousRoute.locale && !route.locale) routesBySource.set(sourceKey, route);
169
+ //#endregion
170
+ //#region source/core/plugin/hooks.ts
171
+ async function runHooks(plugins, hookName, input, ctx) {
172
+ let result = input;
173
+ for (const plugin of plugins) {
174
+ const hook = plugin.hooks?.[hookName];
175
+ if (!hook) continue;
176
+ try {
177
+ result = await hook(result, ctx);
178
+ } catch (err) {
179
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
180
+ }
176
181
  }
177
- return groupRoutesByLocale([...routesBySource.values()].map((route) => ({
178
- ...route,
179
- locale: route.locale ?? defaultLocale
180
- })));
181
- }
182
- function llmsTxtDocsSectionTitle(locale, groupCount, projectConfig) {
183
- if (groupCount <= 1) return "Docs";
184
- return `Docs - ${locale === "default" ? "Default" : llmsTxtLocaleLabel(locale, projectConfig)}`;
182
+ return result;
185
183
  }
186
- function createLlmsTxt(routes, projectConfig) {
187
- const basePath = normalizeBasePath(projectConfig.routePrefix);
188
- const lines = [`# ${projectConfig.title}`, ""];
189
- if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
190
- lines.push("This file lists the source-ready Markdown and OpenAPI artifacts for this documentation site.", "");
191
- const mdxRoutes = routes.filter((route) => route.kind === "mdx" && isLlmsTxtRoute(route) && !route.isBareAlias);
192
- if (mdxRoutes.length > 0) {
193
- const localizedGroups = groupLlmsTxtRoutesByLocale(mdxRoutes, projectConfig);
194
- for (const [locale, localeRoutes] of localizedGroups) {
195
- lines.push(`## ${llmsTxtDocsSectionTitle(locale, localizedGroups.size, projectConfig)}`);
196
- for (const route of localeRoutes) {
197
- const item = llmsTxtListItem(route, basePath);
198
- if (item) lines.push(item);
199
- }
200
- lines.push("");
184
+ async function runDevConfigureServerHooks(plugins, server, ctx) {
185
+ for (const plugin of plugins) {
186
+ const hook = plugin.hooks?.["dev:configureServer"];
187
+ if (!hook) continue;
188
+ try {
189
+ await hook(server, ctx);
190
+ } catch (err) {
191
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
201
192
  }
202
193
  }
203
- const openApiRoutes = routes.filter((route) => route.kind === "openapi" && isLlmsTxtRoute(route) && !route.isBareAlias);
204
- if (openApiRoutes.length > 0) {
205
- if (lines.at(-1) !== "") lines.push("");
206
- lines.push("## OpenAPI");
207
- for (const route of openApiRoutes) {
208
- const item = llmsTxtListItem(route, basePath);
209
- if (item) lines.push(item);
194
+ }
195
+ async function runCollectorHooks(plugins, hookName, ctx) {
196
+ const results = [];
197
+ for (const plugin of plugins) {
198
+ const hook = plugin.hooks?.[hookName];
199
+ if (!hook) continue;
200
+ try {
201
+ const pluginResults = await hook(ctx);
202
+ results.push(...pluginResults);
203
+ } catch (err) {
204
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
210
205
  }
211
206
  }
212
- while (lines.at(-1) === "") lines.pop();
213
- return `${lines.join("\n")}\n`;
214
- }
215
- function createLlmsTxtArtifact(routes, projectConfig) {
216
- return withUtf8Signature(createLlmsTxt(routes, projectConfig));
217
- }
218
- //#endregion
219
- //#region source/plugins/content-artifacts/server.ts
220
- function normalizeRoutePrefix$1(routePrefix) {
221
- if (!routePrefix || routePrefix === "/") return "";
222
- return `/${routePrefix.replace(/^\/+|\/+$/g, "")}`;
223
- }
224
- function resolveContentArtifactPath(url, projectConfig) {
225
- const requestPath = url?.split("?")[0] ?? "";
226
- const basePath = normalizeRoutePrefix$1(projectConfig.routePrefix);
227
- return basePath && requestPath.startsWith(basePath) ? requestPath.slice(basePath.length) || "/" : requestPath;
207
+ return results;
228
208
  }
229
- function resolveContentArtifactType(route) {
230
- return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
209
+ async function runBuildAssetsHooks(plugins, ctx) {
210
+ return runCollectorHooks(plugins, "build:assets", ctx);
231
211
  }
232
- function serveContentArtifacts(req, res, projectConfig, routes) {
233
- const contentPath = resolveContentArtifactPath(req.url, projectConfig);
234
- if (contentPath === "/llms.txt") {
235
- res.statusCode = 200;
236
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
237
- res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
238
- return true;
239
- }
240
- if (contentPath.endsWith(".openapi.yaml")) {
241
- const jsonPath = contentPath.replace(/\.yaml$/, ".json");
242
- const route = routes.find((r) => r.contentArtifactUrl === jsonPath);
243
- if (route?.kind === "openapi" && route.content) {
244
- const spec = JSON.parse(route.content);
245
- res.statusCode = 200;
246
- res.setHeader("Content-Type", "text/yaml; charset=utf-8");
247
- res.end(stringify(spec, { lineWidth: 0 }), "utf8");
248
- return true;
212
+ async function runBuildDoneHooks(plugins, ctx) {
213
+ for (const plugin of plugins) {
214
+ const hook = plugin.hooks?.["build:done"];
215
+ if (!hook) continue;
216
+ try {
217
+ await hook(ctx);
218
+ } catch (err) {
219
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:done" failed: ${err}`, { cause: err });
249
220
  }
250
- return false;
251
221
  }
252
- const route = routes.find((route) => route.contentArtifactUrl === contentPath);
253
- if (!route) return false;
254
- res.statusCode = 200;
255
- res.setHeader("Content-Type", resolveContentArtifactType(route));
256
- res.end(readRouteArtifactContent(route), "utf8");
257
- return true;
258
222
  }
259
223
  //#endregion
260
- //#region source/plugins/content-artifacts/index.ts
261
- function createContentArtifactsPlugin() {
224
+ //#region source/parsers/markdown/frontmatter.ts
225
+ /**
226
+ * 在内容入口统一解析 MDX frontmatter,并返回可供后续流程复用的标准化正文。
227
+ */
228
+ function parseFrontmatter(content) {
229
+ const parsed = matter(content);
262
230
  return {
263
- name: "clarify:content-artifacts",
264
- hooks: {
265
- "routes:resolved": (input) => {
266
- attachContentArtifactUrls(input.routes);
267
- return input;
268
- },
269
- "dev:configureServer": (server, ctx) => {
270
- server.middlewares.use((req, res, next) => {
271
- if (serveContentArtifacts(req, res, ctx.projectConfig, ctx.routes)) return;
272
- next();
273
- });
274
- },
275
- "build:assets": (ctx) => {
276
- const assets = [];
277
- for (const route of ctx.routes) {
278
- if (!route.contentArtifactUrl) continue;
279
- assets.push({
280
- fileName: route.contentArtifactUrl.replace(/^\//, ""),
281
- source: readRouteArtifactContent(route)
282
- });
283
- if (route.kind === "openapi" && route.content) {
284
- const yamlFileName = route.contentArtifactUrl.replace(/\.json$/, ".yaml");
285
- const spec = JSON.parse(route.content);
286
- assets.push({
287
- fileName: yamlFileName.replace(/^\//, ""),
288
- source: stringify(spec, { lineWidth: 0 })
289
- });
290
- }
291
- }
292
- assets.push({
293
- fileName: "llms.txt",
294
- source: createLlmsTxtArtifact(ctx.routes, ctx.projectConfig)
295
- });
296
- return assets;
297
- }
298
- }
231
+ frontmatter: parsed.data,
232
+ content: parsed.content.replace(/^\r?\n/, "")
299
233
  };
300
234
  }
301
235
  //#endregion
302
- //#region source/plugins/html-shell/index.ts
303
- var REL_ICON_RE = /<link\s+[^>]*rel=["'][^"']*\bicon\b[^"']*["'][^>]*>/i;
304
- function inferIconType(href) {
305
- const path = href.split(/[?#]/, 1)[0]?.toLowerCase() ?? href.toLowerCase();
306
- if (path.endsWith(".svg")) return "image/svg+xml";
307
- if (path.endsWith(".png")) return "image/png";
308
- if (path.endsWith(".ico")) return "image/x-icon";
309
- if (path.endsWith(".webp")) return "image/webp";
310
- }
311
- function createIconTag(href, media) {
312
- return {
313
- tag: "link",
314
- attrs: {
315
- rel: "icon",
316
- ...inferIconType(href) ? { type: inferIconType(href) } : {},
317
- href,
318
- ...media ? { media } : {}
319
- },
320
- injectTo: "head"
321
- };
322
- }
323
- function createIconTags(icon) {
324
- if (!icon) return [];
325
- if (typeof icon === "string") return [createIconTag(icon)];
326
- const lightIcon = icon.light ?? icon.dark;
327
- const darkIcon = icon.dark ?? icon.light;
328
- if (!lightIcon || !darkIcon) return [];
329
- if (lightIcon === darkIcon) return [createIconTag(lightIcon)];
330
- return [createIconTag(lightIcon, "(prefers-color-scheme: light)"), createIconTag(darkIcon, "(prefers-color-scheme: dark)")];
331
- }
332
- function createHtmlShellPlugin() {
333
- return {
334
- name: "clarify:html-shell",
335
- hooks: { "html:transform": (input, ctx) => ({
336
- html: input.html,
337
- tags: [
338
- ...input.tags,
339
- ...REL_ICON_RE.test(input.html) ? [] : createIconTags(ctx.projectConfig.favicon ?? ctx.projectConfig.logo),
340
- {
341
- tag: "script",
342
- children: themeBootstrapScript(ctx.projectConfig.theme),
343
- injectTo: "head-prepend"
344
- },
345
- {
346
- tag: "script",
347
- attrs: {
348
- type: "module",
349
- src: input.clientEntryId
350
- },
351
- injectTo: "body"
352
- }
353
- ],
354
- clientEntryId: input.clientEntryId,
355
- dev: input.dev
356
- }) }
357
- };
358
- }
359
- //#endregion
360
- //#region source/parsers/frontmatter.ts
361
- /**
362
- * 在内容入口统一解析 MDX frontmatter,并返回可供后续流程复用的标准化正文。
363
- */
364
- function parseFrontmatter(content) {
365
- const parsed = matter(content);
366
- return {
367
- frontmatter: parsed.data,
368
- content: parsed.content.replace(/^\r?\n/, "")
369
- };
370
- }
371
- //#endregion
372
- //#region source/parsers/content.ts
236
+ //#region source/parsers/content/content.ts
373
237
  async function transformContent(transform, input) {
374
238
  return transform ? await transform(input) : input;
375
239
  }
@@ -400,71 +264,19 @@ function createContentProcessor(transform) {
400
264
  }
401
265
  };
402
266
  }
403
- //#endregion
404
- //#region source/core/hooks.ts
405
- async function runHooks(plugins, hookName, input, ctx) {
406
- let result = input;
407
- for (const plugin of plugins) {
408
- const hook = plugin.hooks?.[hookName];
409
- if (!hook) continue;
410
- try {
411
- result = await hook(result, ctx);
412
- } catch (err) {
413
- throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
414
- }
415
- }
416
- return result;
417
- }
418
- async function runDevConfigureServerHooks(plugins, server, ctx) {
419
- for (const plugin of plugins) {
420
- const hook = plugin.hooks?.["dev:configureServer"];
421
- if (!hook) continue;
422
- try {
423
- await hook(server, ctx);
424
- } catch (err) {
425
- throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
426
- }
427
- }
428
- }
429
- async function runBuildAssetsHooks(plugins, ctx) {
430
- const assets = [];
431
- for (const plugin of plugins) {
432
- const hook = plugin.hooks?.["build:assets"];
433
- if (!hook) continue;
434
- try {
435
- const pluginAssets = await hook(ctx);
436
- assets.push(...pluginAssets);
437
- } catch (err) {
438
- throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
439
- }
440
- }
441
- return assets;
442
- }
443
- async function runBuildDoneHooks(plugins, ctx) {
444
- for (const plugin of plugins) {
445
- const hook = plugin.hooks?.["build:done"];
446
- if (!hook) continue;
447
- try {
448
- await hook(ctx);
449
- } catch (err) {
450
- throw new Error(`[clarify] plugin "${plugin.name}" hook "build:done" failed: ${err}`, { cause: err });
451
- }
452
- }
453
- }
454
- //#endregion
455
- //#region source/core/content.ts
456
- var contentProcessors = /* @__PURE__ */ new WeakMap();
267
+ /**
268
+ * Creates a ContentProcessor that forwards each piece of content through the
269
+ * `content:transform` pipeline hook before it is parsed.
270
+ *
271
+ * Plugins that need to read content (site-discovery, openapi) call this with
272
+ * the plugins list from their hook context. There is no global registry -
273
+ * each call produces an independent processor bound to the given plugins.
274
+ */
457
275
  function createProjectContentProcessor(plugins, ctx) {
458
276
  return createContentProcessor((input) => runHooks(plugins, "content:transform", input, ctx));
459
277
  }
460
- function setProjectContentProcessor(ctx, processor) {
461
- contentProcessors.set(ctx, processor);
462
- }
463
- function getProjectContentProcessor(ctx) {
464
- return contentProcessors.get(ctx);
465
- }
466
278
  //#endregion
467
- //#region source/parsers/routes.ts
279
+ //#region source/parsers/routes/routes.ts
468
280
  function kebabToTitle(str) {
469
281
  return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
470
282
  }
@@ -542,8 +354,14 @@ function resolveLocalizedText(text, locale, fallbackLocale) {
542
354
  }
543
355
  async function findContentRoutes(dir, base = dir, options = {}) {
544
356
  const routes = [];
545
- if (!existsSync(dir)) return routes;
546
- const entries = readdirSync(dir, { withFileTypes: true });
357
+ let dirStat;
358
+ try {
359
+ dirStat = await stat(dir);
360
+ } catch {
361
+ return routes;
362
+ }
363
+ if (!dirStat.isDirectory()) return routes;
364
+ const entries = await readdir(dir, { withFileTypes: true });
547
365
  for (const entry of entries) {
548
366
  const fullPath = join(dir, entry.name);
549
367
  if (entry.isDirectory()) routes.push(...await findContentRoutes(fullPath, base, options));
@@ -551,30 +369,41 @@ async function findContentRoutes(dir, base = dir, options = {}) {
551
369
  const relativePath = relative(base, fullPath);
552
370
  const pathParts = relativePath.replace(/\.mdx?$/, "").split("/");
553
371
  const cleanPath = ("/" + pathParts.map((p) => p === "index" ? "" : p).filter(Boolean).join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
554
- const source = readFileSync(fullPath, "utf-8");
372
+ const source = await readFile(fullPath, "utf-8");
555
373
  const { frontmatter, content } = await (options.contentProcessor ?? createContentProcessor()).processMdx(source, fullPath);
556
- let title = typeof frontmatter.title === "string" ? frontmatter.title : "";
374
+ const page = {
375
+ path: cleanPath,
376
+ filePath: fullPath,
377
+ frontmatter,
378
+ content
379
+ };
380
+ const mdxResult = await compileMdxContent(page.content, fullPath, base);
381
+ let title = typeof page.frontmatter.title === "string" ? page.frontmatter.title : "";
557
382
  if (!title) {
558
383
  const lastPart = pathParts[pathParts.length - 1] ?? "";
559
- title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(content) : lastPart) || "Untitled";
384
+ title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(page.content) : lastPart) || "Untitled";
560
385
  }
561
386
  routes.push({
387
+ kind: "mdx",
388
+ title,
562
389
  path: cleanPath,
563
390
  basePath: cleanPath,
564
391
  filePath: fullPath,
565
392
  virtualModuleId: "virtual:clarify-page/" + relativePath.replace(/\.mdx?$/, "").replace(/\/+/g, "/"),
566
- title,
567
- description: typeof frontmatter.description === "string" ? frontmatter.description : void 0,
568
- keywords: frontmatterKeywords(frontmatter),
569
- kind: "mdx",
570
- frontmatter,
571
- content,
572
- sections: extractMdxSections(content)
393
+ description: typeof page.frontmatter.description === "string" ? page.frontmatter.description : void 0,
394
+ keywords: frontmatterKeywords(page.frontmatter),
395
+ frontmatter: page.frontmatter,
396
+ content: page.content,
397
+ sections: extractMdxSections(page.content),
398
+ diagnostic: mdxResult.ok ? void 0 : mdxResult.diagnostic
573
399
  });
574
400
  }
575
401
  }
576
402
  return routes;
577
403
  }
404
+ function virtualModuleIdForLocalizedRoute(contentRoot, filePath) {
405
+ return "virtual:clarify-page/" + relative(contentRoot, filePath).replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
406
+ }
578
407
  function frontmatterKeywords(frontmatter) {
579
408
  const value = frontmatter.keywords ?? frontmatter.keyword ?? frontmatter.tags;
580
409
  const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
@@ -592,6 +421,63 @@ function withAlternates(route, routes, i18n) {
592
421
  alternates
593
422
  };
594
423
  }
424
+ /**
425
+ * 为默认语言的 locale-prefixed 路由生成无前缀的"裸路径别名",方便不带语言
426
+ * 前缀的 URL 也能访问。标记 `isBareAlias` 以便搜索索引生成时过滤重复索引。
427
+ */
428
+ function generateBareAliases(routes, i18n) {
429
+ const bareRoutes = [];
430
+ const seenBare = new Set(routes.map((r) => r.path));
431
+ for (const route of routes) {
432
+ if (route.locale !== i18n.defaultLocale) continue;
433
+ const bp = route.basePath ?? route.path;
434
+ if (bp === route.path || seenBare.has(bp)) continue;
435
+ seenBare.add(bp);
436
+ bareRoutes.push({
437
+ ...route,
438
+ path: bp,
439
+ isBareAlias: true
440
+ });
441
+ }
442
+ return bareRoutes;
443
+ }
444
+ async function findLocalizedContentRoutes(contentRoot, i18n, options = {}) {
445
+ if (!i18n) return findContentRoutes(contentRoot, contentRoot, options);
446
+ const localizedRoutes = [];
447
+ for (const locale of i18n.locales) {
448
+ const localeRoot = join(contentRoot, locale.code);
449
+ const discovered = await findContentRoutes(localeRoot, localeRoot, options);
450
+ for (const route of discovered) {
451
+ const basePath = route.basePath ?? route.path;
452
+ localizedRoutes.push({
453
+ ...route,
454
+ path: localizedRoutePath(basePath, locale.code, i18n),
455
+ basePath,
456
+ locale: locale.code,
457
+ virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.filePath)
458
+ });
459
+ }
460
+ }
461
+ if (i18n.missing === "fallback") {
462
+ const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
463
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
464
+ for (const sourceRoute of defaultRoutes) {
465
+ const basePath = sourceRoute.basePath ?? sourceRoute.path;
466
+ for (const locale of i18n.locales) {
467
+ const key = `${locale.code}:${basePath}`;
468
+ if (routeByLocaleAndBase.has(key)) continue;
469
+ localizedRoutes.push({
470
+ ...sourceRoute,
471
+ path: localizedRoutePath(basePath, locale.code, i18n),
472
+ locale: locale.code,
473
+ isFallback: true
474
+ });
475
+ }
476
+ }
477
+ }
478
+ const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
479
+ return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, i18n)];
480
+ }
595
481
  function buildNavigation(routes) {
596
482
  const root = [];
597
483
  for (const route of routes) {
@@ -678,19 +564,7 @@ function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
678
564
  const nextRoutes = [...routes, ...additions];
679
565
  const routesWithAlternates = i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, i18n)) : nextRoutes;
680
566
  if (!i18n) return routesWithAlternates;
681
- const bareRoutes = [];
682
- const seenBare = new Set(routesWithAlternates.map((r) => r.path));
683
- for (const route of routesWithAlternates) {
684
- if (route.locale !== i18n.defaultLocale) continue;
685
- const bp = route.basePath ?? route.path;
686
- if (bp === route.path || seenBare.has(bp)) continue;
687
- seenBare.add(bp);
688
- bareRoutes.push({
689
- ...route,
690
- path: bp
691
- });
692
- }
693
- return [...routesWithAlternates, ...bareRoutes];
567
+ return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, i18n)];
694
568
  }
695
569
  function buildNavigationFromPagesConfig(routes, config) {
696
570
  if (!config || config === "FileTree") return buildNavigation(routes);
@@ -794,1108 +668,1802 @@ function buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) {
794
668
  }
795
669
  return result;
796
670
  }
797
- //#endregion
798
- //#region source/plugins/openapi/parser.ts
799
- var OPENAPI_HTTP_METHODS = [
800
- "get",
801
- "put",
802
- "post",
803
- "delete",
804
- "options",
805
- "head",
806
- "patch",
807
- "trace"
808
- ];
809
- function errorMessage(error) {
810
- return error instanceof Error ? error.message : String(error);
811
- }
812
- function isSameOpenAPIFile(url, filePath) {
813
- try {
814
- return resolve(fileURLToPath(url)) === resolve(filePath);
815
- } catch {
816
- return resolve(url) === resolve(filePath);
817
- }
818
- }
819
- function findOpenAPIRoutes(dir, base = dir) {
820
- const routes = [];
821
- if (!existsSync(dir)) return routes;
822
- const entries = readdirSync(dir, { withFileTypes: true });
823
- for (const entry of entries) {
824
- const fullPath = join(dir, entry.name);
825
- if (entry.isDirectory()) {
826
- routes.push(...findOpenAPIRoutes(fullPath, base));
827
- continue;
828
- }
829
- if (!entry.isFile() || !/\.openapi\.(json|yaml|yml)$/.test(entry.name)) continue;
830
- const ref = relative(base, fullPath);
831
- const cleanPath = routePathFromRef(ref);
832
- routes.push({
833
- path: cleanPath,
834
- basePath: cleanPath,
835
- filePath: fullPath,
836
- virtualModuleId: virtualModuleIdFromRef(ref),
837
- title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
838
- kind: "openapi"
839
- });
840
- }
841
- return routes;
842
- }
843
- function operationMatchesTags(operationTags, filterTags) {
844
- if (!filterTags?.length) return true;
845
- return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
846
- }
847
- /** Filter an OpenAPI spec to only include paths that have at least one operation matching the given tags. */
848
- function filterSpecByTags(spec, tags) {
849
- const filteredPaths = {};
850
- const paths = spec.paths ?? {};
851
- for (const path of Object.keys(paths)) {
852
- const pathItem = paths[path];
853
- if (!pathItem) continue;
854
- if (Object.values(pathItem).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredPaths[path] = pathItem;
855
- }
671
+ /** Serialize a ContentRoute into the minimal shape exposed to tooling. */
672
+ function toDevRouteEntry(route) {
856
673
  return {
857
- ...spec,
858
- paths: filteredPaths
674
+ path: route.path,
675
+ filePath: route.filePath,
676
+ locale: route.locale,
677
+ basePath: route.basePath,
678
+ kind: route.kind,
679
+ title: route.title
859
680
  };
860
681
  }
861
- /** Extract endpoint operations from an OpenAPI spec as page sections. */
862
- function extractOpenAPISections(spec, filterTags) {
863
- const sections = [];
864
- const paths = spec.paths ?? {};
865
- for (const [path, pathItem] of Object.entries(paths)) {
866
- if (!pathItem) continue;
867
- for (const method of OPENAPI_HTTP_METHODS) {
868
- const op = pathItem[method];
869
- if (!op || !operationMatchesTags(op.tags, filterTags)) continue;
870
- const title = op.summary ?? `${method.toUpperCase()} ${path}`;
871
- sections.push({
872
- id: slug(`${method} ${path}`),
873
- title,
874
- level: 2,
875
- badge: method.toUpperCase(),
876
- tags: op.tags
877
- });
878
- }
682
+ /**
683
+ * Resolve a single file path to its preview route.
684
+ *
685
+ * Selection order:
686
+ * 1. Routes whose `filePath` exactly matches.
687
+ * 2. Among matches, prefer the one whose `locale` matches the file's locale
688
+ * directory (e.g. `source/zh-CN/x.mdx` → locale `zh-CN`).
689
+ * 3. If no locale match, fall back to the default locale (if i18n is enabled)
690
+ * or the first match.
691
+ */
692
+ function resolveRouteForFile(filePath, routes, defaultLocale) {
693
+ const matches = routes.filter((r) => r.filePath === filePath);
694
+ if (matches.length === 0) return void 0;
695
+ if (matches.length === 1) return matches[0];
696
+ const fileLocale = inferLocaleFromPath(filePath);
697
+ if (fileLocale) {
698
+ const localeMatch = matches.find((r) => r.locale === fileLocale);
699
+ if (localeMatch) return localeMatch;
879
700
  }
880
- return sections;
881
- }
882
- async function readOpenAPISpec(filePath, contentProcessor) {
883
- try {
884
- if (!contentProcessor) return {
885
- ok: true,
886
- spec: await SwaggerParser.dereference(filePath)
887
- };
888
- const source = readFileSync(filePath, "utf-8");
889
- const transformedSource = await contentProcessor.processText(source, "openapi", filePath);
890
- if (transformedSource === source) return {
891
- ok: true,
892
- spec: await SwaggerParser.dereference(filePath)
893
- };
894
- return {
895
- ok: true,
896
- spec: await SwaggerParser.dereference(filePath, { resolve: { transformedOpenAPISource: {
897
- order: 1,
898
- canRead: (file) => isSameOpenAPIFile(file.url, filePath),
899
- read: () => transformedSource
900
- } } })
901
- };
902
- } catch (error) {
903
- return {
904
- ok: false,
905
- diagnostic: {
906
- title: "OpenAPI spec parse failed",
907
- message: `Clarify could not parse ${filePath}.`,
908
- filePath,
909
- cause: errorMessage(error)
910
- }
911
- };
701
+ if (defaultLocale) {
702
+ const defaultMatch = matches.find((r) => r.locale === defaultLocale);
703
+ if (defaultMatch) return defaultMatch;
912
704
  }
913
- }
914
- //#endregion
915
- //#region source/core/virtual-modules.ts
916
- var VIRTUAL_CONFIG = "virtual:clarify/config";
917
- var VIRTUAL_ROUTES = "virtual:clarify/routes";
918
- var VIRTUAL_SERVER_ROUTES = "virtual:clarify/routes/server";
919
- var VIRTUAL_OPENAPI = "virtual:clarify/openapi";
920
- var VIRTUAL_SLOTS = "virtual:clarify/slots";
921
- var VIRTUAL_SLOT = "virtual:clarify/slot";
922
- var VIRTUAL_CLIENT_ENTRY = "virtual:clarify/entry-client";
923
- var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify/entry-client";
924
- function resolveVirtualId(id) {
925
- return "\0" + id;
926
- }
927
- function stripVirtualPrefix(id) {
928
- return id.startsWith("\0") ? id.slice(1) : id;
929
- }
930
- function generateConfigModule(projectConfig, buildOptions) {
931
- return `export const config = ${JSON.stringify({
932
- ...projectConfig,
933
- ...buildOptions
934
- })};`;
935
- }
936
- function moduleSpecifier(value) {
937
- return JSON.stringify(value);
938
- }
939
- function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode = "client") {
940
- const imports = mode === "server" ? routes.map((r, i) => `import Page${i} from ${moduleSpecifier(r.virtualModuleId)};`).join("\n") : "";
941
- const routesArray = routes.map((r, i) => {
942
- const sections = r.sections && r.sections.length > 0 ? `, sections: ${JSON.stringify(r.sections.map((s) => ({
943
- id: s.id,
944
- title: s.title,
945
- level: s.level,
946
- badge: s.badge,
947
- tags: s.tags
948
- })))}` : "";
949
- const contentArtifactUrl = r.contentArtifactUrl ? `, contentArtifactUrl: ${JSON.stringify(r.contentArtifactUrl)}` : "";
950
- const basePath = r.basePath ? `, basePath: ${JSON.stringify(r.basePath)}` : "";
951
- const locale = r.locale ? `, locale: ${JSON.stringify(r.locale)}` : "";
952
- const isFallback = r.isFallback ? ", isFallback: true" : "";
953
- const isBareAlias = r.isBareAlias ? ", isBareAlias: true" : "";
954
- const alternates = r.alternates ? `, alternates: ${JSON.stringify(r.alternates)}` : "";
955
- const description = r.description ? `, description: ${JSON.stringify(r.description)}` : "";
956
- const keywords = r.keywords && r.keywords.length > 0 ? `, keywords: ${JSON.stringify(r.keywords)}` : "";
957
- const sourceUrl = r.sourceUrl ? `, sourceUrl: ${JSON.stringify(r.sourceUrl)}` : "";
958
- const component = mode === "server" ? `Page${i}` : `() => import(${moduleSpecifier(r.virtualModuleId)})`;
959
- const lazy = mode === "client" ? ", lazy: true" : "";
960
- return ` { path: ${JSON.stringify(r.path)}, title: ${JSON.stringify(r.title)}, component: ${component}${lazy}, kind: ${JSON.stringify(r.kind)}${basePath}${locale}${isFallback}${isBareAlias}${alternates}${description}${keywords}${sections}${contentArtifactUrl}${sourceUrl} }`;
961
- }).join(",\n");
962
- const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
963
- return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
964
- }
965
- function createClientEntryModule(options = {}) {
966
- return `
967
- import '@clarify-labs/renderer/style.css';
968
- import { render } from '@clarify-labs/renderer/client';
969
- import { routes, navigation } from '${VIRTUAL_ROUTES}';
970
- import { config } from '${VIRTUAL_CONFIG}';
971
- import { openApis } from '${VIRTUAL_OPENAPI}';
972
- import { runtimeSlots } from '${VIRTUAL_SLOTS}';
973
- render({ config, routes, navigation, openApis, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
705
+ return matches[0];
974
706
  }
975
707
  /**
976
- * Collect every plugin's `slots` declarations into a single runtime registry
977
- * module. Component modules are exported as lazy import factories so the
978
- * renderer can decide whether to code-split (client) or pre-resolve (SSR).
979
- *
980
- * Relative component paths are resolved against the project `root` so that
981
- * the generated `import()` call uses an absolute specifier — Vite cannot
982
- * resolve relative imports from virtual modules.
708
+ * Derive a best-effort preview route for a file that is not in the route
709
+ * manifest (e.g. a draft or newly created file). The path is computed from the
710
+ * file's location relative to the content root, following the same convention
711
+ * as route discovery (`source/{locale}/path/index.mdx` → `/{locale}/path`).
983
712
  *
984
- * Slot name validity is enforced at compile time by the `UISlotName`
985
- * type — no runtime validation is needed.
713
+ * Returns null when the file is outside the content root or is not a
714
+ * recognized content file type.
986
715
  */
987
- function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
988
- const registrations = [];
989
- for (const plugin of plugins) for (const slot of plugin.slots ?? []) registrations.push({
990
- plugin: plugin.name,
991
- slot
716
+ function inferRouteForFile(filePath, contentRoot, defaultLocale) {
717
+ const ref = relative(contentRoot, filePath);
718
+ if (!ref || ref.startsWith("..")) return null;
719
+ const isMdx = /\.mdx?$/.test(filePath);
720
+ const isOpenApi = /\.openapi\.(json|ya?ml)$/.test(filePath);
721
+ if (!isMdx && !isOpenApi) return null;
722
+ const segments = ref.split("/");
723
+ const fileLocale = inferLocaleFromPath(filePath);
724
+ const localeSegmentIndex = fileLocale ? segments.indexOf(fileLocale) : -1;
725
+ const basePath = basePathFromRef(localeSegmentIndex >= 0 ? segments.slice(localeSegmentIndex + 1).join("/") : ref);
726
+ const path = fileLocale && defaultLocale ? localizedRoutePath(basePath, fileLocale, {
727
+ defaultLocale,
728
+ missing: "fallback",
729
+ locales: []
730
+ }) : basePath;
731
+ const title = segments[segments.length - 1]?.replace(/\.(mdx?|openapi\.(json|ya?ml))$/, "") || "Untitled";
732
+ return {
733
+ path: normalizePath(path),
734
+ filePath,
735
+ locale: fileLocale,
736
+ basePath: normalizePath(basePath),
737
+ kind: isMdx ? "mdx" : "openapi",
738
+ title,
739
+ inferred: true
740
+ };
741
+ }
742
+ /** Best-effort locale inference from a file path segment like `.../zh-CN/x.mdx`. */
743
+ function inferLocaleFromPath(filePath) {
744
+ const segments = filePath.split("/");
745
+ for (const segment of segments) if (/^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/.test(segment)) return segment;
746
+ }
747
+ /** Read and parse the JSON body of an IncomingMessage. */
748
+ function readJsonBody(req) {
749
+ return new Promise((resolveBody, reject) => {
750
+ const chunks = [];
751
+ req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
752
+ req.on("error", reject);
753
+ req.on("end", () => {
754
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
755
+ if (!raw) return resolveBody({});
756
+ try {
757
+ resolveBody(JSON.parse(raw));
758
+ } catch (err) {
759
+ reject(err);
760
+ }
761
+ });
992
762
  });
993
- if (registrations.length === 0) return "export const runtimeSlots = {};\n";
994
- const grouped = /* @__PURE__ */ new Map();
995
- for (const { plugin, slot } of registrations) {
996
- if (slot.name.endsWith(".replace")) {
997
- const existing = grouped.get(slot.name);
998
- if (existing) console.warn(`[clarify] Multiple plugins register slot "${slot.name}": "${plugin}" overrides "${existing[existing.length - 1]}". Only the last registration is used.`);
999
- }
1000
- if (!slot.component.startsWith("/")) throw new Error(`[clarify] Plugin "${plugin}" slot "${slot.name}" has an invalid component path "${slot.component}". Component paths must start with "/" to reference the project root.`);
1001
- const componentPath = root + slot.component;
1002
- const entry = `{ plugin: ${JSON.stringify(plugin)}, component: () => import(${moduleSpecifier(componentPath)}) }`;
1003
- const list = grouped.get(slot.name) ?? [];
1004
- list.push(entry);
1005
- grouped.set(slot.name, list);
1006
- }
1007
- return `export const runtimeSlots = {\n${[...grouped.entries()].map(([name, list]) => ` ${JSON.stringify(name)}: [\n ${list.join(",\n ")}\n ]`).join(",\n")}\n};\n`;
1008
763
  }
1009
764
  /**
1010
- * Re-exports the slot context hook from the renderer so plugin components can
1011
- * `import { useSlot } from 'virtual:clarify/slot'` without depending on
1012
- * renderer internals directly.
765
+ * Handle a request to the dev route endpoint. Pure function — does not touch
766
+ * the network directly, only reads the request body and writes to `res`.
1013
767
  */
1014
- function createSlotModule() {
1015
- return `export { useSlot } from '@clarify-labs/renderer';\n`;
1016
- }
1017
- function buildVirtualModules(args) {
1018
- const modules = /* @__PURE__ */ new Map();
1019
- const clientEntryModule = createClientEntryModule({ themeEditor: args.themeEditor });
1020
- const allPlugins = [...args.plugins ?? []];
1021
- modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
1022
- modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
1023
- modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "server"));
1024
- modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
1025
- modules.set(VIRTUAL_OPENAPI, generateOpenApiModule());
1026
- modules.set(VIRTUAL_SLOT, createSlotModule());
1027
- modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1028
- modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1029
- for (const route of args.routes) modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
1030
- return modules;
1031
- }
1032
- function generateOpenApiModule() {
1033
- return `export const openApis = {};`;
768
+ async function handleDevRouteRequest(req, res, routes, context) {
769
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
770
+ let body;
771
+ try {
772
+ body = await readJsonBody(req);
773
+ } catch {
774
+ res.statusCode = 400;
775
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
776
+ return;
777
+ }
778
+ const file = typeof body === "object" && body !== null ? body.file : void 0;
779
+ if (typeof file === "string" && file.length > 0) {
780
+ const route = resolveRouteForFile(file, routes, context.projectConfig.i18n?.defaultLocale);
781
+ if (route) {
782
+ res.end(JSON.stringify(toDevRouteEntry(route)));
783
+ return;
784
+ }
785
+ const inferred = inferRouteForFile(file, context.contentRoot, context.projectConfig.i18n?.defaultLocale);
786
+ res.end(JSON.stringify(inferred));
787
+ return;
788
+ }
789
+ res.end(JSON.stringify(routes.map(toDevRouteEntry)));
1034
790
  }
1035
791
  //#endregion
1036
- //#region source/plugins/openapi/virtual-modules.ts
1037
- var openApiRegistryModuleId = VIRTUAL_OPENAPI;
1038
- var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
1039
- function specVirtualModuleId(specKey) {
1040
- return `${OPENAPI_SPEC_PREFIX}${specKey}`;
1041
- }
1042
- function generateOpenAPIRegistryModule(openApis) {
1043
- return `export const openApis = ${JSON.stringify(openApis)};`;
1044
- }
1045
- /** Generate a virtual module that exports a single spec as default. */
1046
- function generateOpenAPISpecModule(spec) {
1047
- return `export default ${JSON.stringify(spec)};`;
1048
- }
1049
- function generateOpenAPIPageModule(opts) {
1050
- const { specKey, specRegistryKey, tagFilter } = opts;
1051
- return [
1052
- `import { createElement, useState, useEffect, useRef } from 'react';`,
1053
- `import { OpenApiDocument, useOpenApis } from '@clarify-labs/renderer';`,
1054
- `const SPEC_KEY = ${JSON.stringify(specRegistryKey)};`,
1055
- `const TAG_FILTER = ${JSON.stringify(tagFilter ?? void 0)};`,
1056
- `let loadPromise = null;`,
1057
- `function loadSpec() {`,
1058
- ` if (!loadPromise) loadPromise = import(${JSON.stringify(specVirtualModuleId(specKey))}).then(function(m) { return m.default; });`,
1059
- ` return loadPromise;`,
1060
- `}`,
1061
- `export default function OpenApiRoutePage() {`,
1062
- ` const specs = useOpenApis();`,
1063
- ` const serverSpec = specs[SPEC_KEY];`,
1064
- ` const [spec, setSpec] = useState(serverSpec || null);`,
1065
- ` const mountedRef = useRef(false);`,
1066
- ` useEffect(function() {`,
1067
- ` if (mountedRef.current) return;`,
1068
- ` mountedRef.current = true;`,
1069
- ` if (spec) return;`,
1070
- ` loadSpec().then(setSpec);`,
1071
- ` }, []);`,
1072
- ` if (!spec) return null;`,
1073
- ` return createElement(OpenApiDocument, { spec: spec, tagFilter: TAG_FILTER });`,
1074
- `}`
1075
- ].join("\n");
1076
- }
1077
- function generateOpenAPIErrorModule(diagnostic) {
1078
- return `import { createElement } from 'react';
1079
- const diagnostic = ${JSON.stringify(diagnostic)};
1080
- export default function OpenApiErrorRoutePage() {
1081
- return createElement('article', { className: 'flex h-full flex-col pt-16 pb-10' },
1082
- createElement('div', { className: 'prose max-w-none flex-auto dark:prose-invert' },
1083
- createElement('p', { className: 'mb-3 text-xs/6 font-medium tracking-widest text-red-500 uppercase dark:text-red-400' }, 'OpenAPI Error'),
1084
- createElement('h1', null, diagnostic.title),
1085
- createElement('p', { className: 'lead' }, diagnostic.message),
1086
- 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' },
1087
- createElement('div', { className: 'mb-1 font-semibold text-zinc-900 dark:text-white' }, 'File'),
1088
- createElement('code', { className: 'break-all text-zinc-700 dark:text-zinc-300' }, diagnostic.filePath)
1089
- ) : null,
1090
- 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' },
1091
- createElement('div', { className: 'mb-1 font-semibold' }, 'Why it happened'),
1092
- createElement('pre', { className: 'whitespace-pre-wrap break-words text-xs' }, diagnostic.cause)
1093
- ) : null
1094
- )
1095
- );
1096
- }`;
792
+ //#region source/parsers/theme.ts
793
+ function resolveThemeConfig(theme = {}) {
794
+ const preset = theme.preset ?? "default";
795
+ const presetTheme = themePresets[preset];
796
+ return {
797
+ preset,
798
+ tokens: {
799
+ colors: {
800
+ ...presetTheme.tokens.colors,
801
+ ...theme.tokens?.colors
802
+ },
803
+ radius: {
804
+ ...presetTheme.tokens.radius,
805
+ ...theme.tokens?.radius
806
+ }
807
+ },
808
+ layout: {
809
+ ...presetTheme.layout,
810
+ ...theme.layout
811
+ },
812
+ editor: theme.editor ?? presetTheme.editor
813
+ };
1097
814
  }
1098
815
  //#endregion
1099
- //#region source/plugins/openapi/index.ts
1100
- function collectOpenAPIPageItems(config) {
1101
- const items = [];
1102
- function visitPages(pages) {
1103
- if (!pages || pages === "FileTree") return;
1104
- for (const group of pages) for (const item of group.pages) if (typeof item !== "string" && "openapi" in item && (item.path || item.filter?.tags?.length)) items.push(item);
816
+ //#region source/core/config/config-schema.ts
817
+ var clarifyLogoConfigSchema = z.union([z.string(), z.object({
818
+ light: z.string().optional(),
819
+ dark: z.string().optional()
820
+ })]);
821
+ var clarifyFaviconConfigSchema = z.union([z.string(), z.object({
822
+ light: z.string().optional(),
823
+ dark: z.string().optional()
824
+ })]);
825
+ var clarifyLocalizedTextSchema = z.union([z.string(), z.record(z.string(), z.string())]);
826
+ var clarifyLocaleConfigSchema = z.object({
827
+ code: z.string(),
828
+ label: z.string(),
829
+ dir: z.union([z.literal("ltr"), z.literal("rtl")]).optional()
830
+ });
831
+ var clarifyI18nConfigSchema = z.object({
832
+ defaultLocale: z.string().optional(),
833
+ missing: z.union([
834
+ z.literal("fallback"),
835
+ z.literal("404"),
836
+ z.literal("hide")
837
+ ]).optional(),
838
+ locales: z.array(clarifyLocaleConfigSchema).min(1)
839
+ }).superRefine((config, ctx) => {
840
+ const localeCodes = /* @__PURE__ */ new Set();
841
+ for (const [index, locale] of config.locales.entries()) {
842
+ if (localeCodes.has(locale.code)) ctx.addIssue({
843
+ code: z.ZodIssueCode.custom,
844
+ message: `Duplicate locale code "${locale.code}"`,
845
+ path: [
846
+ "locales",
847
+ index,
848
+ "code"
849
+ ]
850
+ });
851
+ localeCodes.add(locale.code);
852
+ }
853
+ if (config.defaultLocale && !localeCodes.has(config.defaultLocale)) ctx.addIssue({
854
+ code: z.ZodIssueCode.custom,
855
+ message: "defaultLocale must be one of i18n.locales",
856
+ path: ["defaultLocale"]
857
+ });
858
+ });
859
+ var clarifyNavbarLinkSchema = z.object({
860
+ label: clarifyLocalizedTextSchema,
861
+ href: z.string(),
862
+ external: z.boolean().optional()
863
+ });
864
+ var clarifyBannerConfigOptionsSchema = z.object({
865
+ content: clarifyLocalizedTextSchema,
866
+ dismissible: z.boolean().optional(),
867
+ link: clarifyNavbarLinkSchema.optional()
868
+ }).strict();
869
+ var clarifyFooterConfigSchema = z.object({
870
+ links: z.array(clarifyNavbarLinkSchema).optional(),
871
+ socials: z.record(z.string(), z.string()).optional(),
872
+ copyright: clarifyLocalizedTextSchema.optional()
873
+ }).strict();
874
+ var clarifyVariableValueSchema = z.lazy(() => z.union([
875
+ z.string(),
876
+ z.number(),
877
+ z.boolean(),
878
+ z.record(z.string(), clarifyVariableValueSchema)
879
+ ]));
880
+ var clarifyVariablesConfigSchema = z.record(z.string(), clarifyVariableValueSchema);
881
+ var clarifySourceConfigSchema = z.object({
882
+ repository: z.string(),
883
+ branch: z.string().optional(),
884
+ directory: z.string().optional()
885
+ });
886
+ var clarifyPagesItemSchema = z.union([
887
+ z.string(),
888
+ z.object({
889
+ page: z.string(),
890
+ path: z.string().optional(),
891
+ title: clarifyLocalizedTextSchema.optional(),
892
+ icon: z.string().optional(),
893
+ redirect: z.string().optional()
894
+ }),
895
+ z.object({
896
+ openapi: z.string(),
897
+ path: z.string().optional(),
898
+ icon: z.string().optional(),
899
+ title: clarifyLocalizedTextSchema.optional(),
900
+ filter: z.object({ tags: z.array(z.string()).optional() }).optional()
901
+ })
902
+ ]);
903
+ var clarifyPagesGroupSchema = z.object({
904
+ group: clarifyLocalizedTextSchema,
905
+ icon: z.string().optional(),
906
+ pages: z.array(clarifyPagesItemSchema)
907
+ });
908
+ var clarifyPagesConfigSchema = z.union([z.array(clarifyPagesGroupSchema), z.literal("FileTree")]);
909
+ var clarifyTabItemSchema = z.object({
910
+ tab: clarifyLocalizedTextSchema,
911
+ icon: z.string().optional(),
912
+ pages: clarifyPagesConfigSchema.optional()
913
+ });
914
+ var clarifyTabsConfigSchema = z.array(clarifyTabItemSchema);
915
+ var clarifyThemePresetSchema = z.enum(["default", "base"]);
916
+ var clarifyThemeModeColorValueSchema = z.object({
917
+ light: z.string().optional(),
918
+ dark: z.string().optional()
919
+ });
920
+ var clarifyThemeColorValueSchema = z.union([z.string(), clarifyThemeModeColorValueSchema]);
921
+ var clarifyThemeColorTokensConfigSchema = z.object({
922
+ primary: clarifyThemeColorValueSchema.optional(),
923
+ accent: clarifyThemeColorValueSchema.optional(),
924
+ background: clarifyThemeColorValueSchema.optional(),
925
+ foreground: clarifyThemeColorValueSchema.optional(),
926
+ surface: clarifyThemeColorValueSchema.optional(),
927
+ muted: clarifyThemeColorValueSchema.optional(),
928
+ border: clarifyThemeColorValueSchema.optional(),
929
+ codeBackground: clarifyThemeColorValueSchema.optional()
930
+ });
931
+ var clarifyThemeRadiusTokensConfigSchema = z.object({
932
+ sm: z.string().optional(),
933
+ md: z.string().optional(),
934
+ lg: z.string().optional(),
935
+ xl: z.string().optional()
936
+ });
937
+ var clarifyThemeTokensConfigSchema = z.object({
938
+ colors: clarifyThemeColorTokensConfigSchema.optional(),
939
+ radius: clarifyThemeRadiusTokensConfigSchema.optional()
940
+ });
941
+ var clarifyThemeLayoutConfigSchema = z.object({ maxWidth: z.string().optional() });
942
+ var clarifyThemeConfigSchema = z.object({
943
+ preset: clarifyThemePresetSchema.optional(),
944
+ tokens: clarifyThemeTokensConfigSchema.optional(),
945
+ layout: clarifyThemeLayoutConfigSchema.optional(),
946
+ editor: z.boolean().optional()
947
+ });
948
+ var clarifyProjectConfigSchema = z.object({
949
+ title: z.string().optional(),
950
+ description: z.string().optional(),
951
+ siteUrl: z.string().optional(),
952
+ source: clarifySourceConfigSchema.optional(),
953
+ logo: clarifyLogoConfigSchema.optional(),
954
+ homeUrl: z.string().optional(),
955
+ favicon: clarifyFaviconConfigSchema.optional(),
956
+ theme: clarifyThemeConfigSchema.optional(),
957
+ routePrefix: z.string().optional(),
958
+ assetPrefix: z.string().optional(),
959
+ navbar: z.object({ links: z.array(clarifyNavbarLinkSchema).optional() }).optional(),
960
+ banner: clarifyBannerConfigOptionsSchema.optional(),
961
+ footer: clarifyFooterConfigSchema.optional(),
962
+ variables: clarifyVariablesConfigSchema.optional(),
963
+ i18n: clarifyI18nConfigSchema.optional(),
964
+ tabs: clarifyTabsConfigSchema.optional()
965
+ });
966
+ //#endregion
967
+ //#region source/core/config/config.ts
968
+ function formatIssuePath(path) {
969
+ return path.reduce((result, segment) => {
970
+ if (typeof segment === "number") return `${result}[${segment}]`;
971
+ const key = String(segment);
972
+ return result ? `${result}.${key}` : key;
973
+ }, "");
974
+ }
975
+ function formatProjectConfigError(error) {
976
+ const issue = error.issues[0];
977
+ if (!issue) return "[clarify] config is invalid";
978
+ const path = formatIssuePath(issue.path);
979
+ if (!path) return `[clarify] config must contain an object: ${issue.message}`;
980
+ return `[clarify] config field "${path}" is invalid: ${issue.message}`;
981
+ }
982
+ function validateProjectConfig(value) {
983
+ const result = clarifyProjectConfigSchema.safeParse(value);
984
+ if (!result.success) throw new Error(formatProjectConfigError(result.error));
985
+ return result.data;
986
+ }
987
+ function resolveI18nConfig(i18n) {
988
+ if (!i18n) return void 0;
989
+ const firstLocale = i18n.locales[0]?.code;
990
+ const defaultLocale = i18n.defaultLocale ?? firstLocale;
991
+ if (!defaultLocale) return void 0;
992
+ return {
993
+ defaultLocale,
994
+ missing: i18n.missing ?? "fallback",
995
+ locales: i18n.locales
996
+ };
997
+ }
998
+ function resolveRoutePrefix(routePrefix) {
999
+ const trimmed = routePrefix?.trim();
1000
+ if (!trimmed || trimmed === "/") return "/";
1001
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
1002
+ }
1003
+ function resolveAssetPrefix(assetPrefix, routePrefix) {
1004
+ const trimmed = assetPrefix?.trim();
1005
+ if (trimmed === void 0) return routePrefix;
1006
+ if (trimmed === "./" || trimmed === "") return trimmed || "/";
1007
+ if (!trimmed || trimmed === "/") return "/";
1008
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(trimmed)) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1009
+ if (trimmed.startsWith("./") || trimmed.startsWith("../")) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1010
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
1011
+ }
1012
+ function resolveProjectConfig(config = {}) {
1013
+ const routePrefix = resolveRoutePrefix(config.routePrefix);
1014
+ return {
1015
+ title: config.title ?? "Clarify Docs",
1016
+ description: config.description ?? "",
1017
+ siteUrl: config.siteUrl,
1018
+ source: config.source,
1019
+ logo: config.logo,
1020
+ homeUrl: config.homeUrl,
1021
+ favicon: config.favicon,
1022
+ routePrefix,
1023
+ assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
1024
+ theme: resolveThemeConfig(config.theme),
1025
+ navbar: config.navbar,
1026
+ banner: config.banner,
1027
+ footer: config.footer,
1028
+ variables: config.variables ?? {},
1029
+ i18n: resolveI18nConfig(config.i18n),
1030
+ tabs: config.tabs
1031
+ };
1032
+ }
1033
+ //#endregion
1034
+ //#region source/core/config/user-config.ts
1035
+ function defineConfig(config) {
1036
+ validateProjectConfig(config);
1037
+ return config;
1038
+ }
1039
+ /**
1040
+ * Config filenames Clarify CLI looks for at the project root, in resolution
1041
+ * order. Exported so other modules (e.g. the dev `project-info` endpoint) can
1042
+ * surface the same list to tooling without duplicating it.
1043
+ */
1044
+ var CONFIG_FILENAMES = [
1045
+ "clarify.ts",
1046
+ "clarify.js",
1047
+ "clarify.json"
1048
+ ];
1049
+ function findClarifyConfigFile(root) {
1050
+ for (const filename of CONFIG_FILENAMES) {
1051
+ const filePath = resolve(root, filename);
1052
+ if (existsSync(filePath)) return filePath;
1053
+ }
1054
+ }
1055
+ function assertConfigObject(config, configFile) {
1056
+ if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error(`[clarify] ${configFile} must export a configuration object`);
1057
+ return config;
1058
+ }
1059
+ function loadJsonConfig(configFile) {
1060
+ const config = assertConfigObject(JSON.parse(readFileSync(configFile, "utf-8")), configFile);
1061
+ const projectConfig = validateProjectConfig(config);
1062
+ const ssg = config.ssg && typeof config.ssg === "object" && !Array.isArray(config.ssg) ? config.ssg : void 0;
1063
+ return {
1064
+ ...projectConfig,
1065
+ ...ssg ? { ssg } : {}
1066
+ };
1067
+ }
1068
+ async function loadClarifyConfig(root, env) {
1069
+ const configFile = findClarifyConfigFile(root);
1070
+ if (!configFile) return {};
1071
+ if (configFile.endsWith(".json")) return loadJsonConfig(configFile);
1072
+ return assertConfigObject((await loadConfigFromFile(env, configFile, root, "silent"))?.config, configFile);
1073
+ }
1074
+ /**
1075
+ * File extensions Clarify treats as content files. Kept in sync with the
1076
+ * patterns used by route discovery in `parsers/routes.ts` and the OpenAPI
1077
+ * page generator. Exposed to tooling so the extension's "is this a content
1078
+ * file?" check stays accurate without duplicating the regex.
1079
+ */
1080
+ var CONTENT_FILE_EXTENSIONS = [
1081
+ ".md",
1082
+ ".mdx",
1083
+ ".openapi.json",
1084
+ ".openapi.yaml",
1085
+ ".openapi.yml"
1086
+ ];
1087
+ /**
1088
+ * Build the project-info response payload from the resolved plugin state.
1089
+ */
1090
+ function buildProjectInfo(context) {
1091
+ const { projectRoot, contentRoot, projectConfig } = context;
1092
+ const response = {
1093
+ configFilenames: CONFIG_FILENAMES,
1094
+ contentFileExtensions: CONTENT_FILE_EXTENSIONS,
1095
+ contentRoot: (contentRoot.startsWith(projectRoot) ? contentRoot.slice(projectRoot.length).replace(/^[/\\]+/, "") : contentRoot) || "source",
1096
+ projectRoot
1097
+ };
1098
+ if (projectConfig.i18n) response.i18n = {
1099
+ defaultLocale: projectConfig.i18n.defaultLocale,
1100
+ locales: projectConfig.i18n.locales.map((l) => l.code)
1101
+ };
1102
+ return response;
1103
+ }
1104
+ /**
1105
+ * Handle a request to the project-info endpoint. Responds to any method
1106
+ * (GET is typical) with the JSON payload — there is no request body to parse.
1107
+ */
1108
+ function handleProjectInfoRequest(_req, res, context) {
1109
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
1110
+ res.end(JSON.stringify(buildProjectInfo(context)));
1111
+ }
1112
+ //#endregion
1113
+ //#region source/core/runtime/virtual-modules.ts
1114
+ var VIRTUAL_CONFIG = "virtual:clarify/config";
1115
+ var VIRTUAL_ROUTES = "virtual:clarify/routes";
1116
+ var VIRTUAL_SERVER_ROUTES = "virtual:clarify/routes/server";
1117
+ var VIRTUAL_OPENAPI = "virtual:clarify/openapi";
1118
+ var VIRTUAL_SLOTS = "virtual:clarify/slots";
1119
+ var VIRTUAL_SLOT = "virtual:clarify/slot";
1120
+ var VIRTUAL_CLIENT_ENTRY = "virtual:clarify/entry-client";
1121
+ var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify/entry-client";
1122
+ function resolveVirtualId(id) {
1123
+ return "\0" + id;
1124
+ }
1125
+ function stripVirtualPrefix(id) {
1126
+ return id.startsWith("\0") ? id.slice(1) : id;
1127
+ }
1128
+ /**
1129
+ * Fixed virtual module IDs (excluding the client entry, which has a special
1130
+ * resolved form). Used to avoid hardcoding the constant list in every
1131
+ * `resolveId` implementation.
1132
+ */
1133
+ var FIXED_VIRTUAL_IDS = [
1134
+ VIRTUAL_CONFIG,
1135
+ VIRTUAL_ROUTES,
1136
+ VIRTUAL_SERVER_ROUTES,
1137
+ VIRTUAL_OPENAPI,
1138
+ VIRTUAL_SLOTS,
1139
+ VIRTUAL_SLOT
1140
+ ];
1141
+ /**
1142
+ * Shared `resolveId` logic for all Clarify virtual modules. Used by both the
1143
+ * Vite core plugin (dev + build) and the SSG virtual plugin so the ID-to-
1144
+ * resolved-ID mapping has a single source of truth.
1145
+ *
1146
+ * Returns the resolved ID (with `\0` prefix per Vite convention) or `null`
1147
+ * if the ID is not a Clarify virtual module.
1148
+ */
1149
+ function resolveVirtualModuleId(id, modules, routes) {
1150
+ if (id === "virtual:clarify/entry-client" || id === RESOLVED_CLIENT_ENTRY) return RESOLVED_CLIENT_ENTRY;
1151
+ for (const virtualId of FIXED_VIRTUAL_IDS) if (id === virtualId || id === resolveVirtualId(virtualId)) return resolveVirtualId(virtualId);
1152
+ const moduleId = stripVirtualPrefix(id);
1153
+ if (modules.has(moduleId)) return resolveVirtualId(moduleId);
1154
+ const route = routes.find((route) => route.virtualModuleId === id || route.virtualModuleId === moduleId);
1155
+ if (route) return resolveVirtualId(route.virtualModuleId);
1156
+ return null;
1157
+ }
1158
+ function generateConfigModule(projectConfig, buildOptions, version) {
1159
+ const runtimeConfig = {
1160
+ ...projectConfig,
1161
+ ...buildOptions
1162
+ };
1163
+ if (version) runtimeConfig.version = version;
1164
+ return `export const config = ${JSON.stringify(runtimeConfig)};`;
1165
+ }
1166
+ function moduleSpecifier(value) {
1167
+ return JSON.stringify(value);
1168
+ }
1169
+ function routeToRuntimeObject(route, component, mode) {
1170
+ const obj = {
1171
+ path: route.path,
1172
+ title: route.title,
1173
+ component,
1174
+ kind: route.kind
1175
+ };
1176
+ if (mode === "client") obj.lazy = true;
1177
+ if (route.basePath) obj.basePath = route.basePath;
1178
+ if (route.locale) obj.locale = route.locale;
1179
+ if (route.isFallback) obj.isFallback = true;
1180
+ if (route.isBareAlias) obj.isBareAlias = true;
1181
+ if (route.alternates) obj.alternates = route.alternates;
1182
+ if (route.description) obj.description = route.description;
1183
+ if (route.keywords?.length) obj.keywords = route.keywords;
1184
+ if (route.contentArtifactUrl) obj.contentArtifactUrl = route.contentArtifactUrl;
1185
+ if (route.sourceUrl) obj.sourceUrl = route.sourceUrl;
1186
+ if (route.sections?.length) obj.sections = route.sections.map((s) => ({
1187
+ id: s.id,
1188
+ title: s.title,
1189
+ level: s.level,
1190
+ badge: s.badge,
1191
+ tags: s.tags
1192
+ }));
1193
+ return obj;
1194
+ }
1195
+ function generateRoutesModule(routes, navigation, mode = "client") {
1196
+ return `${mode === "server" ? routes.map((r, i) => `import Page${i} from ${moduleSpecifier(r.virtualModuleId)};`).join("\n") : `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';`}\n\nexport const routes = [\n${routes.map((r, i) => {
1197
+ const component = mode === "server" ? `Page${i}` : `() => import(${moduleSpecifier(r.virtualModuleId)}).catch((error) => Promise.resolve({ default: createContentDiagnosticComponent({ kind: 'route-load', title: 'This page could not be loaded', message: error instanceof Error ? error.message : String(error), details: error instanceof Error ? error.stack : undefined }) }))`;
1198
+ const placeholder = `__COMPONENT_${i}__`;
1199
+ const obj = routeToRuntimeObject(r, placeholder, mode);
1200
+ return ` ${JSON.stringify(obj, null, 0).replace(`"${placeholder}"`, component).replace(/"([A-Za-z_$][\w$]*)":/g, "$1: ")}`;
1201
+ }).join(",\n")}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
1202
+ }
1203
+ function createClientEntryModule(options = {}) {
1204
+ return `
1205
+ import '@clarify-labs/renderer/style.css';
1206
+ import { render } from '@clarify-labs/renderer/client';
1207
+ import { routes, navigation } from '${VIRTUAL_ROUTES}';
1208
+ import { config } from '${VIRTUAL_CONFIG}';
1209
+ import { openApis } from '${VIRTUAL_OPENAPI}';
1210
+ import { runtimeSlots } from '${VIRTUAL_SLOTS}';
1211
+ render({ config, routes, navigation, openApis, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
1212
+ }
1213
+ /**
1214
+ * Collect every plugin's `slots` declarations into a single runtime registry
1215
+ * module. Component modules are exported as lazy import factories so the
1216
+ * renderer can decide whether to code-split (client) or pre-resolve (SSR).
1217
+ *
1218
+ * Relative component paths are resolved against the project `root` so that
1219
+ * the generated `import()` call uses an absolute specifier — Vite cannot
1220
+ * resolve relative imports from virtual modules.
1221
+ *
1222
+ * Slot name validity is enforced at compile time by the `UISlotName`
1223
+ * type — no runtime validation is needed.
1224
+ */
1225
+ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
1226
+ const registrations = [];
1227
+ for (const plugin of plugins) for (const slot of plugin.slots ?? []) registrations.push({
1228
+ plugin: plugin.name,
1229
+ slot
1230
+ });
1231
+ if (registrations.length === 0) return "export const runtimeSlots = {};\n";
1232
+ const grouped = /* @__PURE__ */ new Map();
1233
+ for (const { plugin, slot } of registrations) {
1234
+ if (slot.name.endsWith(".replace")) {
1235
+ const existing = grouped.get(slot.name);
1236
+ if (existing) console.warn(`[clarify] Multiple plugins register slot "${slot.name}": "${plugin}" overrides "${existing[existing.length - 1]}". Only the last registration is used.`);
1237
+ }
1238
+ if (!slot.component.startsWith("/")) throw new Error(`[clarify] Plugin "${plugin}" slot "${slot.name}" has an invalid component path "${slot.component}". Component paths must start with "/" to reference the project root.`);
1239
+ const componentPath = root + slot.component;
1240
+ const entry = `{ plugin: ${JSON.stringify(plugin)}, component: () => import(${moduleSpecifier(componentPath)}) }`;
1241
+ const list = grouped.get(slot.name) ?? [];
1242
+ list.push(entry);
1243
+ grouped.set(slot.name, list);
1244
+ }
1245
+ return `export const runtimeSlots = {\n${[...grouped.entries()].map(([name, list]) => ` ${JSON.stringify(name)}: [\n ${list.join(",\n ")}\n ]`).join(",\n")}\n};\n`;
1246
+ }
1247
+ function generateMdxErrorModule(diagnostic) {
1248
+ return `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';
1249
+
1250
+ export const contentDiagnostic = ${JSON.stringify({
1251
+ kind: diagnostic.kind ?? "mdx",
1252
+ title: diagnostic.title,
1253
+ message: diagnostic.message,
1254
+ filePath: diagnostic.filePath,
1255
+ details: diagnostic.details
1256
+ })};
1257
+
1258
+ export default createContentDiagnosticComponent(contentDiagnostic);
1259
+ `;
1260
+ }
1261
+ function buildVirtualModules(args) {
1262
+ const modules = /* @__PURE__ */ new Map();
1263
+ const clientEntryModule = createClientEntryModule({ themeEditor: args.themeEditor });
1264
+ const allPlugins = [...args.plugins ?? []];
1265
+ modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions, args.version));
1266
+ modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, "client"));
1267
+ modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, "server"));
1268
+ modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
1269
+ modules.set(VIRTUAL_OPENAPI, "export const openApis = {};");
1270
+ modules.set(VIRTUAL_SLOT, "export { useSlot } from '@clarify-labs/renderer';\n");
1271
+ modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1272
+ modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1273
+ for (const route of args.routes) {
1274
+ const moduleContent = route.diagnostic ? generateMdxErrorModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.filePath)};`;
1275
+ modules.set(route.virtualModuleId, moduleContent);
1276
+ }
1277
+ return modules;
1278
+ }
1279
+ //#endregion
1280
+ //#region source/core/adapters.ts
1281
+ function invalidateVirtualModules(engine, server) {
1282
+ for (const id of engine.modules.keys()) {
1283
+ const moduleNode = server.moduleGraph.getModuleById(resolveVirtualId(id));
1284
+ if (moduleNode) server.moduleGraph.invalidateModule(moduleNode);
1285
+ }
1286
+ }
1287
+ async function refreshDevServer(engine, server) {
1288
+ await engine.refresh();
1289
+ invalidateVirtualModules(engine, server);
1290
+ }
1291
+ function createNormalizedMdxContentPlugin(engine) {
1292
+ return {
1293
+ name: "clarify:normalized-mdx-content",
1294
+ enforce: "pre",
1295
+ transform(_code, id) {
1296
+ if (!/\.mdx?(?:\?|$)/.test(id)) return null;
1297
+ const filePath = id.replace(/\?.*$/, "");
1298
+ const route = engine.routes.find((route) => route.kind === "mdx" && route.filePath === filePath);
1299
+ if (!route || route.content === void 0) return null;
1300
+ return {
1301
+ code: route.content,
1302
+ map: null
1303
+ };
1304
+ }
1305
+ };
1306
+ }
1307
+ function createMdxPlugin() {
1308
+ return mdxPlugin({
1309
+ include: ["**/*.{md,mdx}"],
1310
+ jsxImportSource: "react",
1311
+ providerImportSource: "@clarify-labs/renderer",
1312
+ remarkPlugins,
1313
+ rehypePlugins
1314
+ });
1315
+ }
1316
+ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1317
+ let viteConfig;
1318
+ return {
1319
+ name: "clarify:core",
1320
+ async config() {
1321
+ return {
1322
+ base: engine.projectConfig.assetPrefix,
1323
+ build: {
1324
+ ...engine.generateOptions.outputDirectory ? { outDir: engine.generateOptions.outputDirectory } : {},
1325
+ manifest: true
1326
+ }
1327
+ };
1328
+ },
1329
+ configResolved(config) {
1330
+ viteConfig = config;
1331
+ engine.configureRuntime({
1332
+ command: config.command,
1333
+ mode: config.mode,
1334
+ outputDirectory: config.build.outDir
1335
+ });
1336
+ if (!engine.generateOptions.outputDirectory) engine.generateOptions.outputDirectory = config.build.outDir;
1337
+ },
1338
+ async buildStart() {
1339
+ if (!await engine.beginBuild()) return;
1340
+ engine.writeEnvTypes();
1341
+ },
1342
+ async handleHotUpdate(ctx) {
1343
+ const changedFile = isAbsolute(ctx.file) ? ctx.file : join(engine.root, ctx.file);
1344
+ if (engine.configFilePath && changedFile === engine.configFilePath) {
1345
+ await engine.prepare({
1346
+ command: "serve",
1347
+ mode: viteConfig.mode
1348
+ }, engine.options, { force: true });
1349
+ invalidateVirtualModules(engine, ctx.server);
1350
+ ctx.server.ws.send({ type: "full-reload" });
1351
+ return [];
1352
+ }
1353
+ const relativeContentFile = relative(engine.contentRoot, changedFile);
1354
+ if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !isAbsolute(relativeContentFile)) || !engine.hasContentRouteForFile(changedFile)) return;
1355
+ await refreshDevServer(engine, ctx.server);
1356
+ return [];
1357
+ },
1358
+ resolveId(id) {
1359
+ return resolveVirtualModuleId(id, engine.modules, engine.routes);
1360
+ },
1361
+ load(id) {
1362
+ return engine.loadModule(id);
1363
+ },
1364
+ async configureServer(server) {
1365
+ server.watcher.add(engine.contentRoot);
1366
+ if (engine.configFilePath) server.watcher.add(engine.configFilePath);
1367
+ let reloadQueued = false;
1368
+ const invalidateAndReload = () => {
1369
+ if (reloadQueued) return;
1370
+ reloadQueued = true;
1371
+ queueMicrotask(() => {
1372
+ reloadQueued = false;
1373
+ invalidateVirtualModules(engine, server);
1374
+ server.ws.send({ type: "full-reload" });
1375
+ });
1376
+ };
1377
+ engine.ctx.onRoutesChange(invalidateAndReload);
1378
+ engine.ctx.onNavigationChange(invalidateAndReload);
1379
+ server.middlewares.use((req, res, next) => {
1380
+ if (req.url === "/dev/query-preview-route" && req.method === "POST") return handleDevRouteRequest(req, res, engine.routes, engine.runtimeContext());
1381
+ if (req.url === "/dev/project-info" && (req.method === "GET" || req.method === "HEAD")) return handleProjectInfoRequest(req, res, engine.runtimeContext());
1382
+ next();
1383
+ });
1384
+ const handleContentTreeChange = async (filePath) => {
1385
+ const changedFile = isAbsolute(filePath) ? filePath : join(engine.root, filePath);
1386
+ const relativeContentFile = relative(engine.contentRoot, changedFile);
1387
+ if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !isAbsolute(relativeContentFile))) return;
1388
+ await refreshDevServer(engine, server);
1389
+ };
1390
+ server.watcher.on("add", handleContentTreeChange);
1391
+ server.watcher.on("unlink", handleContentTreeChange);
1392
+ await engine.configureDevServer(server);
1393
+ },
1394
+ transformIndexHtml: {
1395
+ order: "pre",
1396
+ async handler(html, transformCtx) {
1397
+ const clientEntryId = transformCtx.server ? `/@id/${VIRTUAL_CLIENT_ENTRY}` : VIRTUAL_CLIENT_ENTRY;
1398
+ const result = await engine.transformHtml({
1399
+ html,
1400
+ tags: [],
1401
+ clientEntryId,
1402
+ dev: Boolean(transformCtx.server)
1403
+ });
1404
+ return {
1405
+ html: result.html,
1406
+ tags: result.tags
1407
+ };
1408
+ }
1409
+ },
1410
+ async generateBundle() {
1411
+ const assets = await engine.collectBuildAssets();
1412
+ for (const asset of assets) this.emitFile({
1413
+ type: "asset",
1414
+ fileName: asset.fileName,
1415
+ source: asset.source
1416
+ });
1417
+ await engine.endBuild();
1418
+ },
1419
+ async closeBundle() {
1420
+ if (!engine.shouldRunBuild()) return;
1421
+ engine.configureRuntime({
1422
+ outputDirectory: viteConfig.build.outDir,
1423
+ ssrPlugins: [
1424
+ engine.createSSGVirtualPlugin(),
1425
+ normalizedMdxContentPlugin,
1426
+ mdx
1427
+ ]
1428
+ });
1429
+ await engine.runSSG();
1430
+ }
1431
+ };
1432
+ }
1433
+ /**
1434
+ * Creates the Clarify Vite plugins bound to an already-prepared engine.
1435
+ *
1436
+ * The caller is responsible for running `engine.prepare()` (initialize +
1437
+ * discoverSite + buildModules) before constructing the Vite config that
1438
+ * includes these plugins. The adapter never initializes or discovers the
1439
+ * site itself - it only bridges Vite lifecycle hooks to the engine.
1440
+ */
1441
+ function createViteAdapter(engine) {
1442
+ const normalizedMdxContentPlugin = createNormalizedMdxContentPlugin(engine);
1443
+ const mdx = createMdxPlugin();
1444
+ const tailwind = tailwindcss();
1445
+ return [
1446
+ ...react(),
1447
+ ...Array.isArray(tailwind) ? tailwind : [tailwind],
1448
+ normalizedMdxContentPlugin,
1449
+ createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx),
1450
+ mdx
1451
+ ];
1452
+ }
1453
+ var cliPackageVersion = {
1454
+ name: "@clarify-labs/cli",
1455
+ version: "0.10.0",
1456
+ license: "AGPL-3.0-only",
1457
+ type: "module",
1458
+ repository: {
1459
+ "type": "git",
1460
+ "url": "https://github.com/taicode-labs/clarify",
1461
+ "directory": "packages/cli"
1462
+ },
1463
+ main: "output/index.cjs",
1464
+ module: "output/index.js",
1465
+ types: "output/index.d.ts",
1466
+ bin: { "clarify": "bin/clarify.js" },
1467
+ exports: {
1468
+ ".": {
1469
+ "types": "./output/index.d.ts",
1470
+ "import": "./output/index.js",
1471
+ "require": "./output/index.cjs",
1472
+ "default": "./output/index.js"
1473
+ },
1474
+ "./package.json": "./package.json"
1475
+ },
1476
+ files: [
1477
+ "LICENSE",
1478
+ "bin",
1479
+ "output"
1480
+ ],
1481
+ scripts: {
1482
+ "dev": "vite build --watch",
1483
+ "build": "vite build",
1484
+ "lint": "eslint source/",
1485
+ "lint:fix": "eslint source/ --fix",
1486
+ "typecheck": "tsc -p tsconfig.json --noEmit",
1487
+ "test": "vitest run",
1488
+ "test:watch": "vitest"
1489
+ },
1490
+ dependencies: {
1491
+ "@apidevtools/swagger-parser": "^12.1.0",
1492
+ "@clarify-labs/renderer": "workspace:*",
1493
+ "@clarify-labs/templates": "workspace:*",
1494
+ "@mdx-js/mdx": "^3.1.1",
1495
+ "@mdx-js/rollup": "^3.1.1",
1496
+ "@tailwindcss/vite": "^4.3.2",
1497
+ "@vitejs/plugin-react": "^6.0.3",
1498
+ "cac": "^7.0.0",
1499
+ "github-slugger": "^2.0.0",
1500
+ "gray-matter": "^4.0.3",
1501
+ "mdast-util-to-string": "^4.0.0",
1502
+ "mermaid": "^11.4.0",
1503
+ "openapi-types": "^12.1.3",
1504
+ "pagefind": "^1.5.2",
1505
+ "react": "^19.1.0",
1506
+ "react-dom": "^19.1.0",
1507
+ "react-router": "^7.0.0",
1508
+ "react-router-dom": "^7.0.0",
1509
+ "react-zoom-pan-pinch": "^4.0.3",
1510
+ "remark": "^15.0.1",
1511
+ "remark-gfm": "^4.0.1",
1512
+ "shiki": "4.3.0",
1513
+ "unist-util-visit": "^5.1.0",
1514
+ "vite": "^8.1.4",
1515
+ "yaml": "^2.9.0",
1516
+ "zod": "^4.4.3"
1517
+ },
1518
+ devDependencies: {
1519
+ "@types/react": "^19.2.17",
1520
+ "typescript": "^6.0.3",
1521
+ "vite-plugin-dts": "^5.0.3",
1522
+ "vitest": "^4.1.10"
1105
1523
  }
1106
- for (const tab of config.tabs ?? []) visitPages(tab.pages);
1107
- return items;
1524
+ }.version ?? "0.0.0";
1525
+ var cliPackageVersionWithCaret = `^${cliPackageVersion}`;
1526
+ //#endregion
1527
+ //#region source/core/config/options.ts
1528
+ function resolveBuildOptions(options = {}) {
1529
+ return {
1530
+ projectRoot: options.projectRoot ?? process.cwd(),
1531
+ rootDirectory: options.rootDirectory ?? "source",
1532
+ outputDirectory: options.outputDirectory,
1533
+ ssg: { failOnError: options.ssg?.failOnError ?? true }
1534
+ };
1108
1535
  }
1109
- function virtualModuleIdFromPath(path) {
1110
- return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
1536
+ //#endregion
1537
+ //#region source/plugins/content-artifacts/artifacts.ts
1538
+ var UTF8_SIGNATURE = "";
1539
+ function normalizeBasePath(basePath) {
1540
+ if (!basePath || basePath === "/") return "";
1541
+ return "/" + basePath.replace(/^\/+|\/+$/g, "");
1111
1542
  }
1112
- function createConfiguredOpenAPIRoutes(routes, config) {
1113
- const pageItems = collectOpenAPIPageItems(config);
1114
- if (!pageItems.length) return routes;
1115
- const additions = [];
1116
- const existingKeys = new Set(routes.map((route) => `${route.locale ?? ""}:${route.basePath ?? route.path}`));
1117
- for (const item of pageItems) {
1118
- const tagFilter = item.filter?.tags?.filter(Boolean);
1119
- const sourceBasePath = openAPIPagePathFromRef(item.openapi);
1120
- const targetBasePath = openAPIPagePathFromRef(item.openapi, tagFilter, item.path);
1121
- if (sourceBasePath === targetBasePath && !tagFilter?.length) continue;
1122
- for (const route of routes) {
1123
- if (route.kind !== "openapi" || (route.basePath ?? route.path) !== sourceBasePath) continue;
1124
- const key = `${route.locale ?? ""}:${targetBasePath}`;
1125
- if (existingKeys.has(key)) continue;
1126
- existingKeys.add(key);
1127
- additions.push({
1128
- ...route,
1129
- path: route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath,
1130
- basePath: targetBasePath,
1131
- title: route.title,
1132
- virtualModuleId: virtualModuleIdFromPath(targetBasePath),
1133
- openapiTagFilter: tagFilter?.length ? tagFilter : void 0
1134
- });
1543
+ function containsNonAscii(content) {
1544
+ return [...content].some((character) => character.codePointAt(0) > 127);
1545
+ }
1546
+ function withUtf8Signature(content) {
1547
+ if (!containsNonAscii(content) || content.startsWith(UTF8_SIGNATURE)) return content;
1548
+ return `${UTF8_SIGNATURE}${content}`;
1549
+ }
1550
+ function shouldUseUtf8Signature(route) {
1551
+ if (route.kind === "mdx") return true;
1552
+ return false;
1553
+ }
1554
+ function routeToMarkdownArtifactUrl(routePath) {
1555
+ return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.md`;
1556
+ }
1557
+ function routeToOpenAPIArtifactUrl(routePath) {
1558
+ return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi.json`;
1559
+ }
1560
+ function attachContentArtifactUrls(routes) {
1561
+ for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path);
1562
+ }
1563
+ function readRouteContent(route) {
1564
+ if (route.content !== void 0) return route.content;
1565
+ throw new Error(`Route content is missing from route context: ${route.filePath}`);
1566
+ }
1567
+ function readRouteArtifactContent(route) {
1568
+ const content = readRouteContent(route);
1569
+ return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
1570
+ }
1571
+ function isLlmsTxtRoute(route) {
1572
+ return !route.path.split("/").includes("404");
1573
+ }
1574
+ function llmsTxtDescription(route) {
1575
+ if (route.description) return route.description;
1576
+ const sections = route.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
1577
+ if (sections?.length) return `Covers ${sections.join(", ")}.`;
1578
+ if (route.keywords?.length) return `Related topics: ${route.keywords.join(", ")}.`;
1579
+ if (route.kind === "openapi") return "OpenAPI artifact for machine-readable API reference data.";
1580
+ }
1581
+ function llmsTxtListItem(route, basePath) {
1582
+ if (!route.contentArtifactUrl) return void 0;
1583
+ const description = llmsTxtDescription(route);
1584
+ return description ? `- [${route.title}](${basePath}${route.contentArtifactUrl}): ${description}` : `- [${route.title}](${basePath}${route.contentArtifactUrl})`;
1585
+ }
1586
+ function llmsTxtLocaleLabel(locale, projectConfig) {
1587
+ return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
1588
+ }
1589
+ function groupRoutesByLocale(routes) {
1590
+ const groups = /* @__PURE__ */ new Map();
1591
+ for (const route of routes) {
1592
+ const key = route.locale ?? "default";
1593
+ groups.set(key, [...groups.get(key) ?? [], route]);
1594
+ }
1595
+ return groups;
1596
+ }
1597
+ function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
1598
+ const routesBySource = /* @__PURE__ */ new Map();
1599
+ const defaultLocale = projectConfig.i18n?.defaultLocale;
1600
+ for (const route of routes) {
1601
+ const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.contentArtifactUrl ?? route.path}`;
1602
+ const previousRoute = routesBySource.get(sourceKey);
1603
+ if (!previousRoute || previousRoute.locale && !route.locale) routesBySource.set(sourceKey, route);
1604
+ }
1605
+ return groupRoutesByLocale([...routesBySource.values()].map((route) => ({
1606
+ ...route,
1607
+ locale: route.locale ?? defaultLocale
1608
+ })));
1609
+ }
1610
+ function llmsTxtDocsSectionTitle(locale, groupCount, projectConfig) {
1611
+ if (groupCount <= 1) return "Docs";
1612
+ return `Docs - ${locale === "default" ? "Default" : llmsTxtLocaleLabel(locale, projectConfig)}`;
1613
+ }
1614
+ function createLlmsTxt(routes, projectConfig) {
1615
+ const basePath = normalizeBasePath(projectConfig.routePrefix);
1616
+ const lines = [`# ${projectConfig.title}`, ""];
1617
+ if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
1618
+ lines.push("This file lists the source-ready Markdown and OpenAPI artifacts for this documentation site.", "");
1619
+ const mdxRoutes = routes.filter((route) => route.kind === "mdx" && isLlmsTxtRoute(route) && !route.isBareAlias);
1620
+ if (mdxRoutes.length > 0) {
1621
+ const localizedGroups = groupLlmsTxtRoutesByLocale(mdxRoutes, projectConfig);
1622
+ for (const [locale, localeRoutes] of localizedGroups) {
1623
+ lines.push(`## ${llmsTxtDocsSectionTitle(locale, localizedGroups.size, projectConfig)}`);
1624
+ for (const route of localeRoutes) {
1625
+ const item = llmsTxtListItem(route, basePath);
1626
+ if (item) lines.push(item);
1627
+ }
1628
+ lines.push("");
1135
1629
  }
1136
1630
  }
1137
- const nextRoutes = [...routes, ...additions];
1138
- return config.i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n)) : nextRoutes;
1631
+ const openApiRoutes = routes.filter((route) => route.kind === "openapi" && isLlmsTxtRoute(route) && !route.isBareAlias);
1632
+ if (openApiRoutes.length > 0) {
1633
+ if (lines.at(-1) !== "") lines.push("");
1634
+ lines.push("## OpenAPI");
1635
+ for (const route of openApiRoutes) {
1636
+ const item = llmsTxtListItem(route, basePath);
1637
+ if (item) lines.push(item);
1638
+ }
1639
+ }
1640
+ while (lines.at(-1) === "") lines.pop();
1641
+ return `${lines.join("\n")}\n`;
1139
1642
  }
1140
- /** Derive a stable, deduplicated key from an absolute spec file path. */
1141
- function specFileKeyFromPath(filePath, projectRoot) {
1142
- return relative(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1643
+ function createLlmsTxtArtifact(routes, projectConfig) {
1644
+ return withUtf8Signature(createLlmsTxt(routes, projectConfig));
1143
1645
  }
1144
- function createOpenAPIPlugin() {
1145
- /** specs keyed by specFileKey (deduplicated per source file). */
1146
- const specs = /* @__PURE__ */ new Map();
1646
+ //#endregion
1647
+ //#region source/plugins/content-artifacts/server.ts
1648
+ function normalizeRoutePrefix$1(routePrefix) {
1649
+ if (!routePrefix || routePrefix === "/") return "";
1650
+ return `/${routePrefix.replace(/^\/+|\/+$/g, "")}`;
1651
+ }
1652
+ function resolveContentArtifactPath(url, projectConfig) {
1653
+ const requestPath = url?.split("?")[0] ?? "";
1654
+ const basePath = normalizeRoutePrefix$1(projectConfig.routePrefix);
1655
+ return basePath && requestPath.startsWith(basePath) ? requestPath.slice(basePath.length) || "/" : requestPath;
1656
+ }
1657
+ function resolveContentArtifactType(route) {
1658
+ return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
1659
+ }
1660
+ function serveContentArtifacts(req, res, projectConfig, routes) {
1661
+ const contentPath = resolveContentArtifactPath(req.url, projectConfig);
1662
+ if (contentPath === "/llms.txt") {
1663
+ res.statusCode = 200;
1664
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
1665
+ res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
1666
+ return true;
1667
+ }
1668
+ if (contentPath.endsWith(".openapi.yaml")) {
1669
+ const jsonPath = contentPath.replace(/\.yaml$/, ".json");
1670
+ const route = routes.find((r) => r.contentArtifactUrl === jsonPath);
1671
+ if (route?.kind === "openapi" && route.content) {
1672
+ const spec = JSON.parse(route.content);
1673
+ res.statusCode = 200;
1674
+ res.setHeader("Content-Type", "text/yaml; charset=utf-8");
1675
+ res.end(stringify(spec, { lineWidth: 0 }), "utf8");
1676
+ return true;
1677
+ }
1678
+ return false;
1679
+ }
1680
+ const route = routes.find((route) => route.contentArtifactUrl === contentPath);
1681
+ if (!route) return false;
1682
+ res.statusCode = 200;
1683
+ res.setHeader("Content-Type", resolveContentArtifactType(route));
1684
+ res.end(readRouteArtifactContent(route), "utf8");
1685
+ return true;
1686
+ }
1687
+ //#endregion
1688
+ //#region source/plugins/content-artifacts/index.ts
1689
+ function createContentArtifactsPlugin() {
1147
1690
  return {
1148
- name: "clarify:openapi",
1691
+ name: "clarify:content-artifacts",
1149
1692
  hooks: {
1150
- "routes:discover": (input) => ({
1151
- ...input,
1152
- routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
1153
- }),
1154
- "routes:discovered": async (routes, ctx) => {
1155
- specs.clear();
1156
- const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
1157
- const specByFilePath = /* @__PURE__ */ new Map();
1158
- for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
1159
- const specFromCache = specByFilePath.get(route.filePath);
1160
- const result = specFromCache ? {
1161
- ok: true,
1162
- spec: specFromCache
1163
- } : await readOpenAPISpec(route.filePath, getProjectContentProcessor(ctx));
1164
- if (!result.ok) {
1165
- route.diagnostic = result.diagnostic;
1166
- route.title = route.title || "OpenAPI parse error";
1167
- route.sections = [];
1168
- continue;
1169
- }
1170
- const spec = result.spec;
1171
- specByFilePath.set(route.filePath, spec);
1172
- const specKey = specFileKeyFromPath(route.filePath, ctx.generateOptions.projectRoot);
1173
- route.specFileKey = specKey;
1174
- specs.set(specKey, {
1175
- spec,
1176
- filePath: route.filePath
1177
- });
1178
- route.title = spec.info?.title ?? route.title;
1179
- route.description = spec.info?.description ?? route.description;
1180
- route.sections = extractOpenAPISections(spec, route.openapiTagFilter);
1181
- const pageSpec = route.openapiTagFilter?.length ? filterSpecByTags(spec, route.openapiTagFilter) : spec;
1182
- route.content = JSON.stringify(pageSpec);
1183
- }
1184
- return nextRoutes;
1693
+ "routes:resolved": (input) => {
1694
+ attachContentArtifactUrls(input.routes);
1695
+ return input;
1185
1696
  },
1186
- "modules:before": (modules, ctx) => {
1187
- const registryEntries = {};
1188
- for (const route of ctx.routes) {
1189
- if (route.kind !== "openapi" || !route.specFileKey) continue;
1190
- const spec = specs.get(route.specFileKey)?.spec;
1191
- if (!spec) continue;
1192
- const base = (route.basePath ?? route.path).replace(/^\//, "");
1193
- const registryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
1194
- registryEntries[registryKey] = spec;
1195
- }
1196
- modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
1197
- for (const [specKey, entry] of specs) modules.set(specVirtualModuleId(specKey), generateOpenAPISpecModule(entry.spec));
1697
+ "dev:configureServer": (server, ctx) => {
1698
+ server.middlewares.use((req, res, next) => {
1699
+ if (serveContentArtifacts(req, res, ctx.projectConfig, ctx.routes)) return;
1700
+ next();
1701
+ });
1702
+ },
1703
+ "build:assets": (ctx) => {
1704
+ const assets = [];
1198
1705
  for (const route of ctx.routes) {
1199
- if (route.kind !== "openapi") continue;
1200
- if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
1201
- else if (route.specFileKey) {
1202
- const base = (route.basePath ?? route.path).replace(/^\//, "");
1203
- const specRegistryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
1204
- modules.set(route.virtualModuleId, generateOpenAPIPageModule({
1205
- specKey: route.specFileKey,
1206
- specRegistryKey,
1207
- tagFilter: route.openapiTagFilter
1208
- }));
1706
+ if (!route.contentArtifactUrl) continue;
1707
+ assets.push({
1708
+ fileName: route.contentArtifactUrl.replace(/^\//, ""),
1709
+ source: readRouteArtifactContent(route)
1710
+ });
1711
+ if (route.kind === "openapi" && route.content) {
1712
+ const yamlFileName = route.contentArtifactUrl.replace(/\.json$/, ".yaml");
1713
+ const spec = JSON.parse(route.content);
1714
+ assets.push({
1715
+ fileName: yamlFileName.replace(/^\//, ""),
1716
+ source: stringify(spec, { lineWidth: 0 })
1717
+ });
1209
1718
  }
1210
1719
  }
1211
- return modules;
1720
+ assets.push({
1721
+ fileName: "llms.txt",
1722
+ source: createLlmsTxtArtifact(ctx.routes, ctx.projectConfig)
1723
+ });
1724
+ return assets;
1212
1725
  }
1213
1726
  }
1214
1727
  };
1215
1728
  }
1216
1729
  //#endregion
1217
- //#region source/core/search-language.ts
1218
- function toPagefindLanguage(locale) {
1219
- return locale?.trim().replace(/_/g, "-").split("-").filter(Boolean).join("-").toLowerCase() || "en";
1220
- }
1221
- //#endregion
1222
- //#region source/core/temp-dir.ts
1223
- function createClarifyTempDir(name) {
1224
- return mkdtempSync(join(tmpdir(), `clarify-${name}-`));
1730
+ //#region source/plugins/html-shell/index.ts
1731
+ var REL_ICON_RE = /<link\s+[^>]*rel=["'][^"']*\bicon\b[^"']*["'][^>]*>/i;
1732
+ function inferIconType(href) {
1733
+ const path = href.split(/[?#]/, 1)[0]?.toLowerCase() ?? href.toLowerCase();
1734
+ if (path.endsWith(".svg")) return "image/svg+xml";
1735
+ if (path.endsWith(".png")) return "image/png";
1736
+ if (path.endsWith(".ico")) return "image/x-icon";
1737
+ if (path.endsWith(".webp")) return "image/webp";
1225
1738
  }
1226
- function removeClarifyTempDir(path) {
1227
- if (!path) return;
1228
- rmSync(path, {
1229
- recursive: true,
1230
- force: true
1231
- });
1739
+ function createIconTag(href, media) {
1740
+ return {
1741
+ tag: "link",
1742
+ attrs: {
1743
+ rel: "icon",
1744
+ ...inferIconType(href) ? { type: inferIconType(href) } : {},
1745
+ href,
1746
+ ...media ? { media } : {}
1747
+ },
1748
+ injectTo: "head"
1749
+ };
1232
1750
  }
1233
- //#endregion
1234
- //#region source/plugins/search-index/index.ts
1235
- var pagefindContentTypes = {
1236
- ".css": "text/css; charset=utf-8",
1237
- ".js": "text/javascript; charset=utf-8",
1238
- ".json": "application/json; charset=utf-8",
1239
- ".wasm": "application/wasm"
1240
- };
1241
- function assertNoPagefindErrors(action, errors) {
1242
- if (!errors?.length) return;
1243
- throw new Error(`[clarify] Pagefind ${action} failed:\n${errors.join("\n")}`);
1751
+ function createIconTags(icon) {
1752
+ if (!icon) return [];
1753
+ if (typeof icon === "string") return [createIconTag(icon)];
1754
+ const lightIcon = icon.light ?? icon.dark;
1755
+ const darkIcon = icon.dark ?? icon.light;
1756
+ if (!lightIcon || !darkIcon) return [];
1757
+ if (lightIcon === darkIcon) return [createIconTag(lightIcon)];
1758
+ return [createIconTag(lightIcon, "(prefers-color-scheme: light)"), createIconTag(darkIcon, "(prefers-color-scheme: dark)")];
1244
1759
  }
1245
- async function generatePagefindIndex(options) {
1246
- const pagefind = await import("pagefind");
1247
- const { errors, index } = await pagefind.createIndex({
1248
- excludeSelectors: [
1249
- "[data-pagefind-ignore]",
1250
- "script",
1251
- "style",
1252
- "svg"
1253
- ],
1254
- includeCharacters: "._-/#:@",
1255
- keepIndexUrl: false,
1256
- verbose: false,
1257
- writePlayground: false
1258
- });
1259
- assertNoPagefindErrors("createIndex", errors);
1260
- if (!index) throw new Error("[clarify] Pagefind did not create an index.");
1261
- try {
1262
- const addResult = await index.addDirectory({
1263
- path: options.sourceRoot ?? options.outputRoot,
1264
- glob: "**/*.html"
1265
- });
1266
- assertNoPagefindErrors("addDirectory", addResult.errors);
1267
- assertNoPagefindErrors("writeFiles", (await index.writeFiles({ outputPath: join(options.outputRoot, "pagefind") })).errors);
1268
- return addResult.page_count;
1269
- } finally {
1270
- await index.deleteIndex();
1271
- await pagefind.close();
1272
- }
1760
+ function createHtmlShellPlugin() {
1761
+ return {
1762
+ name: "clarify:html-shell",
1763
+ hooks: { "html:transform": (input, ctx) => ({
1764
+ html: input.html,
1765
+ tags: [
1766
+ ...input.tags,
1767
+ ...REL_ICON_RE.test(input.html) ? [] : createIconTags(ctx.projectConfig.favicon ?? ctx.projectConfig.logo),
1768
+ {
1769
+ tag: "script",
1770
+ children: themeBootstrapScript(ctx.projectConfig.theme),
1771
+ injectTo: "head-prepend"
1772
+ },
1773
+ {
1774
+ tag: "script",
1775
+ attrs: {
1776
+ type: "module",
1777
+ src: input.clientEntryId
1778
+ },
1779
+ injectTo: "body"
1780
+ }
1781
+ ],
1782
+ clientEntryId: input.clientEntryId,
1783
+ dev: input.dev
1784
+ }) }
1785
+ };
1273
1786
  }
1274
- function routeSearchContent(route) {
1275
- return [
1276
- route.title,
1277
- route.description,
1278
- route.keywords?.join(" "),
1279
- route.sections?.map((section) => section.title).join(" "),
1280
- route.content
1281
- ].filter(Boolean).join("\n\n");
1787
+ //#endregion
1788
+ //#region source/plugins/openapi/parser.ts
1789
+ var OPENAPI_HTTP_METHODS = [
1790
+ "get",
1791
+ "put",
1792
+ "post",
1793
+ "delete",
1794
+ "options",
1795
+ "head",
1796
+ "patch",
1797
+ "trace"
1798
+ ];
1799
+ function errorMessage(error) {
1800
+ return error instanceof Error ? error.message : String(error);
1282
1801
  }
1283
- async function generateDevSearchIndex(ctx, root) {
1284
- const pagefind = await import("pagefind");
1285
- const { errors, index } = await pagefind.createIndex({
1286
- includeCharacters: "._-/#:@",
1287
- keepIndexUrl: false,
1288
- verbose: false,
1289
- writePlayground: false
1290
- });
1291
- assertNoPagefindErrors("createIndex", errors);
1292
- if (!index) throw new Error("[clarify] Pagefind did not create an index.");
1802
+ function isSameOpenAPIFile(url, filePath) {
1293
1803
  try {
1294
- let pageCount = 0;
1295
- for (const route of ctx.routes) {
1296
- if (route.isBareAlias) continue;
1297
- const content = routeSearchContent(route);
1298
- if (!content.trim()) continue;
1299
- const result = await index.addCustomRecord({
1300
- url: route.path,
1301
- content,
1302
- language: toPagefindLanguage(route.locale ?? ctx.projectConfig.i18n?.defaultLocale),
1303
- meta: { title: route.title }
1304
- });
1305
- assertNoPagefindErrors(`addCustomRecord ${route.path}`, result.errors);
1306
- pageCount += 1;
1307
- }
1308
- assertNoPagefindErrors("writeFiles", (await index.writeFiles({ outputPath: join(root, "pagefind") })).errors);
1309
- console.log(`[clarify] Dev search index generated for ${pageCount} pages.`);
1310
- } finally {
1311
- await index.deleteIndex();
1312
- await pagefind.close();
1804
+ return resolve(fileURLToPath(url)) === resolve(filePath);
1805
+ } catch {
1806
+ return resolve(url) === resolve(filePath);
1313
1807
  }
1314
1808
  }
1315
- function normalizeDevPagefindPath(rawUrl, routePrefix) {
1316
- const pathname = rawUrl?.split("?")[0];
1317
- if (!pathname) return void 0;
1318
- const normalizedPrefix = routePrefix && routePrefix !== "/" ? `/${routePrefix.replace(/^\/+|\/+$/g, "")}` : "";
1319
- const pagefindPath = normalizedPrefix && pathname.startsWith(`${normalizedPrefix}/pagefind/`) ? pathname.slice(normalizedPrefix.length) : pathname;
1320
- return pagefindPath.startsWith("/pagefind/") ? pagefindPath : void 0;
1321
- }
1322
- function serveDevPagefind(server, getRoot, ctx) {
1323
- server.middlewares.use((req, res, next) => {
1324
- const root = getRoot();
1325
- const pagefindPath = normalizeDevPagefindPath(req.url, ctx.projectConfig.routePrefix);
1326
- if (!root || !pagefindPath) {
1327
- next();
1328
- return;
1329
- }
1330
- const filePath = join(root, pagefindPath);
1331
- const relativePath = relative(root, filePath);
1332
- if (relativePath.startsWith("..") || relativePath === "" || !existsSync(filePath)) {
1333
- next();
1334
- return;
1335
- }
1336
- const contentType = pagefindContentTypes[extname(filePath)] ?? "application/octet-stream";
1337
- res.setHeader("Content-Type", contentType);
1338
- createReadStream(filePath).pipe(res);
1339
- });
1340
- }
1341
- function createSearchIndexPlugin() {
1342
- return {
1343
- name: "clarify:search-index",
1344
- hooks: {
1345
- async "dev:configureServer"(server, ctx) {
1346
- let currentRoot;
1347
- const staleRoots = /* @__PURE__ */ new Set();
1348
- const replaceDevIndex = async () => {
1349
- const nextRoot = createClarifyTempDir("pagefind");
1350
- await generateDevSearchIndex(ctx, nextRoot);
1351
- const previousRoot = currentRoot;
1352
- currentRoot = nextRoot;
1353
- if (previousRoot) {
1354
- staleRoots.add(previousRoot);
1355
- setTimeout(() => {
1356
- staleRoots.delete(previousRoot);
1357
- removeClarifyTempDir(previousRoot);
1358
- }, 3e4).unref?.();
1359
- }
1360
- };
1361
- serveDevPagefind(server, () => currentRoot, ctx);
1362
- await replaceDevIndex();
1363
- let regenerateTimer;
1364
- const regenerate = (filePath) => {
1365
- if (!/\.(md|mdx|json|ya?ml)$/i.test(filePath)) return;
1366
- if (regenerateTimer) clearTimeout(regenerateTimer);
1367
- regenerateTimer = setTimeout(() => {
1368
- replaceDevIndex().catch((err) => {
1369
- console.error("[clarify] Failed to regenerate dev search index:", err);
1370
- });
1371
- }, 100);
1372
- };
1373
- server.watcher.on("add", regenerate);
1374
- server.watcher.on("change", regenerate);
1375
- server.watcher.on("unlink", regenerate);
1376
- server.httpServer?.once("close", () => {
1377
- if (regenerateTimer) clearTimeout(regenerateTimer);
1378
- removeClarifyTempDir(currentRoot);
1379
- for (const staleRoot of staleRoots) removeClarifyTempDir(staleRoot);
1380
- });
1381
- },
1382
- async "build:done"(ctx) {
1383
- const outputDirectory = ctx.generateOptions.outputDirectory;
1384
- if (!outputDirectory) return;
1385
- const pageCount = await generatePagefindIndex({ outputRoot: resolve(ctx.generateOptions.projectRoot, outputDirectory) });
1386
- console.log(`[clarify] Search index generated for ${pageCount} pages.`);
1387
- }
1809
+ function findOpenAPIRoutes(dir, base = dir) {
1810
+ const routes = [];
1811
+ if (!existsSync(dir)) return routes;
1812
+ const entries = readdirSync(dir, { withFileTypes: true });
1813
+ for (const entry of entries) {
1814
+ const fullPath = join(dir, entry.name);
1815
+ if (entry.isDirectory()) {
1816
+ routes.push(...findOpenAPIRoutes(fullPath, base));
1817
+ continue;
1388
1818
  }
1389
- };
1819
+ if (!entry.isFile() || !/\.openapi\.(json|yaml|yml)$/.test(entry.name)) continue;
1820
+ const ref = relative(base, fullPath);
1821
+ const cleanPath = routePathFromRef(ref);
1822
+ routes.push({
1823
+ path: cleanPath,
1824
+ basePath: cleanPath,
1825
+ filePath: fullPath,
1826
+ virtualModuleId: virtualModuleIdFromRef(ref),
1827
+ title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
1828
+ kind: "openapi"
1829
+ });
1830
+ }
1831
+ return routes;
1390
1832
  }
1391
- //#endregion
1392
- //#region source/plugins/seo/artifacts.ts
1393
- function trimTrailingSlash(value) {
1394
- return value.replace(/\/+$/, "");
1833
+ function operationMatchesTags(operationTags, filterTags) {
1834
+ if (!filterTags?.length) return true;
1835
+ return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
1395
1836
  }
1396
- function normalizeRoutePrefix(routePrefix) {
1397
- if (!routePrefix || routePrefix === "/") return "";
1398
- return `/${routePrefix.replace(/^\/+|\/+$/g, "")}`;
1837
+ /** Filter an OpenAPI spec to only include paths that have at least one operation matching the given tags. */
1838
+ function filterSpecByTags(spec, tags) {
1839
+ const filteredPaths = {};
1840
+ const paths = spec.paths ?? {};
1841
+ for (const path of Object.keys(paths)) {
1842
+ const pathItem = paths[path];
1843
+ if (!pathItem) continue;
1844
+ if (Object.values(pathItem).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredPaths[path] = pathItem;
1845
+ }
1846
+ return {
1847
+ ...spec,
1848
+ paths: filteredPaths
1849
+ };
1399
1850
  }
1400
- function routeUrl(siteUrl, routePrefix, routePath) {
1401
- const base = trimTrailingSlash(siteUrl);
1402
- return `${base}${normalizeRoutePrefix(routePrefix)}${routePath === "/" ? "" : routePath.replace(/^\/+/, "/")}` || base;
1851
+ /** Extract endpoint operations from an OpenAPI spec as page sections. */
1852
+ function extractOpenAPISections(spec, filterTags) {
1853
+ const sections = [];
1854
+ const paths = spec.paths ?? {};
1855
+ for (const [path, pathItem] of Object.entries(paths)) {
1856
+ if (!pathItem) continue;
1857
+ for (const method of OPENAPI_HTTP_METHODS) {
1858
+ const op = pathItem[method];
1859
+ if (!op || !operationMatchesTags(op.tags, filterTags)) continue;
1860
+ const title = op.summary ?? `${method.toUpperCase()} ${path}`;
1861
+ sections.push({
1862
+ id: slug(`${method} ${path}`),
1863
+ title,
1864
+ level: 2,
1865
+ badge: method.toUpperCase(),
1866
+ tags: op.tags
1867
+ });
1868
+ }
1869
+ }
1870
+ return sections;
1403
1871
  }
1404
- function escapeXml(value) {
1405
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
1872
+ async function readOpenAPISpec(filePath, contentProcessor) {
1873
+ try {
1874
+ if (!contentProcessor) return {
1875
+ ok: true,
1876
+ spec: await SwaggerParser.dereference(filePath)
1877
+ };
1878
+ const source = readFileSync(filePath, "utf-8");
1879
+ const transformedSource = await contentProcessor.processText(source, "openapi", filePath);
1880
+ if (transformedSource === source) return {
1881
+ ok: true,
1882
+ spec: await SwaggerParser.dereference(filePath)
1883
+ };
1884
+ return {
1885
+ ok: true,
1886
+ spec: await SwaggerParser.dereference(filePath, { resolve: { transformedOpenAPISource: {
1887
+ order: 1,
1888
+ canRead: (file) => isSameOpenAPIFile(file.url, filePath),
1889
+ read: () => transformedSource
1890
+ } } })
1891
+ };
1892
+ } catch (error) {
1893
+ return {
1894
+ ok: false,
1895
+ diagnostic: {
1896
+ kind: "openapi",
1897
+ title: "OpenAPI spec parse failed",
1898
+ message: `Clarify could not parse ${filePath}.`,
1899
+ filePath,
1900
+ details: errorMessage(error)
1901
+ }
1902
+ };
1903
+ }
1406
1904
  }
1407
- function publicRoutes(routes) {
1408
- return routes.filter((route) => route.path !== "/404" && !route.isBareAlias);
1905
+ //#endregion
1906
+ //#region source/plugins/openapi/virtual-modules.ts
1907
+ var openApiRegistryModuleId = VIRTUAL_OPENAPI;
1908
+ var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
1909
+ function specVirtualModuleId(specKey) {
1910
+ return `${OPENAPI_SPEC_PREFIX}${specKey}`;
1409
1911
  }
1410
- function createSitemapXml(routes, config) {
1411
- if (!config.siteUrl) return void 0;
1412
- return [
1413
- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1414
- "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">",
1415
- ...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>`),
1416
- "</urlset>",
1417
- ""
1418
- ].join("\n");
1912
+ function generateOpenAPIRegistryModule(openApis) {
1913
+ return `export const openApis = ${JSON.stringify(openApis)};`;
1419
1914
  }
1420
- function createRobotsTxt(config) {
1421
- if (!config.siteUrl) return void 0;
1422
- return [
1423
- "User-agent: *",
1424
- "Allow: /",
1425
- `Sitemap: ${routeUrl(config.siteUrl, config.routePrefix, "/sitemap.xml")}`,
1426
- ""
1427
- ].join("\n");
1915
+ /** Generate a virtual module that exports a single spec as default. */
1916
+ function generateOpenAPISpecModule(spec) {
1917
+ return `export default ${JSON.stringify(spec)};`;
1428
1918
  }
1429
- //#endregion
1430
- //#region source/plugins/seo/index.ts
1431
- function createSeoPlugin() {
1432
- return {
1433
- name: "clarify:seo",
1434
- hooks: { "build:assets"(ctx) {
1435
- const sitemap = createSitemapXml(ctx.routes, ctx.projectConfig);
1436
- const robots = createRobotsTxt(ctx.projectConfig);
1437
- if (!sitemap || !robots) return [];
1438
- return [{
1439
- fileName: "sitemap.xml",
1440
- source: sitemap
1441
- }, {
1442
- fileName: "robots.txt",
1443
- source: robots
1444
- }];
1445
- } }
1446
- };
1919
+ function generateOpenAPIPageModule(opts) {
1920
+ return `import { createOpenApiRouteComponent } from '@clarify-labs/renderer';
1921
+
1922
+ export const routeData = ${JSON.stringify({
1923
+ spec: opts.spec,
1924
+ tagFilter: opts.tagFilter
1925
+ })};
1926
+
1927
+ export default createOpenApiRouteComponent(routeData);
1928
+ `;
1447
1929
  }
1448
- //#endregion
1449
- //#region source/plugins/source-links/source-links.ts
1450
- function trimSlashes(value) {
1451
- return value.replace(/^\/+|\/+$/g, "");
1930
+ function generateOpenAPIErrorModule(diagnostic) {
1931
+ return `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';
1932
+
1933
+ export const contentDiagnostic = ${JSON.stringify({
1934
+ kind: diagnostic.kind ?? "openapi",
1935
+ title: diagnostic.title,
1936
+ message: diagnostic.message,
1937
+ filePath: diagnostic.filePath,
1938
+ details: diagnostic.details
1939
+ })};
1940
+
1941
+ export default createContentDiagnosticComponent(contentDiagnostic);
1942
+ `;
1452
1943
  }
1453
- function normalizeRepositoryUrl(repository) {
1454
- return repository.replace(/\.git$/, "").replace(/\/+$/, "");
1944
+ //#endregion
1945
+ //#region source/plugins/openapi/index.ts
1946
+ function collectOpenAPIPageItems(config) {
1947
+ const items = [];
1948
+ function visitPages(pages) {
1949
+ if (!pages || pages === "FileTree") return;
1950
+ for (const group of pages) for (const item of group.pages) if (typeof item !== "string" && "openapi" in item && (item.path || item.filter?.tags?.length)) items.push(item);
1951
+ }
1952
+ for (const tab of config.tabs ?? []) visitPages(tab.pages);
1953
+ return items;
1455
1954
  }
1456
- function encodePath(path) {
1457
- return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
1955
+ function virtualModuleIdFromPath(path) {
1956
+ return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
1458
1957
  }
1459
- function createSourceUrl(filePath, contentRoot, source) {
1460
- const repository = normalizeRepositoryUrl(source.repository);
1461
- const branch = source.branch ?? "main";
1462
- const sourcePath = [source.directory ? trimSlashes(source.directory) : "", relative(contentRoot, filePath).replaceAll("\\", "/")].filter(Boolean).join("/");
1463
- return `${repository}/edit/${encodeURIComponent(branch)}/${encodePath(sourcePath)}`;
1958
+ function createConfiguredOpenAPIRoutes(routes, config) {
1959
+ const pageItems = collectOpenAPIPageItems(config);
1960
+ if (!pageItems.length) return routes;
1961
+ const additions = [];
1962
+ const existingKeys = new Set(routes.map((route) => `${route.locale ?? ""}:${route.basePath ?? route.path}`));
1963
+ for (const item of pageItems) {
1964
+ const tagFilter = item.filter?.tags?.filter(Boolean);
1965
+ const sourceBasePath = openAPIPagePathFromRef(item.openapi);
1966
+ const targetBasePath = openAPIPagePathFromRef(item.openapi, tagFilter, item.path);
1967
+ if (sourceBasePath === targetBasePath && !tagFilter?.length) continue;
1968
+ for (const route of routes) {
1969
+ if (route.kind !== "openapi" || (route.basePath ?? route.path) !== sourceBasePath) continue;
1970
+ const key = `${route.locale ?? ""}:${targetBasePath}`;
1971
+ if (existingKeys.has(key)) continue;
1972
+ existingKeys.add(key);
1973
+ additions.push({
1974
+ ...route,
1975
+ path: route.locale && config.i18n ? localizedRoutePath(targetBasePath, route.locale, config.i18n) : targetBasePath,
1976
+ basePath: targetBasePath,
1977
+ title: route.title,
1978
+ virtualModuleId: virtualModuleIdFromPath(targetBasePath),
1979
+ openapiTagFilter: tagFilter?.length ? tagFilter : void 0
1980
+ });
1981
+ }
1982
+ }
1983
+ const nextRoutes = [...routes, ...additions];
1984
+ return config.i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n)) : nextRoutes;
1464
1985
  }
1465
- function attachSourceUrls(routes, contentRoot, source) {
1466
- if (!source) return;
1467
- for (const route of routes) route.sourceUrl = createSourceUrl(route.filePath, contentRoot, source);
1986
+ /** Derive a stable, deduplicated key from an absolute spec file path. */
1987
+ function specFileKeyFromPath(filePath, projectRoot) {
1988
+ return relative(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1468
1989
  }
1469
- //#endregion
1470
- //#region source/plugins/source-links/index.ts
1471
- function createSourceLinksPlugin() {
1990
+ function createOpenAPIPlugin() {
1991
+ /** specs keyed by specFileKey (deduplicated per source file). */
1992
+ const specs = /* @__PURE__ */ new Map();
1472
1993
  return {
1473
- name: "clarify:source-links",
1474
- hooks: { "routes:discovered": (routes, ctx) => {
1475
- attachSourceUrls(routes, join(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
1476
- return routes;
1477
- } }
1994
+ name: "clarify:openapi",
1995
+ hooks: {
1996
+ "routes:discover": (input) => ({
1997
+ ...input,
1998
+ routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
1999
+ }),
2000
+ "routes:discovered": async (routes, ctx) => {
2001
+ specs.clear();
2002
+ const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
2003
+ const specByFilePath = /* @__PURE__ */ new Map();
2004
+ for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
2005
+ const specFromCache = specByFilePath.get(route.filePath);
2006
+ const processor = createProjectContentProcessor(ctx.plugins, ctx);
2007
+ const result = specFromCache ? {
2008
+ ok: true,
2009
+ spec: specFromCache
2010
+ } : await readOpenAPISpec(route.filePath, processor);
2011
+ if (!result.ok) {
2012
+ route.diagnostic = result.diagnostic;
2013
+ route.title = route.title || "OpenAPI parse error";
2014
+ route.sections = [];
2015
+ continue;
2016
+ }
2017
+ const spec = result.spec;
2018
+ specByFilePath.set(route.filePath, spec);
2019
+ const specKey = specFileKeyFromPath(route.filePath, ctx.generateOptions.projectRoot);
2020
+ route.specFileKey = specKey;
2021
+ specs.set(specKey, {
2022
+ spec,
2023
+ filePath: route.filePath
2024
+ });
2025
+ route.title = spec.info?.title ?? route.title;
2026
+ route.description = spec.info?.description ?? route.description;
2027
+ route.sections = extractOpenAPISections(spec, route.openapiTagFilter);
2028
+ const pageSpec = route.openapiTagFilter?.length ? filterSpecByTags(spec, route.openapiTagFilter) : spec;
2029
+ route.content = JSON.stringify(pageSpec);
2030
+ }
2031
+ return nextRoutes;
2032
+ },
2033
+ "modules:before": (modules, ctx) => {
2034
+ const registryEntries = {};
2035
+ for (const route of ctx.routes) {
2036
+ if (route.kind !== "openapi" || !route.specFileKey) continue;
2037
+ const spec = specs.get(route.specFileKey)?.spec;
2038
+ if (!spec) continue;
2039
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
2040
+ const registryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
2041
+ registryEntries[registryKey] = spec;
2042
+ }
2043
+ modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
2044
+ for (const [specKey, entry] of specs) modules.set(specVirtualModuleId(specKey), generateOpenAPISpecModule(entry.spec));
2045
+ for (const route of ctx.routes) {
2046
+ if (route.kind !== "openapi") continue;
2047
+ if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
2048
+ else if (route.specFileKey) {
2049
+ const entry = specs.get(route.specFileKey);
2050
+ if (!entry) continue;
2051
+ modules.set(route.virtualModuleId, generateOpenAPIPageModule({
2052
+ spec: entry.spec,
2053
+ tagFilter: route.openapiTagFilter
2054
+ }));
2055
+ }
2056
+ }
2057
+ return modules;
2058
+ }
2059
+ }
1478
2060
  };
1479
2061
  }
1480
2062
  //#endregion
1481
- //#region source/parsers/variables.ts
1482
- var variablePattern = /{{\s*([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)\s*}}/g;
1483
- function getVariableValue(variables, path) {
1484
- let current = variables;
1485
- for (const segment of path.split(".")) {
1486
- if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
1487
- current = current[segment];
1488
- }
1489
- return current;
1490
- }
1491
- function stringifyVariableValue(variables, path, stack) {
1492
- if (stack.includes(path)) throw new Error(`[clarify] variable "${path}" contains a circular reference: ${[...stack, path].join(" -> ")}`);
1493
- const value = getVariableValue(variables, path);
1494
- if (value === void 0 || typeof value === "object") return void 0;
1495
- if (typeof value !== "string") return String(value);
1496
- return value.replace(variablePattern, (match, nestedPath) => stringifyVariableValue(variables, nestedPath, [...stack, path]) ?? match);
2063
+ //#region source/core/project/temp-dir.ts
2064
+ function createClarifyTempDir(name) {
2065
+ return mkdtempSync(join(tmpdir(), `clarify-${name}-`));
1497
2066
  }
1498
- function applyVariables(content, variables = {}) {
1499
- return content.replace(variablePattern, (match, path) => stringifyVariableValue(variables, path, []) ?? match);
2067
+ function removeClarifyTempDir(path) {
2068
+ if (!path) return;
2069
+ rmSync(path, {
2070
+ recursive: true,
2071
+ force: true
2072
+ });
1500
2073
  }
1501
2074
  //#endregion
1502
- //#region source/plugins/variables/index.ts
1503
- function applyVariablesToFrontmatter(frontmatter, variables) {
1504
- const nextFrontmatter = { ...frontmatter };
1505
- for (const key of ["title", "description"]) {
1506
- if (typeof nextFrontmatter[key] !== "string") continue;
1507
- nextFrontmatter[key] = applyVariables(nextFrontmatter[key], variables);
1508
- }
1509
- return nextFrontmatter;
1510
- }
1511
- function createVariablesPlugin() {
1512
- return {
1513
- name: "clarify:variables",
1514
- hooks: { "content:transform": (input, ctx) => ({
1515
- ...input,
1516
- frontmatter: applyVariablesToFrontmatter(input.frontmatter, ctx.projectConfig.variables),
1517
- content: applyVariables(input.content, ctx.projectConfig.variables)
1518
- }) }
1519
- };
2075
+ //#region source/plugins/search-index/search-language.ts
2076
+ function toPagefindLanguage(locale) {
2077
+ return locale?.trim().replace(/_/g, "-").split("-").filter(Boolean).join("-").toLowerCase() || "en";
1520
2078
  }
1521
2079
  //#endregion
1522
- //#region source/core/builtin.ts
1523
- function createBuiltinPlugins(options = {}) {
1524
- const plugins = [
1525
- createVariablesPlugin(),
1526
- createOpenAPIPlugin(),
1527
- createSourceLinksPlugin(),
1528
- createContentArtifactsPlugin(),
1529
- createSeoPlugin(),
1530
- createSearchIndexPlugin()
1531
- ];
1532
- if (options.htmlShell ?? true) plugins.push(createHtmlShellPlugin());
1533
- return plugins;
2080
+ //#region source/plugins/search-index/index.ts
2081
+ var pagefindContentTypes = {
2082
+ ".css": "text/css; charset=utf-8",
2083
+ ".js": "text/javascript; charset=utf-8",
2084
+ ".json": "application/json; charset=utf-8",
2085
+ ".wasm": "application/wasm"
2086
+ };
2087
+ function assertNoPagefindErrors(action, errors) {
2088
+ if (!errors?.length) return;
2089
+ throw new Error(`[clarify] Pagefind ${action} failed:\n${errors.join("\n")}`);
1534
2090
  }
1535
- //#endregion
1536
- //#region source/core/config-schema.ts
1537
- var clarifyLogoConfigSchema = z.union([z.string(), z.object({
1538
- light: z.string().optional(),
1539
- dark: z.string().optional()
1540
- })]);
1541
- var clarifyFaviconConfigSchema = z.union([z.string(), z.object({
1542
- light: z.string().optional(),
1543
- dark: z.string().optional()
1544
- })]);
1545
- var clarifyLocalizedTextSchema = z.union([z.string(), z.record(z.string(), z.string())]);
1546
- var clarifyLocaleConfigSchema = z.object({
1547
- code: z.string(),
1548
- label: z.string(),
1549
- dir: z.union([z.literal("ltr"), z.literal("rtl")]).optional()
1550
- });
1551
- var clarifyI18nConfigSchema = z.object({
1552
- defaultLocale: z.string().optional(),
1553
- missing: z.union([
1554
- z.literal("fallback"),
1555
- z.literal("404"),
1556
- z.literal("hide")
1557
- ]).optional(),
1558
- locales: z.array(clarifyLocaleConfigSchema).min(1)
1559
- }).superRefine((config, ctx) => {
1560
- const localeCodes = /* @__PURE__ */ new Set();
1561
- for (const [index, locale] of config.locales.entries()) {
1562
- if (localeCodes.has(locale.code)) ctx.addIssue({
1563
- code: z.ZodIssueCode.custom,
1564
- message: `Duplicate locale code "${locale.code}"`,
1565
- path: [
1566
- "locales",
1567
- index,
1568
- "code"
1569
- ]
1570
- });
1571
- localeCodes.add(locale.code);
1572
- }
1573
- if (config.defaultLocale && !localeCodes.has(config.defaultLocale)) ctx.addIssue({
1574
- code: z.ZodIssueCode.custom,
1575
- message: "defaultLocale must be one of i18n.locales",
1576
- path: ["defaultLocale"]
2091
+ async function generatePagefindIndex(options, pagefind) {
2092
+ const runtime = pagefind ?? await import("pagefind");
2093
+ const { errors, index } = await runtime.createIndex({
2094
+ excludeSelectors: [
2095
+ "[data-pagefind-ignore]",
2096
+ "script",
2097
+ "style",
2098
+ "svg"
2099
+ ],
2100
+ includeCharacters: "._-/#:@",
2101
+ keepIndexUrl: false,
2102
+ verbose: false,
2103
+ writePlayground: false
1577
2104
  });
1578
- });
1579
- var clarifyNavbarLinkSchema = z.object({
1580
- label: clarifyLocalizedTextSchema,
1581
- href: z.string(),
1582
- external: z.boolean().optional()
1583
- });
1584
- var clarifyBannerConfigSchema = z.object({
1585
- content: clarifyLocalizedTextSchema,
1586
- dismissible: z.boolean().optional(),
1587
- link: clarifyNavbarLinkSchema.optional()
1588
- }).strict();
1589
- var clarifyFooterConfigSchema = z.object({
1590
- links: z.array(clarifyNavbarLinkSchema).optional(),
1591
- socials: z.record(z.string(), z.string()).optional(),
1592
- copyright: clarifyLocalizedTextSchema.optional()
1593
- }).strict();
1594
- var clarifyVariableValueSchema = z.lazy(() => z.union([
1595
- z.string(),
1596
- z.number(),
1597
- z.boolean(),
1598
- z.record(z.string(), clarifyVariableValueSchema)
1599
- ]));
1600
- var clarifyVariablesConfigSchema = z.record(z.string(), clarifyVariableValueSchema);
1601
- var clarifySourceConfigSchema = z.object({
1602
- repository: z.string(),
1603
- branch: z.string().optional(),
1604
- directory: z.string().optional()
1605
- });
1606
- var clarifyPagesItemSchema = z.union([
1607
- z.string(),
1608
- z.object({
1609
- page: z.string(),
1610
- path: z.string().optional(),
1611
- title: clarifyLocalizedTextSchema.optional(),
1612
- icon: z.string().optional(),
1613
- redirect: z.string().optional()
1614
- }),
1615
- z.object({
1616
- openapi: z.string(),
1617
- path: z.string().optional(),
1618
- icon: z.string().optional(),
1619
- title: clarifyLocalizedTextSchema.optional(),
1620
- filter: z.object({ tags: z.array(z.string()).optional() }).optional()
1621
- })
1622
- ]);
1623
- var clarifyPagesGroupSchema = z.object({
1624
- group: clarifyLocalizedTextSchema,
1625
- icon: z.string().optional(),
1626
- pages: z.array(clarifyPagesItemSchema)
1627
- });
1628
- var clarifyPagesConfigSchema = z.union([z.array(clarifyPagesGroupSchema), z.literal("FileTree")]);
1629
- var clarifyTabItemSchema = z.object({
1630
- tab: clarifyLocalizedTextSchema,
1631
- icon: z.string().optional(),
1632
- pages: clarifyPagesConfigSchema.optional()
1633
- });
1634
- var clarifyTabsConfigSchema = z.array(clarifyTabItemSchema);
1635
- var clarifyThemePresetSchema = z.enum(["default", "base"]);
1636
- var clarifyThemeModeColorValueSchema = z.object({
1637
- light: z.string().optional(),
1638
- dark: z.string().optional()
1639
- });
1640
- var clarifyThemeColorValueSchema = z.union([z.string(), clarifyThemeModeColorValueSchema]);
1641
- var clarifyThemeColorTokensConfigSchema = z.object({
1642
- primary: clarifyThemeColorValueSchema.optional(),
1643
- accent: clarifyThemeColorValueSchema.optional(),
1644
- background: clarifyThemeColorValueSchema.optional(),
1645
- foreground: clarifyThemeColorValueSchema.optional(),
1646
- surface: clarifyThemeColorValueSchema.optional(),
1647
- muted: clarifyThemeColorValueSchema.optional(),
1648
- border: clarifyThemeColorValueSchema.optional(),
1649
- codeBackground: clarifyThemeColorValueSchema.optional()
1650
- });
1651
- var clarifyThemeRadiusTokensConfigSchema = z.object({
1652
- sm: z.string().optional(),
1653
- md: z.string().optional(),
1654
- lg: z.string().optional(),
1655
- xl: z.string().optional()
1656
- });
1657
- var clarifyThemeTokensConfigSchema = z.object({
1658
- colors: clarifyThemeColorTokensConfigSchema.optional(),
1659
- radius: clarifyThemeRadiusTokensConfigSchema.optional()
1660
- });
1661
- var clarifyThemeLayoutConfigSchema = z.object({ maxWidth: z.string().optional() });
1662
- var clarifyThemeConfigSchema = z.object({
1663
- preset: clarifyThemePresetSchema.optional(),
1664
- tokens: clarifyThemeTokensConfigSchema.optional(),
1665
- layout: clarifyThemeLayoutConfigSchema.optional(),
1666
- editor: z.boolean().optional()
1667
- });
1668
- var clarifyProjectConfigSchema = z.object({
1669
- title: z.string().optional(),
1670
- description: z.string().optional(),
1671
- siteUrl: z.string().optional(),
1672
- source: clarifySourceConfigSchema.optional(),
1673
- logo: clarifyLogoConfigSchema.optional(),
1674
- homeUrl: z.string().optional(),
1675
- favicon: clarifyFaviconConfigSchema.optional(),
1676
- theme: clarifyThemeConfigSchema.optional(),
1677
- routePrefix: z.string().optional(),
1678
- assetPrefix: z.string().optional(),
1679
- navbar: z.object({ links: z.array(clarifyNavbarLinkSchema).optional() }).optional(),
1680
- banner: clarifyBannerConfigSchema.optional(),
1681
- footer: clarifyFooterConfigSchema.optional(),
1682
- variables: clarifyVariablesConfigSchema.optional(),
1683
- i18n: clarifyI18nConfigSchema.optional(),
1684
- tabs: clarifyTabsConfigSchema.optional()
1685
- });
1686
- //#endregion
1687
- //#region source/core/theme.ts
1688
- function resolveThemeConfig(theme = {}) {
1689
- const preset = theme.preset ?? "default";
1690
- const presetTheme = themePresets[preset];
2105
+ assertNoPagefindErrors("createIndex", errors);
2106
+ if (!index) throw new Error("[clarify] Pagefind did not create an index.");
2107
+ try {
2108
+ const addResult = await index.addDirectory({
2109
+ path: options.sourceRoot ?? options.outputRoot,
2110
+ glob: "**/*.html"
2111
+ });
2112
+ assertNoPagefindErrors("addDirectory", addResult.errors);
2113
+ assertNoPagefindErrors("writeFiles", (await index.writeFiles({ outputPath: join(options.outputRoot, "pagefind") })).errors);
2114
+ return addResult.page_count;
2115
+ } finally {
2116
+ await index.deleteIndex();
2117
+ if (!pagefind) await runtime.close();
2118
+ }
2119
+ }
2120
+ function routeSearchContent(route) {
2121
+ return [
2122
+ route.title,
2123
+ route.description,
2124
+ route.keywords?.join(" "),
2125
+ route.sections?.map((section) => section.title).join(" "),
2126
+ route.content
2127
+ ].filter(Boolean).join("\n\n");
2128
+ }
2129
+ async function generateDevSearchIndex(ctx, root, pagefind) {
2130
+ const { errors, index } = await pagefind.createIndex({
2131
+ includeCharacters: "._-/#:@",
2132
+ keepIndexUrl: false,
2133
+ verbose: false,
2134
+ writePlayground: false
2135
+ });
2136
+ assertNoPagefindErrors("createIndex", errors);
2137
+ if (!index) throw new Error("[clarify] Pagefind did not create an index.");
2138
+ try {
2139
+ let pageCount = 0;
2140
+ for (const route of ctx.routes) {
2141
+ if (route.isBareAlias) continue;
2142
+ const content = routeSearchContent(route);
2143
+ if (!content.trim()) continue;
2144
+ const result = await index.addCustomRecord({
2145
+ url: route.path,
2146
+ content,
2147
+ language: toPagefindLanguage(route.locale ?? ctx.projectConfig.i18n?.defaultLocale),
2148
+ meta: { title: route.title }
2149
+ });
2150
+ assertNoPagefindErrors(`addCustomRecord ${route.path}`, result.errors);
2151
+ pageCount += 1;
2152
+ }
2153
+ assertNoPagefindErrors("writeFiles", (await index.writeFiles({ outputPath: join(root, "pagefind") })).errors);
2154
+ console.log(`[clarify] Dev search index generated for ${pageCount} pages.`);
2155
+ } finally {
2156
+ await index.deleteIndex();
2157
+ }
2158
+ }
2159
+ function normalizeDevPagefindPath(rawUrl, routePrefix) {
2160
+ const pathname = rawUrl?.split("?")[0];
2161
+ if (!pathname) return void 0;
2162
+ const normalizedPrefix = routePrefix && routePrefix !== "/" ? `/${routePrefix.replace(/^\/+|\/+$/g, "")}` : "";
2163
+ const pagefindPath = normalizedPrefix && pathname.startsWith(`${normalizedPrefix}/pagefind/`) ? pathname.slice(normalizedPrefix.length) : pathname;
2164
+ return pagefindPath.startsWith("/pagefind/") ? pagefindPath : void 0;
2165
+ }
2166
+ function serveDevPagefind(server, getRoot, ctx) {
2167
+ server.middlewares.use((req, res, next) => {
2168
+ const root = getRoot();
2169
+ const pagefindPath = normalizeDevPagefindPath(req.url, ctx.projectConfig.routePrefix);
2170
+ if (!root || !pagefindPath) {
2171
+ next();
2172
+ return;
2173
+ }
2174
+ const filePath = join(root, pagefindPath);
2175
+ const relativePath = relative(root, filePath);
2176
+ if (relativePath.startsWith("..") || relativePath === "" || !existsSync(filePath)) {
2177
+ next();
2178
+ return;
2179
+ }
2180
+ const contentType = pagefindContentTypes[extname(filePath)] ?? "application/octet-stream";
2181
+ res.setHeader("Content-Type", contentType);
2182
+ createReadStream(filePath).pipe(res);
2183
+ });
2184
+ }
2185
+ function createSearchIndexPlugin() {
1691
2186
  return {
1692
- preset,
1693
- tokens: {
1694
- colors: {
1695
- ...presetTheme.tokens.colors,
1696
- ...theme.tokens?.colors
2187
+ name: "clarify:search-index",
2188
+ hooks: {
2189
+ async "dev:configureServer"(server, ctx) {
2190
+ const pagefind = await import("pagefind");
2191
+ let currentRoot;
2192
+ const staleRoots = /* @__PURE__ */ new Set();
2193
+ const replaceDevIndex = async () => {
2194
+ const nextRoot = createClarifyTempDir("pagefind");
2195
+ await generateDevSearchIndex(ctx, nextRoot, pagefind);
2196
+ const previousRoot = currentRoot;
2197
+ currentRoot = nextRoot;
2198
+ if (previousRoot) {
2199
+ staleRoots.add(previousRoot);
2200
+ setTimeout(() => {
2201
+ staleRoots.delete(previousRoot);
2202
+ removeClarifyTempDir(previousRoot);
2203
+ }, 3e4).unref?.();
2204
+ }
2205
+ };
2206
+ serveDevPagefind(server, () => currentRoot, ctx);
2207
+ await replaceDevIndex();
2208
+ let regenerateTimer;
2209
+ const regenerate = (filePath) => {
2210
+ if (!/\.(md|mdx|json|ya?ml)$/i.test(filePath)) return;
2211
+ if (regenerateTimer) clearTimeout(regenerateTimer);
2212
+ regenerateTimer = setTimeout(() => {
2213
+ replaceDevIndex().catch((err) => {
2214
+ console.error("[clarify] Failed to regenerate dev search index:", err);
2215
+ });
2216
+ }, 100);
2217
+ };
2218
+ server.watcher.on("add", regenerate);
2219
+ server.watcher.on("change", regenerate);
2220
+ server.watcher.on("unlink", regenerate);
2221
+ server.httpServer?.once("close", () => {
2222
+ if (regenerateTimer) clearTimeout(regenerateTimer);
2223
+ removeClarifyTempDir(currentRoot);
2224
+ for (const staleRoot of staleRoots) removeClarifyTempDir(staleRoot);
2225
+ pagefind.close().catch(() => void 0);
2226
+ });
1697
2227
  },
1698
- radius: {
1699
- ...presetTheme.tokens.radius,
1700
- ...theme.tokens?.radius
2228
+ async "build:done"(ctx) {
2229
+ const outputDirectory = ctx.generateOptions.outputDirectory;
2230
+ if (!outputDirectory) return;
2231
+ const pageCount = await generatePagefindIndex({ outputRoot: resolve(ctx.generateOptions.projectRoot, outputDirectory) });
2232
+ console.log(`[clarify] Search index generated for ${pageCount} pages.`);
1701
2233
  }
1702
- },
1703
- layout: {
1704
- ...presetTheme.layout,
1705
- ...theme.layout
1706
- },
1707
- editor: theme.editor ?? presetTheme.editor
2234
+ }
1708
2235
  };
1709
2236
  }
1710
2237
  //#endregion
1711
- //#region source/core/config.ts
1712
- function formatIssuePath(path) {
1713
- return path.reduce((result, segment) => {
1714
- if (typeof segment === "number") return `${result}[${segment}]`;
1715
- const key = String(segment);
1716
- return result ? `${result}.${key}` : key;
1717
- }, "");
2238
+ //#region source/plugins/seo/artifacts.ts
2239
+ function trimTrailingSlash(value) {
2240
+ return value.replace(/\/+$/, "");
1718
2241
  }
1719
- function formatProjectConfigError(error) {
1720
- const issue = error.issues[0];
1721
- if (!issue) return "[clarify] config is invalid";
1722
- const path = formatIssuePath(issue.path);
1723
- if (!path) return `[clarify] config must contain an object: ${issue.message}`;
1724
- return `[clarify] config field "${path}" is invalid: ${issue.message}`;
2242
+ function normalizeRoutePrefix(routePrefix) {
2243
+ if (!routePrefix || routePrefix === "/") return "";
2244
+ return `/${routePrefix.replace(/^\/+|\/+$/g, "")}`;
1725
2245
  }
1726
- function validateProjectConfig(value) {
1727
- const result = clarifyProjectConfigSchema.safeParse(value);
1728
- if (!result.success) throw new Error(formatProjectConfigError(result.error));
1729
- return result.data;
2246
+ function routeUrl(siteUrl, routePrefix, routePath) {
2247
+ const base = trimTrailingSlash(siteUrl);
2248
+ return `${base}${normalizeRoutePrefix(routePrefix)}${routePath === "/" ? "" : routePath.replace(/^\/+/, "/")}` || base;
1730
2249
  }
1731
- function resolveI18nConfig(i18n) {
1732
- if (!i18n) return void 0;
1733
- const firstLocale = i18n.locales[0]?.code;
1734
- const defaultLocale = i18n.defaultLocale ?? firstLocale;
1735
- if (!defaultLocale) return void 0;
1736
- return {
1737
- defaultLocale,
1738
- missing: i18n.missing ?? "fallback",
1739
- locales: i18n.locales
1740
- };
2250
+ function escapeXml(value) {
2251
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
1741
2252
  }
1742
- function resolveRoutePrefix(routePrefix) {
1743
- const trimmed = routePrefix?.trim();
1744
- if (!trimmed || trimmed === "/") return "/";
1745
- return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
2253
+ function publicRoutes(routes) {
2254
+ return routes.filter((route) => route.path !== "/404" && !route.isBareAlias);
1746
2255
  }
1747
- function resolveAssetPrefix(assetPrefix, routePrefix) {
1748
- const trimmed = assetPrefix?.trim();
1749
- if (trimmed === void 0) return routePrefix;
1750
- if (trimmed === "./" || trimmed === "") return trimmed || "/";
1751
- if (!trimmed || trimmed === "/") return "/";
1752
- if (/^[a-z][a-z\d+.-]*:\/\//i.test(trimmed)) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1753
- if (trimmed.startsWith("./") || trimmed.startsWith("../")) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1754
- return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
2256
+ function createSitemapXml(routes, config) {
2257
+ if (!config.siteUrl) return void 0;
2258
+ return [
2259
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
2260
+ "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">",
2261
+ ...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>`),
2262
+ "</urlset>",
2263
+ ""
2264
+ ].join("\n");
1755
2265
  }
1756
- function resolveProjectConfig(config = {}) {
1757
- const routePrefix = resolveRoutePrefix(config.routePrefix);
2266
+ function createRobotsTxt(config) {
2267
+ if (!config.siteUrl) return void 0;
2268
+ return [
2269
+ "User-agent: *",
2270
+ "Allow: /",
2271
+ `Sitemap: ${routeUrl(config.siteUrl, config.routePrefix, "/sitemap.xml")}`,
2272
+ ""
2273
+ ].join("\n");
2274
+ }
2275
+ //#endregion
2276
+ //#region source/plugins/seo/index.ts
2277
+ function createSeoPlugin() {
1758
2278
  return {
1759
- title: config.title ?? "Clarify Docs",
1760
- description: config.description ?? "",
1761
- siteUrl: config.siteUrl,
1762
- source: config.source,
1763
- logo: config.logo,
1764
- homeUrl: config.homeUrl,
1765
- favicon: config.favicon,
1766
- routePrefix,
1767
- assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
1768
- theme: resolveThemeConfig(config.theme),
1769
- navbar: config.navbar,
1770
- banner: config.banner,
1771
- footer: config.footer,
1772
- variables: config.variables ?? {},
1773
- i18n: resolveI18nConfig(config.i18n),
1774
- tabs: config.tabs
2279
+ name: "clarify:seo",
2280
+ hooks: { "build:assets"(ctx) {
2281
+ const sitemap = createSitemapXml(ctx.routes, ctx.projectConfig);
2282
+ const robots = createRobotsTxt(ctx.projectConfig);
2283
+ if (!sitemap || !robots) return [];
2284
+ return [{
2285
+ fileName: "sitemap.xml",
2286
+ source: sitemap
2287
+ }, {
2288
+ fileName: "robots.txt",
2289
+ source: robots
2290
+ }];
2291
+ } }
1775
2292
  };
1776
2293
  }
1777
- /** Serialize a ContentRoute into the minimal shape exposed to tooling. */
1778
- function toDevRouteEntry(route) {
2294
+ //#endregion
2295
+ //#region source/plugins/source-links/source-links.ts
2296
+ function trimSlashes(value) {
2297
+ return value.replace(/^\/+|\/+$/g, "");
2298
+ }
2299
+ function normalizeRepositoryUrl(repository) {
2300
+ return repository.replace(/\.git$/, "").replace(/\/+$/, "");
2301
+ }
2302
+ function encodePath(path) {
2303
+ return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
2304
+ }
2305
+ function createSourceUrl(filePath, contentRoot, source) {
2306
+ const repository = normalizeRepositoryUrl(source.repository);
2307
+ const branch = source.branch ?? "main";
2308
+ const sourcePath = [source.directory ? trimSlashes(source.directory) : "", relative(contentRoot, filePath).replaceAll("\\", "/")].filter(Boolean).join("/");
2309
+ return `${repository}/edit/${encodeURIComponent(branch)}/${encodePath(sourcePath)}`;
2310
+ }
2311
+ function attachSourceUrls(routes, contentRoot, source) {
2312
+ if (!source) return;
2313
+ for (const route of routes) route.sourceUrl = createSourceUrl(route.filePath, contentRoot, source);
2314
+ }
2315
+ //#endregion
2316
+ //#region source/plugins/source-links/index.ts
2317
+ function createSourceLinksPlugin() {
1779
2318
  return {
1780
- path: route.path,
1781
- filePath: route.filePath,
1782
- locale: route.locale,
1783
- basePath: route.basePath,
1784
- kind: route.kind,
1785
- title: route.title
2319
+ name: "clarify:source-links",
2320
+ hooks: { "routes:discovered": (routes, ctx) => {
2321
+ attachSourceUrls(routes, join(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.source);
2322
+ return routes;
2323
+ } }
1786
2324
  };
1787
2325
  }
1788
- /**
1789
- * Resolve a single file path to its preview route.
1790
- *
1791
- * Selection order:
1792
- * 1. Routes whose `filePath` exactly matches.
1793
- * 2. Among matches, prefer the one whose `locale` matches the file's locale
1794
- * directory (e.g. `source/zh-CN/x.mdx` → locale `zh-CN`).
1795
- * 3. If no locale match, fall back to the default locale (if i18n is enabled)
1796
- * or the first match.
1797
- */
1798
- function resolveRouteForFile(filePath, routes, defaultLocale) {
1799
- const matches = routes.filter((r) => r.filePath === filePath);
1800
- if (matches.length === 0) return void 0;
1801
- if (matches.length === 1) return matches[0];
1802
- const fileLocale = inferLocaleFromPath(filePath);
1803
- if (fileLocale) {
1804
- const localeMatch = matches.find((r) => r.locale === fileLocale);
1805
- if (localeMatch) return localeMatch;
2326
+ //#endregion
2327
+ //#region source/plugins/variables/index.ts
2328
+ var variablePattern = /{{\s*([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)\s*}}/g;
2329
+ function getVariableValue(variables, path) {
2330
+ let current = variables;
2331
+ for (const segment of path.split(".")) {
2332
+ if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
2333
+ current = current[segment];
1806
2334
  }
1807
- if (defaultLocale) {
1808
- const defaultMatch = matches.find((r) => r.locale === defaultLocale);
1809
- if (defaultMatch) return defaultMatch;
2335
+ return current;
2336
+ }
2337
+ function stringifyVariableValue(variables, path, stack) {
2338
+ if (stack.includes(path)) throw new Error(`[clarify] variable "${path}" contains a circular reference: ${[...stack, path].join(" -> ")}`);
2339
+ const value = getVariableValue(variables, path);
2340
+ if (value === void 0 || typeof value === "object") return void 0;
2341
+ if (typeof value !== "string") return String(value);
2342
+ return value.replace(variablePattern, (match, nestedPath) => stringifyVariableValue(variables, nestedPath, [...stack, path]) ?? match);
2343
+ }
2344
+ function applyVariables(content, variables = {}) {
2345
+ return content.replace(variablePattern, (match, path) => stringifyVariableValue(variables, path, []) ?? match);
2346
+ }
2347
+ function applyVariablesToFrontmatter(frontmatter, variables) {
2348
+ const nextFrontmatter = { ...frontmatter };
2349
+ for (const key of ["title", "description"]) {
2350
+ if (typeof nextFrontmatter[key] !== "string") continue;
2351
+ nextFrontmatter[key] = applyVariables(nextFrontmatter[key], variables);
1810
2352
  }
1811
- return matches[0];
2353
+ return nextFrontmatter;
1812
2354
  }
1813
- /**
1814
- * Derive a best-effort preview route for a file that is not in the route
1815
- * manifest (e.g. a draft or newly created file). The path is computed from the
1816
- * file's location relative to the content root, following the same convention
1817
- * as route discovery (`source/{locale}/path/index.mdx` → `/{locale}/path`).
1818
- *
1819
- * Returns null when the file is outside the content root or is not a
1820
- * recognized content file type.
1821
- */
1822
- function inferRouteForFile(filePath, contentRoot, defaultLocale) {
1823
- const ref = relative(contentRoot, filePath);
1824
- if (!ref || ref.startsWith("..")) return null;
1825
- const isMdx = /\.mdx?$/.test(filePath);
1826
- const isOpenApi = /\.openapi\.(json|ya?ml)$/.test(filePath);
1827
- if (!isMdx && !isOpenApi) return null;
1828
- const segments = ref.split("/");
1829
- const fileLocale = inferLocaleFromPath(filePath);
1830
- const localeSegmentIndex = fileLocale ? segments.indexOf(fileLocale) : -1;
1831
- const basePath = basePathFromRef(localeSegmentIndex >= 0 ? segments.slice(localeSegmentIndex + 1).join("/") : ref);
1832
- const path = fileLocale && defaultLocale ? localizedRoutePath(basePath, fileLocale, {
1833
- defaultLocale,
1834
- missing: "fallback",
1835
- locales: []
1836
- }) : basePath;
1837
- const title = segments[segments.length - 1]?.replace(/\.(mdx?|openapi\.(json|ya?ml))$/, "") || "Untitled";
2355
+ function createVariablesPlugin() {
1838
2356
  return {
1839
- path: normalizePath(path),
1840
- filePath,
1841
- locale: fileLocale,
1842
- basePath: normalizePath(basePath),
1843
- kind: isMdx ? "mdx" : "openapi",
1844
- title,
1845
- inferred: true
2357
+ name: "clarify:variables",
2358
+ hooks: { "content:transform": (input, ctx) => ({
2359
+ ...input,
2360
+ frontmatter: applyVariablesToFrontmatter(input.frontmatter, ctx.projectConfig.variables),
2361
+ content: applyVariables(input.content, ctx.projectConfig.variables)
2362
+ }) }
1846
2363
  };
1847
2364
  }
1848
- /** Best-effort locale inference from a file path segment like `.../zh-CN/x.mdx`. */
1849
- function inferLocaleFromPath(filePath) {
1850
- const segments = filePath.split("/");
1851
- for (const segment of segments) if (/^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/.test(segment)) return segment;
1852
- }
1853
- /** Read and parse the JSON body of an IncomingMessage. */
1854
- function readJsonBody(req) {
1855
- return new Promise((resolveBody, reject) => {
1856
- const chunks = [];
1857
- req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
1858
- req.on("error", reject);
1859
- req.on("end", () => {
1860
- const raw = Buffer.concat(chunks).toString("utf-8").trim();
1861
- if (!raw) return resolveBody({});
1862
- try {
1863
- resolveBody(JSON.parse(raw));
1864
- } catch (err) {
1865
- reject(err);
1866
- }
1867
- });
1868
- });
2365
+ //#endregion
2366
+ //#region source/core/plugin/builtin.ts
2367
+ function createBuiltinPlugins(options = {}) {
2368
+ const plugins = [
2369
+ createVariablesPlugin(),
2370
+ createOpenAPIPlugin(),
2371
+ createSourceLinksPlugin(),
2372
+ createContentArtifactsPlugin(),
2373
+ createSeoPlugin(),
2374
+ createSearchIndexPlugin()
2375
+ ];
2376
+ if (options.htmlShell ?? true) plugins.push(createHtmlShellPlugin());
2377
+ return plugins;
1869
2378
  }
1870
- /**
1871
- * Handle a request to the dev route endpoint. Pure function — does not touch
1872
- * the network directly, only reads the request body and writes to `res`.
1873
- */
1874
- async function handleDevRouteRequest(req, res, routes, projectConfig, contentRoot) {
1875
- res.setHeader("Content-Type", "application/json; charset=utf-8");
1876
- let body;
1877
- try {
1878
- body = await readJsonBody(req);
1879
- } catch {
1880
- res.statusCode = 400;
1881
- res.end(JSON.stringify({ error: "Invalid JSON body" }));
1882
- return;
2379
+ //#endregion
2380
+ //#region source/core/plugin/manager.ts
2381
+ function pluginWeight(plugin) {
2382
+ return (plugin.enforce === "pre" ? 1e6 : plugin.enforce === "post" ? -1e6 : 0) + (plugin.priority ?? 0);
2383
+ }
2384
+ function sortPlugins(plugins) {
2385
+ const entries = plugins.map((plugin, index) => ({
2386
+ plugin,
2387
+ index
2388
+ }));
2389
+ const byName = /* @__PURE__ */ new Map();
2390
+ for (const { plugin } of entries) {
2391
+ if (byName.has(plugin.name)) throw new Error(`[clarify] Duplicate plugin name: ${plugin.name}`);
2392
+ byName.set(plugin.name, plugin);
1883
2393
  }
1884
- const file = typeof body === "object" && body !== null ? body.file : void 0;
1885
- if (typeof file === "string" && file.length > 0) {
1886
- const route = resolveRouteForFile(file, routes, projectConfig.i18n?.defaultLocale);
1887
- if (route) {
1888
- res.end(JSON.stringify(toDevRouteEntry(route)));
1889
- return;
2394
+ const sortedByWeight = [...entries].sort((left, right) => {
2395
+ const weightDiff = pluginWeight(right.plugin) - pluginWeight(left.plugin);
2396
+ return weightDiff === 0 ? left.index - right.index : weightDiff;
2397
+ });
2398
+ const visitOrder = new Map(sortedByWeight.map((entry, index) => [entry.plugin.name, index]));
2399
+ const visiting = /* @__PURE__ */ new Set();
2400
+ const visited = /* @__PURE__ */ new Set();
2401
+ const sorted = [];
2402
+ function visit(plugin) {
2403
+ if (visited.has(plugin.name)) return;
2404
+ if (visiting.has(plugin.name)) throw new Error(`[clarify] Circular plugin dependency detected: ${plugin.name}`);
2405
+ visiting.add(plugin.name);
2406
+ const dependencies = [...plugin.dependsOn ?? []].sort((left, right) => (visitOrder.get(left) ?? 0) - (visitOrder.get(right) ?? 0));
2407
+ for (const dependencyName of dependencies) {
2408
+ const dependency = byName.get(dependencyName);
2409
+ if (!dependency) throw new Error(`[clarify] Plugin ${plugin.name} depends on missing plugin: ${dependencyName}`);
2410
+ visit(dependency);
1890
2411
  }
1891
- const inferred = inferRouteForFile(file, contentRoot, projectConfig.i18n?.defaultLocale);
1892
- res.end(JSON.stringify(inferred));
1893
- return;
2412
+ visiting.delete(plugin.name);
2413
+ visited.add(plugin.name);
2414
+ sorted.push(plugin);
1894
2415
  }
1895
- res.end(JSON.stringify(routes.map(toDevRouteEntry)));
2416
+ for (const { plugin } of sortedByWeight) visit(plugin);
2417
+ return sorted;
2418
+ }
2419
+ function loadPlugins(options = {}) {
2420
+ return sortPlugins([...createBuiltinPlugins({ htmlShell: options.htmlShell }), ...options.userPlugins ?? []]);
2421
+ }
2422
+ function loadBuildPlugins(options = {}, pluginOptions = {}) {
2423
+ return loadPlugins({
2424
+ ...pluginOptions,
2425
+ userPlugins: options.plugins
2426
+ });
2427
+ }
2428
+ //#endregion
2429
+ //#region source/core/project/project-context.ts
2430
+ async function resolveProjectContext(options = {}, env = {
2431
+ command: "build",
2432
+ mode: "production"
2433
+ }) {
2434
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
2435
+ const initialBuildOptions = resolveBuildOptions(options);
2436
+ const contentRoot = join(projectRoot, initialBuildOptions.rootDirectory);
2437
+ const configFilePath = findClarifyConfigFile(projectRoot);
2438
+ const config = configFilePath && existsSync(configFilePath) ? await loadClarifyConfig(projectRoot, env) : {};
2439
+ const mergedOptions = {
2440
+ ...config,
2441
+ ...options,
2442
+ projectRoot,
2443
+ rootDirectory: initialBuildOptions.rootDirectory,
2444
+ outputDirectory: initialBuildOptions.outputDirectory
2445
+ };
2446
+ const resolvedProjectConfig = resolveProjectConfig(mergedOptions);
2447
+ const resolvedBuildOptions = resolveBuildOptions(mergedOptions);
2448
+ return {
2449
+ projectRoot,
2450
+ contentRoot,
2451
+ configFilePath,
2452
+ projectConfig: resolvedProjectConfig,
2453
+ buildOptions: resolvedBuildOptions,
2454
+ resolvedOptions: mergedOptions,
2455
+ config,
2456
+ projectContext: {
2457
+ projectRoot,
2458
+ contentRoot,
2459
+ version: cliPackageVersion,
2460
+ projectConfig: resolvedProjectConfig,
2461
+ generateOptions: resolvedBuildOptions
2462
+ }
2463
+ };
1896
2464
  }
1897
2465
  //#endregion
1898
- //#region source/core/env-types.ts
2466
+ //#region source/core/runtime/env-types.ts
1899
2467
  var GENERATED_FILE_NAME = "clarify-env.d.ts";
1900
2468
  /**
1901
2469
  * Template for the generated `clarify-env.d.ts` file.
@@ -1911,18 +2479,6 @@ declare module 'virtual:clarify/slot' {
1911
2479
 
1912
2480
  export function useSlot(): SlotContext
1913
2481
  }
1914
-
1915
- declare module 'virtual:clarify/slots' {
1916
- import type { ComponentType } from 'react'
1917
-
1918
- export type RuntimeSlotEntry = {
1919
- plugin: string
1920
- /** Lazy import factory — yields the default-exported React component. */
1921
- component: () => Promise<{ default: ComponentType }>
1922
- }
1923
-
1924
- export const runtimeSlots: Record<string, RuntimeSlotEntry[]>
1925
- }
1926
2482
  `;
1927
2483
  function generateClarifyEnvDts(_plugins) {
1928
2484
  return CLARIFY_ENV_TEMPLATE;
@@ -1943,191 +2499,79 @@ function writeClarifyEnvDts(projectRoot, plugins) {
1943
2499
  writeFileSync(filePath, next, "utf-8");
1944
2500
  }
1945
2501
  //#endregion
1946
- //#region source/core/options.ts
1947
- function resolveBuildOptions(options = {}) {
1948
- return {
1949
- projectRoot: options.projectRoot ?? process.cwd(),
1950
- rootDirectory: options.rootDirectory ?? "source",
1951
- outputDirectory: options.outputDirectory,
1952
- ssg: { failOnError: options.ssg?.failOnError ?? true }
1953
- };
1954
- }
1955
- //#endregion
1956
- //#region source/core/user-config.ts
1957
- function defineConfig(config) {
1958
- validateProjectConfig(config);
1959
- return config;
1960
- }
2502
+ //#region source/core/runtime/html-template.ts
1961
2503
  /**
1962
- * Config filenames Clarify CLI looks for at the project root, in resolution
1963
- * order. Exported so other modules (e.g. the dev `project-info` endpoint) can
1964
- * surface the same list to tooling without duplicating it.
2504
+ * HTML template manipulation helpers used during SSG.
2505
+ *
2506
+ * The SSG renderer needs to mutate the built `index.html` per route
2507
+ * (title, description/keywords meta, canonical link, `<html lang/dir>`,
2508
+ * and the root `<div>` content). These operations are all string-based
2509
+ * because the template is a raw HTML string, not a parsed DOM. Keeping them
2510
+ * in one module centralizes every HTML-mutation regex so they can be audited
2511
+ * together and so `ssg.ts` stays focused on the render pipeline.
1965
2512
  */
1966
- var CONFIG_FILENAMES = [
1967
- "clarify.ts",
1968
- "clarify.js",
1969
- "clarify.json"
1970
- ];
1971
- function findClarifyConfigFile(root) {
1972
- for (const filename of CONFIG_FILENAMES) {
1973
- const filePath = resolve(root, filename);
1974
- if (existsSync(filePath)) return filePath;
1975
- }
1976
- }
1977
- function assertConfigObject(config, configFile) {
1978
- if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error(`[clarify] ${configFile} must export a configuration object`);
1979
- return config;
2513
+ function resolveProjectConfigFromContext(contextOrConfig) {
2514
+ return "projectConfig" in contextOrConfig ? contextOrConfig.projectConfig : contextOrConfig;
1980
2515
  }
1981
- function loadJsonConfig(configFile) {
1982
- const config = assertConfigObject(JSON.parse(readFileSync(configFile, "utf-8")), configFile);
1983
- const projectConfig = validateProjectConfig(config);
1984
- const ssg = config.ssg && typeof config.ssg === "object" && !Array.isArray(config.ssg) ? config.ssg : void 0;
1985
- return {
1986
- ...projectConfig,
1987
- ...ssg ? { ssg } : {}
1988
- };
2516
+ function routeTitle(contextOrConfig, route) {
2517
+ const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2518
+ const title = route?.title?.trim();
2519
+ if (!title || title === projectConfig.title) return projectConfig.title;
2520
+ return `${title} - ${projectConfig.title}`;
1989
2521
  }
1990
- async function loadClarifyConfig(root, env) {
1991
- const configFile = findClarifyConfigFile(root);
1992
- if (!configFile) return {};
1993
- if (configFile.endsWith(".json")) return loadJsonConfig(configFile);
1994
- return assertConfigObject((await loadConfigFromFile(env, configFile, root, "silent"))?.config, configFile);
2522
+ function canonicalUrl(contextOrConfig, route) {
2523
+ const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2524
+ if (!projectConfig.siteUrl || !route.alternates || !route.locale) return void 0;
2525
+ const prefixedPath = route.alternates[route.locale];
2526
+ if (!prefixedPath || prefixedPath === route.path) return void 0;
2527
+ return `${projectConfig.siteUrl.replace(/\/+$/, "")}${!projectConfig.routePrefix || projectConfig.routePrefix === "/" ? "" : `/${projectConfig.routePrefix.replace(/^\/+|\/+$/g, "")}`}${prefixedPath}`;
1995
2528
  }
1996
- /**
1997
- * File extensions Clarify treats as content files. Kept in sync with the
1998
- * patterns used by route discovery in `parsers/routes.ts` and the OpenAPI
1999
- * page generator. Exposed to tooling so the extension's "is this a content
2000
- * file?" check stays accurate without duplicating the regex.
2001
- */
2002
- var CONTENT_FILE_EXTENSIONS = [
2003
- ".md",
2004
- ".mdx",
2005
- ".openapi.json",
2006
- ".openapi.yaml",
2007
- ".openapi.yml"
2008
- ];
2009
- /**
2010
- * Build the project-info response payload from the resolved plugin state.
2011
- */
2012
- function buildProjectInfo(projectRoot, contentRoot, projectConfig) {
2013
- const response = {
2014
- configFilenames: CONFIG_FILENAMES,
2015
- contentFileExtensions: CONTENT_FILE_EXTENSIONS,
2016
- contentRoot: (contentRoot.startsWith(projectRoot) ? contentRoot.slice(projectRoot.length).replace(/^[/\\]+/, "") : contentRoot) || "source",
2017
- projectRoot
2018
- };
2019
- if (projectConfig.i18n) response.i18n = {
2020
- defaultLocale: projectConfig.i18n.defaultLocale,
2021
- locales: projectConfig.i18n.locales.map((l) => l.code)
2022
- };
2023
- return response;
2529
+ /** Inject `lang` (and optional `dir`) onto the `<html>` tag for the route locale. */
2530
+ function injectHtmlLocaleAttributes(html, contextOrConfig, route) {
2531
+ const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2532
+ const localeCode = route?.locale ?? projectConfig.i18n?.defaultLocale;
2533
+ if (!localeCode) return html;
2534
+ const dir = (projectConfig.i18n?.locales.find((locale) => locale.code === localeCode))?.dir;
2535
+ return html.replace(/<html\b([^>]*)>/i, (_match, attributes) => {
2536
+ let nextAttributes = attributes.replace(/\s+lang=("[^"]*"|'[^']*'|[^\s>]*)/i, "").replace(/\s+dir=("[^"]*"|'[^']*'|[^\s>]*)/i, "");
2537
+ nextAttributes = `${nextAttributes} lang="${escapeHtml(localeCode)}"`;
2538
+ if (dir) nextAttributes = `${nextAttributes} dir="${escapeHtml(dir)}"`;
2539
+ return `<html${nextAttributes}>`;
2540
+ });
2541
+ }
2542
+ /** Set, replace, or remove a `<meta name="...">` tag in `<head>`. */
2543
+ function setNamedMeta(html, name, content) {
2544
+ const metaPattern = new RegExp(`<meta\\b(?=[^>]*\\bname=["']${name}["'])[^>]*>\\n?`, "i");
2545
+ if (!content) return html.replace(metaPattern, "");
2546
+ const meta = `<meta name="${name}" content="${escapeHtml(content)}" />`;
2547
+ if (metaPattern.test(html)) return html.replace(metaPattern, meta);
2548
+ return html.replace("</head>", ` ${meta}\n </head>`);
2549
+ }
2550
+ /** Remove any existing canonical link and (if a URL is resolved) inject a fresh one. */
2551
+ function injectCanonicalUrl(html, contextOrConfig, route) {
2552
+ const url = route ? canonicalUrl(contextOrConfig, route) : void 0;
2553
+ html = html.replace(/<link\b[^>]*\brel=["']canonical["'][^>]*\/?>\n?/gi, "");
2554
+ if (!url) return html;
2555
+ return html.replace("</head>", ` <link rel="canonical" href="${escapeHtml(url)}" />\n </head>`);
2024
2556
  }
2025
2557
  /**
2026
- * Handle a request to the project-info endpoint. Responds to any method
2027
- * (GET is typical) with the JSON payload — there is no request body to parse.
2558
+ * Render the SSR result into the built HTML template: set `<title>`, the
2559
+ * description/keywords meta, the canonical link, the `<html lang/dir>`, and
2560
+ * replace the `#root` content with the rendered app HTML.
2028
2561
  */
2029
- function handleProjectInfoRequest(_req, res, projectRoot, contentRoot, projectConfig) {
2030
- res.setHeader("Content-Type", "application/json; charset=utf-8");
2031
- res.end(JSON.stringify(buildProjectInfo(projectRoot, contentRoot, projectConfig)));
2032
- }
2033
- //#endregion
2034
- //#region source/core/site.ts
2035
- async function discoverRoutesForRoot(routeRoot, locale, plugins, ctx) {
2036
- return (await runHooks(plugins, "routes:discover", {
2037
- contentRoot: routeRoot,
2038
- locale,
2039
- routes: await findContentRoutes(routeRoot, routeRoot, { contentProcessor: getProjectContentProcessor(ctx) })
2040
- }, ctx)).routes;
2041
- }
2042
- async function discoverRoutes(root, contentRoot, plugins, ctx) {
2043
- const i18n = ctx.projectConfig.i18n;
2044
- if (!i18n) return discoverRoutesForRoot(contentRoot, void 0, plugins, ctx);
2045
- const localizedRoutes = [];
2046
- for (const locale of i18n.locales) {
2047
- const discovered = await discoverRoutesForRoot(join(contentRoot, locale.code), locale.code, plugins, ctx);
2048
- for (const route of discovered) {
2049
- const basePath = route.basePath ?? route.path;
2050
- localizedRoutes.push({
2051
- ...route,
2052
- path: localizedRoutePath(basePath, locale.code, i18n),
2053
- basePath,
2054
- locale: locale.code,
2055
- virtualModuleId: virtualModuleIdFromRef(relative(contentRoot, route.filePath))
2056
- });
2057
- }
2058
- }
2059
- if (i18n.missing === "fallback") {
2060
- const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
2061
- const defaultRoutes = localizedRoutes.filter((route) => route.locale === i18n.defaultLocale);
2062
- for (const sourceRoute of defaultRoutes) {
2063
- const basePath = sourceRoute.basePath ?? sourceRoute.path;
2064
- for (const locale of i18n.locales) {
2065
- const key = `${locale.code}:${basePath}`;
2066
- if (routeByLocaleAndBase.has(key)) continue;
2067
- localizedRoutes.push({
2068
- ...sourceRoute,
2069
- path: localizedRoutePath(basePath, locale.code, i18n),
2070
- locale: locale.code,
2071
- isFallback: true
2072
- });
2073
- }
2074
- }
2075
- }
2076
- const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
2077
- const defaultLocale = i18n.defaultLocale;
2078
- const bareRoutes = [];
2079
- const seenBare = new Set(routesWithAlternates.map((r) => r.path));
2080
- for (const route of routesWithAlternates) {
2081
- if (route.locale !== defaultLocale) continue;
2082
- const bp = route.basePath ?? route.path;
2083
- if (bp === route.path || seenBare.has(bp)) continue;
2084
- seenBare.add(bp);
2085
- bareRoutes.push({
2086
- ...route,
2087
- path: bp,
2088
- isBareAlias: true
2089
- });
2090
- }
2091
- return [...routesWithAlternates, ...bareRoutes];
2092
- }
2093
- async function resolveClarifySite(options = {}, resolveOptions = {}) {
2094
- const root = options.projectRoot ?? process.cwd();
2095
- const projectConfig = resolveProjectConfig(options);
2096
- const generateOptions = resolveBuildOptions(options);
2097
- const contentRoot = join(root, generateOptions.rootDirectory);
2098
- const plugins = [...createBuiltinPlugins({ htmlShell: resolveOptions.includeHtmlShellPlugin }), ...options.plugins ?? []];
2099
- const ctx = {
2100
- projectConfig,
2101
- generateOptions,
2102
- routes: [],
2103
- navigation: []
2104
- };
2105
- setProjectContentProcessor(ctx, createProjectContentProcessor(plugins, ctx));
2106
- let routes = await discoverRoutes(root, contentRoot, plugins, ctx);
2107
- routes = await runHooks(plugins, "routes:discovered", routes, ctx);
2108
- routes = applyConfiguredPageRoutePaths(routes, projectConfig.tabs, projectConfig.i18n);
2109
- const defaultNavigation = projectConfig.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes);
2110
- const resolved = await runHooks(plugins, "routes:resolved", {
2111
- routes,
2112
- navigation: defaultNavigation
2113
- }, ctx);
2114
- routes = resolved.routes;
2115
- const navigation = resolved.navigation;
2116
- ctx.routes = routes;
2117
- ctx.navigation = navigation;
2118
- return {
2119
- root,
2120
- contentRoot,
2121
- projectConfig,
2122
- generateOptions,
2123
- plugins,
2124
- ctx,
2125
- routes,
2126
- navigation
2127
- };
2562
+ function injectSSRIntoTemplate(template, appHtml, contextOrConfig, route) {
2563
+ const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
2564
+ let html = injectHtmlLocaleAttributes(template, contextOrConfig, route);
2565
+ html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(routeTitle(contextOrConfig, route))}</title>`);
2566
+ html = setNamedMeta(html, "description", route?.description ?? projectConfig.description);
2567
+ html = setNamedMeta(html, "keywords", route?.keywords?.join(", "));
2568
+ html = injectCanonicalUrl(html, contextOrConfig, route);
2569
+ const dataPagefindIgnore = route?.isBareAlias ? " data-pagefind-ignore" : "";
2570
+ html = html.replace(/<div id="root">([\s\S]*?)<\/div>/, `<div id="root"${dataPagefindIgnore}>${appHtml}</div>`);
2571
+ return html;
2128
2572
  }
2129
2573
  //#endregion
2130
- //#region source/core/runtime-deps.ts
2574
+ //#region source/core/runtime/runtime-deps.ts
2131
2575
  var requireFromCli = createRequire(import.meta.url);
2132
2576
  function resolveCliDependency(id) {
2133
2577
  return requireFromCli.resolve(id);
@@ -2173,6 +2617,26 @@ function createClarifyRuntimeAliases() {
2173
2617
  find: /^react\/jsx-runtime$/,
2174
2618
  replacement: resolveCliDependency("react/jsx-runtime")
2175
2619
  },
2620
+ {
2621
+ find: /^react-dom\/client$/,
2622
+ replacement: resolveCliDependency("react-dom/client")
2623
+ },
2624
+ {
2625
+ find: /^react-dom\/server$/,
2626
+ replacement: resolveCliDependency("react-dom/server")
2627
+ },
2628
+ {
2629
+ find: /^react-dom$/,
2630
+ replacement: resolveCliDependency("react-dom")
2631
+ },
2632
+ {
2633
+ find: /^react-router-dom$/,
2634
+ replacement: resolveCliDependency("react-router-dom")
2635
+ },
2636
+ {
2637
+ find: /^react-router$/,
2638
+ replacement: resolveCliDependency("react-router")
2639
+ },
2176
2640
  {
2177
2641
  find: /^mermaid$/,
2178
2642
  replacement: resolveCliDependency("mermaid")
@@ -2188,7 +2652,7 @@ function createClarifyRuntimeAliases() {
2188
2652
  ];
2189
2653
  }
2190
2654
  //#endregion
2191
- //#region source/core/ssg.ts
2655
+ //#region source/core/runtime/ssg.ts
2192
2656
  function readIndexHtml(outputDirectory) {
2193
2657
  const indexPath = join(outputDirectory, "index.html");
2194
2658
  if (!existsSync(indexPath)) return void 0;
@@ -2198,51 +2662,6 @@ function readIndexHtml(outputDirectory) {
2198
2662
  return;
2199
2663
  }
2200
2664
  }
2201
- function injectHtmlLocaleAttributes(html, projectConfig, route) {
2202
- const localeCode = route?.locale ?? projectConfig.i18n?.defaultLocale;
2203
- if (!localeCode) return html;
2204
- const dir = (projectConfig.i18n?.locales.find((locale) => locale.code === localeCode))?.dir;
2205
- return html.replace(/<html\b([^>]*)>/i, (_match, attributes) => {
2206
- let nextAttributes = attributes.replace(/\s+lang=("[^"]*"|'[^']*'|[^\s>]*)/i, "").replace(/\s+dir=("[^"]*"|'[^']*'|[^\s>]*)/i, "");
2207
- nextAttributes = `${nextAttributes} lang="${escapeHtml(localeCode)}"`;
2208
- if (dir) nextAttributes = `${nextAttributes} dir="${escapeHtml(dir)}"`;
2209
- return `<html${nextAttributes}>`;
2210
- });
2211
- }
2212
- function routeTitle(projectConfig, route) {
2213
- const title = route?.title?.trim();
2214
- if (!title || title === projectConfig.title) return projectConfig.title;
2215
- return `${title} - ${projectConfig.title}`;
2216
- }
2217
- function setNamedMeta(html, name, content) {
2218
- const metaPattern = new RegExp(`<meta\\b(?=[^>]*\\bname=["']${name}["'])[^>]*>\\n?`, "i");
2219
- if (!content) return html.replace(metaPattern, "");
2220
- const meta = `<meta name="${name}" content="${escapeHtml(content)}" />`;
2221
- if (metaPattern.test(html)) return html.replace(metaPattern, meta);
2222
- return html.replace("</head>", ` ${meta}\n </head>`);
2223
- }
2224
- function canonicalUrl(projectConfig, route) {
2225
- if (!projectConfig.siteUrl || !route.alternates || !route.locale) return void 0;
2226
- const prefixedPath = route.alternates[route.locale];
2227
- if (!prefixedPath || prefixedPath === route.path) return void 0;
2228
- return `${projectConfig.siteUrl.replace(/\/+$/, "")}${!projectConfig.routePrefix || projectConfig.routePrefix === "/" ? "" : `/${projectConfig.routePrefix.replace(/^\/+|\/+$/g, "")}`}${prefixedPath}`;
2229
- }
2230
- function injectCanonicalUrl(html, projectConfig, route) {
2231
- const url = route ? canonicalUrl(projectConfig, route) : void 0;
2232
- html = html.replace(/<link\b[^>]*\brel=["']canonical["'][^>]*\/?>\n?/gi, "");
2233
- if (!url) return html;
2234
- return html.replace("</head>", ` <link rel="canonical" href="${escapeHtml(url)}" />\n </head>`);
2235
- }
2236
- function injectSSRIntoTemplate(template, appHtml, projectConfig, route) {
2237
- let html = injectHtmlLocaleAttributes(template, projectConfig, route);
2238
- html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(routeTitle(projectConfig, route))}</title>`);
2239
- html = setNamedMeta(html, "description", route?.description ?? projectConfig.description);
2240
- html = setNamedMeta(html, "keywords", route?.keywords?.join(", "));
2241
- html = injectCanonicalUrl(html, projectConfig, route);
2242
- const dataPagefindIgnore = route?.isBareAlias ? " data-pagefind-ignore" : "";
2243
- html = html.replace(/<div id="root">([\s\S]*?)<\/div>/, `<div id="root"${dataPagefindIgnore}>${appHtml}</div>`);
2244
- return html;
2245
- }
2246
2665
  function isNotFoundRoute(route) {
2247
2666
  return (route.basePath ?? route.path) === "/404";
2248
2667
  }
@@ -2290,12 +2709,12 @@ async function buildSSRBundle(root, ssrEntry, ssrOutDir, plugins) {
2290
2709
  }
2291
2710
  });
2292
2711
  }
2293
- async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundlePath, failOnError = true) {
2712
+ async function renderSSGRoutes(routes, contextOrConfig, outputDirectory, ssrBundlePath, failOnError = true) {
2294
2713
  const { render } = await import(pathToFileURL(ssrBundlePath).href);
2295
2714
  const template = readIndexHtml(outputDirectory);
2296
2715
  if (!template) throw new Error(`[clarify] index.html not found in outputDirectory "${outputDirectory}". Make sure Vite build produces it.`);
2297
2716
  for (const route of routes) try {
2298
- const finalHtml = injectSSRIntoTemplate(template, await render(route.path), projectConfig, route);
2717
+ const finalHtml = injectSSRIntoTemplate(template, await render(route.path), contextOrConfig, route);
2299
2718
  for (const outFile of routeOutputFiles(outputDirectory, route)) {
2300
2719
  mkdirSync(dirname(outFile), { recursive: true });
2301
2720
  writeFileSync(outFile, finalHtml, "utf-8");
@@ -2306,239 +2725,419 @@ async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundle
2306
2725
  }
2307
2726
  }
2308
2727
  //#endregion
2309
- //#region source/core/plugin.ts
2728
+ //#region source/core/runtime/startup.ts
2729
+ function contentDirectoryLabel(projectRoot, contentRoot) {
2730
+ const relativePath = relative(projectRoot, contentRoot);
2731
+ return relativePath && relativePath !== "." ? relativePath : "source";
2732
+ }
2733
+ function contentFileExampleLabel(projectRoot, contentRoot) {
2734
+ return `${contentDirectoryLabel(projectRoot, contentRoot)}/index.mdx`;
2735
+ }
2736
+ function getStartupHints(options) {
2737
+ const hints = [];
2738
+ if (!options.contentDirExists) {
2739
+ const label = contentDirectoryLabel(options.projectRoot, options.contentRoot);
2740
+ hints.push(`Content directory "${label}" was not found. Create it or pass --content <dir> to point Clarify at a different directory.`);
2741
+ } else if (!options.hasRoutes) {
2742
+ const example = contentFileExampleLabel(options.projectRoot, options.contentRoot);
2743
+ hints.push(`No content pages were found yet. Add ${example} to get started.`);
2744
+ }
2745
+ return hints;
2746
+ }
2747
+ function logStartupHints(options) {
2748
+ const hints = getStartupHints(options);
2749
+ if (hints.length === 0) return;
2750
+ const message = hints.map((hint) => `[clarify] ${hint}`).join("\n");
2751
+ console.warn(message);
2752
+ }
2753
+ //#endregion
2754
+ //#region source/core/engine/context.ts
2755
+ var ClarifyContext = class {
2756
+ projectRoot;
2757
+ contentRoot;
2758
+ projectConfig;
2759
+ generateOptions;
2760
+ version;
2761
+ values = /* @__PURE__ */ new Map();
2762
+ routeListeners = /* @__PURE__ */ new Set();
2763
+ navigationListeners = /* @__PURE__ */ new Set();
2764
+ currentRoutes;
2765
+ currentNavigation;
2766
+ currentPlugins;
2767
+ constructor(init) {
2768
+ this.projectRoot = init.projectRoot;
2769
+ this.contentRoot = init.contentRoot;
2770
+ this.projectConfig = init.projectConfig;
2771
+ this.generateOptions = init.generateOptions;
2772
+ this.version = init.version;
2773
+ this.currentRoutes = init.routes ?? [];
2774
+ this.currentNavigation = init.navigation ?? [];
2775
+ this.currentPlugins = init.plugins ?? [];
2776
+ }
2777
+ get routes() {
2778
+ return this.currentRoutes;
2779
+ }
2780
+ set routes(routes) {
2781
+ this.currentRoutes = routes;
2782
+ this.notify(this.routeListeners);
2783
+ }
2784
+ get navigation() {
2785
+ return this.currentNavigation;
2786
+ }
2787
+ set navigation(navigation) {
2788
+ this.currentNavigation = navigation;
2789
+ this.notify(this.navigationListeners);
2790
+ }
2791
+ get plugins() {
2792
+ return this.currentPlugins;
2793
+ }
2794
+ set plugins(plugins) {
2795
+ this.currentPlugins = plugins;
2796
+ }
2797
+ get isI18n() {
2798
+ return Boolean(this.projectConfig.i18n);
2799
+ }
2800
+ get defaultLocale() {
2801
+ return this.projectConfig.i18n?.defaultLocale;
2802
+ }
2803
+ get(key) {
2804
+ return this.values.get(key);
2805
+ }
2806
+ set(key, value) {
2807
+ this.values.set(key, value);
2808
+ }
2809
+ has(key) {
2810
+ return this.values.has(key);
2811
+ }
2812
+ delete(key) {
2813
+ return this.values.delete(key);
2814
+ }
2815
+ onRoutesChange(listener) {
2816
+ this.routeListeners.add(listener);
2817
+ return () => this.routeListeners.delete(listener);
2818
+ }
2819
+ onNavigationChange(listener) {
2820
+ this.navigationListeners.add(listener);
2821
+ return () => this.navigationListeners.delete(listener);
2822
+ }
2823
+ updateProjectState(update) {
2824
+ if (update.projectRoot !== void 0) this.projectRoot = update.projectRoot;
2825
+ if (update.contentRoot !== void 0) this.contentRoot = update.contentRoot;
2826
+ if (update.projectConfig !== void 0) this.projectConfig = update.projectConfig;
2827
+ if (update.generateOptions !== void 0) this.generateOptions = update.generateOptions;
2828
+ if (update.version !== void 0) this.version = update.version;
2829
+ }
2830
+ notify(listeners) {
2831
+ for (const listener of listeners) listener();
2832
+ }
2833
+ };
2834
+ //#endregion
2835
+ //#region source/core/engine/phases.ts
2836
+ async function runTapHooks(plugins, hookName, ctx) {
2837
+ for (const plugin of plugins) {
2838
+ const hook = plugin.hooks?.[hookName];
2839
+ if (!hook) continue;
2840
+ try {
2841
+ await hook(ctx);
2842
+ } catch (err) {
2843
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
2844
+ }
2845
+ }
2846
+ }
2847
+ async function runInterceptHooks(plugins, hookName, ctx) {
2848
+ for (const plugin of plugins) {
2849
+ const hook = plugin.hooks?.[hookName];
2850
+ if (!hook) continue;
2851
+ try {
2852
+ if (await hook(ctx) === false) return false;
2853
+ } catch (err) {
2854
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
2855
+ }
2856
+ }
2857
+ return true;
2858
+ }
2859
+ async function runPhase(plugins, phase, ctx, task) {
2860
+ await runTapHooks(plugins, `before:${phase}`, ctx);
2861
+ const result = await task();
2862
+ await runTapHooks(plugins, `after:${phase}`, ctx);
2863
+ return result;
2864
+ }
2865
+ //#endregion
2866
+ //#region source/core/engine/engine.ts
2310
2867
  function loadVirtualModule(id, modules) {
2311
2868
  const bareId = stripVirtualPrefix(id);
2312
2869
  return modules.get(bareId) ?? modules.get(id) ?? null;
2313
2870
  }
2314
- function clarifyPlugin(options = {}) {
2315
- const root = resolve(options.projectRoot ?? process.cwd());
2316
- let projectConfig = resolveProjectConfig(options);
2317
- let generateOptions = resolveBuildOptions(options);
2318
- const contentRoot = join(root, generateOptions.rootDirectory);
2319
- const configFilePath = findClarifyConfigFile(root);
2320
- let routes = [];
2321
- let clarifyPlugins = [...createBuiltinPlugins(), ...options.plugins ?? []];
2322
- const ctx = {
2323
- projectConfig,
2324
- generateOptions,
2325
- routes,
2326
- navigation: []
2871
+ var ClarifyEngine = class {
2872
+ options;
2873
+ root;
2874
+ configFilePath;
2875
+ ctx;
2876
+ runtime = {
2877
+ command: "build",
2878
+ mode: "production"
2327
2879
  };
2328
- let viteConfig;
2329
- let resolvedNavigation = [];
2330
- let virtualModules = /* @__PURE__ */ new Map();
2331
- async function resolveRoutesAndSpecs(overrides) {
2332
- const site = await resolveClarifySite(overrides ?? options);
2333
- projectConfig = site.projectConfig;
2334
- generateOptions = site.generateOptions;
2335
- routes = site.routes;
2336
- resolvedNavigation = site.navigation;
2337
- clarifyPlugins = site.plugins;
2338
- ctx.projectConfig = projectConfig;
2339
- ctx.generateOptions = generateOptions;
2340
- ctx.routes = routes;
2341
- ctx.navigation = resolvedNavigation;
2342
- }
2343
- async function rebuildVirtualModules() {
2344
- virtualModules = buildVirtualModules({
2345
- projectConfig,
2346
- generateOptions,
2347
- routes,
2348
- navigation: resolvedNavigation,
2349
- plugins: clarifyPlugins,
2350
- themeEditor: viteConfig.command === "serve" || projectConfig.theme.editor
2880
+ virtualModules = /* @__PURE__ */ new Map();
2881
+ buildEnabled = true;
2882
+ initializedOptions;
2883
+ constructor(options = {}) {
2884
+ this.options = options;
2885
+ this.root = resolve(options.projectRoot ?? process.cwd());
2886
+ this.configFilePath = findClarifyConfigFile(this.root) ?? void 0;
2887
+ const placeholderConfig = resolveProjectConfig({});
2888
+ const placeholderOptions = resolveBuildOptions(options);
2889
+ this.ctx = new ClarifyContext({
2890
+ projectRoot: this.root,
2891
+ contentRoot: join(this.root, placeholderOptions.rootDirectory),
2892
+ projectConfig: placeholderConfig,
2893
+ generateOptions: placeholderOptions,
2894
+ version: cliPackageVersion,
2895
+ routes: [],
2896
+ navigation: [],
2897
+ plugins: []
2351
2898
  });
2352
- virtualModules = await runHooks(clarifyPlugins, "modules:before", virtualModules, ctx);
2353
2899
  }
2354
- function invalidateVirtualModules(server) {
2355
- for (const id of virtualModules.keys()) {
2356
- const moduleNode = server.moduleGraph.getModuleById(resolveVirtualId(id));
2357
- if (moduleNode) server.moduleGraph.invalidateModule(moduleNode);
2358
- }
2900
+ configureRuntime(runtime) {
2901
+ this.runtime = {
2902
+ ...this.runtime,
2903
+ ...runtime
2904
+ };
2359
2905
  }
2360
- async function refreshDevServer(server) {
2361
- await resolveRoutesAndSpecs();
2362
- await rebuildVirtualModules();
2363
- invalidateVirtualModules(server);
2364
- server.ws.send({ type: "full-reload" });
2906
+ async initialize(env = {
2907
+ command: this.runtime.command,
2908
+ mode: this.runtime.mode
2909
+ }, options = this.options, prepareOptions = {}) {
2910
+ if (this.initializedOptions && !prepareOptions.force) return this.initializedOptions;
2911
+ const seedPlugins = loadBuildPlugins(options, { htmlShell: prepareOptions.htmlShell });
2912
+ this.ctx.plugins = seedPlugins;
2913
+ const context = await runPhase(seedPlugins, "config:load", this.ctx, () => resolveProjectContext(options, env));
2914
+ this.configFilePath = context.configFilePath;
2915
+ this.ctx.updateProjectState({
2916
+ projectRoot: context.projectRoot,
2917
+ contentRoot: context.contentRoot,
2918
+ projectConfig: context.projectConfig,
2919
+ generateOptions: context.buildOptions,
2920
+ version: context.projectContext.version
2921
+ });
2922
+ await runPhase(seedPlugins, "config:resolve", this.ctx, () => void 0);
2923
+ const finalPlugins = loadBuildPlugins(context.resolvedOptions, { htmlShell: prepareOptions.htmlShell });
2924
+ this.ctx.plugins = finalPlugins;
2925
+ await runPhase(finalPlugins, "plugins:load", this.ctx, () => void 0);
2926
+ this.initializedOptions = context.resolvedOptions;
2927
+ return context.resolvedOptions;
2365
2928
  }
2366
- function hasContentRouteForFile(filePath) {
2367
- return routes.some((route) => route.filePath === filePath);
2929
+ /**
2930
+ * Runs the full pre-build/pre-dev preparation: initialize (config load +
2931
+ * resolve + plugins:load), site discovery (routes + navigation), and module
2932
+ * building. This is the single entry point that prepares the engine before
2933
+ * handing it to a Vite adapter or running a check, so callers never need to
2934
+ * call initialize/discoverSite separately.
2935
+ *
2936
+ * Options:
2937
+ * - `force`: re-run even if already prepared (config-file hot reload).
2938
+ * - `htmlShell`: toggle the HTML shell builtin plugin (default true).
2939
+ * - `skipModules`: skip `buildModules()` (check command).
2940
+ * - `skipHints`: skip `logStartupHints()` (check command).
2941
+ *
2942
+ * Idempotent: calling prepare() again with the same env is a no-op unless
2943
+ * `force` is set.
2944
+ */
2945
+ async prepare(env = {
2946
+ command: this.runtime.command,
2947
+ mode: this.runtime.mode
2948
+ }, options = this.options, prepareOptions = {}) {
2949
+ this.configureRuntime({
2950
+ command: env.command,
2951
+ mode: env.mode
2952
+ });
2953
+ const resolvedOptions = await this.initialize(env, options, prepareOptions);
2954
+ await this.discoverSite();
2955
+ if (!prepareOptions.skipModules) await this.buildModules();
2956
+ if (!prepareOptions.skipHints) this.logStartupHints();
2957
+ return resolvedOptions;
2368
2958
  }
2369
- const normalizedMdxContentPlugin = {
2370
- name: "clarify:normalized-mdx-content",
2371
- enforce: "pre",
2372
- transform(_code, id) {
2373
- if (!/\.mdx?(?:\?|$)/.test(id)) return null;
2374
- const filePath = id.replace(/\?.*$/, "");
2375
- const route = routes.find((route) => route.kind === "mdx" && route.filePath === filePath);
2376
- if (!route || route.content === void 0) return null;
2377
- return {
2378
- code: route.content,
2379
- map: null
2380
- };
2381
- }
2382
- };
2383
- const mdx = mdxPlugin({
2384
- include: ["**/*.{md,mdx}"],
2385
- jsxImportSource: "react",
2386
- providerImportSource: "@clarify-labs/renderer",
2387
- remarkPlugins,
2388
- rehypePlugins
2389
- });
2390
- return [
2391
- react(),
2392
- tailwindcss(),
2393
- normalizedMdxContentPlugin,
2394
- {
2395
- name: "clarify:core",
2396
- async config() {
2397
- await resolveRoutesAndSpecs();
2959
+ get projectConfig() {
2960
+ return this.ctx.projectConfig;
2961
+ }
2962
+ get generateOptions() {
2963
+ return this.ctx.generateOptions;
2964
+ }
2965
+ get contentRoot() {
2966
+ return this.ctx.contentRoot;
2967
+ }
2968
+ get routes() {
2969
+ return this.ctx.routes;
2970
+ }
2971
+ get navigation() {
2972
+ return this.ctx.navigation;
2973
+ }
2974
+ get plugins() {
2975
+ return this.ctx.plugins;
2976
+ }
2977
+ get modules() {
2978
+ return this.virtualModules;
2979
+ }
2980
+ loadModule(id) {
2981
+ return loadVirtualModule(id, this.virtualModules);
2982
+ }
2983
+ async discoverSite() {
2984
+ const plugins = this.ctx.plugins;
2985
+ const contentRoot = this.ctx.contentRoot;
2986
+ const { i18n, tabs } = this.ctx.projectConfig;
2987
+ let routes = await runPhase(plugins, "site:discover", this.ctx, async () => {
2988
+ const options = { contentProcessor: createProjectContentProcessor(plugins, this.ctx) };
2989
+ const discovered = i18n ? await findLocalizedContentRoutes(contentRoot, i18n, options) : await findContentRoutes(contentRoot, contentRoot, options);
2990
+ const result = await runHooks(plugins, "routes:discover", {
2991
+ contentRoot,
2992
+ routes: discovered
2993
+ }, this.ctx);
2994
+ return runHooks(plugins, "routes:discovered", result.routes, this.ctx);
2995
+ });
2996
+ routes = await runPhase(plugins, "content:process", this.ctx, async () => {
2997
+ const pages = routes.map((route) => ({
2998
+ path: route.path,
2999
+ filePath: route.filePath,
3000
+ frontmatter: route.frontmatter ?? {},
3001
+ content: route.content ?? ""
3002
+ }));
3003
+ const resolvedPages = await runHooks(plugins, "pages:resolved", pages, this.ctx);
3004
+ const pageByPath = new Map(resolvedPages.map((p) => [p.path, p]));
3005
+ return routes.map((route) => {
3006
+ const page = pageByPath.get(route.path);
3007
+ if (!page) return route;
2398
3008
  return {
2399
- base: projectConfig.assetPrefix,
2400
- build: {
2401
- ...generateOptions.outputDirectory ? { outDir: generateOptions.outputDirectory } : {},
2402
- manifest: true
2403
- }
2404
- };
2405
- },
2406
- configResolved(config) {
2407
- viteConfig = config;
2408
- if (!generateOptions.outputDirectory) generateOptions.outputDirectory = config.build.outDir;
2409
- },
2410
- async buildStart() {
2411
- await rebuildVirtualModules();
2412
- writeClarifyEnvDts(root, clarifyPlugins);
2413
- },
2414
- async handleHotUpdate(ctx) {
2415
- const changedFile = isAbsolute(ctx.file) ? ctx.file : join(root, ctx.file);
2416
- if (configFilePath && changedFile === configFilePath) {
2417
- await resolveRoutesAndSpecs({
2418
- ...await loadClarifyConfig(root, {
2419
- command: "serve",
2420
- mode: viteConfig.mode
2421
- }),
2422
- projectRoot: options.projectRoot,
2423
- rootDirectory: options.rootDirectory,
2424
- outputDirectory: options.outputDirectory
2425
- });
2426
- await rebuildVirtualModules();
2427
- invalidateVirtualModules(ctx.server);
2428
- ctx.server.ws.send({ type: "full-reload" });
2429
- return [];
2430
- }
2431
- const relativeContentFile = relative(contentRoot, changedFile);
2432
- if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !isAbsolute(relativeContentFile)) || !hasContentRouteForFile(changedFile)) return;
2433
- await refreshDevServer(ctx.server);
2434
- return [];
2435
- },
2436
- resolveId(id) {
2437
- if (id === "virtual:clarify/entry-client" || id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2438
- if (id === "virtual:clarify/config" || id === resolveVirtualId("virtual:clarify/config")) return resolveVirtualId(VIRTUAL_CONFIG);
2439
- if (id === "virtual:clarify/routes" || id === resolveVirtualId("virtual:clarify/routes")) return resolveVirtualId(VIRTUAL_ROUTES);
2440
- if (id === "virtual:clarify/routes/server" || id === resolveVirtualId("virtual:clarify/routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
2441
- if (id === "virtual:clarify/openapi" || id === resolveVirtualId("virtual:clarify/openapi")) return resolveVirtualId(VIRTUAL_OPENAPI);
2442
- if (id === "virtual:clarify/slots" || id === resolveVirtualId("virtual:clarify/slots")) return resolveVirtualId(VIRTUAL_SLOTS);
2443
- if (id === "virtual:clarify/slot" || id === resolveVirtualId("virtual:clarify/slot")) return resolveVirtualId(VIRTUAL_SLOT);
2444
- const moduleId = stripVirtualPrefix(id);
2445
- if (virtualModules.has(moduleId)) return resolveVirtualId(moduleId);
2446
- const route = routes.find((r) => r.virtualModuleId === id || r.virtualModuleId === moduleId);
2447
- if (route) return resolveVirtualId(route.virtualModuleId);
2448
- return null;
2449
- },
2450
- load(id) {
2451
- return loadVirtualModule(id, virtualModules);
2452
- },
2453
- async configureServer(server) {
2454
- server.watcher.add(contentRoot);
2455
- if (configFilePath) server.watcher.add(configFilePath);
2456
- server.middlewares.use((req, res, next) => {
2457
- if (req.url === "/dev/query-preview-route" && req.method === "POST") return handleDevRouteRequest(req, res, routes, projectConfig, contentRoot);
2458
- if (req.url === "/dev/project-info" && (req.method === "GET" || req.method === "HEAD")) return handleProjectInfoRequest(req, res, root, contentRoot, projectConfig);
2459
- next();
2460
- });
2461
- const handleContentTreeChange = async (filePath) => {
2462
- const relativeContentFile = relative(contentRoot, isAbsolute(filePath) ? filePath : join(root, filePath));
2463
- if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !isAbsolute(relativeContentFile))) return;
2464
- await refreshDevServer(server);
3009
+ ...route,
3010
+ frontmatter: page.frontmatter,
3011
+ content: page.content
2465
3012
  };
2466
- server.watcher.on("add", handleContentTreeChange);
2467
- server.watcher.on("unlink", handleContentTreeChange);
2468
- await runDevConfigureServerHooks(clarifyPlugins, server, ctx);
2469
- },
2470
- transformIndexHtml: {
2471
- order: "pre",
2472
- async handler(html, transformCtx) {
2473
- const clientEntryId = transformCtx.server ? `/@id/${VIRTUAL_CLIENT_ENTRY}` : VIRTUAL_CLIENT_ENTRY;
2474
- const result = await runHooks(clarifyPlugins, "html:transform", {
2475
- html,
2476
- tags: [],
2477
- clientEntryId,
2478
- dev: Boolean(transformCtx.server)
2479
- }, ctx);
2480
- return {
2481
- html: result.html,
2482
- tags: result.tags
2483
- };
2484
- }
2485
- },
2486
- async generateBundle(_opts, _bundle) {
2487
- const assets = await runBuildAssetsHooks(clarifyPlugins, ctx);
2488
- for (const asset of assets) this.emitFile({
2489
- type: "asset",
2490
- fileName: asset.fileName,
2491
- source: asset.source
3013
+ });
3014
+ });
3015
+ routes = applyConfiguredPageRoutePaths(routes, tabs, i18n);
3016
+ const navigation = tabs ? i18n ? buildLocalizedNavigationFromTabsConfig(routes, tabs, i18n) ?? {} : buildNavigationFromTabsConfig(routes, tabs) : buildNavigation(routes);
3017
+ const resolved = await runHooks(plugins, "routes:resolved", {
3018
+ routes,
3019
+ navigation
3020
+ }, this.ctx);
3021
+ this.ctx.routes = resolved.routes;
3022
+ this.ctx.navigation = resolved.navigation;
3023
+ }
3024
+ logStartupHints() {
3025
+ logStartupHints({
3026
+ projectRoot: this.root,
3027
+ contentRoot: this.contentRoot,
3028
+ contentDirExists: existsSync(this.contentRoot),
3029
+ hasRoutes: this.routes.length > 0
3030
+ });
3031
+ }
3032
+ async buildModules() {
3033
+ await runPhase(this.plugins, "modules:build", this.ctx, async () => {
3034
+ this.virtualModules = buildVirtualModules({
3035
+ projectConfig: this.projectConfig,
3036
+ generateOptions: this.generateOptions,
3037
+ routes: this.routes,
3038
+ navigation: this.navigation,
3039
+ plugins: this.plugins,
3040
+ themeEditor: this.runtime.command === "serve" || this.runtime.themeEditor === true || this.projectConfig.theme.editor,
3041
+ version: this.ctx.version
3042
+ });
3043
+ this.virtualModules = await runHooks(this.plugins, "modules:before", this.virtualModules, this.ctx);
3044
+ });
3045
+ }
3046
+ writeEnvTypes() {
3047
+ writeClarifyEnvDts(this.root, this.plugins);
3048
+ }
3049
+ hasContentRouteForFile(filePath) {
3050
+ return this.routes.some((route) => route.filePath === filePath);
3051
+ }
3052
+ async collectBuildAssets() {
3053
+ if (!this.buildEnabled) return [];
3054
+ return runBuildAssetsHooks(this.plugins, this.ctx);
3055
+ }
3056
+ async beginBuild() {
3057
+ this.buildEnabled = await runInterceptHooks(this.plugins, "build:shouldRun", this.ctx);
3058
+ if (!this.buildEnabled) return false;
3059
+ await runTapHooks(this.plugins, "before:build", this.ctx);
3060
+ return true;
3061
+ }
3062
+ async endBuild() {
3063
+ if (!this.buildEnabled) return;
3064
+ await runTapHooks(this.plugins, "after:build", this.ctx);
3065
+ }
3066
+ shouldRunBuild() {
3067
+ return this.buildEnabled;
3068
+ }
3069
+ async configureDevServer(server) {
3070
+ await runPhase(this.plugins, "dev:server", this.ctx, async () => {
3071
+ await runDevConfigureServerHooks(this.plugins, server, this.ctx);
3072
+ });
3073
+ }
3074
+ async runBuildDone() {
3075
+ if (!this.buildEnabled) return;
3076
+ await runBuildDoneHooks(this.plugins, this.ctx);
3077
+ }
3078
+ async runSSG() {
3079
+ if (!this.buildEnabled) return;
3080
+ await runPhase(this.plugins, "ssg", this.ctx, async () => {
3081
+ if (process.env.SKIP_CLARIFY_SSG) return;
3082
+ if (!await runInterceptHooks(this.plugins, "ssg:shouldRun", this.ctx)) return;
3083
+ const outputDir = this.runtime.outputDirectory ?? this.generateOptions.outputDirectory;
3084
+ if (!outputDir) throw new Error("[clarify] outputDirectory is required before SSG runs");
3085
+ const ssrOutputDir = join(outputDir, ".ssr");
3086
+ let tempEntryPath;
3087
+ try {
3088
+ tempEntryPath = createTempEntryFile(SSR_ENTRY_CODE);
3089
+ await (this.runtime.buildSSRBundle ?? ((options) => buildSSRBundle(options.root, options.ssrEntry, options.ssrOutDir, options.plugins)))({
3090
+ root: this.root,
3091
+ ssrEntry: tempEntryPath,
3092
+ ssrOutDir: ssrOutputDir,
3093
+ plugins: this.runtime.ssrPlugins ?? []
2492
3094
  });
2493
- },
2494
- async closeBundle() {
2495
- if (process.env.SKIP_CLARIFY_SSG) {
2496
- await runBuildDoneHooks(clarifyPlugins, ctx);
2497
- return;
2498
- }
2499
- const outputDir = viteConfig.build.outDir;
2500
- const ssrOutputDir = join(outputDir, ".ssr");
2501
- let tempEntryPath;
2502
- try {
2503
- tempEntryPath = createTempEntryFile(SSR_ENTRY_CODE);
2504
- await buildSSRBundle(root, tempEntryPath, ssrOutputDir, [
2505
- {
2506
- name: "clarify:virtual-ssg",
2507
- resolveId(id) {
2508
- if (id === "virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2509
- if (id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2510
- if (id === "virtual:clarify/routes/server") return id;
2511
- if (id === "virtual:clarify/openapi") return id;
2512
- if (id === "virtual:clarify/config") return id;
2513
- if (id === "virtual:clarify/routes") return id;
2514
- if (id === "virtual:clarify/slots") return id;
2515
- if (id === "virtual:clarify/slot") return id;
2516
- if (virtualModules.has(stripVirtualPrefix(id))) return stripVirtualPrefix(id);
2517
- if (routes.find((r) => r.virtualModuleId === id)) return id;
2518
- return null;
2519
- },
2520
- load: (id) => loadVirtualModule(id, virtualModules)
2521
- },
2522
- normalizedMdxContentPlugin,
2523
- mdx
2524
- ]);
2525
- const ssrBundlePath = join(ssrOutputDir, "entry-server.js");
2526
- await renderSSGRoutes(routes, projectConfig, outputDir, ssrBundlePath, generateOptions.ssg.failOnError);
2527
- } catch (err) {
2528
- console.error("[clarify] SSG failed:", err);
2529
- if (generateOptions.ssg.failOnError) throw err;
2530
- } finally {
2531
- if (tempEntryPath) try {
2532
- rmSync(tempEntryPath, { force: true });
2533
- } catch {}
2534
- }
2535
- await runBuildDoneHooks(clarifyPlugins, ctx);
3095
+ const ssrBundlePath = join(ssrOutputDir, "entry-server.js");
3096
+ await renderSSGRoutes(this.routes, this.runtimeContext(), outputDir, ssrBundlePath, this.generateOptions.ssg.failOnError);
3097
+ } catch (err) {
3098
+ console.error("[clarify] SSG failed:", err);
3099
+ if (this.generateOptions.ssg.failOnError) throw err;
3100
+ } finally {
3101
+ if (tempEntryPath) try {
3102
+ rmSync(tempEntryPath, { force: true });
3103
+ } catch {}
2536
3104
  }
2537
- },
2538
- mdx
2539
- ].flat().filter(Boolean);
3105
+ });
3106
+ await this.runBuildDone();
3107
+ }
3108
+ async refresh() {
3109
+ await this.discoverSite();
3110
+ await this.buildModules();
3111
+ }
3112
+ /**
3113
+ * Runs the `html:transform` pipeline hook and returns the transformed
3114
+ * input. Adapters call this from their `transformIndexHtml` hook instead
3115
+ * of invoking `runHooks` directly, keeping hook execution inside Engine.
3116
+ */
3117
+ async transformHtml(input) {
3118
+ return runHooks(this.plugins, "html:transform", input, this.ctx);
3119
+ }
3120
+ runtimeContext() {
3121
+ return {
3122
+ projectRoot: this.root,
3123
+ contentRoot: this.contentRoot,
3124
+ projectConfig: this.projectConfig,
3125
+ generateOptions: this.generateOptions,
3126
+ version: this.ctx.version
3127
+ };
3128
+ }
3129
+ createSSGVirtualPlugin() {
3130
+ return {
3131
+ name: "clarify:virtual-ssg",
3132
+ resolveId: (id) => resolveVirtualModuleId(id, this.virtualModules, this.routes),
3133
+ load: (id) => this.loadModule(id)
3134
+ };
3135
+ }
3136
+ };
3137
+ function createClarifyEngine(options = {}) {
3138
+ return new ClarifyEngine(options);
2540
3139
  }
2541
3140
  //#endregion
2542
- export { loadClarifyConfig as a, defineConfig as i, createClarifyRuntimeAliases as n, createClarifyTempDir as o, resolveClarifySite as r, clarifyPlugin as t };
3141
+ export { cliPackageVersion as a, defineConfig as c, createClarifyTempDir as i, createClarifyEngine as n, cliPackageVersionWithCaret as o, createClarifyRuntimeAliases as r, createViteAdapter as s, ClarifyEngine as t };
2543
3142
 
2544
- //# sourceMappingURL=plugin-poS3N83A.js.map
3143
+ //# sourceMappingURL=engine-3f5Tpa-6.js.map