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