@clarify-labs/cli 0.11.19 → 0.11.20

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.
@@ -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("&", "&amp;").replaceAll("\"", "&quot;");
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
- var remarkPlugins = markdownRemarkPlugins;
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
- rehypeSlug,
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: parsed.content.replace(/^\r?\n/, "")
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/routes.ts
782
+ //#region source/parsers/routes/heading-metadata.ts
361
783
  function kebabToTitle(str) {
362
- return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
363
- }
364
- function parseMdxTree(content) {
365
- return remark.parse(content);
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 diagnostic = await compileRouteDiagnostic(page.content, fullPath, base);
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] : extractH1(page.content) : lastPart) || "Untitled";
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: entry.name.toLowerCase().endsWith(".mdx") ? "markdown+jsx" : "markdown",
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: extractMdxSections(page.content)
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) => (route.kind === "markdown+jsx" || route.kind === "markdown") && route.source.filePath === filePath);
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.19",
2238
+ version: "0.11.20",
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 contentPath = resolveContentArtifactPath(req.url, projectConfig);
2012
- if (contentPath === "/llms.txt") {
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", resolveContentArtifactType(route));
2034
- res.end(readRouteArtifactContent(route), "utf8");
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
- return routes.map((route, routeIndex) => {
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;
@@ -4024,8 +4610,9 @@ var ClarifyEngine = class {
4024
4610
  * - `skipModules`: skip `buildModules()` (check command).
4025
4611
  * - `skipHints`: skip `logStartupHints()` (check command).
4026
4612
  *
4027
- * Idempotent: calling prepare() again with the same env is a no-op unless
4028
- * `force` is set.
4613
+ * Configuration initialization is cached unless `force` is set. Discovery
4614
+ * and virtual module generation intentionally run on every call so callers
4615
+ * observe content changes without rebuilding the Engine instance.
4029
4616
  */
4030
4617
  async prepare(env = {
4031
4618
  command: this.runtime.command,
@@ -4145,10 +4732,11 @@ var ClarifyEngine = class {
4145
4732
  assertNoContentDiagnostics(this.routes);
4146
4733
  const outputDir = this.runtime.outputDirectory ?? this.generateOptions.outputDirectory;
4147
4734
  if (!outputDir) throw new Error("[clarify] outputDirectory is required before SSG runs");
4148
- const ssrOutputDir = join(outputDir, ".ssr");
4735
+ let ssrOutputDir;
4149
4736
  let tempEntryPath;
4150
4737
  try {
4151
4738
  tempEntryPath = createTempEntryFile(SSR_ENTRY_CODE);
4739
+ ssrOutputDir = createClarifyTempDir("ssr-output");
4152
4740
  await (this.runtime.buildSSRBundle ?? ((options) => buildSSRBundle(options.root, options.ssrEntry, options.ssrOutDir, options.plugins)))({
4153
4741
  root: this.root,
4154
4742
  ssrEntry: tempEntryPath,
@@ -4161,9 +4749,8 @@ var ClarifyEngine = class {
4161
4749
  console.error("[clarify] SSG failed:", err);
4162
4750
  throw err;
4163
4751
  } finally {
4164
- if (tempEntryPath) try {
4165
- rmSync(tempEntryPath, { force: true });
4166
- } catch {}
4752
+ removeClarifyTempDir(tempEntryPath ? resolve(tempEntryPath, "..") : void 0);
4753
+ removeClarifyTempDir(ssrOutputDir);
4167
4754
  }
4168
4755
  });
4169
4756
  await this.runBuildDone();
@@ -4203,4 +4790,4 @@ function createClarifyEngine(options = {}) {
4203
4790
  //#endregion
4204
4791
  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
4792
 
4206
- //# sourceMappingURL=engine-DH8Hi6Fr.js.map
4793
+ //# sourceMappingURL=engine-BgfGFPKK.js.map