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