@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.
@@ -30,18 +30,14 @@ _vitejs_plugin_react = __toESM(_vitejs_plugin_react, 1);
30
30
  let rehype_raw = require("rehype-raw");
31
31
  rehype_raw = __toESM(rehype_raw, 1);
32
32
  let _mdx_js_mdx = require("@mdx-js/mdx");
33
+ let github_slugger = require("github-slugger");
34
+ github_slugger = __toESM(github_slugger, 1);
35
+ let _clarify_labs_renderer = require("@clarify-labs/renderer");
33
36
  let _shikijs_rehype = require("@shikijs/rehype");
34
37
  _shikijs_rehype = __toESM(_shikijs_rehype, 1);
35
- let rehype_slug = require("rehype-slug");
36
- rehype_slug = __toESM(rehype_slug, 1);
37
38
  let shiki = require("shiki");
38
39
  let unist_util_visit = require("unist-util-visit");
39
- let _clarify_labs_renderer = require("@clarify-labs/renderer");
40
40
  let node_fs_promises = require("node:fs/promises");
41
- let github_slugger = require("github-slugger");
42
- github_slugger = __toESM(github_slugger, 1);
43
- let mdast_util_to_string = require("mdast-util-to-string");
44
- let remark = require("remark");
45
41
  let gray_matter = require("gray-matter");
46
42
  gray_matter = __toESM(gray_matter, 1);
47
43
  let node_child_process = require("node:child_process");
@@ -76,6 +72,402 @@ function createContentDiagnostic(options) {
76
72
  };
77
73
  }
78
74
  //#endregion
