@clarify-labs/cli 0.10.1 → 0.10.3

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.
@@ -2,13 +2,16 @@ import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path
2
2
  import mdxPlugin from "@mdx-js/rollup";
3
3
  import tailwindcss from "@tailwindcss/vite";
4
4
  import react from "@vitejs/plugin-react";
5
+ import rehypeRaw from "rehype-raw";
5
6
  import { compile } from "@mdx-js/mdx";
6
- import GithubSlugger, { slug } from "github-slugger";
7
- import { toString } from "mdast-util-to-string";
8
- import { bundledLanguages, createCssVariablesTheme, createHighlighter } from "shiki";
7
+ import rehypeShiki from "@shikijs/rehype";
8
+ import rehypeSlug from "rehype-slug";
9
+ import { createCssVariablesTheme } from "shiki";
9
10
  import { visit } from "unist-util-visit";
10
- import { markdownRemarkPlugins, themeBootstrapScript, themePresets } from "@clarify-labs/renderer";
11
+ import { markdownRemarkPlugins, parseCodeMeta, themeBootstrapScript, themePresets } from "@clarify-labs/renderer";
11
12
  import { readFile, readdir, stat } from "node:fs/promises";
13
+ import GithubSlugger, { slug } from "github-slugger";
14
+ import { toString } from "mdast-util-to-string";
12
15
  import { remark } from "remark";
13
16
  import matter from "gray-matter";
14
17
  import { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
@@ -19,38 +22,29 @@ import { fileURLToPath, pathToFileURL } from "node:url";
19
22
  import SwaggerParser from "@apidevtools/swagger-parser";
20
23
  import { tmpdir } from "node:os";
21
24
  import { createRequire } from "node:module";
22
- //#region source/parsers/markdown/utils.ts
23
- function escapeHtml(text) {
24
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
25
+ //#region source/parsers/content/diagnostic.ts
26
+ function displayPath(filePath, projectRoot) {
27
+ if (!filePath) return void 0;
28
+ if (!projectRoot) return filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "");
29
+ const normalized = relative(projectRoot, filePath);
30
+ return normalized && !normalized.startsWith("..") && !isAbsolute(normalized) ? normalized : filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "");
31
+ }
32
+ function createContentDiagnostic(options) {
33
+ const { error } = options;
34
+ return {
35
+ kind: options.kind,
36
+ title: options.title,
37
+ message: options.message,
38
+ filePath: displayPath(options.filePath, options.projectRoot),
39
+ details: error instanceof Error ? options.includeStack ? error.stack ?? error.message : error.message : String(error)
40
+ };
25
41
  }
26
42
  //#endregion
27
43
  //#region source/parsers/markdown/mdx.ts
28
44
  function getLanguage(className) {
29
45
  return (Array.isArray(className) ? className : typeof className === "string" ? className.split(/\s+/) : []).find((item) => typeof item === "string" && item.startsWith("language-"))?.replace(/^language-/, "") || "txt";
30
46
  }
31
- function getTextContent(node) {
32
- if (!node) return "";
33
- if (typeof node.value === "string") return node.value;
34
- return node.children?.map(getTextContent).join("") ?? "";
35
- }
36
- function renderPlainCode(code) {
37
- return code.split("\n").map((line) => `<span>${escapeHtml(line)}</span>`).join("\n");
38
- }
39
- function isBundledLanguage(language) {
40
- return language in bundledLanguages;
41
- }
42
- var highlighterPromise;
43
47
  var cssVariablesTheme = createCssVariablesTheme();
