@clarify-labs/cli 0.11.3 → 0.11.5

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.
@@ -112,11 +112,11 @@ var rehypePlugins = [
112
112
  ];
113
113
  function createContentCompileDiagnostic(options) {
114
114
  const { format, phase, error, filePath, projectRoot } = options;
115
- const label = format === "mdx" ? "MDX" : "Markdown";
115
+ const label = format === "markdown+jsx" ? "MDX" : "Markdown";
116
116
  return createContentDiagnostic({
117
117
  kind: format,
118
118
  title: `${label} ${phase} error`,
119
- 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.`,
119
+ message: phase === "syntax" ? `This page could not be compiled. Fix the ${label} 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.`,
120
120
  error,
121
121
  filePath,
122
122
  projectRoot,
@@ -129,19 +129,24 @@ function createContentCompileDiagnostic(options) {
129
129
  *
130
130
  * `rehypePlugins` includes `rehypeShiki`, which loads grammar/WASM assets and
131
131
  * is expensive. The real highlighting happens at Vite build time via the
132
- * `@mdx-js/rollup` plugins (see `createMarkdownPlugin` and `createMdxPlugin`
133
- * in `core/adapters.ts`), which
134
- * runs the full remark + rehype pipeline. Re-running it here, during route
135
- * discovery (Phase 3), would compile every `.mdx` file twice and invoke Shiki
136
- * once per file per `engine.refresh()` — see ARCHITECTURE.md §2.2 which states
137
- * "不应在 core 中执行代码高亮".
132
+ * `@mdx-js/rollup` plugin (see `createContentCompilerPlugin` in
133
+ * `core/adapters.ts`), which runs the full remark + rehype pipeline.
134
+ * Re-running it here, during route discovery (Phase 3), would compile every
135
+ * `.mdx` file twice and invoke Shiki once per file per `engine.refresh()` -
136
+ * see ARCHITECTURE.md §2.2 which states "不应在 core 中执行代码高亮".
138
137
  *
139
138
  * The remark pipeline still runs (shared with the Vite plugin via
140
- * `remarkPlugins`), so MDX/JSX syntax errors — the only class of error this
141
- * diagnostic is meant to surface ahead of the Vite build — are caught. The
139
+ * `remarkPlugins`), so MDX/JSX syntax errors - the only class of error this
140
+ * diagnostic is meant to surface ahead of the Vite build - are caught. The
142
141
  * output is discarded; only the thrown error is used.
142
+ *
143
+ * `.md` files are handled by `compileMarkdownContent` in `markdown.ts`, which
144
+ * mirrors the adapter's markdown compiler semantics (`format: 'md'`, raw HTML
145
+ * allowed). Keep the two compilers separate so each format can evolve its own
146
+ * diagnostic rules without contaminating the other.
143
147
  */
144
- async function compileMdxContent(content, filePath, projectRoot) {
148
+ async function compileMdxContent(content, options = {}) {
149
+ const { filePath, projectRoot } = options;
145
150
  try {
146
151
  await (0, _mdx_js_mdx.compile)(content, {
147
152
  jsx: true,
@@ -153,7 +158,7 @@ async function compileMdxContent(content, filePath, projectRoot) {
153
158
  return {
154
159
  ok: false,
155
160
  diagnostic: createContentCompileDiagnostic({
156
- format: "mdx",
161
+ format: "markdown+jsx",
157
162
  phase: "syntax",
158
163
  error,
159
164
  filePath,
@@ -163,6 +168,21 @@ async function compileMdxContent(content, filePath, projectRoot) {
163
168
  }
164
169
  }
165
170
  //#endregion
171
+ //#region source/core/runtime/module-ids.ts
172
+ var PAGE_PREFIX = "virtual:clarify-page/";
173
+ var CONTENT_PREFIX = "virtual:clarify-content/";
174
+ function pageVirtualModuleId(path) {
175
+ return `${PAGE_PREFIX}${path.replace(/^\/+/, "").replace(/\/index$/, "").replace(/\/+/g, "/") || "index"}`;
176
+ }
177
+ function pageVirtualModuleIdFromRef(ref) {
178
+ return pageVirtualModuleId(ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, ""));
179
+ }
180
+ function contentVirtualModuleId(filePath, contentRoot) {
181
+ const sourcePath = (0, node_path.relative)(contentRoot, filePath).replace(/\\/g, "/");
182
+ if (!sourcePath || sourcePath.startsWith("../") || (0, node_path.isAbsolute)(sourcePath)) throw new Error(`Content source must be inside the content root: ${filePath}`);
183
+ return `${CONTENT_PREFIX}${sourcePath}`;
184
+ }
185
+ //#endregion
166
186
  //#region source/core/plugin/hooks.ts
167
187
  async function runHooks(plugins, hookName, input, ctx) {
168
188
  let result = input;
@@ -178,32 +198,43 @@ async function runHooks(plugins, hookName, input, ctx) {
178
198
  return result;
179
199
  }
180
200
  async function runDevConfigureServerHooks(plugins, server, ctx) {
201
+ const postHooks = [];
181
202
  for (const plugin of plugins) {
182
203
  const hook = plugin.hooks?.["dev:configureServer"];
183
204
  if (!hook) continue;
184
205
  try {
185
- await hook(server, ctx);
206
+ const postHook = await hook(server, ctx);
207
+ if (postHook) postHooks.push(postHook);
186
208
  } catch (err) {
187
209
  throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
188
210
  }
189
211
  }
212
+ return postHooks;
190
213
  }
191
- async function runCollectorHooks(plugins, hookName, ctx) {
192
- const results = [];
214
+ async function runBuildAssetsHooks(plugins, ctx) {
215
+ const assets = [];
216
+ const producerByFileName = /* @__PURE__ */ new Map();
193
217
  for (const plugin of plugins) {
194
- const hook = plugin.hooks?.[hookName];
218
+ const hook = plugin.hooks?.["build:assets"];
195
219
  if (!hook) continue;
220
+ let pluginAssets;
196
221
  try {
197
- const pluginResults = await hook(ctx);
198
- results.push(...pluginResults);
222
+ pluginAssets = await hook(ctx);
199
223
  } catch (err) {
200
- throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
224
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
225
+ }
226
+ for (const asset of pluginAssets) {
227
+ const fileName = asset.fileName.replace(/\\/g, "/").replace(/^\/+/, "");
228
+ const previousProducer = producerByFileName.get(fileName);
229
+ if (previousProducer) throw new Error(`[clarify] build asset "${fileName}" is emitted by both plugin "${previousProducer}" and plugin "${plugin.name}"`);
230
+ producerByFileName.set(fileName, plugin.name);
231
+ assets.push({
232
+ ...asset,
233
+ fileName
234
+ });
201
235
  }
202
236
  }
203
- return results;
204
- }
205
- async function runBuildAssetsHooks(plugins, ctx) {
206
- return runCollectorHooks(plugins, "build:assets", ctx);
237
+ return assets;
207
238
  }
208
239
  async function runBuildDoneHooks(plugins, ctx) {
209
240
  for (const plugin of plugins) {
@@ -238,7 +269,7 @@ function createContentProcessor(transform) {
238
269
  async processMdx(source, filePath) {
239
270
  const { frontmatter, content } = parseFrontmatter(source);
240
271
  const transformed = await transformContent(transform, {
241
- kind: "mdx",
272
+ kind: "markdown+jsx",
242
273
  source,
243
274
  filePath,
244
275
  frontmatter,
@@ -272,6 +303,54 @@ function createProjectContentProcessor(plugins, ctx) {
272
303
  return createContentProcessor((input) => runHooks(plugins, "content:transform", input, ctx));
273
304
  }
274
305
  //#endregion
306
+ //#region source/parsers/markdown/markdown.ts
307
+ /**
308
+ * Validate Markdown content for diagnostics WITHOUT running the build-time
309
+ * rehype pipeline (Shiki highlighting, slug injection, code-block parsing).
310
+ *
311
+ * `rehypePlugins` includes `rehypeShiki`, which loads grammar/WASM assets and
312
+ * is expensive. The real highlighting happens at Vite build time via the
313
+ * `@mdx-js/rollup` plugin (see `createContentCompilerPlugin` in
314
+ * `core/adapters.ts`), which runs the full remark + rehype pipeline.
315
+ * Re-running it here, during route discovery (Phase 3), would compile every
316
+ * `.md` file twice and invoke Shiki once per file per `engine.refresh()` -
317
+ * see ARCHITECTURE.md §2.2 which states "不应在 core 中执行代码高亮".
318
+ *
319
+ * `format: 'md'` mirrors the adapter's markdown compiler semantics (no JSX,
320
+ * raw HTML allowed via `remarkRehypeOptions`), so the same class of syntax
321
+ * errors that would break the Vite build is surfaced as a diagnostic here
322
+ * instead of leaking out as an opaque
323
+ * `Failed to fetch dynamically imported module` at runtime.
324
+ *
325
+ * MDX files are handled by `compileMdxContent` in `mdx.ts`. Keep the two
326
+ * compilers separate so each format can evolve its own diagnostic rules
327
+ * without contaminating the other.
328
+ */
329
+ async function compileMarkdownContent(content, options = {}) {
330
+ const { filePath, projectRoot } = options;
331
+ try {
332
+ await (0, _mdx_js_mdx.compile)(content, {
333
+ format: "md",
334
+ jsx: true,
335
+ providerImportSource: "@clarify-labs/renderer",
336
+ remarkPlugins,
337
+ remarkRehypeOptions: { allowDangerousHtml: true }
338
+ });
339
+ return { ok: true };
340
+ } catch (error) {
341
+ return {
342
+ ok: false,
343
+ diagnostic: createContentCompileDiagnostic({
344
+ format: "markdown",
345
+ phase: "syntax",
346
+ error,
347
+ filePath,
348
+ projectRoot
349
+ })
350
+ };
351
+ }
352
+ }
353
+ //#endregion
275
354
  //#region source/parsers/routes/routes.ts
276
355
  function kebabToTitle(str) {
277
356
  return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
@@ -280,8 +359,10 @@ function parseMdxTree(content) {
280
359
  return remark.remark.parse(content);
281
360
  }
282
361
  async function compileRouteDiagnostic(content, filePath, baseDir) {
283
- if ((0, node_path.extname)(filePath).toLowerCase() !== ".mdx") return void 0;
284
- const result = await compileMdxContent(content, filePath, baseDir);
362
+ const result = await ((0, node_path.extname)(filePath).toLowerCase() === ".md" ? compileMarkdownContent : compileMdxContent)(content, {
363
+ filePath,
364
+ projectRoot: baseDir
365
+ });
285
366
  return result.ok ? void 0 : result.diagnostic;
286
367
  }
287
368
  /** 从内容中提取第一个 H1 标题 */
@@ -340,9 +421,6 @@ function openAPIPagePathFromRef(ref, tagFilter, explicitPath) {
340
421
  function routePathFromRef(ref, explicitPath) {
341
422
  return explicitPath ? normalizePath(explicitPath) : basePathFromRef(ref);
342
423
  }
343
- function virtualModuleIdFromRef(ref) {
344
- return "virtual:clarify-page/" + ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
345
- }
346
424
  function localizedRoutePath(basePath, locale, _i18n) {
347
425
  const normalizedBasePath = normalizePath(basePath);
348
426
  const prefix = normalizePath(locale);
@@ -385,7 +463,7 @@ async function findContentRoutes(dir, base = dir, options = {}) {
385
463
  title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(page.content) : lastPart) || "Untitled";
386
464
  }
387
465
  routes.push({
388
- kind: "mdx",
466
+ kind: entry.name.toLowerCase().endsWith(".mdx") ? "markdown+jsx" : "markdown",
389
467
  path: cleanPath,
390
468
  basePath: cleanPath,
391
469
  meta: {
@@ -394,7 +472,10 @@ async function findContentRoutes(dir, base = dir, options = {}) {
394
472
  keywords: frontmatterKeywords(page.frontmatter),
395
473
  sections: extractMdxSections(page.content)
396
474
  },
397
- module: { virtualModuleId: "virtual:clarify-page/" + relativePath.replace(/\.mdx?$/, "").replace(/\/+/g, "/") },
475
+ module: {
476
+ pageVirtualModuleId: pageVirtualModuleId(relativePath.replace(/\.mdx?$/, "")),
477
+ contentVirtualModuleId: contentVirtualModuleId(fullPath, base)
478
+ },
398
479
  source: {
399
480
  filePath: fullPath,
400
481
  frontmatter: page.frontmatter,
@@ -406,14 +487,24 @@ async function findContentRoutes(dir, base = dir, options = {}) {
406
487
  }
407
488
  return routes;
408
489
  }
409
- function virtualModuleIdForLocalizedRoute(contentRoot, filePath) {
410
- return "virtual:clarify-page/" + (0, node_path.relative)(contentRoot, filePath).replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
490
+ function pageVirtualModuleIdForLocalizedRoute(contentRoot, filePath) {
491
+ return pageVirtualModuleIdFromRef((0, node_path.relative)(contentRoot, filePath));
411
492
  }
412
493
  function frontmatterKeywords(frontmatter) {
413
494
  const value = frontmatter.keywords ?? frontmatter.keyword ?? frontmatter.tags;
414
495
  const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
415
496
  return keywords.length > 0 ? keywords : void 0;
416
497
  }
498
+ function withPageIdentity(route, path) {
499
+ return {
500
+ ...route,
501
+ path,
502
+ module: {
503
+ ...route.module,
504
+ pageVirtualModuleId: pageVirtualModuleId(path)
505
+ }
506
+ };
507
+ }
417
508
  function withAlternates(route, routes, locales) {
418
509
  const basePath = route.basePath ?? route.path;
419
510
  const routeByLocaleAndBase = new Map(routes.filter((r) => !r.locale || r.path === `/${r.locale}` || r.path.startsWith(`/${r.locale}/`)).map((r) => [`${r.locale ?? ""}:${r.basePath ?? r.path}`, r]));
@@ -439,8 +530,7 @@ function generateBareAliases(routes, locales) {
439
530
  if (bp === route.path || seenBare.has(bp)) continue;
440
531
  seenBare.add(bp);
441
532
  bareRoutes.push({
442
- ...route,
443
- path: bp,
533
+ ...withPageIdentity(route, bp),
444
534
  isBareAlias: true
445
535
  });
446
536
  }
@@ -459,7 +549,11 @@ async function findLocalizedContentRoutes(contentRoot, locales, options = {}) {
459
549
  path: localizedRoutePath(basePath, locale.code, locales),
460
550
  basePath,
461
551
  locale: locale.code,
462
- module: { virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.source.filePath) }
552
+ module: {
553
+ ...route.module,
554
+ pageVirtualModuleId: pageVirtualModuleIdForLocalizedRoute(contentRoot, route.source.filePath),
555
+ contentVirtualModuleId: contentVirtualModuleId(route.source.filePath, contentRoot)
556
+ }
463
557
  });
464
558
  }
465
559
  }
@@ -471,9 +565,9 @@ async function findLocalizedContentRoutes(contentRoot, locales, options = {}) {
471
565
  for (const locale of locales.locales) {
472
566
  const key = `${locale.code}:${basePath}`;
473
567
  if (routeByLocaleAndBase.has(key)) continue;
568
+ const path = localizedRoutePath(basePath, locale.code, locales);
474
569
  localizedRoutes.push({
475
- ...sourceRoute,
476
- path: localizedRoutePath(basePath, locale.code, locales),
570
+ ...withPageIdentity(sourceRoute, path),
477
571
  locale: locale.code,
478
572
  isFallback: true
479
573
  });
@@ -568,13 +662,13 @@ function applyConfiguredPageRoutePaths(routes, tabs, locales) {
568
662
  const targetBasePath = routePathFromRef(intent.ref, intent.path);
569
663
  if (sourceBasePath === targetBasePath) continue;
570
664
  for (const route of routes) {
571
- if (route.kind !== "mdx" || (route.basePath ?? route.path) !== sourceBasePath) continue;
665
+ if (route.kind === "openapi" || (route.basePath ?? route.path) !== sourceBasePath) continue;
572
666
  const key = `${route.locale ?? ""}:${targetBasePath}`;
573
667
  if (existingKeys.has(key)) continue;
574
668
  existingKeys.add(key);
669
+ const path = route.locale && locales ? localizedRoutePath(targetBasePath, route.locale, locales) : targetBasePath;
575
670
  additions.push({
576
- ...route,
577
- path: route.locale && locales ? localizedRoutePath(targetBasePath, route.locale, locales) : targetBasePath,
671
+ ...withPageIdentity(route, path),
578
672
  basePath: targetBasePath
579
673
  });
580
674
  }
@@ -582,7 +676,8 @@ function applyConfiguredPageRoutePaths(routes, tabs, locales) {
582
676
  const nextRoutes = [...routes, ...additions];
583
677
  const routesWithAlternates = locales ? nextRoutes.map((route) => withAlternates(route, nextRoutes, locales)) : nextRoutes;
584
678
  if (!locales) return routesWithAlternates;
585
- return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, locales)];
679
+ const markdownRoutes = routesWithAlternates.filter((route) => route.kind !== "openapi");
680
+ return [...routesWithAlternates, ...generateBareAliases(markdownRoutes, locales)];
586
681
  }
587
682
  function buildNavigationFromPagesConfig(routes, config) {
588
683
  if (!config || config === "FileTree") return buildNavigation(routes);
@@ -776,7 +871,7 @@ function inferRouteForFile(filePath, contentRoot, defaultLocale) {
776
871
  filePath,
777
872
  locale: fileLocale,
778
873
  basePath: normalizePath(basePath),
779
- kind: isMdx ? "mdx" : "openapi",
874
+ kind: isMdx ? "markdown+jsx" : "openapi",
780
875
  title,
781
876
  inferred: true
782
877
  };
@@ -986,18 +1081,16 @@ var clarifyThemeConfigSchema = zod.z.object({
986
1081
  tokens: clarifyThemeTokensConfigSchema.optional(),
987
1082
  layout: clarifyThemeLayoutConfigSchema.optional()
988
1083
  }).strict();
989
- function featureSchema(shape, defaults) {
1084
+ function featureSchema(shape, defaults, enabledByDefault = true) {
1085
+ const resolvedDefaults = {
1086
+ enabled: enabledByDefault,
1087
+ ...defaults
1088
+ };
990
1089
  const objectSchema = zod.z.object({
991
- enabled: zod.z.boolean().default(true),
1090
+ enabled: zod.z.boolean().default(enabledByDefault),
992
1091
  ...shape
993
- }).strict().default({
994
- enabled: true,
995
- ...defaults
996
- });
997
- return zod.z.union([zod.z.boolean(), objectSchema]).default({
998
- enabled: true,
999
- ...defaults
1000
- }).transform((value) => typeof value === "boolean" ? {
1092
+ }).strict().default(resolvedDefaults);
1093
+ return zod.z.union([zod.z.boolean(), objectSchema]).default(resolvedDefaults).transform((value) => typeof value === "boolean" ? {
1001
1094
  enabled: value,
1002
1095
  ...defaults
1003
1096
  } : value);
@@ -1005,7 +1098,7 @@ function featureSchema(shape, defaults) {
1005
1098
  var clarifyFeaturesConfigSchema = zod.z.object({
1006
1099
  search: featureSchema({ mcp: zod.z.boolean().default(true) }, { mcp: true }),
1007
1100
  repository: featureSchema(clarifyRepositoryConfigSchema.shape, {}),
1008
- themeEditor: featureSchema({}, {}),
1101
+ themeEditor: featureSchema({}, {}, false),
1009
1102
  openapi: featureSchema({ playground: zod.z.boolean().default(true) }, { playground: true })
1010
1103
  }).strict();
1011
1104
  var clarifyProjectConfigSchema = zod.z.object({
@@ -1197,6 +1290,9 @@ function resolveVirtualId(id) {
1197
1290
  function stripVirtualPrefix(id) {
1198
1291
  return id.startsWith("\0") ? id.slice(1) : id;
1199
1292
  }
1293
+ function virtualContentModuleId(route) {
1294
+ return route.module.contentVirtualModuleId;
1295
+ }
1200
1296
  /**
1201
1297
  * Fixed virtual module IDs (excluding the client entry, which has a special
1202
1298
  * resolved form). Used to avoid hardcoding the constant list in every
@@ -1224,8 +1320,10 @@ function resolveVirtualModuleId(id, modules, routes) {
1224
1320
  for (const virtualId of FIXED_VIRTUAL_IDS) if (id === virtualId || id === resolveVirtualId(virtualId)) return resolveVirtualId(virtualId);
1225
1321
  const moduleId = stripVirtualPrefix(id);
1226
1322
  if (modules.has(moduleId)) return resolveVirtualId(moduleId);
1227
- const route = routes.find((route) => route.module.virtualModuleId === id || route.module.virtualModuleId === moduleId);
1228
- if (route) return resolveVirtualId(route.module.virtualModuleId);
1323
+ const contentRoute = routes.find((route) => (route.kind === "markdown" || route.kind === "markdown+jsx") && route.module.contentVirtualModuleId === moduleId);
1324
+ if (contentRoute) return contentRoute.source.filePath;
1325
+ const route = routes.find((route) => route.module.pageVirtualModuleId === id || route.module.pageVirtualModuleId === moduleId);
1326
+ if (route) return resolveVirtualId(route.module.pageVirtualModuleId);
1229
1327
  return null;
1230
1328
  }
1231
1329
  function generateConfigModule(projectConfig, version) {
@@ -1279,8 +1377,8 @@ function routeToRuntimeManifestEntry(route, component, mode) {
1279
1377
  return entry;
1280
1378
  }
1281
1379
  function generateRoutesModule(routes, navigation, mode = "client") {
1282
- return `${mode === "server" ? routes.map((r, i) => `import Page${i} from ${moduleSpecifier(r.module.virtualModuleId)};`).join("\n") : `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';`}\n\nexport const routes = [\n${routes.map((r, i) => {
1283
- const component = mode === "server" ? `Page${i}` : `() => import(${moduleSpecifier(r.module.virtualModuleId)}).catch((error) => Promise.resolve({ default: createContentDiagnosticComponent({ kind: 'route-load', title: 'This page could not be loaded', message: error instanceof Error ? error.message : String(error), details: error instanceof Error ? error.stack : undefined }) }))`;
1380
+ return `${mode === "server" ? routes.map((r, i) => `import Page${i} from ${moduleSpecifier(r.module.pageVirtualModuleId)};`).join("\n") : `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';`}\n\nexport const routes = [\n${routes.map((r, i) => {
1381
+ const component = mode === "server" ? `Page${i}` : `() => import(${moduleSpecifier(r.module.pageVirtualModuleId)}).catch((error) => Promise.resolve({ default: createContentDiagnosticComponent({ kind: 'route-load', title: 'This page could not be loaded', message: error instanceof Error ? error.message : String(error), details: error instanceof Error ? error.stack : undefined }) }))`;
1284
1382
  const placeholder = `__COMPONENT_${i}__`;
1285
1383
  const manifestEntry = routeToRuntimeManifestEntry(r, placeholder, mode);
1286
1384
  return ` ${JSON.stringify(manifestEntry, null, 0).replace(`"${placeholder}"`, component).replace(/"([A-Za-z_$][\w$]*)":/g, "$1: ")}`;
@@ -1352,13 +1450,14 @@ function buildVirtualModules(args) {
1352
1450
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, "client"));
1353
1451
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, "server"));
1354
1452
  modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
1355
- modules.set(VIRTUAL_OPENAPI, "export const openApiSpecs = {};");
1356
1453
  modules.set(VIRTUAL_SLOT, "export { useSlot } from '@clarify-labs/renderer';\n");
1357
- modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1454
+ modules.set(VIRTUAL_OPENAPI, "export const openApiSpecs = {};");
1358
1455
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1456
+ modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1359
1457
  for (const route of args.routes) {
1360
- const moduleContent = route.diagnostic ? generateContentDiagnosticModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1361
- modules.set(route.module.virtualModuleId, moduleContent);
1458
+ const moduleContent = route.diagnostic ? generateContentDiagnosticModule(route.diagnostic) : route.kind === "markdown" || route.kind === "markdown+jsx" ? `export { default } from ${moduleSpecifier(virtualContentModuleId(route))};` : null;
1459
+ if (moduleContent === null) continue;
1460
+ modules.set(route.module.pageVirtualModuleId, moduleContent);
1362
1461
  }
1363
1462
  return modules;
1364
1463
  }
@@ -1399,7 +1498,7 @@ function createNormalizedContentPlugin(engine) {
1399
1498
  transform(_code, id) {
1400
1499
  if (!/\.mdx?(?:\?|$)/.test(id)) return null;
1401
1500
  const filePath = id.replace(/\?.*$/, "");
1402
- const route = engine.routes.find((route) => route.kind === "mdx" && route.source.filePath === filePath);
1501
+ const route = engine.routes.find((route) => (route.kind === "markdown+jsx" || route.kind === "markdown") && route.source.filePath === filePath);
1403
1502
  if (!route || route.source.content === void 0) return null;
1404
1503
  return {
1405
1504
  code: route.source.content,
@@ -1514,7 +1613,10 @@ function createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown,
1514
1613
  };
1515
1614
  server.watcher.on("add", handleContentTreeChange);
1516
1615
  server.watcher.on("unlink", handleContentTreeChange);
1517
- await engine.configureDevServer(server);
1616
+ const postHooks = await engine.configureDevServer(server);
1617
+ if (postHooks.length > 0) return () => {
1618
+ for (const postHook of postHooks) postHook();
1619
+ };
1518
1620
  },
1519
1621
  transformIndexHtml: {
1520
1622
  order: "pre",
@@ -1569,7 +1671,7 @@ function createViteAdapter(engine) {
1569
1671
  const errorPolicy = () => contentCompileErrorPolicy;
1570
1672
  const normalizedContentPlugin = createNormalizedContentPlugin(engine);
1571
1673
  const markdown = createContentCompilerPlugin("markdown", engine.root, errorPolicy);
1572
- const mdx = createContentCompilerPlugin("mdx", engine.root, errorPolicy);
1674
+ const mdx = createContentCompilerPlugin("markdown+jsx", engine.root, errorPolicy);
1573
1675
  const compilePolicyPlugin = {
1574
1676
  name: "clarify:content-compile-policy",
1575
1677
  configResolved(config) {
@@ -1589,7 +1691,7 @@ function createViteAdapter(engine) {
1589
1691
  }
1590
1692
  var cliPackageVersion = {
1591
1693
  name: "@clarify-labs/cli",
1592
- version: "0.11.3",
1694
+ version: "0.11.5",
1593
1695
  license: "AGPL-3.0-only",
1594
1696
  type: "module",
1595
1697
  repository: {
@@ -1689,7 +1791,7 @@ function withUtf8Signature(content) {
1689
1791
  return `${UTF8_SIGNATURE}${content}`;
1690
1792
  }
1691
1793
  function shouldUseUtf8Signature(route) {
1692
- if (route.kind === "mdx") return true;
1794
+ if (route.kind === "markdown+jsx" || route.kind === "markdown") return true;
1693
1795
  return false;
1694
1796
  }
1695
1797
  function routeToMarkdownArtifactUrl(routePath) {
@@ -1761,9 +1863,9 @@ function createLlmsTxt(routes, projectConfig) {
1761
1863
  const lines = [`# ${projectConfig.title}`, ""];
1762
1864
  if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
1763
1865
  lines.push("This file lists the source-ready Markdown and OpenAPI artifacts for this documentation site.", "");
1764
- const mdxRoutes = routes.filter((route) => route.kind === "mdx" && isLlmsTxtRoute(route) && !route.isBareAlias);
1765
- if (mdxRoutes.length > 0) {
1766
- const localizedGroups = groupLlmsTxtRoutesByLocale(mdxRoutes, projectConfig);
1866
+ const markdownRoutes = routes.filter((route) => (route.kind === "markdown+jsx" || route.kind === "markdown") && isLlmsTxtRoute(route) && !route.isBareAlias);
1867
+ if (markdownRoutes.length > 0) {
1868
+ const localizedGroups = groupLlmsTxtRoutesByLocale(markdownRoutes, projectConfig);
1767
1869
  for (const [locale, localeRoutes] of localizedGroups) {
1768
1870
  lines.push(`## ${llmsTxtDocsSectionTitle(locale, localizedGroups.size, projectConfig)}`);
1769
1871
  for (const route of localeRoutes) {
@@ -1840,10 +1942,12 @@ function createContentArtifactsPlugin() {
1840
1942
  return input;
1841
1943
  },
1842
1944
  "dev:configureServer": (server, ctx) => {
1843
- server.middlewares.use((req, res, next) => {
1844
- if (serveContentArtifacts(req, res, ctx.projectConfig, ctx.routes)) return;
1845
- next();
1846
- });
1945
+ return () => {
1946
+ server.middlewares.use((req, res, next) => {
1947
+ if (serveContentArtifacts(req, res, ctx.projectConfig, ctx.routes)) return;
1948
+ next();
1949
+ });
1950
+ };
1847
1951
  },
1848
1952
  "build:assets": (ctx) => {
1849
1953
  const assets = [];
@@ -2334,7 +2438,7 @@ function findOpenAPIRoutes(dir, base = dir) {
2334
2438
  basePath: cleanPath,
2335
2439
  kind: "openapi",
2336
2440
  meta: { title: kebabToTitle(cleanPath.split("/").pop() ?? "API") },
2337
- module: { virtualModuleId: virtualModuleIdFromRef(ref) },
2441
+ module: { pageVirtualModuleId: pageVirtualModuleIdFromRef(ref) },
2338
2442
  source: { filePath: fullPath }
2339
2443
  });
2340
2444
  }
@@ -2458,34 +2562,7 @@ async function readOpenAPISpec(filePath, contentProcessor, projectRoot) {
2458
2562
  }
2459
2563
  }
2460
2564
  //#endregion
2461
- //#region source/plugins/openapi/virtual-modules.ts
2462
- var openApiRegistryModuleId = VIRTUAL_OPENAPI;
2463
- var openApiServerRegistryModuleId = VIRTUAL_OPENAPI_SERVER;
2464
- var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
2465
- function specVirtualModuleId(specKey) {
2466
- return `${OPENAPI_SPEC_PREFIX}${specKey}`;
2467
- }
2468
- function generateOpenAPIRegistryModule(openApiSpecs) {
2469
- return `export const openApiSpecs = {\n${Object.entries(openApiSpecs).map(([key, moduleId]) => ` ${JSON.stringify(key)}: () => import(${JSON.stringify(moduleId)}),`).join("\n")}\n};`;
2470
- }
2471
- function generateOpenAPIServerRegistryModule(openApiSpecs) {
2472
- return `export const openApiSpecs = ${JSON.stringify(openApiSpecs)};`;
2473
- }
2474
- /** Generate a virtual module that exports a single spec as default. */
2475
- function generateOpenAPISpecModule(spec) {
2476
- return `export default ${JSON.stringify(spec)};`;
2477
- }
2478
- function generateOpenAPIPageModule(opts) {
2479
- return `import { createOpenApiRouteComponent } from '@clarify-labs/renderer';
2480
- import spec from ${JSON.stringify(opts.specModuleId)};
2481
-
2482
- export const routeData = {};
2483
-
2484
- export default createOpenApiRouteComponent({ ...routeData, spec });
2485
- `;
2486
- }
2487
- //#endregion
2488
- //#region source/plugins/openapi/index.ts
2565
+ //#region source/plugins/openapi/routes.ts
2489
2566
  function collectOpenAPIPageIntents(config) {
2490
2567
  const intents = [];
2491
2568
  function visitItems(items) {
@@ -2505,9 +2582,6 @@ function collectOpenAPIPageIntents(config) {
2505
2582
  for (const tab of config.navigation?.tabs ?? []) visitPages(tab.pages);
2506
2583
  return intents;
2507
2584
  }
2508
- function virtualModuleIdFromPath(path) {
2509
- return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
2510
- }
2511
2585
  function cloneOpenAPIRoute(route, overrides = {}) {
2512
2586
  return {
2513
2587
  ...route,
@@ -2525,7 +2599,7 @@ function cloneOpenAPIRoute(route, overrides = {}) {
2525
2599
  ...overrides
2526
2600
  };
2527
2601
  }
2528
- function findOpenAPIRoutesForContentRoot(contentRoot, locales) {
2602
+ function discoverOpenAPIRoutes(contentRoot, locales) {
2529
2603
  if (!locales) return findOpenAPIRoutes(contentRoot);
2530
2604
  const localizedRoutes = [];
2531
2605
  for (const locale of locales.locales) {
@@ -2538,31 +2612,30 @@ function findOpenAPIRoutesForContentRoot(contentRoot, locales) {
2538
2612
  path,
2539
2613
  basePath,
2540
2614
  locale: locale.code,
2541
- module: { virtualModuleId: virtualModuleIdFromPath(path) }
2615
+ module: { pageVirtualModuleId: pageVirtualModuleId(path) }
2542
2616
  });
2543
2617
  }
2544
2618
  }
2545
- if (locales.missing === "fallback") {
2546
- const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
2547
- const defaultRoutes = localizedRoutes.filter((route) => route.locale === locales.default);
2548
- for (const sourceRoute of defaultRoutes) {
2549
- const basePath = sourceRoute.basePath ?? sourceRoute.path;
2550
- for (const locale of locales.locales) {
2551
- const key = `${locale.code}:${basePath}`;
2552
- if (routeByLocaleAndBase.has(key)) continue;
2553
- const path = localizedRoutePath(basePath, locale.code, locales);
2554
- localizedRoutes.push(cloneOpenAPIRoute(sourceRoute, {
2555
- path,
2556
- locale: locale.code,
2557
- module: { virtualModuleId: virtualModuleIdFromPath(path) },
2558
- isFallback: true
2559
- }));
2560
- }
2619
+ if (locales.missing !== "fallback") return localizedRoutes;
2620
+ const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
2621
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === locales.default);
2622
+ for (const sourceRoute of defaultRoutes) {
2623
+ const basePath = sourceRoute.basePath ?? sourceRoute.path;
2624
+ for (const locale of locales.locales) {
2625
+ const key = `${locale.code}:${basePath}`;
2626
+ if (routeByLocaleAndBase.has(key)) continue;
2627
+ const path = localizedRoutePath(basePath, locale.code, locales);
2628
+ localizedRoutes.push(cloneOpenAPIRoute(sourceRoute, {
2629
+ path,
2630
+ locale: locale.code,
2631
+ module: { pageVirtualModuleId: pageVirtualModuleId(path) },
2632
+ isFallback: true
2633
+ }));
2561
2634
  }
2562
2635
  }
2563
2636
  return localizedRoutes;
2564
2637
  }
2565
- function createConfiguredOpenAPIRoutes(routes, config) {
2638
+ function expandConfiguredOpenAPIRoutes(routes, config) {
2566
2639
  const openAPIIntents = collectOpenAPIPageIntents(config);
2567
2640
  if (!openAPIIntents.length) return routes;
2568
2641
  const additions = [];
@@ -2581,7 +2654,7 @@ function createConfiguredOpenAPIRoutes(routes, config) {
2581
2654
  additions.push(cloneOpenAPIRoute(route, {
2582
2655
  path,
2583
2656
  basePath: targetBasePath,
2584
- module: { virtualModuleId: virtualModuleIdFromPath(path) },
2657
+ module: { pageVirtualModuleId: pageVirtualModuleId(path) },
2585
2658
  openapi: {
2586
2659
  ...route.openapi,
2587
2660
  tagFilter: tagFilter?.length ? tagFilter : void 0
@@ -2595,19 +2668,47 @@ function createConfiguredOpenAPIRoutes(routes, config) {
2595
2668
  const bareAliases = [];
2596
2669
  const seenBare = new Set(routesWithAlternates.map((route) => route.path));
2597
2670
  for (const route of routesWithAlternates) {
2598
- if (route.locale !== config.locales.default) continue;
2671
+ if (route.kind !== "openapi" || route.locale !== config.locales.default) continue;
2599
2672
  const basePath = route.basePath ?? route.path;
2600
2673
  if (basePath === route.path || seenBare.has(basePath)) continue;
2601
2674
  seenBare.add(basePath);
2602
2675
  bareAliases.push(cloneOpenAPIRoute(route, {
2603
2676
  path: basePath,
2604
- module: { virtualModuleId: virtualModuleIdFromPath(basePath) },
2677
+ module: { pageVirtualModuleId: pageVirtualModuleId(basePath) },
2605
2678
  isBareAlias: true
2606
2679
  }));
2607
2680
  }
2608
2681
  return [...routesWithAlternates, ...bareAliases];
2609
2682
  }
2610
- /** Derive a stable, deduplicated id from an absolute spec file path. */
2683
+ //#endregion
2684
+ //#region source/plugins/openapi/virtual-modules.ts
2685
+ var openApiRegistryModuleId = VIRTUAL_OPENAPI;
2686
+ var openApiServerRegistryModuleId = VIRTUAL_OPENAPI_SERVER;
2687
+ var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
2688
+ function specVirtualModuleId(specKey) {
2689
+ return `${OPENAPI_SPEC_PREFIX}${specKey}`;
2690
+ }
2691
+ function generateOpenAPIRegistryModule(openApiSpecs) {
2692
+ return `export const openApiSpecs = {\n${Object.entries(openApiSpecs).map(([key, moduleId]) => ` ${JSON.stringify(key)}: () => import(${JSON.stringify(moduleId)}),`).join("\n")}\n};`;
2693
+ }
2694
+ function generateOpenAPIServerRegistryModule(openApiSpecs) {
2695
+ return `export const openApiSpecs = ${JSON.stringify(openApiSpecs)};`;
2696
+ }
2697
+ /** Generate a virtual module that exports a single spec as default. */
2698
+ function generateOpenAPISpecModule(spec) {
2699
+ return `export default ${JSON.stringify(spec)};`;
2700
+ }
2701
+ function generateOpenAPIPageModule(opts) {
2702
+ return `import { createOpenApiRouteComponent } from '@clarify-labs/renderer';
2703
+ import spec from ${JSON.stringify(opts.specModuleId)};
2704
+
2705
+ export const routeData = {};
2706
+
2707
+ export default createOpenApiRouteComponent({ ...routeData, spec });
2708
+ `;
2709
+ }
2710
+ //#endregion
2711
+ //#region source/plugins/openapi/state.ts
2611
2712
  function sourceSpecIdFromPath(filePath, projectRoot) {
2612
2713
  return (0, node_path.relative)(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
2613
2714
  }
@@ -2619,90 +2720,94 @@ function openAPIRegistryKeys(route) {
2619
2720
  const keys = /* @__PURE__ */ new Set();
2620
2721
  const path = (route.path || "/").replace(/^\//, "");
2621
2722
  const base = (route.basePath ?? route.path).replace(/^\//, "");
2622
- if (route.locale) keys.add(`virtual:clarify-page/${route.locale}/${base}`);
2623
- if (path) keys.add(`virtual:clarify-page/${path}`);
2624
- else keys.add("virtual:clarify-page/index");
2625
- if (!route.locale && base) keys.add(`virtual:clarify-page/${base}`);
2723
+ if (route.locale) keys.add(pageVirtualModuleId(`${route.locale}/${base}`));
2724
+ keys.add(pageVirtualModuleId(path));
2725
+ if (!route.locale && base) keys.add(pageVirtualModuleId(base));
2626
2726
  return [...keys];
2627
2727
  }
2728
+ var OpenAPIPluginState = class {
2729
+ specModules = /* @__PURE__ */ new Map();
2730
+ async enrichRoutes(routes, ctx) {
2731
+ this.specModules.clear();
2732
+ const specByFilePath = /* @__PURE__ */ new Map();
2733
+ for (const route of routes) {
2734
+ if (route.kind !== "openapi") continue;
2735
+ const specFromCache = specByFilePath.get(route.source.filePath);
2736
+ const processor = createProjectContentProcessor(ctx.plugins, ctx);
2737
+ const result = specFromCache ? {
2738
+ ok: true,
2739
+ spec: specFromCache
2740
+ } : await readOpenAPISpec(route.source.filePath, processor, ctx.generateOptions.projectRoot);
2741
+ if (!result.ok) {
2742
+ route.diagnostic = result.diagnostic;
2743
+ route.meta.title = route.meta.title || "OpenAPI parse error";
2744
+ route.meta.sections = [];
2745
+ continue;
2746
+ }
2747
+ const spec = normalizeOpenAPISpecSectionIds(result.spec);
2748
+ specByFilePath.set(route.source.filePath, spec);
2749
+ const sourceSpecId = sourceSpecIdFromPath(route.source.filePath, ctx.generateOptions.projectRoot);
2750
+ route.openapi = {
2751
+ ...route.openapi,
2752
+ sourceSpecId
2753
+ };
2754
+ route.meta.title = spec.info?.title ?? route.meta.title;
2755
+ route.meta.description = spec.info?.description ?? route.meta.description;
2756
+ route.meta.sections = extractOpenAPISections(spec, route.openapi.tagFilter);
2757
+ const pageSpec = route.openapi.tagFilter?.length ? filterSpecByTags(spec, route.openapi.tagFilter) : spec;
2758
+ const routeSpecModuleId = routeSpecModuleIdFromSourceSpecId(sourceSpecId, route.openapi.tagFilter);
2759
+ route.openapi.routeSpecModuleId = routeSpecModuleId;
2760
+ this.specModules.set(routeSpecModuleId, pageSpec);
2761
+ route.source.content = JSON.stringify(pageSpec);
2762
+ }
2763
+ return routes;
2764
+ }
2765
+ contributeModules(modules, routes) {
2766
+ const registryEntries = {};
2767
+ const serverRegistryEntries = {};
2768
+ for (const route of routes) {
2769
+ if (route.kind !== "openapi" || !route.openapi?.sourceSpecId) continue;
2770
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2771
+ const spec = this.specModules.get(routeSpecModuleId);
2772
+ if (!spec) continue;
2773
+ for (const registryKey of openAPIRegistryKeys(route)) {
2774
+ registryEntries[registryKey] = specVirtualModuleId(routeSpecModuleId);
2775
+ serverRegistryEntries[registryKey] = spec;
2776
+ }
2777
+ }
2778
+ modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
2779
+ modules.set(openApiServerRegistryModuleId, generateOpenAPIServerRegistryModule(serverRegistryEntries));
2780
+ for (const [routeSpecModuleId, spec] of this.specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2781
+ for (const route of routes) {
2782
+ if (route.kind !== "openapi") continue;
2783
+ if (route.diagnostic) {
2784
+ modules.set(route.module.pageVirtualModuleId, generateContentDiagnosticModule(route.diagnostic));
2785
+ continue;
2786
+ }
2787
+ if (!route.openapi?.sourceSpecId) continue;
2788
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2789
+ if (!this.specModules.has(routeSpecModuleId)) continue;
2790
+ modules.set(route.module.pageVirtualModuleId, generateOpenAPIPageModule({ specModuleId: specVirtualModuleId(routeSpecModuleId) }));
2791
+ }
2792
+ return modules;
2793
+ }
2794
+ };
2795
+ //#endregion
2796
+ //#region source/plugins/openapi/index.ts
2628
2797
  function createOpenAPIPlugin() {
2629
- /** specs keyed by sourceSpecId (deduplicated per source file). */
2630
- const specs = /* @__PURE__ */ new Map();
2631
- const specModules = /* @__PURE__ */ new Map();
2798
+ const state = new OpenAPIPluginState();
2632
2799
  return {
2633
2800
  name: "clarify:openapi",
2634
2801
  hooks: {
2635
2802
  "routes:discover": (input, ctx) => ({
2636
2803
  ...input,
2637
- routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.locales)]
2804
+ routes: [...input.routes, ...discoverOpenAPIRoutes(input.contentRoot, ctx.projectConfig.locales)]
2638
2805
  }),
2639
2806
  "routes:discovered": async (routes, ctx) => {
2640
- specs.clear();
2641
- specModules.clear();
2642
- const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
2643
- const specByFilePath = /* @__PURE__ */ new Map();
2644
- for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
2645
- const specFromCache = specByFilePath.get(route.source.filePath);
2646
- const processor = createProjectContentProcessor(ctx.plugins, ctx);
2647
- const result = specFromCache ? {
2648
- ok: true,
2649
- spec: specFromCache
2650
- } : await readOpenAPISpec(route.source.filePath, processor, ctx.generateOptions.projectRoot);
2651
- if (!result.ok) {
2652
- route.diagnostic = result.diagnostic;
2653
- route.meta.title = route.meta.title || "OpenAPI parse error";
2654
- route.meta.sections = [];
2655
- continue;
2656
- }
2657
- const spec = normalizeOpenAPISpecSectionIds(result.spec);
2658
- specByFilePath.set(route.source.filePath, spec);
2659
- const sourceSpecId = sourceSpecIdFromPath(route.source.filePath, ctx.generateOptions.projectRoot);
2660
- route.openapi = {
2661
- ...route.openapi,
2662
- sourceSpecId
2663
- };
2664
- specs.set(sourceSpecId, {
2665
- spec,
2666
- filePath: route.source.filePath
2667
- });
2668
- route.meta.title = spec.info?.title ?? route.meta.title;
2669
- route.meta.description = spec.info?.description ?? route.meta.description;
2670
- route.meta.sections = extractOpenAPISections(spec, route.openapi?.tagFilter);
2671
- const pageSpec = route.openapi?.tagFilter?.length ? filterSpecByTags(spec, route.openapi.tagFilter) : spec;
2672
- const routeSpecModuleId = routeSpecModuleIdFromSourceSpecId(sourceSpecId, route.openapi?.tagFilter);
2673
- route.openapi.routeSpecModuleId = routeSpecModuleId;
2674
- specModules.set(routeSpecModuleId, pageSpec);
2675
- route.source.content = JSON.stringify(pageSpec);
2676
- }
2677
- return nextRoutes;
2807
+ const expandedRoutes = expandConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
2808
+ return state.enrichRoutes(expandedRoutes, ctx);
2678
2809
  },
2679
- "modules:before": (modules, ctx) => {
2680
- const registryEntries = {};
2681
- const serverRegistryEntries = {};
2682
- for (const route of ctx.routes) {
2683
- if (route.kind !== "openapi" || !route.openapi?.sourceSpecId) continue;
2684
- const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2685
- const spec = specModules.get(routeSpecModuleId) ?? specs.get(route.openapi.sourceSpecId)?.spec;
2686
- if (!spec) continue;
2687
- for (const registryKey of openAPIRegistryKeys(route)) {
2688
- registryEntries[registryKey] = specVirtualModuleId(routeSpecModuleId);
2689
- serverRegistryEntries[registryKey] = spec;
2690
- }
2691
- }
2692
- modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
2693
- modules.set(openApiServerRegistryModuleId, generateOpenAPIServerRegistryModule(serverRegistryEntries));
2694
- for (const [routeSpecModuleId, spec] of specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2695
- for (const route of ctx.routes) {
2696
- if (route.kind !== "openapi") continue;
2697
- if (route.diagnostic) modules.set(route.module.virtualModuleId, generateContentDiagnosticModule(route.diagnostic));
2698
- else if (route.openapi?.sourceSpecId) {
2699
- const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2700
- if (!specModules.has(routeSpecModuleId)) continue;
2701
- modules.set(route.module.virtualModuleId, generateOpenAPIPageModule({ specModuleId: specVirtualModuleId(routeSpecModuleId) }));
2702
- }
2703
- }
2704
- return modules;
2705
- }
2810
+ "modules:before": (modules, ctx) => state.contributeModules(modules, ctx.routes)
2706
2811
  }
2707
2812
  };
2708
2813
  }
@@ -3111,6 +3216,145 @@ async function resolveProjectContext(options = {}, env = {
3111
3216
  };
3112
3217
  }
3113
3218
  //#endregion
3219
+ //#region source/core/routing/route-analysis.ts
3220
+ function routeDescription(route) {
3221
+ return `${route.kind} ${route.source.filePath}`;
3222
+ }
3223
+ function collectConflicts(routes, keyOf) {
3224
+ const grouped = /* @__PURE__ */ new Map();
3225
+ for (const route of routes) {
3226
+ const key = keyOf(route);
3227
+ grouped.set(key, [...grouped.get(key) ?? [], route]);
3228
+ }
3229
+ return grouped;
3230
+ }
3231
+ function describeRouteConflict(route) {
3232
+ return routeDescription(route);
3233
+ }
3234
+ function findRouteConflicts(routes) {
3235
+ const conflicts = [];
3236
+ for (const [path, pathRoutes] of collectConflicts(routes, (route) => route.path)) {
3237
+ if (pathRoutes.length < 2) continue;
3238
+ conflicts.push({
3239
+ kind: "path",
3240
+ key: path,
3241
+ label: `path ${path}`,
3242
+ routes: pathRoutes
3243
+ });
3244
+ }
3245
+ for (const [pageModuleId, moduleRoutes] of collectConflicts(routes, (route) => route.module.pageVirtualModuleId)) {
3246
+ if (moduleRoutes.length < 2) continue;
3247
+ if (moduleRoutes.every((route) => route.path === moduleRoutes[0].path)) continue;
3248
+ conflicts.push({
3249
+ kind: "page-module",
3250
+ key: pageModuleId,
3251
+ label: `page module ${pageModuleId}`,
3252
+ routes: moduleRoutes
3253
+ });
3254
+ }
3255
+ return conflicts;
3256
+ }
3257
+ function formatRouteConflicts(conflicts) {
3258
+ return conflicts.map((conflict) => `- ${conflict.label}: ${conflict.routes.map(describeRouteConflict).join(", ")}`).join("\n");
3259
+ }
3260
+ //#endregion
3261
+ //#region source/core/routing/route-resolution.ts
3262
+ async function resolveRoutePages(routes, plugins, ctx) {
3263
+ const routeToken = Symbol("clarifyRouteToken");
3264
+ const resolvedPages = await runHooks(plugins, "pages:resolved", routes.map((route, routeIndex) => ({
3265
+ path: route.path,
3266
+ filePath: route.source.filePath,
3267
+ frontmatter: route.source.frontmatter ?? {},
3268
+ content: route.source.content ?? "",
3269
+ [routeToken]: routeIndex
3270
+ })), ctx);
3271
+ const pageByRouteIndex = /* @__PURE__ */ new Map();
3272
+ for (const page of resolvedPages) {
3273
+ const routeIndex = page[routeToken];
3274
+ if (!Number.isInteger(routeIndex) || routeIndex < 0 || routeIndex >= routes.length) throw new Error("[clarify] pages:resolved hooks must preserve each page route identity");
3275
+ if (pageByRouteIndex.has(routeIndex)) throw new Error("[clarify] pages:resolved hooks returned the same page route identity more than once");
3276
+ pageByRouteIndex.set(routeIndex, page);
3277
+ }
3278
+ if (pageByRouteIndex.size !== routes.length) throw new Error("[clarify] pages:resolved hooks must return exactly one page for each route");
3279
+ return routes.map((route, routeIndex) => {
3280
+ const page = pageByRouteIndex.get(routeIndex);
3281
+ return {
3282
+ ...route,
3283
+ source: {
3284
+ ...route.source,
3285
+ frontmatter: page.frontmatter,
3286
+ content: page.content
3287
+ }
3288
+ };
3289
+ });
3290
+ }
3291
+ function buildNavigationTree(routes, ctx) {
3292
+ const tabs = ctx.projectConfig.navigation?.tabs;
3293
+ if (!tabs) return {
3294
+ kind: "flat",
3295
+ nodes: buildNavigation(routes)
3296
+ };
3297
+ if (!ctx.projectConfig.locales) return buildNavigationFromTabsConfig(routes, tabs);
3298
+ return buildLocalizedNavigationFromTabsConfig(routes, tabs, ctx.projectConfig.locales) ?? {
3299
+ kind: "localized-tabbed",
3300
+ locales: {}
3301
+ };
3302
+ }
3303
+ function assertNoRouteConflicts(routes) {
3304
+ const conflicts = findRouteConflicts(routes);
3305
+ if (conflicts.length > 0) throw new Error(`[clarify] Route conflicts prevent the site from being built:\n${formatRouteConflicts(conflicts)}`);
3306
+ }
3307
+ function createRouteConflictDiagnostic(label, routes) {
3308
+ return {
3309
+ kind: "route-load",
3310
+ title: "Route conflict",
3311
+ message: `This page cannot be rendered because ${label} is used by multiple routes.`,
3312
+ details: routes.map(describeRouteConflict).join("\n")
3313
+ };
3314
+ }
3315
+ function createDevRouteConflictRoutes(routes) {
3316
+ const conflicts = findRouteConflicts(routes);
3317
+ if (conflicts.length === 0) return routes;
3318
+ const conflictedRoutes = /* @__PURE__ */ new Map();
3319
+ for (const conflict of conflicts) for (const route of conflict.routes) conflictedRoutes.set(route, conflict);
3320
+ function createConflictRoute(route, conflict) {
3321
+ return {
3322
+ ...route,
3323
+ module: {
3324
+ ...route.module,
3325
+ pageVirtualModuleId: `${route.module.pageVirtualModuleId}__conflict`
3326
+ },
3327
+ diagnostic: createRouteConflictDiagnostic(conflict.label, conflict.routes)
3328
+ };
3329
+ }
3330
+ const usedPaths = /* @__PURE__ */ new Set();
3331
+ return routes.flatMap((route) => {
3332
+ const conflict = conflictedRoutes.get(route);
3333
+ if (!conflict) return [route];
3334
+ if (usedPaths.has(route.path)) return [];
3335
+ usedPaths.add(route.path);
3336
+ return [createConflictRoute(route, conflict)];
3337
+ });
3338
+ }
3339
+ function handleRouteConflicts(routes, development) {
3340
+ if (development) return createDevRouteConflictRoutes(routes);
3341
+ assertNoRouteConflicts(routes);
3342
+ return routes;
3343
+ }
3344
+ async function resolveRouteState(routes, plugins, ctx, development) {
3345
+ const tabs = ctx.projectConfig.navigation?.tabs;
3346
+ const configuredRoutes = applyConfiguredPageRoutePaths(routes, tabs, ctx.projectConfig.locales);
3347
+ const resolved = await runHooks(plugins, "routes:resolved", {
3348
+ routes: configuredRoutes,
3349
+ navigation: buildNavigationTree(configuredRoutes, ctx)
3350
+ }, ctx);
3351
+ const finalRoutes = handleRouteConflicts(resolved.routes, development);
3352
+ return {
3353
+ routes: finalRoutes,
3354
+ navigation: finalRoutes === resolved.routes ? resolved.navigation : buildNavigationTree(finalRoutes, ctx)
3355
+ };
3356
+ }
3357
+ //#endregion
3114
3358
  //#region source/core/runtime/env-types.ts
3115
3359
  var GENERATED_FILE_NAME = "clarify-env.d.ts";
3116
3360
  /**
@@ -3651,7 +3895,7 @@ var ClarifyEngine = class {
3651
3895
  async discoverSite() {
3652
3896
  const plugins = this.ctx.plugins;
3653
3897
  const contentRoot = this.ctx.contentRoot;
3654
- const { locales, navigation: navigationConfig } = this.ctx.projectConfig;
3898
+ const { locales } = this.ctx.projectConfig;
3655
3899
  let routes = await runPhase(plugins, "site:discover", this.ctx, async () => {
3656
3900
  const options = { contentProcessor: createProjectContentProcessor(plugins, this.ctx) };
3657
3901
  const discovered = locales ? await findLocalizedContentRoutes(contentRoot, locales, options) : await findContentRoutes(contentRoot, contentRoot, options);
@@ -3659,43 +3903,10 @@ var ClarifyEngine = class {
3659
3903
  contentRoot,
3660
3904
  routes: discovered
3661
3905
  }, this.ctx);
3662
- return runHooks(plugins, "routes:discovered", result.routes, this.ctx);
3663
- });
3664
- routes = await runPhase(plugins, "content:process", this.ctx, async () => {
3665
- const pages = routes.map((route) => ({
3666
- path: route.path,
3667
- filePath: route.source.filePath,
3668
- frontmatter: route.source.frontmatter ?? {},
3669
- content: route.source.content ?? ""
3670
- }));
3671
- const resolvedPages = await runHooks(plugins, "pages:resolved", pages, this.ctx);
3672
- const pageByPath = new Map(resolvedPages.map((p) => [p.path, p]));
3673
- return routes.map((route) => {
3674
- const page = pageByPath.get(route.path);
3675
- if (!page) return route;
3676
- return {
3677
- ...route,
3678
- source: {
3679
- ...route.source,
3680
- frontmatter: page.frontmatter,
3681
- content: page.content
3682
- }
3683
- };
3684
- });
3906
+ return await runHooks(plugins, "routes:discovered", result.routes, this.ctx);
3685
3907
  });
3686
- const tabs = navigationConfig?.tabs;
3687
- routes = applyConfiguredPageRoutePaths(routes, tabs, locales);
3688
- const navigation = tabs ? locales ? buildLocalizedNavigationFromTabsConfig(routes, tabs, locales) ?? {
3689
- kind: "localized-tabbed",
3690
- locales: {}
3691
- } : buildNavigationFromTabsConfig(routes, tabs) : {
3692
- kind: "flat",
3693
- nodes: buildNavigation(routes)
3694
- };
3695
- const resolved = await runHooks(plugins, "routes:resolved", {
3696
- routes,
3697
- navigation
3698
- }, this.ctx);
3908
+ routes = await runPhase(plugins, "content:process", this.ctx, () => resolveRoutePages(routes, plugins, this.ctx));
3909
+ const resolved = await resolveRouteState(routes, plugins, this.ctx, this.runtime.command === "serve");
3699
3910
  this.ctx.routes = resolved.routes;
3700
3911
  this.ctx.navigation = resolved.navigation;
3701
3912
  }
@@ -3745,8 +3956,8 @@ var ClarifyEngine = class {
3745
3956
  return this.buildEnabled;
3746
3957
  }
3747
3958
  async configureDevServer(server) {
3748
- await runPhase(this.plugins, "dev:server", this.ctx, async () => {
3749
- await runDevConfigureServerHooks(this.plugins, server, this.ctx);
3959
+ return runPhase(this.plugins, "dev:server", this.ctx, () => {
3960
+ return runDevConfigureServerHooks(this.plugins, server, this.ctx);
3750
3961
  });
3751
3962
  }
3752
3963
  async runBuildDone() {
@@ -3871,6 +4082,12 @@ Object.defineProperty(exports, "deserializeSearchIndex", {
3871
4082
  return deserializeSearchIndex;
3872
4083
  }
3873
4084
  });
4085
+ Object.defineProperty(exports, "findRouteConflicts", {
4086
+ enumerable: true,
4087
+ get: function() {
4088
+ return findRouteConflicts;
4089
+ }
4090
+ });
3874
4091
  Object.defineProperty(exports, "searchMcpIndex", {
3875
4092
  enumerable: true,
3876
4093
  get: function() {
@@ -3878,4 +4095,4 @@ Object.defineProperty(exports, "searchMcpIndex", {
3878
4095
  }
3879
4096
  });
3880
4097
 
3881
- //# sourceMappingURL=engine-6WmOk7xk.cjs.map
4098
+ //# sourceMappingURL=engine-VgAP_Ms6.cjs.map