@clarify-labs/cli 0.11.19 → 0.11.21
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.cjs +1 -1
- package/output/cli.js +1 -1
- package/output/core/engine/engine.d.ts +3 -2
- package/output/core/runtime/ssg.d.ts +1 -0
- package/output/{engine-0ROqtF4o.cjs → engine-B_mPOpWr.cjs} +711 -111
- package/output/engine-B_mPOpWr.cjs.map +1 -0
- package/output/{engine-DH8Hi6Fr.js → engine-Ccma7jrA.js} +711 -110
- package/output/engine-Ccma7jrA.js.map +1 -0
- package/output/index.cjs +1 -1
- package/output/index.js +1 -1
- package/output/parsers/content/content.d.ts +1 -0
- package/output/parsers/markdown/frontmatter.d.ts +1 -0
- package/output/parsers/markdown/headings.d.ts +24 -0
- package/output/parsers/markdown/mdx.d.ts +1 -0
- package/output/parsers/routes/heading-metadata.d.ts +7 -0
- package/output/parsers/routes/routes.d.ts +2 -1
- package/output/plugins/content-artifacts/provider.d.ts +3 -0
- package/output/plugins/openapi/aggregation.d.ts +12 -0
- package/output/plugins/openapi/artifacts.d.ts +6 -0
- package/output/types.d.ts +7 -1
- package/package.json +3 -3
- package/output/engine-0ROqtF4o.cjs.map +0 -1
- package/output/engine-DH8Hi6Fr.js.map +0 -1
|
@@ -3,16 +3,13 @@ import mdxPlugin from "@mdx-js/rollup";
|
|
|
3
3
|
import tailwindcss from "@tailwindcss/vite";
|
|
4
4
|
import react from "@vitejs/plugin-react";
|
|
5
5
|
import rehypeRaw from "rehype-raw";
|
|
6
|
-
import { compile } from "@mdx-js/mdx";
|
|
6
|
+
import { compile, createProcessor } from "@mdx-js/mdx";
|
|
7
|
+
import GithubSlugger, { slug } from "github-slugger";
|
|
8
|
+
import { markdownRemarkPlugins, parseCodeMeta, themeBootstrapScript, themePresets } from "@clarify-labs/renderer";
|
|
7
9
|
import rehypeShiki from "@shikijs/rehype";
|
|
8
|
-
import rehypeSlug from "rehype-slug";
|
|
9
10
|
import { createCssVariablesTheme } from "shiki";
|
|
10
11
|
import { visit } from "unist-util-visit";
|
|
11
|
-
import { markdownRemarkPlugins, parseCodeMeta, themeBootstrapScript, themePresets } from "@clarify-labs/renderer";
|
|
12
12
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
13
|
-
import GithubSlugger, { slug } from "github-slugger";
|
|
14
|
-
import { toString } from "mdast-util-to-string";
|
|
15
|
-
import { remark } from "remark";
|
|
16
13
|
import matter from "gray-matter";
|
|
17
14
|
import { spawnSync } from "node:child_process";
|
|
18
15
|
import { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -45,6 +42,402 @@ function createContentDiagnostic(options) {
|
|
|
45
42
|
};
|
|
46
43
|
}
|
|
47
44
|
//#endregion
|
|
45
|
+
//#region source/parsers/markdown/headings.ts
|
|
46
|
+
var HEADING_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
47
|
+
var INTERNAL_HEADING_ID_PREFIX = "clarify-internal-heading-id:";
|
|
48
|
+
var EXPLICIT_ID_PATTERN = /\s+\{#([^{}]*)\}$/;
|
|
49
|
+
var HEADING_REMARK_PLUGINS = markdownRemarkPlugins;
|
|
50
|
+
function visitHeadings(node, callback) {
|
|
51
|
+
if (node.type === "heading") callback(node);
|
|
52
|
+
for (const child of node.children ?? []) visitHeadings(child, callback);
|
|
53
|
+
}
|
|
54
|
+
function visitHtml(node, callback) {
|
|
55
|
+
if (node.type === "html") callback(node);
|
|
56
|
+
for (const child of node.children ?? []) visitHtml(child, callback);
|
|
57
|
+
}
|
|
58
|
+
function visitMdxIntrinsicHeadings(node, callback) {
|
|
59
|
+
if ((node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && node.name && /^h[1-6]$/.test(node.name)) callback(node);
|
|
60
|
+
for (const child of node.children ?? []) visitMdxIntrinsicHeadings(child, callback);
|
|
61
|
+
}
|
|
62
|
+
function visitHastHeadings(node, callback) {
|
|
63
|
+
if (node.type === "element" && node.tagName && /^h[1-6]$/.test(node.tagName)) callback(node);
|
|
64
|
+
for (const child of node.children ?? []) visitHastHeadings(child, callback);
|
|
65
|
+
}
|
|
66
|
+
function hastText$1(node) {
|
|
67
|
+
if (node.type === "text") return node.value ?? "";
|
|
68
|
+
return (node.children ?? []).map(hastText$1).join("");
|
|
69
|
+
}
|
|
70
|
+
function applySourceEdits(content, edits) {
|
|
71
|
+
return edits.sort((left, right) => right.start - left.start).reduce((result, edit) => result.slice(0, edit.start) + edit.replacement + result.slice(edit.end), content);
|
|
72
|
+
}
|
|
73
|
+
function headingPosition(node, lineOffset) {
|
|
74
|
+
return {
|
|
75
|
+
line: (node.position?.start.line ?? 1) + lineOffset,
|
|
76
|
+
column: node.position?.start.column ?? 1
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function formatPosition(position) {
|
|
80
|
+
return `${position.line}:${position.column}`;
|
|
81
|
+
}
|
|
82
|
+
function finalTextNode(heading) {
|
|
83
|
+
const node = heading.children?.at(-1);
|
|
84
|
+
return node?.type === "text" && typeof node.value === "string" ? node : void 0;
|
|
85
|
+
}
|
|
86
|
+
function contentEndOffset(heading) {
|
|
87
|
+
return (heading.children?.at(-1))?.position?.end.offset ?? heading.position?.end.offset;
|
|
88
|
+
}
|
|
89
|
+
function explicitIdInfo(heading, content) {
|
|
90
|
+
const text = finalTextNode(heading);
|
|
91
|
+
if (!text?.position) return void 0;
|
|
92
|
+
const startOffset = text.position.start.offset;
|
|
93
|
+
const endOffset = text.position.end.offset;
|
|
94
|
+
if (startOffset === void 0 || endOffset === void 0) return void 0;
|
|
95
|
+
const match = content.slice(startOffset, endOffset).match(EXPLICIT_ID_PATTERN);
|
|
96
|
+
if (!match || match.index === void 0) return void 0;
|
|
97
|
+
return {
|
|
98
|
+
id: match[1] ?? "",
|
|
99
|
+
start: startOffset + match.index,
|
|
100
|
+
end: startOffset + match.index + match[0].length
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function maskExplicitHeadingMarkers(content) {
|
|
104
|
+
const tree = createProcessor({
|
|
105
|
+
format: "md",
|
|
106
|
+
remarkPlugins: HEADING_REMARK_PLUGINS
|
|
107
|
+
}).parse(content);
|
|
108
|
+
const edits = [];
|
|
109
|
+
visitHeadings(tree, (heading) => {
|
|
110
|
+
const marker = explicitIdInfo(heading, content);
|
|
111
|
+
if (marker) edits.push({
|
|
112
|
+
start: marker.start,
|
|
113
|
+
end: marker.end,
|
|
114
|
+
replacement: "x".repeat(marker.end - marker.start)
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return applySourceEdits(content, edits);
|
|
118
|
+
}
|
|
119
|
+
function parseHeadingTree(content, kind) {
|
|
120
|
+
if (kind === "markdown") return createProcessor({
|
|
121
|
+
format: "md",
|
|
122
|
+
remarkPlugins: HEADING_REMARK_PLUGINS
|
|
123
|
+
}).parse(content);
|
|
124
|
+
const maskedContent = maskExplicitHeadingMarkers(content);
|
|
125
|
+
try {
|
|
126
|
+
return createProcessor({
|
|
127
|
+
format: "mdx",
|
|
128
|
+
remarkPlugins: HEADING_REMARK_PLUGINS
|
|
129
|
+
}).parse(maskedContent);
|
|
130
|
+
} catch {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function renderedMarkdownHeadingTitles(tree, content, kind) {
|
|
135
|
+
const originalText = /* @__PURE__ */ new Map();
|
|
136
|
+
visitHeadings(tree, (heading) => {
|
|
137
|
+
const marker = explicitIdInfo(heading, content);
|
|
138
|
+
const text = finalTextNode(heading);
|
|
139
|
+
if (!marker || !text || text.value === void 0) return;
|
|
140
|
+
originalText.set(text, text.value);
|
|
141
|
+
text.value = text.value.slice(0, -(marker.end - marker.start)).replace(/\s+$/, "");
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
let hast;
|
|
145
|
+
const captureHast = () => (tree) => {
|
|
146
|
+
hast = tree;
|
|
147
|
+
};
|
|
148
|
+
createProcessor({
|
|
149
|
+
format: kind === "markdown" ? "md" : "mdx",
|
|
150
|
+
remarkPlugins: HEADING_REMARK_PLUGINS,
|
|
151
|
+
...kind === "markdown" ? { remarkRehypeOptions: { allowDangerousHtml: true } } : {},
|
|
152
|
+
rehypePlugins: kind === "markdown" ? [rehypeRaw, captureHast] : [captureHast]
|
|
153
|
+
}).runSync(tree, { value: content });
|
|
154
|
+
const titles = /* @__PURE__ */ new Map();
|
|
155
|
+
if (!hast) return titles;
|
|
156
|
+
visitHastHeadings(hast, (heading) => {
|
|
157
|
+
const offset = heading.position?.start.offset;
|
|
158
|
+
if (offset !== void 0) titles.set(offset, hastText$1(heading));
|
|
159
|
+
});
|
|
160
|
+
return titles;
|
|
161
|
+
} finally {
|
|
162
|
+
for (const [text, value] of originalText) text.value = value;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function isHtmlWhitespace(character) {
|
|
166
|
+
return character === " " || character === "\n" || character === "\f" || character === "\r" || character === " ";
|
|
167
|
+
}
|
|
168
|
+
function isMdxWhitespace(character) {
|
|
169
|
+
return /\s/u.test(character ?? "");
|
|
170
|
+
}
|
|
171
|
+
function openingTagAttributeOffset(content, start, end, isWhitespace) {
|
|
172
|
+
let state = "tagName";
|
|
173
|
+
let quote;
|
|
174
|
+
for (let index = start; index < end; index++) {
|
|
175
|
+
const character = content[index];
|
|
176
|
+
const closesTag = character === ">";
|
|
177
|
+
const selfClosesTag = character === "/" && content[index + 1] === ">";
|
|
178
|
+
if (state === "quotedAttributeValue") {
|
|
179
|
+
if (character === quote) {
|
|
180
|
+
quote = void 0;
|
|
181
|
+
state = "afterQuotedAttributeValue";
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (state === "unquotedAttributeValue") {
|
|
186
|
+
if (closesTag) return index;
|
|
187
|
+
if (isWhitespace(character)) state = "beforeAttributeName";
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (closesTag) return index;
|
|
191
|
+
if (selfClosesTag && state !== "beforeAttributeValue") {
|
|
192
|
+
let cursor = index;
|
|
193
|
+
while (cursor > start && isWhitespace(content[cursor - 1])) cursor--;
|
|
194
|
+
return cursor;
|
|
195
|
+
}
|
|
196
|
+
if (state === "tagName") {
|
|
197
|
+
if (isWhitespace(character)) state = "beforeAttributeName";
|
|
198
|
+
} else if (state === "beforeAttributeName") {
|
|
199
|
+
if (!isWhitespace(character)) state = "attributeName";
|
|
200
|
+
} else if (state === "attributeName") {
|
|
201
|
+
if (character === "=") state = "beforeAttributeValue";
|
|
202
|
+
else if (isWhitespace(character)) state = "afterAttributeName";
|
|
203
|
+
} else if (state === "afterAttributeName") {
|
|
204
|
+
if (character === "=") state = "beforeAttributeValue";
|
|
205
|
+
else if (!isWhitespace(character)) state = "attributeName";
|
|
206
|
+
} else if (state === "beforeAttributeValue") {
|
|
207
|
+
if (character === "\"" || character === "'") {
|
|
208
|
+
quote = character;
|
|
209
|
+
state = "quotedAttributeValue";
|
|
210
|
+
} else if (!isWhitespace(character)) state = "unquotedAttributeValue";
|
|
211
|
+
} else if (state === "afterQuotedAttributeValue" && isWhitespace(character)) state = "beforeAttributeName";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function rawHtmlHeadings(tree, content, lineOffset) {
|
|
215
|
+
let hasRawHtml = false;
|
|
216
|
+
visitHtml(tree, () => {
|
|
217
|
+
hasRawHtml = true;
|
|
218
|
+
});
|
|
219
|
+
if (!hasRawHtml) return [];
|
|
220
|
+
const markdownOffsets = /* @__PURE__ */ new Set();
|
|
221
|
+
visitHeadings(tree, (heading) => {
|
|
222
|
+
const offset = heading.position?.start.offset;
|
|
223
|
+
if (offset !== void 0) markdownOffsets.add(offset);
|
|
224
|
+
});
|
|
225
|
+
let hast;
|
|
226
|
+
const captureHast = () => (tree) => {
|
|
227
|
+
hast = tree;
|
|
228
|
+
};
|
|
229
|
+
createProcessor({
|
|
230
|
+
format: "md",
|
|
231
|
+
remarkRehypeOptions: { allowDangerousHtml: true },
|
|
232
|
+
rehypePlugins: [rehypeRaw, captureHast]
|
|
233
|
+
}).runSync(tree, { value: content });
|
|
234
|
+
if (!hast) return [];
|
|
235
|
+
const headings = [];
|
|
236
|
+
visitHastHeadings(hast, (heading) => {
|
|
237
|
+
const offset = heading.position?.start.offset;
|
|
238
|
+
const headingEnd = heading.position?.end.offset;
|
|
239
|
+
if (offset === void 0 || headingEnd === void 0 || markdownOffsets.has(offset)) return;
|
|
240
|
+
const tagEnd = openingTagAttributeOffset(content, offset, headingEnd, isHtmlWhitespace);
|
|
241
|
+
if (tagEnd === void 0) return;
|
|
242
|
+
const rawId = heading.properties?.id;
|
|
243
|
+
headings.push({
|
|
244
|
+
title: hastText$1(heading),
|
|
245
|
+
...typeof rawId === "string" && rawId ? { id: rawId } : {},
|
|
246
|
+
position: {
|
|
247
|
+
line: (heading.position?.start.line ?? 1) + lineOffset,
|
|
248
|
+
column: heading.position?.start.column ?? 1
|
|
249
|
+
},
|
|
250
|
+
offset,
|
|
251
|
+
attributeOffset: tagEnd
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
return headings;
|
|
255
|
+
}
|
|
256
|
+
function staticMdxText(node) {
|
|
257
|
+
if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
|
|
258
|
+
if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
|
|
259
|
+
if (!node.name || !/^[a-z][a-z0-9-]*$/.test(node.name)) return void 0;
|
|
260
|
+
} else if (!node.children) return;
|
|
261
|
+
const parts = [];
|
|
262
|
+
for (const child of node.children ?? []) {
|
|
263
|
+
const text = staticMdxText(child);
|
|
264
|
+
if (text === void 0) return void 0;
|
|
265
|
+
parts.push(text);
|
|
266
|
+
}
|
|
267
|
+
return parts.join("");
|
|
268
|
+
}
|
|
269
|
+
function literalMdxId(node) {
|
|
270
|
+
let result = { kind: "missing" };
|
|
271
|
+
for (const attribute of node.attributes ?? []) {
|
|
272
|
+
if (attribute.type !== "mdxJsxAttribute") {
|
|
273
|
+
result = { kind: "unknown" };
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (attribute.name !== "id") continue;
|
|
277
|
+
result = typeof attribute.value === "string" ? {
|
|
278
|
+
kind: "literal",
|
|
279
|
+
id: attribute.value
|
|
280
|
+
} : { kind: "unknown" };
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
function mdxIntrinsicHeadings(tree, content, lineOffset) {
|
|
285
|
+
const headings = [];
|
|
286
|
+
visitMdxIntrinsicHeadings(tree, (heading) => {
|
|
287
|
+
const position = heading.position;
|
|
288
|
+
const offset = position?.start.offset;
|
|
289
|
+
const end = position?.end.offset;
|
|
290
|
+
const title = staticMdxText(heading);
|
|
291
|
+
const literalId = literalMdxId(heading);
|
|
292
|
+
if (!position || offset === void 0 || end === void 0 || literalId.kind !== "literal") return;
|
|
293
|
+
const tagEnd = openingTagAttributeOffset(content, offset, end, isMdxWhitespace);
|
|
294
|
+
if (tagEnd === void 0) return;
|
|
295
|
+
headings.push({
|
|
296
|
+
...title !== void 0 ? { title } : {},
|
|
297
|
+
id: literalId.id,
|
|
298
|
+
position: {
|
|
299
|
+
line: position.start.line + lineOffset,
|
|
300
|
+
column: position.start.column
|
|
301
|
+
},
|
|
302
|
+
offset,
|
|
303
|
+
attributeOffset: tagEnd
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
return headings;
|
|
307
|
+
}
|
|
308
|
+
function escapeHtmlAttribute(value) {
|
|
309
|
+
return value.replaceAll("&", "&").replaceAll("\"", """);
|
|
310
|
+
}
|
|
311
|
+
function analyzeHeadings(content, options) {
|
|
312
|
+
const lineOffset = options.lineOffset ?? 0;
|
|
313
|
+
const headings = [];
|
|
314
|
+
const edits = [];
|
|
315
|
+
const errors = [];
|
|
316
|
+
const ids = /* @__PURE__ */ new Map();
|
|
317
|
+
const documentHeadings = [];
|
|
318
|
+
const markdownHeadingInputs = /* @__PURE__ */ new Map();
|
|
319
|
+
const tree = parseHeadingTree(content, options.kind);
|
|
320
|
+
if (!tree) return {
|
|
321
|
+
headings,
|
|
322
|
+
normalizedContent: content
|
|
323
|
+
};
|
|
324
|
+
const renderedTitles = renderedMarkdownHeadingTitles(tree, content, options.kind);
|
|
325
|
+
const claimId = (id, position) => {
|
|
326
|
+
const owner = ids.get(id);
|
|
327
|
+
if (owner) {
|
|
328
|
+
errors.push(`Heading ID "${id}" at ${formatPosition(position)} conflicts with the heading at ${formatPosition(owner.position)}.`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
ids.set(id, {
|
|
332
|
+
id,
|
|
333
|
+
position
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
visitHeadings(tree, (node) => {
|
|
337
|
+
const marker = explicitIdInfo(node, content);
|
|
338
|
+
const title = renderedTitles.get(node.position?.start.offset ?? 0) ?? "";
|
|
339
|
+
const hasValidExplicitId = marker !== void 0 && HEADING_ID_PATTERN.test(marker.id);
|
|
340
|
+
const heading = {
|
|
341
|
+
level: node.depth ?? 1,
|
|
342
|
+
title,
|
|
343
|
+
id: hasValidExplicitId ? marker.id : "",
|
|
344
|
+
position: headingPosition(node, lineOffset)
|
|
345
|
+
};
|
|
346
|
+
if (marker && !hasValidExplicitId) errors.push(`Invalid heading ID "${marker.id}" at ${formatPosition(heading.position)}. IDs must match [a-z0-9]+(?:-[a-z0-9]+)*.`);
|
|
347
|
+
headings.push(heading);
|
|
348
|
+
markdownHeadingInputs.set(heading, {
|
|
349
|
+
...marker ? { marker } : {},
|
|
350
|
+
hasValidExplicitId,
|
|
351
|
+
contentEnd: contentEndOffset(node)
|
|
352
|
+
});
|
|
353
|
+
documentHeadings.push({
|
|
354
|
+
title,
|
|
355
|
+
offset: node.position?.start.offset ?? 0,
|
|
356
|
+
markdown: heading
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
const renderedHeadings = options.kind === "markdown" ? rawHtmlHeadings(tree, content, lineOffset) : mdxIntrinsicHeadings(tree, content, lineOffset);
|
|
360
|
+
for (const rendered of renderedHeadings) documentHeadings.push({
|
|
361
|
+
...rendered.title !== void 0 ? { title: rendered.title } : {},
|
|
362
|
+
offset: rendered.offset,
|
|
363
|
+
rendered
|
|
364
|
+
});
|
|
365
|
+
documentHeadings.sort((left, right) => left.offset - right.offset);
|
|
366
|
+
const slugger = new GithubSlugger();
|
|
367
|
+
for (const documentHeading of documentHeadings) {
|
|
368
|
+
if (documentHeading.markdown) {
|
|
369
|
+
const heading = documentHeading.markdown;
|
|
370
|
+
const input = markdownHeadingInputs.get(heading);
|
|
371
|
+
if (input.hasValidExplicitId) {
|
|
372
|
+
edits.push({
|
|
373
|
+
start: input.marker.start,
|
|
374
|
+
end: input.marker.end,
|
|
375
|
+
replacement: ` [](clarify-internal-heading-id:${heading.id})`
|
|
376
|
+
});
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (documentHeading.rendered?.id !== void 0) continue;
|
|
381
|
+
if (documentHeading.title === void 0) continue;
|
|
382
|
+
const generatedId = slugger.slug(documentHeading.title);
|
|
383
|
+
if (documentHeading.markdown) {
|
|
384
|
+
const heading = documentHeading.markdown;
|
|
385
|
+
const input = markdownHeadingInputs.get(heading);
|
|
386
|
+
heading.id = generatedId;
|
|
387
|
+
const link = ` [](clarify-internal-heading-id:${heading.id})`;
|
|
388
|
+
if (input.marker) edits.push({
|
|
389
|
+
start: input.marker.start,
|
|
390
|
+
end: input.marker.end,
|
|
391
|
+
replacement: link
|
|
392
|
+
});
|
|
393
|
+
else if (input.contentEnd !== void 0) edits.push({
|
|
394
|
+
start: input.contentEnd,
|
|
395
|
+
end: input.contentEnd,
|
|
396
|
+
replacement: link
|
|
397
|
+
});
|
|
398
|
+
} else if (documentHeading.rendered && documentHeading.rendered.id === void 0) {
|
|
399
|
+
documentHeading.rendered.id = generatedId;
|
|
400
|
+
edits.push({
|
|
401
|
+
start: documentHeading.rendered.attributeOffset,
|
|
402
|
+
end: documentHeading.rendered.attributeOffset,
|
|
403
|
+
replacement: ` id="${escapeHtmlAttribute(generatedId)}"`
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
for (const documentHeading of documentHeadings) if (documentHeading.markdown) claimId(documentHeading.markdown.id, documentHeading.markdown.position);
|
|
408
|
+
else if (documentHeading.rendered?.id) claimId(documentHeading.rendered.id, documentHeading.rendered.position);
|
|
409
|
+
const diagnostic = errors.length === 0 ? void 0 : createContentDiagnostic({
|
|
410
|
+
kind: options.kind,
|
|
411
|
+
title: "Heading ID error",
|
|
412
|
+
message: "This page has invalid or conflicting heading IDs. Fix the IDs below, then reload the route.",
|
|
413
|
+
error: new Error(errors.join("\n")),
|
|
414
|
+
filePath: options.filePath,
|
|
415
|
+
projectRoot: options.projectRoot
|
|
416
|
+
});
|
|
417
|
+
return {
|
|
418
|
+
headings,
|
|
419
|
+
normalizedContent: applySourceEdits(content, edits),
|
|
420
|
+
diagnostic
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function remarkApplyHeadingIds() {
|
|
424
|
+
return (tree) => {
|
|
425
|
+
visitHeadings(tree, (heading) => {
|
|
426
|
+
const finalNode = heading.children?.at(-1);
|
|
427
|
+
if (finalNode?.type !== "link" || finalNode.children?.length !== 0 || typeof finalNode.url !== "string" || !finalNode.url.startsWith(INTERNAL_HEADING_ID_PREFIX)) return;
|
|
428
|
+
const id = finalNode.url.slice(28);
|
|
429
|
+
heading.children?.pop();
|
|
430
|
+
const previousNode = heading.children?.at(-1);
|
|
431
|
+
if (previousNode?.type === "text" && typeof previousNode.value === "string") previousNode.value = previousNode.value.replace(/ $/, "");
|
|
432
|
+
heading.data = heading.data ?? {};
|
|
433
|
+
heading.data.hProperties = {
|
|
434
|
+
...heading.data.hProperties,
|
|
435
|
+
id
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
//#endregion
|
|
48
441
|
//#region source/parsers/markdown/mdx.ts
|
|
49
442
|
function getLanguage(className) {
|
|
50
443
|
return (Array.isArray(className) ? className : typeof className === "string" ? className.split(/\s+/) : []).find((item) => typeof item === "string" && item.startsWith("language-"))?.replace(/^language-/, "") || "txt";
|
|
@@ -67,9 +460,34 @@ function rehypeParseCodeBlocks() {
|
|
|
67
460
|
});
|
|
68
461
|
};
|
|
69
462
|
}
|
|
70
|
-
|
|
463
|
+
function hastText(node) {
|
|
464
|
+
if (node.type === "text") return node.value ?? "";
|
|
465
|
+
return (node.children ?? []).map(hastText).join("");
|
|
466
|
+
}
|
|
467
|
+
function isHeadingElement(node) {
|
|
468
|
+
return Boolean(node.tagName && /^h[1-6]$/.test(node.tagName));
|
|
469
|
+
}
|
|
470
|
+
function rehypeAddMissingHeadingIds() {
|
|
471
|
+
return (tree) => {
|
|
472
|
+
const slugger = new GithubSlugger();
|
|
473
|
+
const reservedIds = /* @__PURE__ */ new Set();
|
|
474
|
+
visit(tree, "element", (node) => {
|
|
475
|
+
if (isHeadingElement(node) && typeof node.properties?.id === "string" && node.properties.id) reservedIds.add(node.properties.id);
|
|
476
|
+
});
|
|
477
|
+
visit(tree, "element", (node) => {
|
|
478
|
+
if (!isHeadingElement(node)) return;
|
|
479
|
+
const title = hastText(node);
|
|
480
|
+
let fallbackId = slugger.slug(title);
|
|
481
|
+
if (node.properties?.id) return;
|
|
482
|
+
while (reservedIds.has(fallbackId)) fallbackId = slugger.slug(title);
|
|
483
|
+
node.properties = node.properties ?? {};
|
|
484
|
+
node.properties.id = fallbackId;
|
|
485
|
+
});
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
var remarkPlugins = [remarkApplyHeadingIds, ...markdownRemarkPlugins];
|
|
71
489
|
var rehypePlugins = [
|
|
72
|
-
|
|
490
|
+
rehypeAddMissingHeadingIds,
|
|
73
491
|
rehypeParseCodeBlocks,
|
|
74
492
|
[rehypeShiki, {
|
|
75
493
|
theme: cssVariablesTheme,
|
|
@@ -224,9 +642,12 @@ async function runBuildDoneHooks(plugins, ctx) {
|
|
|
224
642
|
*/
|
|
225
643
|
function parseFrontmatter(content) {
|
|
226
644
|
const parsed = matter(content);
|
|
645
|
+
const body = parsed.content.replace(/^\r?\n/, "");
|
|
646
|
+
const prefix = content.slice(0, content.length - body.length);
|
|
227
647
|
return {
|
|
228
648
|
frontmatter: parsed.data,
|
|
229
|
-
content:
|
|
649
|
+
content: body,
|
|
650
|
+
lineOffset: [...prefix.matchAll(/\r\n|\r|\n/g)].length
|
|
230
651
|
};
|
|
231
652
|
}
|
|
232
653
|
//#endregion
|
|
@@ -237,7 +658,7 @@ async function transformContent(transform, input) {
|
|
|
237
658
|
function createContentProcessor(transform) {
|
|
238
659
|
return {
|
|
239
660
|
async processMdx(source, filePath) {
|
|
240
|
-
const { frontmatter, content } = parseFrontmatter(source);
|
|
661
|
+
const { frontmatter, content, lineOffset } = parseFrontmatter(source);
|
|
241
662
|
const transformed = await transformContent(transform, {
|
|
242
663
|
kind: "markdown+jsx",
|
|
243
664
|
source,
|
|
@@ -247,7 +668,8 @@ function createContentProcessor(transform) {
|
|
|
247
668
|
});
|
|
248
669
|
return {
|
|
249
670
|
frontmatter: transformed.frontmatter,
|
|
250
|
-
content: transformed.content
|
|
671
|
+
content: transformed.content,
|
|
672
|
+
lineOffset
|
|
251
673
|
};
|
|
252
674
|
},
|
|
253
675
|
async processText(source, kind, filePath) {
|
|
@@ -357,13 +779,47 @@ function resolveUpdatedAt(frontmatter, detectedAt) {
|
|
|
357
779
|
return normalizeDate(typeof frontmatter.updatedAt === "string" ? frontmatter.updatedAt : void 0) ?? detectedAt;
|
|
358
780
|
}
|
|
359
781
|
//#endregion
|
|
360
|
-
//#region source/parsers/routes/
|
|
782
|
+
//#region source/parsers/routes/heading-metadata.ts
|
|
361
783
|
function kebabToTitle(str) {
|
|
362
|
-
return str.split("-").map((
|
|
363
|
-
}
|
|
364
|
-
function
|
|
365
|
-
return
|
|
784
|
+
return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
785
|
+
}
|
|
786
|
+
function headingMetadata(analysis) {
|
|
787
|
+
return { sections: analysis.headings.filter((heading) => heading.level === 2 || heading.level === 3).map((heading) => ({
|
|
788
|
+
id: heading.id,
|
|
789
|
+
title: heading.title,
|
|
790
|
+
level: heading.level
|
|
791
|
+
})) };
|
|
792
|
+
}
|
|
793
|
+
function resolvedRouteTitle(route, analysis) {
|
|
794
|
+
const frontmatterTitle = route.source.frontmatter?.title;
|
|
795
|
+
if (typeof frontmatterTitle === "string" && frontmatterTitle) return frontmatterTitle;
|
|
796
|
+
const routePath = route.basePath ?? route.path;
|
|
797
|
+
return kebabToTitle(routePath === "/" ? analysis.headings.find((heading) => heading.level === 1)?.title ?? "" : routePath.split("/").filter(Boolean).at(-1) ?? "") || "Untitled";
|
|
798
|
+
}
|
|
799
|
+
async function reanalyzeMarkdownRoute(route, projectRoot) {
|
|
800
|
+
if (route.kind !== "markdown" && route.kind !== "markdown+jsx") return route;
|
|
801
|
+
const analysis = analyzeHeadings(route.source.content ?? "", {
|
|
802
|
+
kind: route.kind,
|
|
803
|
+
filePath: route.source.filePath,
|
|
804
|
+
projectRoot,
|
|
805
|
+
lineOffset: route.source.lineOffset
|
|
806
|
+
});
|
|
807
|
+
const result = await (route.kind === "markdown" ? compileMarkdownContent : compileMdxContent)(analysis.normalizedContent, {
|
|
808
|
+
filePath: route.source.filePath,
|
|
809
|
+
projectRoot
|
|
810
|
+
});
|
|
811
|
+
return {
|
|
812
|
+
...route,
|
|
813
|
+
meta: {
|
|
814
|
+
...route.meta,
|
|
815
|
+
title: resolvedRouteTitle(route, analysis),
|
|
816
|
+
...headingMetadata(analysis)
|
|
817
|
+
},
|
|
818
|
+
diagnostic: analysis.diagnostic ?? (result.ok ? void 0 : result.diagnostic)
|
|
819
|
+
};
|
|
366
820
|
}
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region source/parsers/routes/routes.ts
|
|
367
823
|
async function compileRouteDiagnostic(content, filePath, baseDir) {
|
|
368
824
|
const result = await (extname(filePath).toLowerCase() === ".md" ? compileMarkdownContent : compileMdxContent)(content, {
|
|
369
825
|
filePath,
|
|
@@ -371,30 +827,6 @@ async function compileRouteDiagnostic(content, filePath, baseDir) {
|
|
|
371
827
|
});
|
|
372
828
|
return result.ok ? void 0 : result.diagnostic;
|
|
373
829
|
}
|
|
374
|
-
/** 从内容中提取第一个 H1 标题 */
|
|
375
|
-
function extractH1(content) {
|
|
376
|
-
const tree = parseMdxTree(content);
|
|
377
|
-
let title = "";
|
|
378
|
-
visit(tree, "heading", (node) => {
|
|
379
|
-
if (!title && node.depth === 1) title = toString(node);
|
|
380
|
-
});
|
|
381
|
-
return title;
|
|
382
|
-
}
|
|
383
|
-
/** 从 MDX/Markdown 内容中提取 H2/H3 章节 */
|
|
384
|
-
function extractMdxSections(content) {
|
|
385
|
-
const sections = [];
|
|
386
|
-
const slugger = new GithubSlugger();
|
|
387
|
-
visit(parseMdxTree(content), "heading", (node) => {
|
|
388
|
-
if (node.depth !== 2 && node.depth !== 3) return;
|
|
389
|
-
const title = toString(node);
|
|
390
|
-
sections.push({
|
|
391
|
-
id: slugger.slug(title),
|
|
392
|
-
title,
|
|
393
|
-
level: node.depth
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
return sections;
|
|
397
|
-
}
|
|
398
830
|
function navigationSections(sections) {
|
|
399
831
|
return sections.map((section) => ({
|
|
400
832
|
id: section.id,
|
|
@@ -451,11 +883,12 @@ async function findContentRoutes(dir, base = dir, options = {}) {
|
|
|
451
883
|
const fullPath = join(dir, entry.name);
|
|
452
884
|
if (entry.isDirectory()) routes.push(...await findContentRoutes(fullPath, base, options));
|
|
453
885
|
else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
|
|
886
|
+
const kind = entry.name.toLowerCase().endsWith(".mdx") ? "markdown+jsx" : "markdown";
|
|
454
887
|
const relativePath = relative(base, fullPath);
|
|
455
888
|
const pathParts = relativePath.replace(/\.mdx?$/, "").split("/");
|
|
456
889
|
const cleanPath = ("/" + pathParts.map((p) => p === "index" ? "" : p).filter(Boolean).join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
457
890
|
const source = await readFile(fullPath, "utf-8");
|
|
458
|
-
const { frontmatter, content } = await (options.contentProcessor ?? createContentProcessor()).processMdx(source, fullPath);
|
|
891
|
+
const { frontmatter, content, lineOffset } = await (options.contentProcessor ?? createContentProcessor()).processMdx(source, fullPath);
|
|
459
892
|
const updatedAt = resolveUpdatedAt(frontmatter, await getFileUpdatedAt(fullPath, base));
|
|
460
893
|
const page = {
|
|
461
894
|
path: cleanPath,
|
|
@@ -463,14 +896,21 @@ async function findContentRoutes(dir, base = dir, options = {}) {
|
|
|
463
896
|
frontmatter,
|
|
464
897
|
content
|
|
465
898
|
};
|
|
466
|
-
const
|
|
899
|
+
const analysis = analyzeHeadings(page.content, {
|
|
900
|
+
kind,
|
|
901
|
+
filePath: fullPath,
|
|
902
|
+
projectRoot: base,
|
|
903
|
+
lineOffset
|
|
904
|
+
});
|
|
905
|
+
const compileDiagnostic = await compileRouteDiagnostic(analysis.normalizedContent, fullPath, base);
|
|
906
|
+
const { sections } = headingMetadata(analysis);
|
|
467
907
|
let title = typeof page.frontmatter.title === "string" ? page.frontmatter.title : "";
|
|
468
908
|
if (!title) {
|
|
469
909
|
const lastPart = pathParts[pathParts.length - 1] ?? "";
|
|
470
|
-
title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] :
|
|
910
|
+
title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : analysis.headings.find((heading) => heading.level === 1)?.title ?? "" : lastPart) || "Untitled";
|
|
471
911
|
}
|
|
472
912
|
routes.push({
|
|
473
|
-
kind
|
|
913
|
+
kind,
|
|
474
914
|
path: cleanPath,
|
|
475
915
|
basePath: cleanPath,
|
|
476
916
|
meta: {
|
|
@@ -480,7 +920,7 @@ async function findContentRoutes(dir, base = dir, options = {}) {
|
|
|
480
920
|
group: typeof page.frontmatter.group === "string" ? page.frontmatter.group : void 0,
|
|
481
921
|
layout: page.frontmatter.layout === "documentation" || page.frontmatter.layout === "blog" ? page.frontmatter.layout : void 0,
|
|
482
922
|
updatedAt,
|
|
483
|
-
sections
|
|
923
|
+
sections
|
|
484
924
|
},
|
|
485
925
|
module: {
|
|
486
926
|
pageVirtualModuleId: pageVirtualModuleId(relativePath.replace(/\.mdx?$/, "")),
|
|
@@ -489,9 +929,10 @@ async function findContentRoutes(dir, base = dir, options = {}) {
|
|
|
489
929
|
source: {
|
|
490
930
|
filePath: fullPath,
|
|
491
931
|
frontmatter: page.frontmatter,
|
|
492
|
-
content: page.content
|
|
932
|
+
content: page.content,
|
|
933
|
+
lineOffset
|
|
493
934
|
},
|
|
494
|
-
diagnostic
|
|
935
|
+
diagnostic: analysis.diagnostic ?? compileDiagnostic
|
|
495
936
|
});
|
|
496
937
|
}
|
|
497
938
|
}
|
|
@@ -1597,10 +2038,14 @@ function createNormalizedContentPlugin(engine) {
|
|
|
1597
2038
|
transform(_code, id) {
|
|
1598
2039
|
if (!/\.mdx?(?:\?|$)/.test(id)) return null;
|
|
1599
2040
|
const filePath = id.replace(/\?.*$/, "");
|
|
1600
|
-
const route = engine.routes.find((route) =>
|
|
1601
|
-
if (!route || route.source.content === void 0) return null;
|
|
2041
|
+
const route = engine.routes.find((route) => route.source.filePath === filePath);
|
|
2042
|
+
if (!route || route.kind !== "markdown+jsx" && route.kind !== "markdown" || route.source.content === void 0) return null;
|
|
1602
2043
|
return {
|
|
1603
|
-
code: route.source.content,
|
|
2044
|
+
code: analyzeHeadings(route.source.content, {
|
|
2045
|
+
kind: route.kind,
|
|
2046
|
+
filePath,
|
|
2047
|
+
projectRoot: engine.root
|
|
2048
|
+
}).normalizedContent,
|
|
1604
2049
|
map: null
|
|
1605
2050
|
};
|
|
1606
2051
|
}
|
|
@@ -1790,7 +2235,7 @@ function createViteAdapter(engine) {
|
|
|
1790
2235
|
}
|
|
1791
2236
|
var cliPackageVersion = {
|
|
1792
2237
|
name: "@clarify-labs/cli",
|
|
1793
|
-
version: "0.11.
|
|
2238
|
+
version: "0.11.21",
|
|
1794
2239
|
license: "AGPL-3.0-only",
|
|
1795
2240
|
type: "module",
|
|
1796
2241
|
repository: {
|
|
@@ -1993,6 +2438,191 @@ function createLlmsTxtArtifact(routes, projectConfig) {
|
|
|
1993
2438
|
return withUtf8Signature(createLlmsTxt(routes, projectConfig));
|
|
1994
2439
|
}
|
|
1995
2440
|
//#endregion
|
|
2441
|
+
//#region source/plugins/openapi/aggregation.ts
|
|
2442
|
+
function mergeOpenAPIRecord(target, source, section, filePath) {
|
|
2443
|
+
if (!source) return;
|
|
2444
|
+
for (const [key, value] of Object.entries(source)) {
|
|
2445
|
+
const existing = target[key];
|
|
2446
|
+
if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(value)) throw new Error(`[clarify] Cannot aggregate OpenAPI specs: conflicting ${section} entry "${key}" in ${filePath}.`);
|
|
2447
|
+
target[key] = value;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
function openAPIVersionFamily(version) {
|
|
2451
|
+
return version.split(".").slice(0, 2).join(".");
|
|
2452
|
+
}
|
|
2453
|
+
var OPENAPI_OPERATION_METHODS = /* @__PURE__ */ new Set([
|
|
2454
|
+
"get",
|
|
2455
|
+
"put",
|
|
2456
|
+
"post",
|
|
2457
|
+
"delete",
|
|
2458
|
+
"options",
|
|
2459
|
+
"head",
|
|
2460
|
+
"patch",
|
|
2461
|
+
"trace"
|
|
2462
|
+
]);
|
|
2463
|
+
function sourceComponentPrefix(source) {
|
|
2464
|
+
return (source.id || source.filePath.split(/[\\/]/).pop()?.replace(/\.openapi\.(?:json|ya?ml)$/i, "") || "service").replace(/[^a-zA-Z0-9._-]/g, "_") || "service";
|
|
2465
|
+
}
|
|
2466
|
+
function securitySchemeNameMap(source, sourceSchemes, targetSchemes) {
|
|
2467
|
+
const names = /* @__PURE__ */ new Map();
|
|
2468
|
+
for (const [name, definition] of Object.entries(sourceSchemes)) {
|
|
2469
|
+
const existing = targetSchemes[name];
|
|
2470
|
+
if (existing === void 0 || JSON.stringify(existing) === JSON.stringify(definition)) {
|
|
2471
|
+
targetSchemes[name] = definition;
|
|
2472
|
+
names.set(name, name);
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
const prefix = sourceComponentPrefix(source);
|
|
2476
|
+
let candidate = `${prefix}__${name}`;
|
|
2477
|
+
let suffix = 2;
|
|
2478
|
+
while (targetSchemes[candidate] !== void 0 && JSON.stringify(targetSchemes[candidate]) !== JSON.stringify(definition)) candidate = `${prefix}__${name}_${suffix++}`;
|
|
2479
|
+
targetSchemes[candidate] = definition;
|
|
2480
|
+
names.set(name, candidate);
|
|
2481
|
+
}
|
|
2482
|
+
return names;
|
|
2483
|
+
}
|
|
2484
|
+
function rewriteSecurityRequirements(value, schemeNames) {
|
|
2485
|
+
if (!Array.isArray(value)) return value;
|
|
2486
|
+
return value.map((requirement) => {
|
|
2487
|
+
if (!requirement || typeof requirement !== "object" || Array.isArray(requirement)) return requirement;
|
|
2488
|
+
return Object.fromEntries(Object.entries(requirement).map(([name, scopes]) => [schemeNames.get(name) ?? name, scopes]));
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
function localizePathItemContext(pathItemValue, documentServers, documentSecurity, hasDocumentSecurity, schemeNames) {
|
|
2492
|
+
if (!pathItemValue || typeof pathItemValue !== "object" || Array.isArray(pathItemValue)) return pathItemValue;
|
|
2493
|
+
const pathItem = structuredClone(pathItemValue);
|
|
2494
|
+
const pathServers = Object.hasOwn(pathItem, "servers") ? pathItem.servers : documentServers;
|
|
2495
|
+
for (const [method, operationValue] of Object.entries(pathItem)) {
|
|
2496
|
+
if (!OPENAPI_OPERATION_METHODS.has(method) || !operationValue || typeof operationValue !== "object" || Array.isArray(operationValue)) continue;
|
|
2497
|
+
const operation = operationValue;
|
|
2498
|
+
if (!Object.hasOwn(operation, "servers") && pathServers !== void 0) operation.servers = structuredClone(pathServers);
|
|
2499
|
+
if (Object.hasOwn(operation, "security")) operation.security = rewriteSecurityRequirements(operation.security, schemeNames);
|
|
2500
|
+
else if (hasDocumentSecurity) operation.security = rewriteSecurityRequirements(structuredClone(documentSecurity), schemeNames);
|
|
2501
|
+
}
|
|
2502
|
+
delete pathItem.servers;
|
|
2503
|
+
return pathItem;
|
|
2504
|
+
}
|
|
2505
|
+
function localizeOpenAPIContexts(spec, schemeNames) {
|
|
2506
|
+
const document = spec;
|
|
2507
|
+
const hasDocumentSecurity = Object.hasOwn(document, "security");
|
|
2508
|
+
const localizeEntries = (entries) => entries && Object.fromEntries(Object.entries(entries).map(([name, pathItem]) => [name, localizePathItemContext(pathItem, document.servers, document.security, hasDocumentSecurity, schemeNames)]));
|
|
2509
|
+
return {
|
|
2510
|
+
paths: localizeEntries(spec.paths) ?? {},
|
|
2511
|
+
webhooks: localizeEntries("webhooks" in spec ? spec.webhooks : void 0)
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
function aggregateOpenAPISources(sources, info) {
|
|
2515
|
+
const paths = {};
|
|
2516
|
+
const webhooks = {};
|
|
2517
|
+
const components = {};
|
|
2518
|
+
const tags = /* @__PURE__ */ new Map();
|
|
2519
|
+
let openapiVersion;
|
|
2520
|
+
for (const source of sources) {
|
|
2521
|
+
const { document: spec, filePath } = source;
|
|
2522
|
+
if (openapiVersion && openAPIVersionFamily(openapiVersion) !== openAPIVersionFamily(spec.openapi)) throw new Error(`[clarify] Cannot aggregate OpenAPI specs: incompatible versions "${openapiVersion}" and "${spec.openapi}" in ${filePath}.`);
|
|
2523
|
+
openapiVersion ??= spec.openapi;
|
|
2524
|
+
components.securitySchemes ??= {};
|
|
2525
|
+
const sourceComponents = spec.components;
|
|
2526
|
+
const localized = localizeOpenAPIContexts(spec, securitySchemeNameMap(source, sourceComponents?.securitySchemes ?? {}, components.securitySchemes));
|
|
2527
|
+
mergeOpenAPIRecord(paths, localized.paths, "paths", filePath);
|
|
2528
|
+
mergeOpenAPIRecord(webhooks, localized.webhooks, "webhooks", filePath);
|
|
2529
|
+
for (const [section, entries] of Object.entries(sourceComponents ?? {})) {
|
|
2530
|
+
if (section === "securitySchemes") continue;
|
|
2531
|
+
components[section] ??= {};
|
|
2532
|
+
mergeOpenAPIRecord(components[section], entries, `components.${section}`, filePath);
|
|
2533
|
+
}
|
|
2534
|
+
for (const tag of spec.tags ?? []) {
|
|
2535
|
+
const existing = tags.get(tag.name);
|
|
2536
|
+
if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(tag)) throw new Error(`[clarify] Cannot aggregate OpenAPI specs: conflicting tag "${tag.name}" in ${filePath}.`);
|
|
2537
|
+
tags.set(tag.name, tag);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
if (components.securitySchemes && Object.keys(components.securitySchemes).length === 0) delete components.securitySchemes;
|
|
2541
|
+
return {
|
|
2542
|
+
openapi: openapiVersion ?? "3.1.0",
|
|
2543
|
+
info: {
|
|
2544
|
+
title: info.title,
|
|
2545
|
+
description: info.description,
|
|
2546
|
+
version: info.version ?? "1.0.0"
|
|
2547
|
+
},
|
|
2548
|
+
paths,
|
|
2549
|
+
...Object.keys(webhooks).length > 0 ? { webhooks } : {},
|
|
2550
|
+
...Object.keys(components).length > 0 ? { components } : {},
|
|
2551
|
+
...tags.size > 0 ? { tags: [...tags.values()] } : {}
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
//#endregion
|
|
2555
|
+
//#region source/plugins/openapi/artifacts.ts
|
|
2556
|
+
function selectRootOpenAPISources(routes, projectConfig) {
|
|
2557
|
+
const defaultLocale = projectConfig.locales?.default;
|
|
2558
|
+
const sourcesByFilePath = /* @__PURE__ */ new Map();
|
|
2559
|
+
for (const route of routes) {
|
|
2560
|
+
if (route.kind !== "openapi" || route.diagnostic || !route.source.content || route.openapi?.tagFilter?.length || route.isBareAlias || defaultLocale && route.locale && route.locale !== defaultLocale) continue;
|
|
2561
|
+
sourcesByFilePath.set(route.source.filePath, {
|
|
2562
|
+
id: route.openapi?.sourceSpecId ?? route.source.filePath.split(/[\\/]/).pop()?.replace(/\.openapi\.(?:json|ya?ml)$/i, "") ?? "service",
|
|
2563
|
+
filePath: route.source.filePath,
|
|
2564
|
+
document: JSON.parse(route.source.content)
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
return [...sourcesByFilePath.values()];
|
|
2568
|
+
}
|
|
2569
|
+
function createRootOpenAPISpec(routes, projectConfig) {
|
|
2570
|
+
return aggregateOpenAPISources(selectRootOpenAPISources(routes, projectConfig), {
|
|
2571
|
+
title: projectConfig.title,
|
|
2572
|
+
description: projectConfig.description
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
function createOpenAPIRouteArtifacts(route) {
|
|
2576
|
+
if (route.kind !== "openapi" || !route.source.content) return [];
|
|
2577
|
+
const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
|
|
2578
|
+
if (!contentArtifactUrl) return [];
|
|
2579
|
+
return [{
|
|
2580
|
+
fileName: contentArtifactUrl.replace(/^\//, ""),
|
|
2581
|
+
contentType: "application/json; charset=utf-8",
|
|
2582
|
+
source: route.source.content
|
|
2583
|
+
}, {
|
|
2584
|
+
fileName: contentArtifactUrl.replace(/\.json$/, ".yaml").replace(/^\//, ""),
|
|
2585
|
+
contentType: "text/yaml; charset=utf-8",
|
|
2586
|
+
source: stringify(JSON.parse(route.source.content), { lineWidth: 0 })
|
|
2587
|
+
}];
|
|
2588
|
+
}
|
|
2589
|
+
function createRootOpenAPIArtifact(routes, projectConfig) {
|
|
2590
|
+
return {
|
|
2591
|
+
fileName: "openapi.json",
|
|
2592
|
+
contentType: "application/json; charset=utf-8",
|
|
2593
|
+
source: JSON.stringify(createRootOpenAPISpec(routes, projectConfig), null, 2)
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
//#endregion
|
|
2597
|
+
//#region source/plugins/content-artifacts/provider.ts
|
|
2598
|
+
function createContentArtifacts(routes, projectConfig) {
|
|
2599
|
+
const artifacts = [];
|
|
2600
|
+
for (const route of routes) {
|
|
2601
|
+
if (route.kind === "openapi") {
|
|
2602
|
+
artifacts.push(...createOpenAPIRouteArtifacts(route));
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2605
|
+
const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
|
|
2606
|
+
if (!contentArtifactUrl) continue;
|
|
2607
|
+
artifacts.push({
|
|
2608
|
+
fileName: contentArtifactUrl.replace(/^\//, ""),
|
|
2609
|
+
contentType: "text/markdown; charset=utf-8",
|
|
2610
|
+
source: readRouteArtifactContent(route)
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
artifacts.push({
|
|
2614
|
+
fileName: "llms.txt",
|
|
2615
|
+
contentType: "text/plain; charset=utf-8",
|
|
2616
|
+
source: createLlmsTxtArtifact(routes, projectConfig)
|
|
2617
|
+
});
|
|
2618
|
+
artifacts.push(createRootOpenAPIArtifact(routes, projectConfig));
|
|
2619
|
+
return artifacts;
|
|
2620
|
+
}
|
|
2621
|
+
function findContentArtifact(path, routes, projectConfig) {
|
|
2622
|
+
const fileName = path.replace(/^\/+/, "");
|
|
2623
|
+
return createContentArtifacts(routes, projectConfig).find((artifact) => artifact.fileName === fileName);
|
|
2624
|
+
}
|
|
2625
|
+
//#endregion
|
|
1996
2626
|
//#region source/plugins/content-artifacts/server.ts
|
|
1997
2627
|
function normalizeRoutePrefix$1(routePrefix) {
|
|
1998
2628
|
if (!routePrefix || routePrefix === "/") return "";
|
|
@@ -2004,34 +2634,12 @@ function resolveContentArtifactPath(url, projectConfig) {
|
|
|
2004
2634
|
const hasBasePath = requestPath === basePath || requestPath.startsWith(`${basePath}/`);
|
|
2005
2635
|
return basePath && hasBasePath ? requestPath.slice(basePath.length) || "/" : requestPath;
|
|
2006
2636
|
}
|
|
2007
|
-
function resolveContentArtifactType(route) {
|
|
2008
|
-
return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
|
|
2009
|
-
}
|
|
2010
2637
|
function serveContentArtifacts(req, res, projectConfig, routes) {
|
|
2011
|
-
const
|
|
2012
|
-
if (
|
|
2013
|
-
res.statusCode = 200;
|
|
2014
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
2015
|
-
res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
|
|
2016
|
-
return true;
|
|
2017
|
-
}
|
|
2018
|
-
if (contentPath.endsWith(".openapi.yaml")) {
|
|
2019
|
-
const jsonPath = contentPath.replace(/\.yaml$/, ".json");
|
|
2020
|
-
const route = routes.find((route) => route.artifacts?.contentArtifactUrl === jsonPath);
|
|
2021
|
-
if (route?.kind === "openapi" && route.source.content) {
|
|
2022
|
-
const spec = JSON.parse(route.source.content);
|
|
2023
|
-
res.statusCode = 200;
|
|
2024
|
-
res.setHeader("Content-Type", "text/yaml; charset=utf-8");
|
|
2025
|
-
res.end(stringify(spec, { lineWidth: 0 }), "utf8");
|
|
2026
|
-
return true;
|
|
2027
|
-
}
|
|
2028
|
-
return false;
|
|
2029
|
-
}
|
|
2030
|
-
const route = routes.find((route) => route.artifacts?.contentArtifactUrl === contentPath);
|
|
2031
|
-
if (!route) return false;
|
|
2638
|
+
const artifact = findContentArtifact(resolveContentArtifactPath(req.url, projectConfig), routes, projectConfig);
|
|
2639
|
+
if (!artifact) return false;
|
|
2032
2640
|
res.statusCode = 200;
|
|
2033
|
-
res.setHeader("Content-Type",
|
|
2034
|
-
res.end(
|
|
2641
|
+
res.setHeader("Content-Type", artifact.contentType);
|
|
2642
|
+
res.end(artifact.source);
|
|
2035
2643
|
return true;
|
|
2036
2644
|
}
|
|
2037
2645
|
//#endregion
|
|
@@ -2052,30 +2660,7 @@ function createContentArtifactsPlugin() {
|
|
|
2052
2660
|
});
|
|
2053
2661
|
};
|
|
2054
2662
|
},
|
|
2055
|
-
"build:assets": (ctx) =>
|
|
2056
|
-
const assets = [];
|
|
2057
|
-
for (const route of ctx.routes) {
|
|
2058
|
-
const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
|
|
2059
|
-
if (!contentArtifactUrl) continue;
|
|
2060
|
-
assets.push({
|
|
2061
|
-
fileName: contentArtifactUrl.replace(/^\//, ""),
|
|
2062
|
-
source: readRouteArtifactContent(route)
|
|
2063
|
-
});
|
|
2064
|
-
if (route.kind === "openapi" && route.source.content) {
|
|
2065
|
-
const yamlFileName = contentArtifactUrl.replace(/\.json$/, ".yaml");
|
|
2066
|
-
const spec = JSON.parse(route.source.content);
|
|
2067
|
-
assets.push({
|
|
2068
|
-
fileName: yamlFileName.replace(/^\//, ""),
|
|
2069
|
-
source: stringify(spec, { lineWidth: 0 })
|
|
2070
|
-
});
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
assets.push({
|
|
2074
|
-
fileName: "llms.txt",
|
|
2075
|
-
source: createLlmsTxtArtifact(ctx.routes, ctx.projectConfig)
|
|
2076
|
-
});
|
|
2077
|
-
return assets;
|
|
2078
|
-
}
|
|
2663
|
+
"build:assets": (ctx) => createContentArtifacts(ctx.routes, ctx.projectConfig)
|
|
2079
2664
|
}
|
|
2080
2665
|
};
|
|
2081
2666
|
}
|
|
@@ -3416,7 +4001,7 @@ async function resolveRoutePages(routes, plugins, ctx) {
|
|
|
3416
4001
|
pageByRouteIndex.set(routeIndex, page);
|
|
3417
4002
|
}
|
|
3418
4003
|
if (pageByRouteIndex.size !== routes.length) throw new Error("[clarify] pages:resolved hooks must return exactly one page for each route");
|
|
3419
|
-
|
|
4004
|
+
const updatedRoutes = routes.map((route, routeIndex) => {
|
|
3420
4005
|
const page = pageByRouteIndex.get(routeIndex);
|
|
3421
4006
|
return {
|
|
3422
4007
|
...route,
|
|
@@ -3427,6 +4012,7 @@ async function resolveRoutePages(routes, plugins, ctx) {
|
|
|
3427
4012
|
}
|
|
3428
4013
|
};
|
|
3429
4014
|
});
|
|
4015
|
+
return Promise.all(updatedRoutes.map((route) => reanalyzeMarkdownRoute(route, ctx.projectRoot)));
|
|
3430
4016
|
}
|
|
3431
4017
|
function buildNavigationTree(routes, ctx) {
|
|
3432
4018
|
const tabs = ctx.projectConfig.navigation?.tabs;
|
|
@@ -3779,6 +4365,20 @@ async function buildSSRBundle(root, ssrEntry, ssrOutDir, plugins) {
|
|
|
3779
4365
|
}
|
|
3780
4366
|
});
|
|
3781
4367
|
}
|
|
4368
|
+
function resolveSSRBundlePath(ssrOutputDir) {
|
|
4369
|
+
for (const entryName of [
|
|
4370
|
+
"entry-server.js",
|
|
4371
|
+
"entry-server.mjs",
|
|
4372
|
+
"entry-server.cjs"
|
|
4373
|
+
]) {
|
|
4374
|
+
const candidate = join(ssrOutputDir, entryName);
|
|
4375
|
+
if (existsSync(candidate)) return candidate;
|
|
4376
|
+
}
|
|
4377
|
+
const fallback = readdirSync(ssrOutputDir).filter((name) => name.startsWith("entry-server.")).sort((a, b) => a.localeCompare(b))[0];
|
|
4378
|
+
if (fallback) return join(ssrOutputDir, fallback);
|
|
4379
|
+
const files = readdirSync(ssrOutputDir).sort((a, b) => a.localeCompare(b)).slice(0, 20);
|
|
4380
|
+
throw new Error(`[clarify] SSR entry bundle not found in "${ssrOutputDir}". Tried entry-server.js, entry-server.mjs, entry-server.cjs. Found files: ${files.length > 0 ? files.join(", ") : "(empty directory)"}`);
|
|
4381
|
+
}
|
|
3782
4382
|
async function renderSSGRoutes(routes, contextOrConfig, outputDirectory, ssrBundlePath) {
|
|
3783
4383
|
const { render } = await import(pathToFileURL(ssrBundlePath).href);
|
|
3784
4384
|
const template = readIndexHtml(outputDirectory);
|
|
@@ -4024,8 +4624,9 @@ var ClarifyEngine = class {
|
|
|
4024
4624
|
* - `skipModules`: skip `buildModules()` (check command).
|
|
4025
4625
|
* - `skipHints`: skip `logStartupHints()` (check command).
|
|
4026
4626
|
*
|
|
4027
|
-
*
|
|
4028
|
-
*
|
|
4627
|
+
* Configuration initialization is cached unless `force` is set. Discovery
|
|
4628
|
+
* and virtual module generation intentionally run on every call so callers
|
|
4629
|
+
* observe content changes without rebuilding the Engine instance.
|
|
4029
4630
|
*/
|
|
4030
4631
|
async prepare(env = {
|
|
4031
4632
|
command: this.runtime.command,
|
|
@@ -4145,25 +4746,25 @@ var ClarifyEngine = class {
|
|
|
4145
4746
|
assertNoContentDiagnostics(this.routes);
|
|
4146
4747
|
const outputDir = this.runtime.outputDirectory ?? this.generateOptions.outputDirectory;
|
|
4147
4748
|
if (!outputDir) throw new Error("[clarify] outputDirectory is required before SSG runs");
|
|
4148
|
-
|
|
4749
|
+
let ssrOutputDir;
|
|
4149
4750
|
let tempEntryPath;
|
|
4150
4751
|
try {
|
|
4151
4752
|
tempEntryPath = createTempEntryFile(SSR_ENTRY_CODE);
|
|
4753
|
+
ssrOutputDir = createClarifyTempDir("ssr-output");
|
|
4152
4754
|
await (this.runtime.buildSSRBundle ?? ((options) => buildSSRBundle(options.root, options.ssrEntry, options.ssrOutDir, options.plugins)))({
|
|
4153
4755
|
root: this.root,
|
|
4154
4756
|
ssrEntry: tempEntryPath,
|
|
4155
4757
|
ssrOutDir: ssrOutputDir,
|
|
4156
4758
|
plugins: this.runtime.ssrPlugins ?? []
|
|
4157
4759
|
});
|
|
4158
|
-
const ssrBundlePath =
|
|
4760
|
+
const ssrBundlePath = resolveSSRBundlePath(ssrOutputDir);
|
|
4159
4761
|
await renderSSGRoutes(this.routes, this.runtimeContext(), outputDir, ssrBundlePath);
|
|
4160
4762
|
} catch (err) {
|
|
4161
4763
|
console.error("[clarify] SSG failed:", err);
|
|
4162
4764
|
throw err;
|
|
4163
4765
|
} finally {
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
} catch {}
|
|
4766
|
+
removeClarifyTempDir(tempEntryPath ? resolve(tempEntryPath, "..") : void 0);
|
|
4767
|
+
removeClarifyTempDir(ssrOutputDir);
|
|
4167
4768
|
}
|
|
4168
4769
|
});
|
|
4169
4770
|
await this.runBuildDone();
|
|
@@ -4203,4 +4804,4 @@ function createClarifyEngine(options = {}) {
|
|
|
4203
4804
|
//#endregion
|
|
4204
4805
|
export { createClarifyTempDir as a, cliPackageVersion as c, createViteAdapter as d, defineConfig as f, findRouteConflicts as i, cliPackageVersionWithCaret as l, createClarifyEngine as n, deserializeSearchIndex as o, createClarifyRuntimeAliases as r, searchMcpIndex as s, ClarifyEngine as t, printCliVersion as u };
|
|
4205
4806
|
|
|
4206
|
-
//# sourceMappingURL=engine-
|
|
4807
|
+
//# sourceMappingURL=engine-Ccma7jrA.js.map
|