44
- async function getHighlighter() {
45
- highlighterPromise = highlighterPromise ?? createHighlighter({
46
- themes: [cssVariablesTheme],
47
- langs: []
48
- });
49
- return highlighterPromise;
50
- }
51
- function renderHighlightedCode(html) {
52
- return (/<code[^>]*>([\s\S]*?)<\/code>/.exec(html)?.[1] ?? html).split("\n").map((line) => line.trimStart().startsWith("<span") ? line : `<span>${line}</span>`).join("\n");
53
- }
54
48
  function rehypeParseCodeBlocks() {
55
49
  return (tree) => {
56
50
  visit(tree, "element", (node, _nodeIndex, parentNode) => {
@@ -68,65 +62,31 @@ function rehypeParseCodeBlocks() {
68
62
  });
69
63
  };
70
64
  }
71
- function rehypeShiki() {
72
- return async (tree) => {
73
- const shikiHighlighter = await getHighlighter();
74
- const languages = /* @__PURE__ */ new Set();
75
- visit(tree, "element", (node) => {
76
- if (node.tagName !== "pre") return;
77
- const codeNode = node.children?.[0];
78
- if (codeNode?.tagName !== "code") return;
79
- const language = typeof node.properties?.language === "string" ? node.properties.language : getLanguage(codeNode.properties?.className);
80
- if (language && language !== "txt" && isBundledLanguage(language)) languages.add(language);
81
- });
82
- if (languages.size > 0) await shikiHighlighter.loadLanguage(...languages);
83
- visit(tree, "element", (node) => {
84
- if (node.tagName !== "pre") return;
85
- const codeNode = node.children?.[0];
86
- if (codeNode?.tagName !== "code") return;
87
- node.properties = node.properties ?? {};
88
- codeNode.properties = codeNode.properties ?? {};
89
- const code = getTextContent(codeNode);
90
- const language = typeof node.properties.language === "string" ? node.properties.language : getLanguage(codeNode.properties.className);
91
- node.properties.code = code;
92
- codeNode.properties.code = code;
93
- codeNode.properties.language = language;
94
- let highlighted = renderPlainCode(code);
95
- if (language && language !== "txt") try {
96
- highlighted = renderHighlightedCode(shikiHighlighter.codeToHtml(code, {
97
- lang: language,
98
- theme: cssVariablesTheme
99
- }));
100
- } catch {
101
- highlighted = renderPlainCode(code);
102
- }
103
- codeNode.children = [{
104
- type: "text",
105
- value: highlighted
106
- }];
107
- });
108
- };
109
- }
110
- function rehypeSlugSections() {
111
- return (tree) => {
112
- const slugger = new GithubSlugger();
113
- visit(tree, "element", (node) => {
114
- if (node.tagName !== "h2" && node.tagName !== "h3") return;
115
- node.properties = node.properties ?? {};
116
- if (node.properties.id) return;
117
- node.properties.id = slugger.slug(toString(node));
118
- });
119
- };
120
- }
121
65
  var remarkPlugins = markdownRemarkPlugins;
122
66
  var rehypePlugins = [
123
- rehypeSlugSections,
67
+ rehypeSlug,
124
68
  rehypeParseCodeBlocks,
125
- rehypeShiki
69
+ [rehypeShiki, {
70
+ theme: cssVariablesTheme,
71
+ langs: [],
72
+ lazy: true,
73
+ addLanguageClass: true,
74
+ fallbackLanguage: "text",
75
+ parseMetaString: parseCodeMeta
76
+ }]
126
77
  ];
127
- function formatMdxDiagnostic(error) {
128
- if (error instanceof Error) return error.message;
129
- return String(error);
78
+ function createContentCompileDiagnostic(options) {
79
+ const { format, phase, error, filePath, projectRoot } = options;
80
+ const label = format === "mdx" ? "MDX" : "Markdown";
81
+ return createContentDiagnostic({
82
+ kind: format,
83
+ title: `${label} ${phase} error`,
84
+ message: phase === "syntax" ? "This page could not be compiled. Fix the MDX syntax or component usage above, then reload the route." : `This page could not be compiled. Fix the ${label} syntax or plugin error below, then reload the route.`,
85
+ error,
86
+ filePath,
87
+ projectRoot,
88
+ includeStack: phase === "compilation"
89
+ });
130
90
  }
131
91
  /**
132
92
  * Validate MDX content for diagnostics WITHOUT running the build-time rehype
@@ -134,7 +94,8 @@ function formatMdxDiagnostic(error) {
134
94
  *
135
95
  * `rehypePlugins` includes `rehypeShiki`, which loads grammar/WASM assets and
136
96
  * is expensive. The real highlighting happens at Vite build time via the
137
- * `@mdx-js/rollup` plugin (see `createMdxPlugin` in `core/adapters.ts`), which
97
+ * `@mdx-js/rollup` plugins (see `createMarkdownPlugin` and `createMdxPlugin`
98
+ * in `core/adapters.ts`), which
138
99
  * runs the full remark + rehype pipeline. Re-running it here, during route
139
100
  * discovery (Phase 3), would compile every `.mdx` file twice and invoke Shiki
140
101
  * once per file per `engine.refresh()` — see ARCHITECTURE.md §2.2 which states
@@ -156,13 +117,13 @@ async function compileMdxContent(content, filePath, projectRoot) {
156
117
  } catch (error) {
157
118
  return {
158
119
  ok: false,
159
- diagnostic: {
160
- kind: "mdx",
161
- title: "MDX syntax error",
162
- message: "This page could not be compiled. Fix the MDX syntax or component usage above, then reload the route.",
163
- filePath: filePath && projectRoot ? filePath.replace(projectRoot + "/", "").replace(/^\/+/, "") : filePath ? filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "") : void 0,
164
- details: formatMdxDiagnostic(error)
165
- }
120
+ diagnostic: createContentCompileDiagnostic({
121
+ format: "mdx",
122
+ phase: "syntax",
123
+ error,
124
+ filePath,
125
+ projectRoot
126
+ })
166
127
  };
167
128
  }
168
129
  }
@@ -283,6 +244,11 @@ function kebabToTitle(str) {
283
244
  function parseMdxTree(content) {
284
245
  return remark.parse(content);
285
246
  }
247
+ async function compileRouteDiagnostic(content, filePath, baseDir) {
248
+ if (extname(filePath).toLowerCase() !== ".mdx") return void 0;
249
+ const result = await compileMdxContent(content, filePath, baseDir);
250
+ return result.ok ? void 0 : result.diagnostic;
251
+ }
286
252
  /** 从内容中提取第一个 H1 标题 */
287
253
  function extractH1(content) {
288
254
  const tree = parseMdxTree(content);
@@ -377,7 +343,7 @@ async function findContentRoutes(dir, base = dir, options = {}) {
377
343
  frontmatter,
378
344
  content
379
345
  };
380
- const mdxResult = await compileMdxContent(page.content, fullPath, base);
346
+ const diagnostic = await compileRouteDiagnostic(page.content, fullPath, base);
381
347
  let title = typeof page.frontmatter.title === "string" ? page.frontmatter.title : "";
382
348
  if (!title) {
383
349
  const lastPart = pathParts[pathParts.length - 1] ?? "";
@@ -399,7 +365,7 @@ async function findContentRoutes(dir, base = dir, options = {}) {
399
365
  frontmatter: page.frontmatter,
400
366
  content: page.content
401
367
  },
402
- diagnostic: mdxResult.ok ? void 0 : mdxResult.diagnostic
368
+ diagnostic
403
369
  });
404
370
  }
405
371
  }