75
+ //#region source/parsers/markdown/headings.ts
76
+ var HEADING_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
77
+ var INTERNAL_HEADING_ID_PREFIX = "clarify-internal-heading-id:";
78
+ var EXPLICIT_ID_PATTERN = /\s+\{#([^{}]*)\}$/;
79
+ var HEADING_REMARK_PLUGINS = _clarify_labs_renderer.markdownRemarkPlugins;
80
+ function visitHeadings(node, callback) {
81
+ if (node.type === "heading") callback(node);
82
+ for (const child of node.children ?? []) visitHeadings(child, callback);
83
+ }
84
+ function visitHtml(node, callback) {
85
+ if (node.type === "html") callback(node);
86
+ for (const child of node.children ?? []) visitHtml(child, callback);
87
+ }
88
+ function visitMdxIntrinsicHeadings(node, callback) {
89
+ if ((node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && node.name && /^h[1-6]$/.test(node.name)) callback(node);
90
+ for (const child of node.children ?? []) visitMdxIntrinsicHeadings(child, callback);
91
+ }
92
+ function visitHastHeadings(node, callback) {
93
+ if (node.type === "element" && node.tagName && /^h[1-6]$/.test(node.tagName)) callback(node);
94
+ for (const child of node.children ?? []) visitHastHeadings(child, callback);
95
+ }
96
+ function hastText$1(node) {
97
+ if (node.type === "text") return node.value ?? "";
98
+ return (node.children ?? []).map(hastText$1).join("");
99
+ }
100
+ function applySourceEdits(content, edits) {
101
+ return edits.sort((left, right) => right.start - left.start).reduce((result, edit) => result.slice(0, edit.start) + edit.replacement + result.slice(edit.end), content);
102
+ }
103
+ function headingPosition(node, lineOffset) {
104
+ return {
105
+ line: (node.position?.start.line ?? 1) + lineOffset,
106
+ column: node.position?.start.column ?? 1
107
+ };
108
+ }
109
+ function formatPosition(position) {
110
+ return `${position.line}:${position.column}`;
111
+ }
112
+ function finalTextNode(heading) {
113
+ const node = heading.children?.at(-1);
114
+ return node?.type === "text" && typeof node.value === "string" ? node : void 0;
115
+ }
116
+ function contentEndOffset(heading) {
117
+ return (heading.children?.at(-1))?.position?.end.offset ?? heading.position?.end.offset;
118
+ }
119
+ function explicitIdInfo(heading, content) {
120
+ const text = finalTextNode(heading);
121
+ if (!text?.position) return void 0;
122
+ const startOffset = text.position.start.offset;
123
+ const endOffset = text.position.end.offset;
124
+ if (startOffset === void 0 || endOffset === void 0) return void 0;
125
+ const match = content.slice(startOffset, endOffset).match(EXPLICIT_ID_PATTERN);
126
+ if (!match || match.index === void 0) return void 0;
127
+ return {
128
+ id: match[1] ?? "",
129
+ start: startOffset + match.index,
130
+ end: startOffset + match.index + match[0].length
131
+ };
132
+ }
133
+ function maskExplicitHeadingMarkers(content) {
134
+ const tree = (0, _mdx_js_mdx.createProcessor)({
135
+ format: "md",
136
+ remarkPlugins: HEADING_REMARK_PLUGINS
137
+ }).parse(content);
138
+ const edits = [];
139
+ visitHeadings(tree, (heading) => {
140
+ const marker = explicitIdInfo(heading, content);
141
+ if (marker) edits.push({
142
+ start: marker.start,
143
+ end: marker.end,
144
+ replacement: "x".repeat(marker.end - marker.start)
145
+ });
146
+ });
147
+ return applySourceEdits(content, edits);
148
+ }
149
+ function parseHeadingTree(content, kind) {
150
+ if (kind === "markdown") return (0, _mdx_js_mdx.createProcessor)({
151
+ format: "md",
152
+ remarkPlugins: HEADING_REMARK_PLUGINS
153
+ }).parse(content);
154
+ const maskedContent = maskExplicitHeadingMarkers(content);
155
+ try {
156
+ return (0, _mdx_js_mdx.createProcessor)({
157
+ format: "mdx",
158
+ remarkPlugins: HEADING_REMARK_PLUGINS
159
+ }).parse(maskedContent);
160
+ } catch {
161
+ return;
162
+ }
163
+ }
164
+ function renderedMarkdownHeadingTitles(tree, content, kind) {
165
+ const originalText = /* @__PURE__ */ new Map();
166
+ visitHeadings(tree, (heading) => {
167
+ const marker = explicitIdInfo(heading, content);
168
+ const text = finalTextNode(heading);
169
+ if (!marker || !text || text.value === void 0) return;
170
+ originalText.set(text, text.value);
171
+ text.value = text.value.slice(0, -(marker.end - marker.start)).replace(/\s+$/, "");
172
+ });
173
+ try {
174
+ let hast;
175
+ const captureHast = () => (tree) => {
176
+ hast = tree;
177
+ };
178
+ (0, _mdx_js_mdx.createProcessor)({
179
+ format: kind === "markdown" ? "md" : "mdx",
180
+ remarkPlugins: HEADING_REMARK_PLUGINS,
181
+ ...kind === "markdown" ? { remarkRehypeOptions: { allowDangerousHtml: true } } : {},
182
+ rehypePlugins: kind === "markdown" ? [rehype_raw.default, captureHast] : [captureHast]
183
+ }).runSync(tree, { value: content });
184
+ const titles = /* @__PURE__ */ new Map();
185
+ if (!hast) return titles;
186
+ visitHastHeadings(hast, (heading) => {
187
+ const offset = heading.position?.start.offset;
188
+ if (offset !== void 0) titles.set(offset, hastText$1(heading));
189
+ });
190
+ return titles;
191
+ } finally {
192
+ for (const [text, value] of originalText) text.value = value;
193
+ }
194
+ }
195
+ function isHtmlWhitespace(character) {
196
+ return character === " " || character === "\n" || character === "\f" || character === "\r" || character === " ";
197
+ }
198
+ function isMdxWhitespace(character) {
199
+ return /\s/u.test(character ?? "");
200
+ }
201
+ function openingTagAttributeOffset(content, start, end, isWhitespace) {
202
+ let state = "tagName";
203
+ let quote;
204
+ for (let index = start; index < end; index++) {
205
+ const character = content[index];
206
+ const closesTag = character === ">";
207
+ const selfClosesTag = character === "/" && content[index + 1] === ">";
208
+ if (state === "quotedAttributeValue") {
209
+ if (character === quote) {
210
+ quote = void 0;
211
+ state = "afterQuotedAttributeValue";
212
+ }
213
+ continue;
214
+ }
215
+ if (state === "unquotedAttributeValue") {
216
+ if (closesTag) return index;
217
+ if (isWhitespace(character)) state = "beforeAttributeName";
218
+ continue;
219
+ }
220
+ if (closesTag) return index;
221
+ if (selfClosesTag && state !== "beforeAttributeValue") {
222
+ let cursor = index;
223
+ while (cursor > start && isWhitespace(content[cursor - 1])) cursor--;
224
+ return cursor;
225
+ }
226
+ if (state === "tagName") {
227
+ if (isWhitespace(character)) state = "beforeAttributeName";
228
+ } else if (state === "beforeAttributeName") {
229
+ if (!isWhitespace(character)) state = "attributeName";
230
+ } else if (state === "attributeName") {
231
+ if (character === "=") state = "beforeAttributeValue";
232
+ else if (isWhitespace(character)) state = "afterAttributeName";
233
+ } else if (state === "afterAttributeName") {
234
+ if (character === "=") state = "beforeAttributeValue";
235
+ else if (!isWhitespace(character)) state = "attributeName";
236
+ } else if (state === "beforeAttributeValue") {
237
+ if (character === "\"" || character === "'") {
238
+ quote = character;
239
+ state = "quotedAttributeValue";
240
+ } else if (!isWhitespace(character)) state = "unquotedAttributeValue";
241
+ } else if (state === "afterQuotedAttributeValue" && isWhitespace(character)) state = "beforeAttributeName";
242
+ }
243
+ }
244
+ function rawHtmlHeadings(tree, content, lineOffset) {
245
+ let hasRawHtml = false;
246
+ visitHtml(tree, () => {
247
+ hasRawHtml = true;
248
+ });
249
+ if (!hasRawHtml) return [];
250
+ const markdownOffsets = /* @__PURE__ */ new Set();
251
+ visitHeadings(tree, (heading) => {
252
+ const offset = heading.position?.start.offset;
253
+ if (offset !== void 0) markdownOffsets.add(offset);
254
+ });
255
+ let hast;
256
+ const captureHast = () => (tree) => {
257
+ hast = tree;
258
+ };
259
+ (0, _mdx_js_mdx.createProcessor)({
260
+ format: "md",
261
+ remarkRehypeOptions: { allowDangerousHtml: true },
262
+ rehypePlugins: [rehype_raw.default, captureHast]
263
+ }).runSync(tree, { value: content });
264
+ if (!hast) return [];
265
+ const headings = [];
266
+ visitHastHeadings(hast, (heading) => {
267
+ const offset = heading.position?.start.offset;
268
+ const headingEnd = heading.position?.end.offset;
269
+ if (offset === void 0 || headingEnd === void 0 || markdownOffsets.has(offset)) return;
270
+ const tagEnd = openingTagAttributeOffset(content, offset, headingEnd, isHtmlWhitespace);
271
+ if (tagEnd === void 0) return;
272
+ const rawId = heading.properties?.id;
273
+ headings.push({
274
+ title: hastText$1(heading),
275
+ ...typeof rawId === "string" && rawId ? { id: rawId } : {},
276
+ position: {
277
+ line: (heading.position?.start.line ?? 1) + lineOffset,
278
+ column: heading.position?.start.column ?? 1
279
+ },
280
+ offset,
281
+ attributeOffset: tagEnd
282
+ });
283
+ });
284
+ return headings;
285
+ }
286
+ function staticMdxText(node) {
287
+ if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
288
+ if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
289
+ if (!node.name || !/^[a-z][a-z0-9-]*$/.test(node.name)) return void 0;
290
+ } else if (!node.children) return;
291
+ const parts = [];
292
+ for (const child of node.children ?? []) {
293
+ const text = staticMdxText(child);
294
+ if (text === void 0) return void 0;
295
+ parts.push(text);
296
+ }
297
+ return parts.join("");
298
+ }
299
+ function literalMdxId(node) {
300
+ let result = { kind: "missing" };
301
+ for (const attribute of node.attributes ?? []) {
302
+ if (attribute.type !== "mdxJsxAttribute") {
303
+ result = { kind: "unknown" };
304
+ continue;
305
+ }
306
+ if (attribute.name !== "id") continue;
307
+ result = typeof attribute.value === "string" ? {
308
+ kind: "literal",
309
+ id: attribute.value
310
+ } : { kind: "unknown" };
311
+ }
312
+ return result;
313
+ }
314
+ function mdxIntrinsicHeadings(tree, content, lineOffset) {
315
+ const headings = [];
316
+ visitMdxIntrinsicHeadings(tree, (heading) => {
317
+ const position = heading.position;
318
+ const offset = position?.start.offset;
319
+ const end = position?.end.offset;
320
+ const title = staticMdxText(heading);
321
+ const literalId = literalMdxId(heading);
322
+ if (!position || offset === void 0 || end === void 0 || literalId.kind !== "literal") return;
323
+ const tagEnd = openingTagAttributeOffset(content, offset, end, isMdxWhitespace);
324
+ if (tagEnd === void 0) return;
325
+ headings.push({
326
+ ...title !== void 0 ? { title } : {},
327
+ id: literalId.id,
328
+ position: {
329
+ line: position.start.line + lineOffset,
330
+ column: position.start.column
331
+ },
332
+ offset,
333
+ attributeOffset: tagEnd
334
+ });
335
+ });
336
+ return headings;
337
+ }
338
+ function escapeHtmlAttribute(value) {
339
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;");
340
+ }
341
+ function analyzeHeadings(content, options) {
342
+ const lineOffset = options.lineOffset ?? 0;
343
+ const headings = [];
344
+ const edits = [];
345
+ const errors = [];
346
+ const ids = /* @__PURE__ */ new Map();
347
+ const documentHeadings = [];
348
+ const markdownHeadingInputs = /* @__PURE__ */ new Map();
349
+ const tree = parseHeadingTree(content, options.kind);
350
+ if (!tree) return {
351
+ headings,
352
+ normalizedContent: content
353
+ };
354
+ const renderedTitles = renderedMarkdownHeadingTitles(tree, content, options.kind);
355
+ const claimId = (id, position) => {
356
+ const owner = ids.get(id);
357
+ if (owner) {
358
+ errors.push(`Heading ID "${id}" at ${formatPosition(position)} conflicts with the heading at ${formatPosition(owner.position)}.`);
359
+ return;
360
+ }
361
+ ids.set(id, {
362
+ id,
363
+ position
364
+ });
365
+ };
366
+ visitHeadings(tree, (node) => {
367
+ const marker = explicitIdInfo(node, content);
368
+ const title = renderedTitles.get(node.position?.start.offset ?? 0) ?? "";
369
+ const hasValidExplicitId = marker !== void 0 && HEADING_ID_PATTERN.test(marker.id);
370
+ const heading = {
371
+ level: node.depth ?? 1,
372
+ title,
373
+ id: hasValidExplicitId ? marker.id : "",
374
+ position: headingPosition(node, lineOffset)
375
+ };
376
+ if (marker && !hasValidExplicitId) errors.push(`Invalid heading ID "${marker.id}" at ${formatPosition(heading.position)}. IDs must match [a-z0-9]+(?:-[a-z0-9]+)*.`);
377
+ headings.push(heading);
378
+ markdownHeadingInputs.set(heading, {
379
+ ...marker ? { marker } : {},
380
+ hasValidExplicitId,
381
+ contentEnd: contentEndOffset(node)
382
+ });
383
+ documentHeadings.push({
384
+ title,
385
+ offset: node.position?.start.offset ?? 0,
386
+ markdown: heading
387
+ });
388
+ });
389
+ const renderedHeadings = options.kind === "markdown" ? rawHtmlHeadings(tree, content, lineOffset) : mdxIntrinsicHeadings(tree, content, lineOffset);
390
+ for (const rendered of renderedHeadings) documentHeadings.push({
391
+ ...rendered.title !== void 0 ? { title: rendered.title } : {},
392
+ offset: rendered.offset,
393
+ rendered
394
+ });
395
+ documentHeadings.sort((left, right) => left.offset - right.offset);
396
+ const slugger = new github_slugger.default();
397
+ for (const documentHeading of documentHeadings) {
398
+ if (documentHeading.markdown) {
399
+ const heading = documentHeading.markdown;
400
+ const input = markdownHeadingInputs.get(heading);
401
+ if (input.hasValidExplicitId) {
402
+ edits.push({
403
+ start: input.marker.start,
404
+ end: input.marker.end,
405
+ replacement: ` [](clarify-internal-heading-id:${heading.id})`
406
+ });
407
+ continue;
408
+ }
409
+ }
410
+ if (documentHeading.rendered?.id !== void 0) continue;
411
+ if (documentHeading.title === void 0) continue;
412
+ const generatedId = slugger.slug(documentHeading.title);
413
+ if (documentHeading.markdown) {
414
+ const heading = documentHeading.markdown;
415
+ const input = markdownHeadingInputs.get(heading);
416
+ heading.id = generatedId;
417
+ const link = ` [](clarify-internal-heading-id:${heading.id})`;
418
+ if (input.marker) edits.push({
419
+ start: input.marker.start,
420
+ end: input.marker.end,
421
+ replacement: link
422
+ });
423
+ else if (input.contentEnd !== void 0) edits.push({
424
+ start: input.contentEnd,
425
+ end: input.contentEnd,
426
+ replacement: link
427
+ });
428
+ } else if (documentHeading.rendered && documentHeading.rendered.id === void 0) {
429
+ documentHeading.rendered.id = generatedId;
430
+ edits.push({
431
+ start: documentHeading.rendered.attributeOffset,
432
+ end: documentHeading.rendered.attributeOffset,
433
+ replacement: ` id="${escapeHtmlAttribute(generatedId)}"`
434
+ });
435
+ }
436
+ }
437
+ for (const documentHeading of documentHeadings) if (documentHeading.markdown) claimId(documentHeading.markdown.id, documentHeading.markdown.position);
438
+ else if (documentHeading.rendered?.id) claimId(documentHeading.rendered.id, documentHeading.rendered.position);
439
+ const diagnostic = errors.length === 0 ? void 0 : createContentDiagnostic({
440
+ kind: options.kind,
441
+ title: "Heading ID error",
442
+ message: "This page has invalid or conflicting heading IDs. Fix the IDs below, then reload the route.",
443
+ error: new Error(errors.join("\n")),
444
+ filePath: options.filePath,
445
+ projectRoot: options.projectRoot
446
+ });
447
+ return {
448
+ headings,
449
+ normalizedContent: applySourceEdits(content, edits),
450
+ diagnostic
451
+ };
452
+ }
453
+ function remarkApplyHeadingIds() {
454
+ return (tree) => {
455
+ visitHeadings(tree, (heading) => {
456
+ const finalNode = heading.children?.at(-1);
457
+ if (finalNode?.type !== "link" || finalNode.children?.length !== 0 || typeof finalNode.url !== "string" || !finalNode.url.startsWith(INTERNAL_HEADING_ID_PREFIX)) return;
458
+ const id = finalNode.url.slice(28);
459
+ heading.children?.pop();
460
+ const previousNode = heading.children?.at(-1);
461
+ if (previousNode?.type === "text" && typeof previousNode.value === "string") previousNode.value = previousNode.value.replace(/ $/, "");
462
+ heading.data = heading.data ?? {};
463
+ heading.data.hProperties = {
464
+ ...heading.data.hProperties,
465
+ id
466
+ };
467
+ });
468
+ };
469
+ }
470
+ //#endregion
79
471
  //#region source/parsers/markdown/mdx.ts
80
472
  function getLanguage(className) {
81
473
  return (Array.isArray(className) ? className : typeof className === "string" ? className.split(/\s+/) : []).find((item) => typeof item === "string" && item.startsWith("language-"))?.replace(/^language-/, "") || "txt";
@@ -98,9 +490,34 @@ function rehypeParseCodeBlocks() {
98
490
  });
99
491
  };