@@ -513,6 +479,7 @@ function routeIntentFromPagesItem(item) {
513
479
  kind: "page",
514
480
  ref: item
515
481
  };
482
+ if ("group" in item) throw new Error("Navigation groups do not resolve to route intents");
516
483
  if ("openapi" in item) return {
517
484
  kind: "openapi",
518
485
  ref: item.openapi,
@@ -540,15 +507,19 @@ function firstNavigationPath(nodes) {
540
507
  }
541
508
  function collectExplicitPagePathIntents(tabs) {
542
509
  const intents = [];
543
- for (const tab of tabs ?? []) {
544
- if (!tab.pages || tab.pages === "FileTree") continue;
545
- for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) {
510
+ const collectItems = (items) => {
511
+ for (const item of items) if (typeof item !== "string" && "group" in item) collectItems(item.pages);
512
+ else if (typeof item !== "string" && "page" in item && item.path) {
546
513
  const intent = routeIntentFromPagesItem(item);
547
514
  if (intent.kind === "page" && intent.path) intents.push({
548
515
  ...intent,
549
516
  path: intent.path
550
517
  });
551
518
  }
519
+ };
520
+ for (const tab of tabs ?? []) {
521
+ if (!tab.pages || tab.pages === "FileTree") continue;
522
+ for (const group of tab.pages) collectItems(group.pages);
552
523
  }
553
524
  return intents;
554
525
  }
@@ -599,28 +570,38 @@ function buildNavigationFromTabsConfig(routes, tabs) {
599
570
  }
600
571
  function buildNavigationFromConfig(routes, config) {
601
572
  const routeMap = new Map(routes.map((r) => [r.path, r]));
602
- return config.map((group) => {
603
- const children = group.pages.map((item) => {
604
- const intent = routeIntentFromPagesItem(item);
605
- if (intent.kind === "openapi") {
606
- const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
607
- const route = routeMap.get(path);
608
- return {
609
- path,
610
- title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
611
- icon: intent.icon,
612
- sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
613
- };
614
- }
615
- const path = routePathFromRef(intent.ref, intent.path);
573
+ const buildItem = (item) => {
574
+ if (typeof item !== "string" && "group" in item) {
575
+ const children = item.pages.map(buildItem);
576
+ return {
577
+ path: firstNavigationPath(children),
578
+ title: resolveLocalizedText(item.group, "", "") ?? "",
579
+ icon: item.icon,
580
+ children
581
+ };
582
+ }
583
+ const intent = routeIntentFromPagesItem(item);
584
+ if (intent.kind === "openapi") {
585
+ const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
616
586
  const route = routeMap.get(path);
617
587
  return {
618
- path: intent.redirect ? normalizePath(intent.redirect) : path,
588
+ path,
619
589
  title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
620
590
  icon: intent.icon,
621
591
  sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
622
592
  };
623
- });
593
+ }
594
+ const path = routePathFromRef(intent.ref, intent.path);
595
+ const route = routeMap.get(path);
596
+ return {
597
+ path: intent.redirect ? normalizePath(intent.redirect) : path,
598
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
599
+ icon: intent.icon,
600
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
601
+ };
602
+ };
603
+ return config.map((group) => {
604
+ const children = group.pages.map(buildItem);
624
605
  return {
625
606
  path: children[0]?.path ?? "/",
626
607
  title: resolveLocalizedText(group.group, "", "") ?? "",
@@ -642,19 +623,29 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
642
623
  path: route.basePath ?? route.path
643
624
  }))), locale, i18n);
644
625
  const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
645
- return config.map((group) => {
646
- const children = group.pages.map((item) => {
647
- const intent = routeIntentFromPagesItem(item);
648
- const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
649
- const route = routeMap.get(basePath);
650
- const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
626
+ const buildItem = (item) => {
627
+ if (typeof item !== "string" && "group" in item) {
628
+ const children = item.pages.map(buildItem);
651
629
  return {
652
- path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, i18n) : void 0) ?? path,
653
- title: resolveLocalizedText(intent.title, locale, i18n.defaultLocale) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
654
- icon: intent.icon,
655
- sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
630
+ path: firstNavigationPath(children),
631
+ title: resolveLocalizedText(item.group, locale, i18n.defaultLocale) ?? "",
632
+ icon: item.icon,
633
+ children
656
634
  };
657
- });
635
+ }
636
+ const intent = routeIntentFromPagesItem(item);
637
+ const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
638
+ const route = routeMap.get(basePath);
639
+ const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
640
+ return {
641
+ path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, i18n) : void 0) ?? path,
642
+ title: resolveLocalizedText(intent.title, locale, i18n.defaultLocale) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
643
+ icon: intent.icon,
644
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
645
+ };
646
+ };
647
+ return config.map((group) => {
648
+ const children = group.pages.map(buildItem);
658
649
  return {
659
650
  path: children[0]?.path ?? localizedRoutePath("/", locale, i18n),
660
651
  title: resolveLocalizedText(group.group, locale, i18n.defaultLocale) ?? "",
@@ -899,8 +890,9 @@ var clarifySourceConfigSchema = z.object({
899
890
  branch: z.string().optional(),
900
891
  directory: z.string().optional()
901
892
  });
902
- var clarifyPagesItemSchema = z.union([
893
+ var clarifyPagesItemSchema = z.lazy(() => z.union([
903
894
  z.string(),
895
+ clarifyPagesGroupSchema,
904
896
  z.object({
905
897
  page: z.string(),
906
898
  path: z.string().optional(),
@@ -915,12 +907,12 @@ var clarifyPagesItemSchema = z.union([
915
907
  title: clarifyLocalizedTextSchema.optional(),
916
908
  filter: z.object({ tags: z.array(z.string()).optional() }).optional()
917
909
  })
918
- ]);
919
- var clarifyPagesGroupSchema = z.object({
910
+ ]));
911
+ var clarifyPagesGroupSchema = z.lazy(() => z.object({
920
912
  group: clarifyLocalizedTextSchema,
921
913
  icon: z.string().optional(),
922
914
  pages: z.array(clarifyPagesItemSchema)
923
- });
915
+ }));
924
916
  var clarifyPagesConfigSchema = z.union([z.array(clarifyPagesGroupSchema), z.literal("FileTree")]);
925
917
  var clarifyTabItemSchema = z.object({
926
918
  tab: clarifyLocalizedTextSchema,
@@ -1268,16 +1260,10 @@ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
1268
1260
  }
1269
1261
  return `export const runtimeSlots = {\n${[...grouped.entries()].map(([name, list]) => ` ${JSON.stringify(name)}: [\n ${list.join(",\n ")}\n ]`).join(",\n")}\n};\n`;
1270
1262
  }
1271
- function generateMdxErrorModule(diagnostic) {
1263
+ function generateContentDiagnosticModule(diagnostic) {
1272
1264
  return `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';
1273
1265
 
1274
- export const contentDiagnostic = ${JSON.stringify({
1275
- kind: diagnostic.kind ?? "mdx",
1276
- title: diagnostic.title,
1277
- message: diagnostic.message,
1278
- filePath: diagnostic.filePath,
1279
- details: diagnostic.details
1280
- })};
1266
+ export const contentDiagnostic = ${JSON.stringify(diagnostic)};
1281
1267
 
1282
1268
  export default createContentDiagnosticComponent(contentDiagnostic);
1283
1269
  `;
@@ -1295,7 +1281,7 @@ function buildVirtualModules(args) {
1295
1281
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1296
1282
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1297
1283
  for (const route of args.routes) {
1298
- const moduleContent = route.diagnostic ? generateMdxErrorModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1284
+ const moduleContent = route.diagnostic ? generateContentDiagnosticModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1299
1285
  modules.set(route.module.virtualModuleId, moduleContent);
1300
1286
  }
1301
1287
  return modules;
@@ -1330,9 +1316,9 @@ async function refreshDevServer(engine, server) {
1330
1316
  invalidateVirtualModules(engine, server);
1331
1317
  return hasDevStructureChanged(before, createDevStructureSnapshot(engine.routes, engine.navigation));
1332
1318
  }
1333
- function createNormalizedMdxContentPlugin(engine) {
1319
+ function createNormalizedContentPlugin(engine) {
1334
1320
  return {
1335
- name: "clarify:normalized-mdx-content",
1321
+ name: "clarify:normalized-content",
1336
1322
  enforce: "pre",
1337
1323
  transform(_code, id) {
1338
1324
  if (!/\.mdx?(?:\?|$)/.test(id)) return null;
@@ -1346,16 +1332,44 @@ function createNormalizedMdxContentPlugin(engine) {
1346
1332
  }
1347
1333
  };
1348
1334
  }
1349
- function createMdxPlugin() {
1350
- return mdxPlugin({
1351
- include: ["**/*.{md,mdx}"],
1335
+ function createContentCompileTransform(transform, format, projectRoot, errorPolicy = () => "diagnostic") {
1336
+ return async function contentCompileTransform(code, id) {
1337
+ try {
1338
+ return await transform.call(this, code, id);
1339
+ } catch (error) {
1340
+ if (errorPolicy() === "throw") throw error;
1341
+ return {
1342
+ code: generateContentDiagnosticModule(createContentCompileDiagnostic({
1343
+ format,
1344
+ phase: "compilation",
1345
+ error,
1346
+ filePath: id.replace(/\?.*$/, ""),
1347
+ projectRoot
1348
+ })),
1349
+ map: null
1350
+ };
1351
+ }
1352
+ };
1353
+ }
1354
+ function createContentCompilerPlugin(format, projectRoot, errorPolicy) {
1355
+ const markdown = format === "markdown";
1356
+ const plugin = mdxPlugin({
1357
+ include: [markdown ? "**/*.md" : "**/*.mdx"],
1358
+ format: markdown ? "md" : "mdx",
1352
1359
  jsxImportSource: "react",
1353
1360
  providerImportSource: "@clarify-labs/renderer",
1354
1361
  remarkPlugins,
1355
- rehypePlugins
1362
+ ...markdown ? { remarkRehypeOptions: { allowDangerousHtml: true } } : {},
1363
+ rehypePlugins: markdown ? [rehypeRaw, ...rehypePlugins] : rehypePlugins
1356
1364
  });
1365
+ const transform = typeof plugin.transform === "function" ? plugin.transform : plugin.transform?.handler;
1366
+ if (!transform) return plugin;
1367
+ return {
1368
+ ...plugin,
1369
+ transform: createContentCompileTransform(transform, format, projectRoot, errorPolicy)
1370
+ };
1357
1371
  }
1358
- function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1372
+ function createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx) {
1359
1373
  let viteConfig;
1360
1374
  return {
1361
1375
  name: "clarify:core",
@@ -1457,7 +1471,8 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1457
1471
  outputDirectory: viteConfig.build.outDir,
1458
1472
  ssrPlugins: [
1459
1473
  engine.createSSGVirtualPlugin(),
1460
- normalizedMdxContentPlugin,
1474
+ normalizedContentPlugin,
1475
+ markdown,
1461
1476
  mdx
1462
1477
  ]
1463
1478
  });
@@ -1474,20 +1489,31 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1474
1489
  * site itself - it only bridges Vite lifecycle hooks to the engine.
1475
1490
  */
1476
1491
  function createViteAdapter(engine) {
1477
- const normalizedMdxContentPlugin = createNormalizedMdxContentPlugin(engine);
1478
- const mdx = createMdxPlugin();
1492
+ let contentCompileErrorPolicy = "diagnostic";
1493
+ const errorPolicy = () => contentCompileErrorPolicy;
1494
+ const normalizedContentPlugin = createNormalizedContentPlugin(engine);
1495
+ const markdown = createContentCompilerPlugin("markdown", engine.root, errorPolicy);
1496
+ const mdx = createContentCompilerPlugin("mdx", engine.root, errorPolicy);
1497
+ const compilePolicyPlugin = {
1498
+ name: "clarify:content-compile-policy",
1499
+ configResolved(config) {
1500
+ contentCompileErrorPolicy = config.command === "build" && engine.generateOptions.ssg.failOnError ? "throw" : "diagnostic";
1501
+ }
1502
+ };
1479
1503
  const tailwind = tailwindcss();
1480
1504
  return [
1481
1505
  ...react(),
1482
1506
  ...Array.isArray(tailwind) ? tailwind : [tailwind],
1483
- normalizedMdxContentPlugin,
1484
- createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx),
1507
+ compilePolicyPlugin,
1508
+ normalizedContentPlugin,
1509
+ createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx),
1510
+ markdown,
1485
1511
  mdx
1486
1512
  ];
1487
1513
  }
1488
1514
  var cliPackageVersion = {
1489
1515
  name: "@clarify-labs/cli",
1490
- version: "0.10.1",
1516
+ version: "0.10.3",
1491
1517
  license: "AGPL-3.0-only",
1492
1518
  type: "module",
1493
1519
  repository: {
@@ -1528,6 +1554,7 @@ var cliPackageVersion = {
1528
1554
  "@clarify-labs/templates": "workspace:*",
1529
1555
  "@mdx-js/mdx": "^3.1.1",
1530
1556
  "@mdx-js/rollup": "^3.1.1",
1557
+ "@shikijs/rehype": "^4.3.0",
1531
1558
  "@tailwindcss/vite": "^4.3.2",
1532
1559
  "@vitejs/plugin-react": "^6.0.3",
1533
1560
  "cac": "^7.0.0",
@@ -1542,6 +1569,8 @@ var cliPackageVersion = {
1542
1569
  "react-router": "^7.0.0",
1543
1570
  "react-router-dom": "^7.0.0",
1544
1571
  "react-zoom-pan-pinch": "^4.0.3",
1572
+ "rehype-raw": "^7.0.0",
1573
+ "rehype-slug": "^6.0.0",
1545
1574
  "remark": "^15.0.1",
1546
1575
  "remark-gfm": "^4.0.1",
1547
1576
  "shiki": "4.3.0",
@@ -1837,9 +1866,6 @@ var OPENAPI_HTTP_METHODS = [
1837
1866
  "trace"
1838
1867
  ];
1839
1868
  var CLARIFY_OPENAPI_SECTION_ID_EXTENSION = "x-clarify-section-id";
1840
- function errorMessage(error) {
1841
- return error instanceof Error ? error.message : String(error);
1842
- }
1843
1869
  function isSameOpenAPIFile(url, filePath) {
1844
1870
  try {
1845
1871
  return resolve(fileURLToPath(url)) === resolve(filePath);
@@ -1954,7 +1980,7 @@ function extractOpenAPISections(spec, filterTags) {
1954
1980
  }
1955
1981
  return sections;
1956
1982
  }
1957
- async function readOpenAPISpec(filePath, contentProcessor) {
1983
+ async function readOpenAPISpec(filePath, contentProcessor, projectRoot) {
1958
1984
  try {
1959
1985
  if (!contentProcessor) return {
1960
1986
  ok: true,
@@ -1977,13 +2003,14 @@ async function readOpenAPISpec(filePath, contentProcessor) {
1977
2003
  } catch (error) {
1978
2004
  return {
1979
2005
  ok: false,
1980
- diagnostic: {
2006
+ diagnostic: createContentDiagnostic({
1981
2007
  kind: "openapi",
1982
2008
  title: "OpenAPI spec parse failed",
1983
- message: `Clarify could not parse ${filePath}.`,
2009
+ message: "Clarify could not parse this OpenAPI specification.",
2010
+ error,
1984
2011
  filePath,
1985
- details: errorMessage(error)
1986
- }
2012
+ projectRoot
2013
+ })
1987
2014
  };
1988
2015
  }
1989
2016
  }
@@ -2014,31 +2041,24 @@ export const routeData = {};
2014
2041
  export default createOpenApiRouteComponent({ ...routeData, spec });
2015
2042
  `;
2016
2043
  }
2017
- function generateOpenAPIErrorModule(diagnostic) {
2018
- return `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';
2019
-
2020
- export const contentDiagnostic = ${JSON.stringify({
2021
- kind: diagnostic.kind ?? "openapi",
2022
- title: diagnostic.title,
2023
- message: diagnostic.message,
2024
- filePath: diagnostic.filePath,
2025
- details: diagnostic.details
2026
- })};
2027
-
2028
- export default createContentDiagnosticComponent(contentDiagnostic);
2029
- `;
2030
- }
2031
2044
  //#endregion
2032
2045
  //#region source/plugins/openapi/index.ts
2033
2046
  function collectOpenAPIPageIntents(config) {
2034
2047
  const intents = [];
2035
- function visitPages(pages) {
2036
- if (!pages || pages === "FileTree") return;
2037
- for (const group of pages) for (const item of group.pages) {
2048
+ function visitItems(items) {
2049
+ for (const item of items) {
2050
+ if (typeof item !== "string" && "group" in item) {
2051
+ visitItems(item.pages);
2052
+ continue;
2053
+ }
2038
2054
  const intent = routeIntentFromPagesItem(item);
2039
2055
  if (intent.kind === "openapi" && (intent.path || intent.tagFilter?.length)) intents.push(intent);
2040
2056
  }
2041
2057
  }
2058
+ function visitPages(pages) {
2059
+ if (!pages || pages === "FileTree") return;
2060
+ for (const group of pages) visitItems(group.pages);
2061
+ }
2042
2062
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
2043
2063
  return intents;
2044
2064
  }
@@ -2184,7 +2204,7 @@ function createOpenAPIPlugin() {
2184
2204
  const result = specFromCache ? {
2185
2205
  ok: true,
2186
2206
  spec: specFromCache
2187
- } : await readOpenAPISpec(route.source.filePath, processor);
2207
+ } : await readOpenAPISpec(route.source.filePath, processor, ctx.generateOptions.projectRoot);
2188
2208
  if (!result.ok) {
2189
2209
  route.diagnostic = result.diagnostic;
2190
2210
  route.meta.title = route.meta.title || "OpenAPI parse error";
@@ -2231,7 +2251,7 @@ function createOpenAPIPlugin() {
2231
2251
  for (const [routeSpecModuleId, spec] of specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2232
2252
  for (const route of ctx.routes) {
2233
2253
  if (route.kind !== "openapi") continue;
2234
- if (route.diagnostic) modules.set(route.module.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
2254
+ if (route.diagnostic) modules.set(route.module.virtualModuleId, generateContentDiagnosticModule(route.diagnostic));
2235
2255
  else if (route.openapi?.sourceSpecId) {
2236
2256
  const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2237
2257
  if (!specModules.has(routeSpecModuleId)) continue;
@@ -2686,6 +2706,11 @@ function writeClarifyEnvDts(projectRoot, plugins) {
2686
2706
  writeFileSync(filePath, next, "utf-8");
2687
2707
  }
2688
2708
  //#endregion
2709
+ //#region source/parsers/markdown/utils.ts
2710
+ function escapeHtml(text) {
2711
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2712
+ }
2713
+ //#endregion
2689
2714
  //#region source/core/runtime/html-template.ts
2690
2715
  /**
2691
2716
  * HTML template manipulation helpers used during SSG.
@@ -3058,6 +3083,12 @@ function loadVirtualModule(id, modules) {
3058
3083
  const bareId = stripVirtualPrefix(id);
3059
3084
  return modules.get(bareId) ?? modules.get(id) ?? null;
3060
3085
  }
3086
+ function assertNoContentDiagnostics(routes) {
3087
+ const failures = routes.filter((route) => route.diagnostic);
3088
+ if (failures.length === 0) return;
3089
+ const summary = failures.map((route) => `- ${route.path}: ${route.diagnostic?.title ?? "Content error"}${route.diagnostic?.filePath ? ` (${route.diagnostic.filePath})` : ""}`).join("\n");
3090
+ throw new Error(`[clarify] Content diagnostics prevented the build:\n${summary}`);
3091
+ }
3061
3092
  var ClarifyEngine = class {
3062
3093
  options;
3063
3094
  root;
@@ -3282,6 +3313,7 @@ var ClarifyEngine = class {
3282
3313
  await runPhase(this.plugins, "ssg", this.ctx, async () => {
3283
3314
  if (process.env.SKIP_CLARIFY_SSG) return;
3284
3315
  if (!await runInterceptHooks(this.plugins, "ssg:shouldRun", this.ctx)) return;
3316
+ if (this.generateOptions.ssg.failOnError) assertNoContentDiagnostics(this.routes);
3285
3317
  const outputDir = this.runtime.outputDirectory ?? this.generateOptions.outputDirectory;
3286
3318
  if (!outputDir) throw new Error("[clarify] outputDirectory is required before SSG runs");
3287
3319
  const ssrOutputDir = join(outputDir, ".ssr");
@@ -3342,4 +3374,4 @@ function createClarifyEngine(options = {}) {
3342
3374
  //#endregion
3343
3375
  export { cliPackageVersion as a, defineConfig as c, createClarifyTempDir as i, createClarifyEngine as n, cliPackageVersionWithCaret as o, createClarifyRuntimeAliases as r, createViteAdapter as s, ClarifyEngine as t };
3344
3376
 
3345
- //# sourceMappingURL=engine-Ca_2MCj9.js.map
3377
+ //# sourceMappingURL=engine-CyFtr8Pz.js.map