100
492
  }
101
- var remarkPlugins = _clarify_labs_renderer.markdownRemarkPlugins;
493
+ function hastText(node) {
494
+ if (node.type === "text") return node.value ?? "";
495
+ return (node.children ?? []).map(hastText).join("");
496
+ }
497
+ function isHeadingElement(node) {
498
+ return Boolean(node.tagName && /^h[1-6]$/.test(node.tagName));
499
+ }
500
+ function rehypeAddMissingHeadingIds() {
501
+ return (tree) => {
502
+ const slugger = new github_slugger.default();
503
+ const reservedIds = /* @__PURE__ */ new Set();
504
+ (0, unist_util_visit.visit)(tree, "element", (node) => {
505
+ if (isHeadingElement(node) && typeof node.properties?.id === "string" && node.properties.id) reservedIds.add(node.properties.id);
506
+ });
507
+ (0, unist_util_visit.visit)(tree, "element", (node) => {
508
+ if (!isHeadingElement(node)) return;
509
+ const title = hastText(node);
510
+ let fallbackId = slugger.slug(title);
511
+ if (node.properties?.id) return;
512
+ while (reservedIds.has(fallbackId)) fallbackId = slugger.slug(title);
513
+ node.properties = node.properties ?? {};
514
+ node.properties.id = fallbackId;
515
+ });
516
+ };
517
+ }
518
+ var remarkPlugins = [remarkApplyHeadingIds, ..._clarify_labs_renderer.markdownRemarkPlugins];
102
519
  var rehypePlugins = [
103
- rehype_slug.default,
520
+ rehypeAddMissingHeadingIds,
104
521
  rehypeParseCodeBlocks,
105
522
  [_shikijs_rehype.default, {
106
523
  theme: cssVariablesTheme,
@@ -255,9 +672,12 @@ async function runBuildDoneHooks(plugins, ctx) {
255
672
  */
256
673
  function parseFrontmatter(content) {
257
674
  const parsed = (0, gray_matter.default)(content);
675
+ const body = parsed.content.replace(/^\r?\n/, "");
676
+ const prefix = content.slice(0, content.length - body.length);
258
677
  return {
259
678
  frontmatter: parsed.data,
260
- content: parsed.content.replace(/^\r?\n/, "")
679
+ content: body,
680
+ lineOffset: [...prefix.matchAll(/\r\n|\r|\n/g)].length
261
681
  };
262
682
  }
263
683
  //#endregion
@@ -268,7 +688,7 @@ async function transformContent(transform, input) {
268
688
  function createContentProcessor(transform) {
269
689
  return {
270
690
  async processMdx(source, filePath) {
271
- const { frontmatter, content } = parseFrontmatter(source);
691
+ const { frontmatter, content, lineOffset } = parseFrontmatter(source);
272
692
  const transformed = await transformContent(transform, {
273
693
  kind: "markdown+jsx",
274
694
  source,
@@ -278,7 +698,8 @@ function createContentProcessor(transform) {
278
698
  });
279
699
  return {
280
700
  frontmatter: transformed.frontmatter,
281
- content: transformed.content
701
+ content: transformed.content,
702
+ lineOffset
282
703
  };
283
704
  },
284
705
  async processText(source, kind, filePath) {
@@ -388,13 +809,47 @@ function resolveUpdatedAt(frontmatter, detectedAt) {
388
809
  return normalizeDate(typeof frontmatter.updatedAt === "string" ? frontmatter.updatedAt : void 0) ?? detectedAt;
389
810
  }
390
811
  //#endregion
391
- //#region source/parsers/routes/routes.ts
812
+ //#region source/parsers/routes/heading-metadata.ts
392
813
  function kebabToTitle(str) {
393
- return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
394
- }
395
- function parseMdxTree(content) {
396
- return remark.remark.parse(content);
814
+ return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
815
+ }
816
+ function headingMetadata(analysis) {
817
+ return { sections: analysis.headings.filter((heading) => heading.level === 2 || heading.level === 3).map((heading) => ({
818
+ id: heading.id,
819
+ title: heading.title,
820
+ level: heading.level
821
+ })) };
822
+ }
823
+ function resolvedRouteTitle(route, analysis) {
824
+ const frontmatterTitle = route.source.frontmatter?.title;
825
+ if (typeof frontmatterTitle === "string" && frontmatterTitle) return frontmatterTitle;
826
+ const routePath = route.basePath ?? route.path;
827
+ return kebabToTitle(routePath === "/" ? analysis.headings.find((heading) => heading.level === 1)?.title ?? "" : routePath.split("/").filter(Boolean).at(-1) ?? "") || "Untitled";
828
+ }
829
+ async function reanalyzeMarkdownRoute(route, projectRoot) {
830
+ if (route.kind !== "markdown" && route.kind !== "markdown+jsx") return route;
831
+ const analysis = analyzeHeadings(route.source.content ?? "", {
832
+ kind: route.kind,
833
+ filePath: route.source.filePath,
834
+ projectRoot,
835
+ lineOffset: route.source.lineOffset
836
+ });
837
+ const result = await (route.kind === "markdown" ? compileMarkdownContent : compileMdxContent)(analysis.normalizedContent, {
838
+ filePath: route.source.filePath,
839
+ projectRoot
840
+ });
841
+ return {
842
+ ...route,
843
+ meta: {
844
+ ...route.meta,
845
+ title: resolvedRouteTitle(route, analysis),
846
+ ...headingMetadata(analysis)
847
+ },
848
+ diagnostic: analysis.diagnostic ?? (result.ok ? void 0 : result.diagnostic)
849
+ };
397
850
  }
851
+ //#endregion
852
+ //#region source/parsers/routes/routes.ts
398
853
  async function compileRouteDiagnostic(content, filePath, baseDir) {
399
854
  const result = await ((0, node_path.extname)(filePath).toLowerCase() === ".md" ? compileMarkdownContent : compileMdxContent)(content, {
400
855
  filePath,
@@ -402,30 +857,6 @@ async function compileRouteDiagnostic(content, filePath, baseDir) {
402
857
  });
403
858
  return result.ok ? void 0 : result.diagnostic;
404
859
  }
405
- /** 从内容中提取第一个 H1 标题 */
406
- function extractH1(content) {
407
- const tree = parseMdxTree(content);
408
- let title = "";
409
- (0, unist_util_visit.visit)(tree, "heading", (node) => {
410
- if (!title && node.depth === 1) title = (0, mdast_util_to_string.toString)(node);
411
- });
412
- return title;
413
- }
414
- /** 从 MDX/Markdown 内容中提取 H2/H3 章节 */
415
- function extractMdxSections(content) {
416
- const sections = [];
417
- const slugger = new github_slugger.default();
418
- (0, unist_util_visit.visit)(parseMdxTree(content), "heading", (node) => {
419
- if (node.depth !== 2 && node.depth !== 3) return;
420
- const title = (0, mdast_util_to_string.toString)(node);
421
- sections.push({
422
- id: slugger.slug(title),
423
- title,
424
- level: node.depth
425
- });
426
- });
427
- return sections;
428
- }
429
860
  function navigationSections(sections) {
430
861
  return sections.map((section) => ({
431
862
  id: section.id,
@@ -482,11 +913,12 @@ async function findContentRoutes(dir, base = dir, options = {}) {
482
913
  const fullPath = (0, node_path.join)(dir, entry.name);
483
914
  if (entry.isDirectory()) routes.push(...await findContentRoutes(fullPath, base, options));
484
915
  else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
916
+ const kind = entry.name.toLowerCase().endsWith(".mdx") ? "markdown+jsx" : "markdown";
485
917
  const relativePath = (0, node_path.relative)(base, fullPath);
486
918
  const pathParts = relativePath.replace(/\.mdx?$/, "").split("/");
487
919
  const cleanPath = ("/" + pathParts.map((p) => p === "index" ? "" : p).filter(Boolean).join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
488
920
  const source = await (0, node_fs_promises.readFile)(fullPath, "utf-8");
489
- const { frontmatter, content } = await (options.contentProcessor ?? createContentProcessor()).processMdx(source, fullPath);
921
+ const { frontmatter, content, lineOffset } = await (options.contentProcessor ?? createContentProcessor()).processMdx(source, fullPath);
490
922
  const updatedAt = resolveUpdatedAt(frontmatter, await getFileUpdatedAt(fullPath, base));
491
923
  const page = {
492
924
  path: cleanPath,
@@ -494,14 +926,21 @@ async function findContentRoutes(dir, base = dir, options = {}) {
494
926
  frontmatter,
495
927
  content
496
928
  };
497
- const diagnostic = await compileRouteDiagnostic(page.content, fullPath, base);
929
+ const analysis = analyzeHeadings(page.content, {
930
+ kind,
931
+ filePath: fullPath,
932
+ projectRoot: base,
933
+ lineOffset
934
+ });
935
+ const compileDiagnostic = await compileRouteDiagnostic(analysis.normalizedContent, fullPath, base);
936
+ const { sections } = headingMetadata(analysis);
498
937
  let title = typeof page.frontmatter.title === "string" ? page.frontmatter.title : "";
499
938
  if (!title) {
500
939
  const lastPart = pathParts[pathParts.length - 1] ?? "";
501
- title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(page.content) : lastPart) || "Untitled";
940
+ title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : analysis.headings.find((heading) => heading.level === 1)?.title ?? "" : lastPart) || "Untitled";
502
941
  }
503
942
  routes.push({
504
- kind: entry.name.toLowerCase().endsWith(".mdx") ? "markdown+jsx" : "markdown",
943
+ kind,
505
944
  path: cleanPath,
506
945
  basePath: cleanPath,
507
946
  meta: {
@@ -511,7 +950,7 @@ async function findContentRoutes(dir, base = dir, options = {}) {
511
950
  group: typeof page.frontmatter.group === "string" ? page.frontmatter.group : void 0,
512
951
  layout: page.frontmatter.layout === "documentation" || page.frontmatter.layout === "blog" ? page.frontmatter.layout : void 0,
513
952
  updatedAt,
514
- sections: extractMdxSections(page.content)
953
+ sections
515
954
  },
516
955
  module: {
517
956
  pageVirtualModuleId: pageVirtualModuleId(relativePath.replace(/\.mdx?$/, "")),
@@ -520,9 +959,10 @@ async function findContentRoutes(dir, base = dir, options = {}) {
520
959
  source: {
521
960
  filePath: fullPath,
522
961
  frontmatter: page.frontmatter,
523
- content: page.content
962
+ content: page.content,
963
+ lineOffset
524
964
  },
525
- diagnostic
965
+ diagnostic: analysis.diagnostic ?? compileDiagnostic
526
966
  });
527
967
  }
528
968
  }
@@ -1628,10 +2068,14 @@ function createNormalizedContentPlugin(engine) {
1628
2068
  transform(_code, id) {
1629
2069
  if (!/\.mdx?(?:\?|$)/.test(id)) return null;
1630
2070
  const filePath = id.replace(/\?.*$/, "");
1631
- const route = engine.routes.find((route) => (route.kind === "markdown+jsx" || route.kind === "markdown") && route.source.filePath === filePath);
1632
- if (!route || route.source.content === void 0) return null;
2071
+ const route = engine.routes.find((route) => route.source.filePath === filePath);
2072
+ if (!route || route.kind !== "markdown+jsx" && route.kind !== "markdown" || route.source.content === void 0) return null;
1633
2073
  return {
1634
- code: route.source.content,
2074
+ code: analyzeHeadings(route.source.content, {
2075
+ kind: route.kind,
2076
+ filePath,
2077
+ projectRoot: engine.root
2078
+ }).normalizedContent,
1635
2079
  map: null
1636
2080
  };
1637
2081
  }
@@ -1821,7 +2265,7 @@ function createViteAdapter(engine) {
1821
2265
  }
1822
2266
  var cliPackageVersion = {
1823
2267
  name: "@clarify-labs/cli",
1824
- version: "0.11.19",
2268
+ version: "0.11.21",
1825
2269
  license: "AGPL-3.0-only",
1826
2270
  type: "module",
1827
2271
  repository: {
@@ -2024,6 +2468,191 @@ function createLlmsTxtArtifact(routes, projectConfig) {
2024
2468
  return withUtf8Signature(createLlmsTxt(routes, projectConfig));
2025
2469
  }
2026
2470
  //#endregion
2471
+ //#region source/plugins/openapi/aggregation.ts
2472
+ function mergeOpenAPIRecord(target, source, section, filePath) {
2473
+ if (!source) return;
2474
+ for (const [key, value] of Object.entries(source)) {
2475
+ const existing = target[key];
2476
+ if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(value)) throw new Error(`[clarify] Cannot aggregate OpenAPI specs: conflicting ${section} entry "${key}" in ${filePath}.`);
2477
+ target[key] = value;
2478
+ }
2479
+ }
2480
+ function openAPIVersionFamily(version) {
2481
+ return version.split(".").slice(0, 2).join(".");
2482
+ }
2483
+ var OPENAPI_OPERATION_METHODS = /* @__PURE__ */ new Set([
2484
+ "get",
2485
+ "put",
2486
+ "post",
2487
+ "delete",
2488
+ "options",
2489
+ "head",
2490
+ "patch",
2491
+ "trace"
2492
+ ]);
2493
+ function sourceComponentPrefix(source) {
2494
+ return (source.id || source.filePath.split(/[\\/]/).pop()?.replace(/\.openapi\.(?:json|ya?ml)$/i, "") || "service").replace(/[^a-zA-Z0-9._-]/g, "_") || "service";
2495
+ }
2496
+ function securitySchemeNameMap(source, sourceSchemes, targetSchemes) {
2497
+ const names = /* @__PURE__ */ new Map();
2498
+ for (const [name, definition] of Object.entries(sourceSchemes)) {
2499
+ const existing = targetSchemes[name];
2500
+ if (existing === void 0 || JSON.stringify(existing) === JSON.stringify(definition)) {
2501
+ targetSchemes[name] = definition;
2502
+ names.set(name, name);
2503
+ continue;
2504
+ }
2505
+ const prefix = sourceComponentPrefix(source);
2506
+ let candidate = `${prefix}__${name}`;
2507
+ let suffix = 2;
2508
+ while (targetSchemes[candidate] !== void 0 && JSON.stringify(targetSchemes[candidate]) !== JSON.stringify(definition)) candidate = `${prefix}__${name}_${suffix++}`;
2509
+ targetSchemes[candidate] = definition;
2510
+ names.set(name, candidate);
2511
+ }
2512
+ return names;
2513
+ }
2514
+ function rewriteSecurityRequirements(value, schemeNames) {
2515
+ if (!Array.isArray(value)) return value;
2516
+ return value.map((requirement) => {
2517
+ if (!requirement || typeof requirement !== "object" || Array.isArray(requirement)) return requirement;
2518
+ return Object.fromEntries(Object.entries(requirement).map(([name, scopes]) => [schemeNames.get(name) ?? name, scopes]));
2519
+ });
2520
+ }
2521
+ function localizePathItemContext(pathItemValue, documentServers, documentSecurity, hasDocumentSecurity, schemeNames) {
2522
+ if (!pathItemValue || typeof pathItemValue !== "object" || Array.isArray(pathItemValue)) return pathItemValue;
2523
+ const pathItem = structuredClone(pathItemValue);
2524
+ const pathServers = Object.hasOwn(pathItem, "servers") ? pathItem.servers : documentServers;
2525
+ for (const [method, operationValue] of Object.entries(pathItem)) {
2526
+ if (!OPENAPI_OPERATION_METHODS.has(method) || !operationValue || typeof operationValue !== "object" || Array.isArray(operationValue)) continue;
2527
+ const operation = operationValue;
2528
+ if (!Object.hasOwn(operation, "servers") && pathServers !== void 0) operation.servers = structuredClone(pathServers);
2529
+ if (Object.hasOwn(operation, "security")) operation.security = rewriteSecurityRequirements(operation.security, schemeNames);
2530
+ else if (hasDocumentSecurity) operation.security = rewriteSecurityRequirements(structuredClone(documentSecurity), schemeNames);
2531
+ }
2532
+ delete pathItem.servers;
2533
+ return pathItem;
2534
+ }
2535
+ function localizeOpenAPIContexts(spec, schemeNames) {
2536
+ const document = spec;
2537
+ const hasDocumentSecurity = Object.hasOwn(document, "security");
2538
+ const localizeEntries = (entries) => entries && Object.fromEntries(Object.entries(entries).map(([name, pathItem]) => [name, localizePathItemContext(pathItem, document.servers, document.security, hasDocumentSecurity, schemeNames)]));
2539
+ return {
2540
+ paths: localizeEntries(spec.paths) ?? {},
2541
+ webhooks: localizeEntries("webhooks" in spec ? spec.webhooks : void 0)
2542
+ };
2543
+ }
2544
+ function aggregateOpenAPISources(sources, info) {
2545
+ const paths = {};
2546
+ const webhooks = {};
2547
+ const components = {};
2548
+ const tags = /* @__PURE__ */ new Map();
2549
+ let openapiVersion;
2550
+ for (const source of sources) {
2551
+ const { document: spec, filePath } = source;
2552
+ if (openapiVersion && openAPIVersionFamily(openapiVersion) !== openAPIVersionFamily(spec.openapi)) throw new Error(`[clarify] Cannot aggregate OpenAPI specs: incompatible versions "${openapiVersion}" and "${spec.openapi}" in ${filePath}.`);
2553
+ openapiVersion ??= spec.openapi;
2554
+ components.securitySchemes ??= {};
2555
+ const sourceComponents = spec.components;
2556
+ const localized = localizeOpenAPIContexts(spec, securitySchemeNameMap(source, sourceComponents?.securitySchemes ?? {}, components.securitySchemes));
2557
+ mergeOpenAPIRecord(paths, localized.paths, "paths", filePath);
2558
+ mergeOpenAPIRecord(webhooks, localized.webhooks, "webhooks", filePath);
2559
+ for (const [section, entries] of Object.entries(sourceComponents ?? {})) {
2560
+ if (section === "securitySchemes") continue;
2561
+ components[section] ??= {};
2562
+ mergeOpenAPIRecord(components[section], entries, `components.${section}`, filePath);
2563
+ }
2564
+ for (const tag of spec.tags ?? []) {
2565
+ const existing = tags.get(tag.name);
2566
+ if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(tag)) throw new Error(`[clarify] Cannot aggregate OpenAPI specs: conflicting tag "${tag.name}" in ${filePath}.`);
2567
+ tags.set(tag.name, tag);
2568
+ }
2569
+ }
2570
+ if (components.securitySchemes && Object.keys(components.securitySchemes).length === 0) delete components.securitySchemes;
2571
+ return {
2572
+ openapi: openapiVersion ?? "3.1.0",
2573
+ info: {
2574
+ title: info.title,
2575
+ description: info.description,
2576
+ version: info.version ?? "1.0.0"
2577
+ },
2578
+ paths,
2579
+ ...Object.keys(webhooks).length > 0 ? { webhooks } : {},
2580
+ ...Object.keys(components).length > 0 ? { components } : {},
2581
+ ...tags.size > 0 ? { tags: [...tags.values()] } : {}
2582
+ };
2583
+ }
2584
+ //#endregion
2585
+ //#region source/plugins/openapi/artifacts.ts
2586
+ function selectRootOpenAPISources(routes, projectConfig) {
2587
+ const defaultLocale = projectConfig.locales?.default;
2588
+ const sourcesByFilePath = /* @__PURE__ */ new Map();
2589
+ for (const route of routes) {
2590
+ if (route.kind !== "openapi" || route.diagnostic || !route.source.content || route.openapi?.tagFilter?.length || route.isBareAlias || defaultLocale && route.locale && route.locale !== defaultLocale) continue;
2591
+ sourcesByFilePath.set(route.source.filePath, {
2592
+ id: route.openapi?.sourceSpecId ?? route.source.filePath.split(/[\\/]/).pop()?.replace(/\.openapi\.(?:json|ya?ml)$/i, "") ?? "service",
2593
+ filePath: route.source.filePath,
2594
+ document: JSON.parse(route.source.content)
2595
+ });
2596
+ }
2597
+ return [...sourcesByFilePath.values()];
2598
+ }
2599
+ function createRootOpenAPISpec(routes, projectConfig) {
2600
+ return aggregateOpenAPISources(selectRootOpenAPISources(routes, projectConfig), {
2601
+ title: projectConfig.title,
2602
+ description: projectConfig.description
2603
+ });
2604
+ }
2605
+ function createOpenAPIRouteArtifacts(route) {
2606
+ if (route.kind !== "openapi" || !route.source.content) return [];
2607
+ const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
2608
+ if (!contentArtifactUrl) return [];
2609
+ return [{
2610
+ fileName: contentArtifactUrl.replace(/^\//, ""),
2611
+ contentType: "application/json; charset=utf-8",
2612
+ source: route.source.content
2613
+ }, {
2614
+ fileName: contentArtifactUrl.replace(/\.json$/, ".yaml").replace(/^\//, ""),
2615
+ contentType: "text/yaml; charset=utf-8",
2616
+ source: (0, yaml.stringify)(JSON.parse(route.source.content), { lineWidth: 0 })
2617
+ }];
2618
+ }
2619
+ function createRootOpenAPIArtifact(routes, projectConfig) {
2620
+ return {
2621
+ fileName: "openapi.json",
2622
+ contentType: "application/json; charset=utf-8",
2623
+ source: JSON.stringify(createRootOpenAPISpec(routes, projectConfig), null, 2)
2624
+ };
2625
+ }
2626
+ //#endregion
2627
+ //#region source/plugins/content-artifacts/provider.ts
2628
+ function createContentArtifacts(routes, projectConfig) {
2629
+ const artifacts = [];
2630
+ for (const route of routes) {
2631
+ if (route.kind === "openapi") {
2632
+ artifacts.push(...createOpenAPIRouteArtifacts(route));
2633
+ continue;
2634
+ }
2635
+ const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
2636
+ if (!contentArtifactUrl) continue;
2637
+ artifacts.push({
2638
+ fileName: contentArtifactUrl.replace(/^\//, ""),
2639
+ contentType: "text/markdown; charset=utf-8",
2640
+ source: readRouteArtifactContent(route)
2641
+ });
2642
+ }
2643
+ artifacts.push({
2644
+ fileName: "llms.txt",
2645
+ contentType: "text/plain; charset=utf-8",
2646
+ source: createLlmsTxtArtifact(routes, projectConfig)
2647
+ });
2648
+ artifacts.push(createRootOpenAPIArtifact(routes, projectConfig));
2649
+ return artifacts;
2650
+ }
2651
+ function findContentArtifact(path, routes, projectConfig) {
2652
+ const fileName = path.replace(/^\/+/, "");
2653
+ return createContentArtifacts(routes, projectConfig).find((artifact) => artifact.fileName === fileName);
2654
+ }
2655
+ //#endregion
2027
2656
  //#region source/plugins/content-artifacts/server.ts
2028
2657
  function normalizeRoutePrefix$1(routePrefix) {
2029
2658
  if (!routePrefix || routePrefix === "/") return "";
@@ -2035,34 +2664,12 @@ function resolveContentArtifactPath(url, projectConfig) {
2035
2664
  const hasBasePath = requestPath === basePath || requestPath.startsWith(`${basePath}/`);
2036
2665
  return basePath && hasBasePath ? requestPath.slice(basePath.length) || "/" : requestPath;
2037
2666
  }
2038
- function resolveContentArtifactType(route) {
2039
- return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
2040
- }
2041
2667
  function serveContentArtifacts(req, res, projectConfig, routes) {
2042
- const contentPath = resolveContentArtifactPath(req.url, projectConfig);
2043
- if (contentPath === "/llms.txt") {
2044
- res.statusCode = 200;
2045
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
2046
- res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
2047
- return true;
2048
- }
2049
- if (contentPath.endsWith(".openapi.yaml")) {
2050
- const jsonPath = contentPath.replace(/\.yaml$/, ".json");
2051
- const route = routes.find((route) => route.artifacts?.contentArtifactUrl === jsonPath);
2052
- if (route?.kind === "openapi" && route.source.content) {
2053
- const spec = JSON.parse(route.source.content);
2054
- res.statusCode = 200;
2055
- res.setHeader("Content-Type", "text/yaml; charset=utf-8");
2056
- res.end((0, yaml.stringify)(spec, { lineWidth: 0 }), "utf8");
2057
- return true;
2058
- }
2059
- return false;
2060
- }
2061
- const route = routes.find((route) => route.artifacts?.contentArtifactUrl === contentPath);
2062
- if (!route) return false;
2668
+ const artifact = findContentArtifact(resolveContentArtifactPath(req.url, projectConfig), routes, projectConfig);
2669
+ if (!artifact) return false;
2063
2670
  res.statusCode = 200;
2064
- res.setHeader("Content-Type", resolveContentArtifactType(route));
2065
- res.end(readRouteArtifactContent(route), "utf8");
2671
+ res.setHeader("Content-Type", artifact.contentType);
2672
+ res.end(artifact.source);
2066
2673
  return true;
2067
2674
  }
2068
2675
  //#endregion
@@ -2083,30 +2690,7 @@ function createContentArtifactsPlugin() {
2083
2690
  });
2084
2691
  };
2085
2692
  },
2086
- "build:assets": (ctx) => {
2087
- const assets = [];
2088
- for (const route of ctx.routes) {
2089
- const contentArtifactUrl = route.artifacts?.contentArtifactUrl;
2090
- if (!contentArtifactUrl) continue;
2091
- assets.push({
2092
- fileName: contentArtifactUrl.replace(/^\//, ""),
2093
- source: readRouteArtifactContent(route)
2094
- });
2095
- if (route.kind === "openapi" && route.source.content) {
2096
- const yamlFileName = contentArtifactUrl.replace(/\.json$/, ".yaml");
2097
- const spec = JSON.parse(route.source.content);
2098
- assets.push({
2099
- fileName: yamlFileName.replace(/^\//, ""),
2100
- source: (0, yaml.stringify)(spec, { lineWidth: 0 })
2101
- });
2102
- }
2103
- }
2104
- assets.push({
2105
- fileName: "llms.txt",
2106
- source: createLlmsTxtArtifact(ctx.routes, ctx.projectConfig)
2107
- });
2108
- return assets;
2109
- }
2693
+ "build:assets": (ctx) => createContentArtifacts(ctx.routes, ctx.projectConfig)
2110
2694
  }
2111
2695
  };
2112
2696
  }
@@ -3447,7 +4031,7 @@ async function resolveRoutePages(routes, plugins, ctx) {
3447
4031
  pageByRouteIndex.set(routeIndex, page);
3448
4032
  }
3449
4033
  if (pageByRouteIndex.size !== routes.length) throw new Error("[clarify] pages:resolved hooks must return exactly one page for each route");
3450
- return routes.map((route, routeIndex) => {
4034
+ const updatedRoutes = routes.map((route, routeIndex) => {
3451
4035
  const page = pageByRouteIndex.get(routeIndex);
3452
4036
  return {
3453
4037
  ...route,
@@ -3458,6 +4042,7 @@ async function resolveRoutePages(routes, plugins, ctx) {
3458
4042
  }
3459
4043
  };
3460
4044
  });
4045
+ return Promise.all(updatedRoutes.map((route) => reanalyzeMarkdownRoute(route, ctx.projectRoot)));
3461
4046
  }
3462
4047
  function buildNavigationTree(routes, ctx) {
3463
4048
  const tabs = ctx.projectConfig.navigation?.tabs;
@@ -3810,6 +4395,20 @@ async function buildSSRBundle(root, ssrEntry, ssrOutDir, plugins) {
3810
4395
  }
3811
4396
  });
3812
4397
  }
4398
+ function resolveSSRBundlePath(ssrOutputDir) {
4399
+ for (const entryName of [
4400
+ "entry-server.js",
4401
+ "entry-server.mjs",
4402
+ "entry-server.cjs"
4403
+ ]) {
4404
+ const candidate = (0, node_path.join)(ssrOutputDir, entryName);
4405
+ if ((0, node_fs.existsSync)(candidate)) return candidate;
4406
+ }
4407
+ const fallback = (0, node_fs.readdirSync)(ssrOutputDir).filter((name) => name.startsWith("entry-server.")).sort((a, b) => a.localeCompare(b))[0];
4408
+ if (fallback) return (0, node_path.join)(ssrOutputDir, fallback);
4409
+ const files = (0, node_fs.readdirSync)(ssrOutputDir).sort((a, b) => a.localeCompare(b)).slice(0, 20);
4410
+ 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)"}`);
4411
+ }
3813
4412
  async function renderSSGRoutes(routes, contextOrConfig, outputDirectory, ssrBundlePath) {
3814
4413
  const { render } = await import((0, node_url.pathToFileURL)(ssrBundlePath).href);
3815
4414
  const template = readIndexHtml(outputDirectory);
@@ -4055,8 +4654,9 @@ var ClarifyEngine = class {
4055
4654
  * - `skipModules`: skip `buildModules()` (check command).
4056
4655
  * - `skipHints`: skip `logStartupHints()` (check command).
4057
4656
  *
4058
- * Idempotent: calling prepare() again with the same env is a no-op unless
4059
- * `force` is set.
4657
+ * Configuration initialization is cached unless `force` is set. Discovery
4658
+ * and virtual module generation intentionally run on every call so callers
4659
+ * observe content changes without rebuilding the Engine instance.
4060
4660
  */
4061
4661
  async prepare(env = {
4062
4662
  command: this.runtime.command,
@@ -4176,25 +4776,25 @@ var ClarifyEngine = class {
4176
4776
  assertNoContentDiagnostics(this.routes);
4177
4777
  const outputDir = this.runtime.outputDirectory ?? this.generateOptions.outputDirectory;
4178
4778
  if (!outputDir) throw new Error("[clarify] outputDirectory is required before SSG runs");
4179
- const ssrOutputDir = (0, node_path.join)(outputDir, ".ssr");
4779
+ let ssrOutputDir;
4180
4780
  let tempEntryPath;
4181
4781
  try {
4182
4782
  tempEntryPath = createTempEntryFile(SSR_ENTRY_CODE);
4783
+ ssrOutputDir = createClarifyTempDir("ssr-output");
4183
4784
  await (this.runtime.buildSSRBundle ?? ((options) => buildSSRBundle(options.root, options.ssrEntry, options.ssrOutDir, options.plugins)))({
4184
4785
  root: this.root,
4185
4786
  ssrEntry: tempEntryPath,
4186
4787
  ssrOutDir: ssrOutputDir,
4187
4788
  plugins: this.runtime.ssrPlugins ?? []
4188
4789
  });
4189
- const ssrBundlePath = (0, node_path.join)(ssrOutputDir, "entry-server.js");
4790
+ const ssrBundlePath = resolveSSRBundlePath(ssrOutputDir);
4190
4791
  await renderSSGRoutes(this.routes, this.runtimeContext(), outputDir, ssrBundlePath);
4191
4792
  } catch (err) {
4192
4793
  console.error("[clarify] SSG failed:", err);
4193
4794
  throw err;
4194
4795
  } finally {
4195
- if (tempEntryPath) try {
4196
- (0, node_fs.rmSync)(tempEntryPath, { force: true });
4197
- } catch {}
4796
+ removeClarifyTempDir(tempEntryPath ? (0, node_path.resolve)(tempEntryPath, "..") : void 0);
4797
+ removeClarifyTempDir(ssrOutputDir);
4198
4798
  }
4199
4799
  });
4200
4800
  await this.runBuildDone();
@@ -4305,4 +4905,4 @@ Object.defineProperty(exports, "searchMcpIndex", {
4305
4905
  }
4306
4906
  });
4307
4907
 
4308
- //# sourceMappingURL=engine-0ROqtF4o.cjs.map
4908
+ //# sourceMappingURL=engine-B_mPOpWr.cjs.map