@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.
@@ -81,11 +81,11 @@ var rehypePlugins = [
81
81
  ];
82
82
  function createContentCompileDiagnostic(options) {
83
83
  const { format, phase, error, filePath, projectRoot } = options;
84
- const label = format === "mdx" ? "MDX" : "Markdown";
84
+ const label = format === "markdown+jsx" ? "MDX" : "Markdown";
85
85
  return createContentDiagnostic({
86
86
  kind: format,
87
87
  title: `${label} ${phase} error`,
88
- 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.`,
88
+ 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.`,
89
89
  error,
90
90
  filePath,
91
91
  projectRoot,
@@ -98,19 +98,24 @@ function createContentCompileDiagnostic(options) {
98
98
  *
99
99
  * `rehypePlugins` includes `rehypeShiki`, which loads grammar/WASM assets and
100
100
  * is expensive. The real highlighting happens at Vite build time via the
101
- * `@mdx-js/rollup` plugins (see `createMarkdownPlugin` and `createMdxPlugin`
102
- * in `core/adapters.ts`), which
103
- * runs the full remark + rehype pipeline. Re-running it here, during route
104
- * discovery (Phase 3), would compile every `.mdx` file twice and invoke Shiki
105
- * once per file per `engine.refresh()` — see ARCHITECTURE.md §2.2 which states
106
- * "不应在 core 中执行代码高亮".
101
+ * `@mdx-js/rollup` plugin (see `createContentCompilerPlugin` in
102
+ * `core/adapters.ts`), which runs the full remark + rehype pipeline.
103
+ * Re-running it here, during route discovery (Phase 3), would compile every
104
+ * `.mdx` file twice and invoke Shiki once per file per `engine.refresh()` -
105
+ * see ARCHITECTURE.md §2.2 which states "不应在 core 中执行代码高亮".
107
106
  *
108
107
  * The remark pipeline still runs (shared with the Vite plugin via
109
- * `remarkPlugins`), so MDX/JSX syntax errors — the only class of error this
110
- * diagnostic is meant to surface ahead of the Vite build — are caught. The
108
+ * `remarkPlugins`), so MDX/JSX syntax errors - the only class of error this
109
+ * diagnostic is meant to surface ahead of the Vite build - are caught. The
111
110
  * output is discarded; only the thrown error is used.
111
+ *
112
+ * `.md` files are handled by `compileMarkdownContent` in `markdown.ts`, which
113
+ * mirrors the adapter's markdown compiler semantics (`format: 'md'`, raw HTML
114
+ * allowed). Keep the two compilers separate so each format can evolve its own
115
+ * diagnostic rules without contaminating the other.
112
116
  */
113
- async function compileMdxContent(content, filePath, projectRoot) {
117
+ async function compileMdxContent(content, options = {}) {
118
+ const { filePath, projectRoot } = options;
114
119
  try {
115
120
  await compile(content, {
116
121
  jsx: true,
@@ -122,7 +127,7 @@ async function compileMdxContent(content, filePath, projectRoot) {
122
127
  return {
123
128
  ok: false,
124
129
  diagnostic: createContentCompileDiagnostic({
125
- format: "mdx",
130
+ format: "markdown+jsx",
126
131
  phase: "syntax",
127
132
  error,
128
133
  filePath,
@@ -132,6 +137,21 @@ async function compileMdxContent(content, filePath, projectRoot) {
132
137
  }
133
138
  }
134
139
  //#endregion
140
+ //#region source/core/runtime/module-ids.ts
141
+ var PAGE_PREFIX = "virtual:clarify-page/";
142
+ var CONTENT_PREFIX = "virtual:clarify-content/";
143
+ function pageVirtualModuleId(path) {
144
+ return `${PAGE_PREFIX}${path.replace(/^\/+/, "").replace(/\/index$/, "").replace(/\/+/g, "/") || "index"}`;
145
+ }
146
+ function pageVirtualModuleIdFromRef(ref) {
147
+ return pageVirtualModuleId(ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, ""));
148
+ }
149
+ function contentVirtualModuleId(filePath, contentRoot) {
150
+ const sourcePath = relative(contentRoot, filePath).replace(/\\/g, "/");
151
+ if (!sourcePath || sourcePath.startsWith("../") || isAbsolute(sourcePath)) throw new Error(`Content source must be inside the content root: ${filePath}`);
152
+ return `${CONTENT_PREFIX}${sourcePath}`;
153
+ }
154
+ //#endregion
135
155
  //#region source/core/plugin/hooks.ts
136
156
  async function runHooks(plugins, hookName, input, ctx) {
137
157
  let result = input;
@@ -147,32 +167,43 @@ async function runHooks(plugins, hookName, input, ctx) {
147
167
  return result;
148
168
  }
149
169
  async function runDevConfigureServerHooks(plugins, server, ctx) {
170
+ const postHooks = [];
150
171
  for (const plugin of plugins) {
151
172
  const hook = plugin.hooks?.["dev:configureServer"];
152
173
  if (!hook) continue;
153
174
  try {
154
- await hook(server, ctx);
175
+ const postHook = await hook(server, ctx);
176
+ if (postHook) postHooks.push(postHook);
155
177
  } catch (err) {
156
178
  throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
157
179
  }
158
180
  }
181
+ return postHooks;
159
182
  }
160
- async function runCollectorHooks(plugins, hookName, ctx) {
161
- const results = [];
183
+ async function runBuildAssetsHooks(plugins, ctx) {
184
+ const assets = [];
185
+ const producerByFileName = /* @__PURE__ */ new Map();
162
186
  for (const plugin of plugins) {
163
- const hook = plugin.hooks?.[hookName];
187
+ const hook = plugin.hooks?.["build:assets"];
164
188
  if (!hook) continue;
189
+ let pluginAssets;
165
190
  try {
166
- const pluginResults = await hook(ctx);
167
- results.push(...pluginResults);
191
+ pluginAssets = await hook(ctx);
168
192
  } catch (err) {
169
- throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
193
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
194
+ }
195
+ for (const asset of pluginAssets) {
196
+ const fileName = asset.fileName.replace(/\\/g, "/").replace(/^\/+/, "");
197
+ const previousProducer = producerByFileName.get(fileName);
198
+ if (previousProducer) throw new Error(`[clarify] build asset "${fileName}" is emitted by both plugin "${previousProducer}" and plugin "${plugin.name}"`);
199
+ producerByFileName.set(fileName, plugin.name);
200
+ assets.push({
201
+ ...asset,
202
+ fileName
203
+ });
170
204
  }
171
205
  }
172
- return results;
173
- }
174
- async function runBuildAssetsHooks(plugins, ctx) {
175
- return runCollectorHooks(plugins, "build:assets", ctx);
206
+ return assets;
176
207
  }
177
208
  async function runBuildDoneHooks(plugins, ctx) {
178
209
  for (const plugin of plugins) {
@@ -207,7 +238,7 @@ function createContentProcessor(transform) {
207
238
  async processMdx(source, filePath) {
208
239
  const { frontmatter, content } = parseFrontmatter(source);
209
240
  const transformed = await transformContent(transform, {
210
- kind: "mdx",
241
+ kind: "markdown+jsx",
211
242
  source,
212
243
  filePath,
213
244
  frontmatter,
@@ -241,6 +272,54 @@ function createProjectContentProcessor(plugins, ctx) {
241
272
  return createContentProcessor((input) => runHooks(plugins, "content:transform", input, ctx));
242
273
  }
243
274
  //#endregion
275
+ //#region source/parsers/markdown/markdown.ts
276
+ /**
277
+ * Validate Markdown content for diagnostics WITHOUT running the build-time
278
+ * rehype pipeline (Shiki highlighting, slug injection, code-block parsing).
279
+ *
280
+ * `rehypePlugins` includes `rehypeShiki`, which loads grammar/WASM assets and
281
+ * is expensive. The real highlighting happens at Vite build time via the
282
+ * `@mdx-js/rollup` plugin (see `createContentCompilerPlugin` in
283
+ * `core/adapters.ts`), which runs the full remark + rehype pipeline.
284
+ * Re-running it here, during route discovery (Phase 3), would compile every
285
+ * `.md` file twice and invoke Shiki once per file per `engine.refresh()` -
286
+ * see ARCHITECTURE.md §2.2 which states "不应在 core 中执行代码高亮".
287
+ *
288
+ * `format: 'md'` mirrors the adapter's markdown compiler semantics (no JSX,
289
+ * raw HTML allowed via `remarkRehypeOptions`), so the same class of syntax
290
+ * errors that would break the Vite build is surfaced as a diagnostic here
291
+ * instead of leaking out as an opaque
292
+ * `Failed to fetch dynamically imported module` at runtime.
293
+ *
294
+ * MDX files are handled by `compileMdxContent` in `mdx.ts`. Keep the two
295
+ * compilers separate so each format can evolve its own diagnostic rules
296
+ * without contaminating the other.
297
+ */
298
+ async function compileMarkdownContent(content, options = {}) {
299
+ const { filePath, projectRoot } = options;
300
+ try {
301
+ await compile(content, {
302
+ format: "md",
303
+ jsx: true,
304
+ providerImportSource: "@clarify-labs/renderer",
305
+ remarkPlugins,
306
+ remarkRehypeOptions: { allowDangerousHtml: true }
307
+ });
308
+ return { ok: true };
309
+ } catch (error) {
310
+ return {
311
+ ok: false,
312
+ diagnostic: createContentCompileDiagnostic({
313
+ format: "markdown",
314
+ phase: "syntax",
315
+ error,
316
+ filePath,
317
+ projectRoot
318
+ })
319
+ };
320
+ }
321
+ }
322
+ //#endregion
244
323
  //#region source/parsers/routes/routes.ts
245
324
  function kebabToTitle(str) {
246
325
  return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
@@ -249,8 +328,10 @@ function parseMdxTree(content) {
249
328
  return remark.parse(content);
250
329
  }
251
330
  async function compileRouteDiagnostic(content, filePath, baseDir) {
252
- if (extname(filePath).toLowerCase() !== ".mdx") return void 0;
253
- const result = await compileMdxContent(content, filePath, baseDir);
331
+ const result = await (extname(filePath).toLowerCase() === ".md" ? compileMarkdownContent : compileMdxContent)(content, {
332
+ filePath,
333
+ projectRoot: baseDir
334
+ });
254
335
  return result.ok ? void 0 : result.diagnostic;
255
336
  }
256
337
  /** 从内容中提取第一个 H1 标题 */
@@ -309,9 +390,6 @@ function openAPIPagePathFromRef(ref, tagFilter, explicitPath) {
309
390
  function routePathFromRef(ref, explicitPath) {
310
391
  return explicitPath ? normalizePath(explicitPath) : basePathFromRef(ref);
311
392
  }
312
- function virtualModuleIdFromRef(ref) {
313
- return "virtual:clarify-page/" + ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
314
- }
315
393
  function localizedRoutePath(basePath, locale, _i18n) {
316
394
  const normalizedBasePath = normalizePath(basePath);
317
395
  const prefix = normalizePath(locale);
@@ -354,7 +432,7 @@ async function findContentRoutes(dir, base = dir, options = {}) {
354
432
  title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(page.content) : lastPart) || "Untitled";
355
433
  }
356
434
  routes.push({
357
- kind: "mdx",
435
+ kind: entry.name.toLowerCase().endsWith(".mdx") ? "markdown+jsx" : "markdown",
358
436
  path: cleanPath,
359
437
  basePath: cleanPath,
360
438
  meta: {
@@ -363,7 +441,10 @@ async function findContentRoutes(dir, base = dir, options = {}) {
363
441
  keywords: frontmatterKeywords(page.frontmatter),
364
442
  sections: extractMdxSections(page.content)
365
443
  },
366
- module: { virtualModuleId: "virtual:clarify-page/" + relativePath.replace(/\.mdx?$/, "").replace(/\/+/g, "/") },
444
+ module: {
445
+ pageVirtualModuleId: pageVirtualModuleId(relativePath.replace(/\.mdx?$/, "")),
446
+ contentVirtualModuleId: contentVirtualModuleId(fullPath, base)
447
+ },
367
448
  source: {
368
449
  filePath: fullPath,
369
450
  frontmatter: page.frontmatter,
@@ -375,14 +456,24 @@ async function findContentRoutes(dir, base = dir, options = {}) {
375
456
  }
376
457
  return routes;
377
458
  }
378
- function virtualModuleIdForLocalizedRoute(contentRoot, filePath) {
379
- return "virtual:clarify-page/" + relative(contentRoot, filePath).replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
459
+ function pageVirtualModuleIdForLocalizedRoute(contentRoot, filePath) {
460
+ return pageVirtualModuleIdFromRef(relative(contentRoot, filePath));
380
461
  }
381
462
  function frontmatterKeywords(frontmatter) {
382
463
  const value = frontmatter.keywords ?? frontmatter.keyword ?? frontmatter.tags;
383
464
  const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
384
465
  return keywords.length > 0 ? keywords : void 0;
385
466
  }
467
+ function withPageIdentity(route, path) {
468
+ return {
469
+ ...route,
470
+ path,
471
+ module: {
472
+ ...route.module,
473
+ pageVirtualModuleId: pageVirtualModuleId(path)
474
+ }
475
+ };
476
+ }
386
477
  function withAlternates(route, routes, locales) {
387
478
  const basePath = route.basePath ?? route.path;
388
479
  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]));
@@ -408,8 +499,7 @@ function generateBareAliases(routes, locales) {
408
499
  if (bp === route.path || seenBare.has(bp)) continue;
409
500
  seenBare.add(bp);
410
501
  bareRoutes.push({
411
- ...route,
412
- path: bp,
502
+ ...withPageIdentity(route, bp),
413
503
  isBareAlias: true
414
504
  });
415
505
  }
@@ -428,7 +518,11 @@ async function findLocalizedContentRoutes(contentRoot, locales, options = {}) {
428
518
  path: localizedRoutePath(basePath, locale.code, locales),
429
519
  basePath,
430
520
  locale: locale.code,
431
- module: { virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.source.filePath) }
521
+ module: {
522
+ ...route.module,
523
+ pageVirtualModuleId: pageVirtualModuleIdForLocalizedRoute(contentRoot, route.source.filePath),
524
+ contentVirtualModuleId: contentVirtualModuleId(route.source.filePath, contentRoot)
525
+ }
432
526
  });
433
527
  }
434
528
  }
@@ -440,9 +534,9 @@ async function findLocalizedContentRoutes(contentRoot, locales, options = {}) {
440
534
  for (const locale of locales.locales) {
441
535
  const key = `${locale.code}:${basePath}`;
442
536
  if (routeByLocaleAndBase.has(key)) continue;
537
+ const path = localizedRoutePath(basePath, locale.code, locales);
443
538
  localizedRoutes.push({
444
- ...sourceRoute,
445
- path: localizedRoutePath(basePath, locale.code, locales),
539
+ ...withPageIdentity(sourceRoute, path),
446
540
  locale: locale.code,
447
541
  isFallback: true
448
542
  });
@@ -537,13 +631,13 @@ function applyConfiguredPageRoutePaths(routes, tabs, locales) {
537
631
  const targetBasePath = routePathFromRef(intent.ref, intent.path);
538
632
  if (sourceBasePath === targetBasePath) continue;
539
633
  for (const route of routes) {
540
- if (route.kind !== "mdx" || (route.basePath ?? route.path) !== sourceBasePath) continue;
634
+ if (route.kind === "openapi" || (route.basePath ?? route.path) !== sourceBasePath) continue;
541
635
  const key = `${route.locale ?? ""}:${targetBasePath}`;
542
636
  if (existingKeys.has(key)) continue;
543
637
  existingKeys.add(key);
638
+ const path = route.locale && locales ? localizedRoutePath(targetBasePath, route.locale, locales) : targetBasePath;
544
639
  additions.push({
545
- ...route,
546
- path: route.locale && locales ? localizedRoutePath(targetBasePath, route.locale, locales) : targetBasePath,
640
+ ...withPageIdentity(route, path),
547
641
  basePath: targetBasePath
548
642
  });
549
643
  }
@@ -551,7 +645,8 @@ function applyConfiguredPageRoutePaths(routes, tabs, locales) {
551
645
  const nextRoutes = [...routes, ...additions];
552
646
  const routesWithAlternates = locales ? nextRoutes.map((route) => withAlternates(route, nextRoutes, locales)) : nextRoutes;
553
647
  if (!locales) return routesWithAlternates;
554
- return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, locales)];
648
+ const markdownRoutes = routesWithAlternates.filter((route) => route.kind !== "openapi");
649
+ return [...routesWithAlternates, ...generateBareAliases(markdownRoutes, locales)];
555
650
  }
556
651
  function buildNavigationFromPagesConfig(routes, config) {
557
652
  if (!config || config === "FileTree") return buildNavigation(routes);
@@ -745,7 +840,7 @@ function inferRouteForFile(filePath, contentRoot, defaultLocale) {
745
840
  filePath,
746
841
  locale: fileLocale,
747
842
  basePath: normalizePath(basePath),
748
- kind: isMdx ? "mdx" : "openapi",
843
+ kind: isMdx ? "markdown+jsx" : "openapi",
749
844
  title,
750
845
  inferred: true
751
846
  };
@@ -955,18 +1050,16 @@ var clarifyThemeConfigSchema = z.object({
955
1050
  tokens: clarifyThemeTokensConfigSchema.optional(),
956
1051
  layout: clarifyThemeLayoutConfigSchema.optional()
957
1052
  }).strict();
958
- function featureSchema(shape, defaults) {
1053
+ function featureSchema(shape, defaults, enabledByDefault = true) {
1054
+ const resolvedDefaults = {
1055
+ enabled: enabledByDefault,
1056
+ ...defaults
1057
+ };
959
1058
  const objectSchema = z.object({
960
- enabled: z.boolean().default(true),
1059
+ enabled: z.boolean().default(enabledByDefault),
961
1060
  ...shape
962
- }).strict().default({
963
- enabled: true,
964
- ...defaults
965
- });
966
- return z.union([z.boolean(), objectSchema]).default({
967
- enabled: true,
968
- ...defaults
969
- }).transform((value) => typeof value === "boolean" ? {
1061
+ }).strict().default(resolvedDefaults);
1062
+ return z.union([z.boolean(), objectSchema]).default(resolvedDefaults).transform((value) => typeof value === "boolean" ? {
970
1063
  enabled: value,
971
1064
  ...defaults
972
1065
  } : value);
@@ -974,7 +1067,7 @@ function featureSchema(shape, defaults) {
974
1067
  var clarifyFeaturesConfigSchema = z.object({
975
1068
  search: featureSchema({ mcp: z.boolean().default(true) }, { mcp: true }),
976
1069
  repository: featureSchema(clarifyRepositoryConfigSchema.shape, {}),
977
- themeEditor: featureSchema({}, {}),
1070
+ themeEditor: featureSchema({}, {}, false),
978
1071
  openapi: featureSchema({ playground: z.boolean().default(true) }, { playground: true })
979
1072
  }).strict();
980
1073
  var clarifyProjectConfigSchema = z.object({
@@ -1166,6 +1259,9 @@ function resolveVirtualId(id) {
1166
1259
  function stripVirtualPrefix(id) {
1167
1260
  return id.startsWith("\0") ? id.slice(1) : id;
1168
1261
  }
1262
+ function virtualContentModuleId(route) {
1263
+ return route.module.contentVirtualModuleId;
1264
+ }
1169
1265
  /**
1170
1266
  * Fixed virtual module IDs (excluding the client entry, which has a special
1171
1267
  * resolved form). Used to avoid hardcoding the constant list in every
@@ -1193,8 +1289,10 @@ function resolveVirtualModuleId(id, modules, routes) {
1193
1289
  for (const virtualId of FIXED_VIRTUAL_IDS) if (id === virtualId || id === resolveVirtualId(virtualId)) return resolveVirtualId(virtualId);
1194
1290
  const moduleId = stripVirtualPrefix(id);
1195
1291
  if (modules.has(moduleId)) return resolveVirtualId(moduleId);
1196
- const route = routes.find((route) => route.module.virtualModuleId === id || route.module.virtualModuleId === moduleId);
1197
- if (route) return resolveVirtualId(route.module.virtualModuleId);
1292
+ const contentRoute = routes.find((route) => (route.kind === "markdown" || route.kind === "markdown+jsx") && route.module.contentVirtualModuleId === moduleId);
1293
+ if (contentRoute) return contentRoute.source.filePath;
1294
+ const route = routes.find((route) => route.module.pageVirtualModuleId === id || route.module.pageVirtualModuleId === moduleId);
1295
+ if (route) return resolveVirtualId(route.module.pageVirtualModuleId);
1198
1296
  return null;
1199
1297
  }
1200
1298
  function generateConfigModule(projectConfig, version) {
@@ -1248,8 +1346,8 @@ function routeToRuntimeManifestEntry(route, component, mode) {
1248
1346
  return entry;
1249
1347
  }
1250
1348
  function generateRoutesModule(routes, navigation, mode = "client") {
1251
- 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) => {
1252
- 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 }) }))`;
1349
+ 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) => {
1350
+ 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 }) }))`;
1253
1351
  const placeholder = `__COMPONENT_${i}__`;
1254
1352
  const manifestEntry = routeToRuntimeManifestEntry(r, placeholder, mode);
1255
1353
  return ` ${JSON.stringify(manifestEntry, null, 0).replace(`"${placeholder}"`, component).replace(/"([A-Za-z_$][\w$]*)":/g, "$1: ")}`;
@@ -1321,13 +1419,14 @@ function buildVirtualModules(args) {
1321
1419
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, "client"));
1322
1420
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, "server"));
1323
1421
  modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
1324
- modules.set(VIRTUAL_OPENAPI, "export const openApiSpecs = {};");
1325
1422
  modules.set(VIRTUAL_SLOT, "export { useSlot } from '@clarify-labs/renderer';\n");
1326
- modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1423
+ modules.set(VIRTUAL_OPENAPI, "export const openApiSpecs = {};");
1327
1424
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1425
+ modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1328
1426
  for (const route of args.routes) {
1329
- const moduleContent = route.diagnostic ? generateContentDiagnosticModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1330
- modules.set(route.module.virtualModuleId, moduleContent);
1427
+ const moduleContent = route.diagnostic ? generateContentDiagnosticModule(route.diagnostic) : route.kind === "markdown" || route.kind === "markdown+jsx" ? `export { default } from ${moduleSpecifier(virtualContentModuleId(route))};` : null;
1428
+ if (moduleContent === null) continue;
1429
+ modules.set(route.module.pageVirtualModuleId, moduleContent);
1331
1430
  }
1332
1431
  return modules;
1333
1432
  }
@@ -1368,7 +1467,7 @@ function createNormalizedContentPlugin(engine) {
1368
1467
  transform(_code, id) {
1369
1468
  if (!/\.mdx?(?:\?|$)/.test(id)) return null;
1370
1469
  const filePath = id.replace(/\?.*$/, "");
1371
- const route = engine.routes.find((route) => route.kind === "mdx" && route.source.filePath === filePath);
1470
+ const route = engine.routes.find((route) => (route.kind === "markdown+jsx" || route.kind === "markdown") && route.source.filePath === filePath);
1372
1471
  if (!route || route.source.content === void 0) return null;
1373
1472
  return {
1374
1473
  code: route.source.content,
@@ -1483,7 +1582,10 @@ function createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown,
1483
1582
  };
1484
1583
  server.watcher.on("add", handleContentTreeChange);
1485
1584
  server.watcher.on("unlink", handleContentTreeChange);
1486
- await engine.configureDevServer(server);
1585
+ const postHooks = await engine.configureDevServer(server);
1586
+ if (postHooks.length > 0) return () => {
1587
+ for (const postHook of postHooks) postHook();
1588
+ };
1487
1589
  },
1488
1590
  transformIndexHtml: {
1489
1591
  order: "pre",
@@ -1538,7 +1640,7 @@ function createViteAdapter(engine) {
1538
1640
  const errorPolicy = () => contentCompileErrorPolicy;
1539
1641
  const normalizedContentPlugin = createNormalizedContentPlugin(engine);
1540
1642
  const markdown = createContentCompilerPlugin("markdown", engine.root, errorPolicy);
1541
- const mdx = createContentCompilerPlugin("mdx", engine.root, errorPolicy);
1643
+ const mdx = createContentCompilerPlugin("markdown+jsx", engine.root, errorPolicy);
1542
1644
  const compilePolicyPlugin = {
1543
1645
  name: "clarify:content-compile-policy",
1544
1646
  configResolved(config) {
@@ -1558,7 +1660,7 @@ function createViteAdapter(engine) {
1558
1660
  }
1559
1661
  var cliPackageVersion = {
1560
1662
  name: "@clarify-labs/cli",
1561
- version: "0.11.3",
1663
+ version: "0.11.5",
1562
1664
  license: "AGPL-3.0-only",
1563
1665
  type: "module",
1564
1666
  repository: {
@@ -1658,7 +1760,7 @@ function withUtf8Signature(content) {
1658
1760
  return `${UTF8_SIGNATURE}${content}`;
1659
1761
  }
1660
1762
  function shouldUseUtf8Signature(route) {
1661
- if (route.kind === "mdx") return true;
1763
+ if (route.kind === "markdown+jsx" || route.kind === "markdown") return true;
1662
1764
  return false;
1663
1765
  }
1664
1766
  function routeToMarkdownArtifactUrl(routePath) {
@@ -1730,9 +1832,9 @@ function createLlmsTxt(routes, projectConfig) {
1730
1832
  const lines = [`# ${projectConfig.title}`, ""];
1731
1833
  if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
1732
1834
  lines.push("This file lists the source-ready Markdown and OpenAPI artifacts for this documentation site.", "");
1733
- const mdxRoutes = routes.filter((route) => route.kind === "mdx" && isLlmsTxtRoute(route) && !route.isBareAlias);
1734
- if (mdxRoutes.length > 0) {
1735
- const localizedGroups = groupLlmsTxtRoutesByLocale(mdxRoutes, projectConfig);
1835
+ const markdownRoutes = routes.filter((route) => (route.kind === "markdown+jsx" || route.kind === "markdown") && isLlmsTxtRoute(route) && !route.isBareAlias);
1836
+ if (markdownRoutes.length > 0) {
1837
+ const localizedGroups = groupLlmsTxtRoutesByLocale(markdownRoutes, projectConfig);
1736
1838
  for (const [locale, localeRoutes] of localizedGroups) {
1737
1839
  lines.push(`## ${llmsTxtDocsSectionTitle(locale, localizedGroups.size, projectConfig)}`);
1738
1840
  for (const route of localeRoutes) {
@@ -1809,10 +1911,12 @@ function createContentArtifactsPlugin() {
1809
1911
  return input;
1810
1912
  },
1811
1913
  "dev:configureServer": (server, ctx) => {
1812
- server.middlewares.use((req, res, next) => {
1813
- if (serveContentArtifacts(req, res, ctx.projectConfig, ctx.routes)) return;
1814
- next();
1815
- });
1914
+ return () => {
1915
+ server.middlewares.use((req, res, next) => {
1916
+ if (serveContentArtifacts(req, res, ctx.projectConfig, ctx.routes)) return;
1917
+ next();
1918
+ });
1919
+ };
1816
1920
  },
1817
1921
  "build:assets": (ctx) => {
1818
1922
  const assets = [];
@@ -2303,7 +2407,7 @@ function findOpenAPIRoutes(dir, base = dir) {
2303
2407
  basePath: cleanPath,
2304
2408
  kind: "openapi",
2305
2409
  meta: { title: kebabToTitle(cleanPath.split("/").pop() ?? "API") },
2306
- module: { virtualModuleId: virtualModuleIdFromRef(ref) },
2410
+ module: { pageVirtualModuleId: pageVirtualModuleIdFromRef(ref) },
2307
2411
  source: { filePath: fullPath }
2308
2412
  });
2309
2413
  }
@@ -2427,34 +2531,7 @@ async function readOpenAPISpec(filePath, contentProcessor, projectRoot) {
2427
2531
  }
2428
2532
  }
2429
2533
  //#endregion
2430
- //#region source/plugins/openapi/virtual-modules.ts
2431
- var openApiRegistryModuleId = VIRTUAL_OPENAPI;
2432
- var openApiServerRegistryModuleId = VIRTUAL_OPENAPI_SERVER;
2433
- var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
2434
- function specVirtualModuleId(specKey) {
2435
- return `${OPENAPI_SPEC_PREFIX}${specKey}`;
2436
- }
2437
- function generateOpenAPIRegistryModule(openApiSpecs) {
2438
- return `export const openApiSpecs = {\n${Object.entries(openApiSpecs).map(([key, moduleId]) => ` ${JSON.stringify(key)}: () => import(${JSON.stringify(moduleId)}),`).join("\n")}\n};`;
2439
- }
2440
- function generateOpenAPIServerRegistryModule(openApiSpecs) {
2441
- return `export const openApiSpecs = ${JSON.stringify(openApiSpecs)};`;
2442
- }
2443
- /** Generate a virtual module that exports a single spec as default. */
2444
- function generateOpenAPISpecModule(spec) {
2445
- return `export default ${JSON.stringify(spec)};`;
2446
- }
2447
- function generateOpenAPIPageModule(opts) {
2448
- return `import { createOpenApiRouteComponent } from '@clarify-labs/renderer';
2449
- import spec from ${JSON.stringify(opts.specModuleId)};
2450
-
2451
- export const routeData = {};
2452
-
2453
- export default createOpenApiRouteComponent({ ...routeData, spec });
2454
- `;
2455
- }
2456
- //#endregion
2457
- //#region source/plugins/openapi/index.ts
2534
+ //#region source/plugins/openapi/routes.ts
2458
2535
  function collectOpenAPIPageIntents(config) {
2459
2536
  const intents = [];
2460
2537
  function visitItems(items) {
@@ -2474,9 +2551,6 @@ function collectOpenAPIPageIntents(config) {
2474
2551
  for (const tab of config.navigation?.tabs ?? []) visitPages(tab.pages);
2475
2552
  return intents;
2476
2553
  }
2477
- function virtualModuleIdFromPath(path) {
2478
- return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
2479
- }
2480
2554
  function cloneOpenAPIRoute(route, overrides = {}) {
2481
2555
  return {
2482
2556
  ...route,
@@ -2494,7 +2568,7 @@ function cloneOpenAPIRoute(route, overrides = {}) {
2494
2568
  ...overrides
2495
2569
  };
2496
2570
  }
2497
- function findOpenAPIRoutesForContentRoot(contentRoot, locales) {
2571
+ function discoverOpenAPIRoutes(contentRoot, locales) {
2498
2572
  if (!locales) return findOpenAPIRoutes(contentRoot);
2499
2573
  const localizedRoutes = [];
2500
2574
  for (const locale of locales.locales) {
@@ -2507,31 +2581,30 @@ function findOpenAPIRoutesForContentRoot(contentRoot, locales) {
2507
2581
  path,
2508
2582
  basePath,
2509
2583
  locale: locale.code,
2510
- module: { virtualModuleId: virtualModuleIdFromPath(path) }
2584
+ module: { pageVirtualModuleId: pageVirtualModuleId(path) }
2511
2585
  });
2512
2586
  }
2513
2587
  }
2514
- if (locales.missing === "fallback") {
2515
- const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
2516
- const defaultRoutes = localizedRoutes.filter((route) => route.locale === locales.default);
2517
- for (const sourceRoute of defaultRoutes) {
2518
- const basePath = sourceRoute.basePath ?? sourceRoute.path;
2519
- for (const locale of locales.locales) {
2520
- const key = `${locale.code}:${basePath}`;
2521
- if (routeByLocaleAndBase.has(key)) continue;
2522
- const path = localizedRoutePath(basePath, locale.code, locales);
2523
- localizedRoutes.push(cloneOpenAPIRoute(sourceRoute, {
2524
- path,
2525
- locale: locale.code,
2526
- module: { virtualModuleId: virtualModuleIdFromPath(path) },
2527
- isFallback: true
2528
- }));
2529
- }
2588
+ if (locales.missing !== "fallback") return localizedRoutes;
2589
+ const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
2590
+ const defaultRoutes = localizedRoutes.filter((route) => route.locale === locales.default);
2591
+ for (const sourceRoute of defaultRoutes) {
2592
+ const basePath = sourceRoute.basePath ?? sourceRoute.path;
2593
+ for (const locale of locales.locales) {
2594
+ const key = `${locale.code}:${basePath}`;
2595
+ if (routeByLocaleAndBase.has(key)) continue;
2596
+ const path = localizedRoutePath(basePath, locale.code, locales);
2597
+ localizedRoutes.push(cloneOpenAPIRoute(sourceRoute, {
2598
+ path,
2599
+ locale: locale.code,
2600
+ module: { pageVirtualModuleId: pageVirtualModuleId(path) },
2601
+ isFallback: true
2602
+ }));
2530
2603
  }
2531
2604
  }
2532
2605
  return localizedRoutes;
2533
2606
  }
2534
- function createConfiguredOpenAPIRoutes(routes, config) {
2607
+ function expandConfiguredOpenAPIRoutes(routes, config) {
2535
2608
  const openAPIIntents = collectOpenAPIPageIntents(config);
2536
2609
  if (!openAPIIntents.length) return routes;
2537
2610
  const additions = [];
@@ -2550,7 +2623,7 @@ function createConfiguredOpenAPIRoutes(routes, config) {
2550
2623
  additions.push(cloneOpenAPIRoute(route, {
2551
2624
  path,
2552
2625
  basePath: targetBasePath,
2553
- module: { virtualModuleId: virtualModuleIdFromPath(path) },
2626
+ module: { pageVirtualModuleId: pageVirtualModuleId(path) },
2554
2627
  openapi: {
2555
2628
  ...route.openapi,
2556
2629
  tagFilter: tagFilter?.length ? tagFilter : void 0
@@ -2564,19 +2637,47 @@ function createConfiguredOpenAPIRoutes(routes, config) {
2564
2637
  const bareAliases = [];
2565
2638
  const seenBare = new Set(routesWithAlternates.map((route) => route.path));
2566
2639
  for (const route of routesWithAlternates) {
2567
- if (route.locale !== config.locales.default) continue;
2640
+ if (route.kind !== "openapi" || route.locale !== config.locales.default) continue;
2568
2641
  const basePath = route.basePath ?? route.path;
2569
2642
  if (basePath === route.path || seenBare.has(basePath)) continue;
2570
2643
  seenBare.add(basePath);
2571
2644
  bareAliases.push(cloneOpenAPIRoute(route, {
2572
2645
  path: basePath,
2573
- module: { virtualModuleId: virtualModuleIdFromPath(basePath) },
2646
+ module: { pageVirtualModuleId: pageVirtualModuleId(basePath) },
2574
2647
  isBareAlias: true
2575
2648
  }));
2576
2649
  }
2577
2650
  return [...routesWithAlternates, ...bareAliases];
2578
2651
  }
2579
- /** Derive a stable, deduplicated id from an absolute spec file path. */
2652
+ //#endregion
2653
+ //#region source/plugins/openapi/virtual-modules.ts
2654
+ var openApiRegistryModuleId = VIRTUAL_OPENAPI;
2655
+ var openApiServerRegistryModuleId = VIRTUAL_OPENAPI_SERVER;
2656
+ var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
2657
+ function specVirtualModuleId(specKey) {
2658
+ return `${OPENAPI_SPEC_PREFIX}${specKey}`;
2659
+ }
2660
+ function generateOpenAPIRegistryModule(openApiSpecs) {
2661
+ return `export const openApiSpecs = {\n${Object.entries(openApiSpecs).map(([key, moduleId]) => ` ${JSON.stringify(key)}: () => import(${JSON.stringify(moduleId)}),`).join("\n")}\n};`;
2662
+ }
2663
+ function generateOpenAPIServerRegistryModule(openApiSpecs) {
2664
+ return `export const openApiSpecs = ${JSON.stringify(openApiSpecs)};`;
2665
+ }
2666
+ /** Generate a virtual module that exports a single spec as default. */
2667
+ function generateOpenAPISpecModule(spec) {
2668
+ return `export default ${JSON.stringify(spec)};`;
2669
+ }
2670
+ function generateOpenAPIPageModule(opts) {
2671
+ return `import { createOpenApiRouteComponent } from '@clarify-labs/renderer';
2672
+ import spec from ${JSON.stringify(opts.specModuleId)};
2673
+
2674
+ export const routeData = {};
2675
+
2676
+ export default createOpenApiRouteComponent({ ...routeData, spec });
2677
+ `;
2678
+ }
2679
+ //#endregion
2680
+ //#region source/plugins/openapi/state.ts
2580
2681
  function sourceSpecIdFromPath(filePath, projectRoot) {
2581
2682
  return relative(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
2582
2683
  }
@@ -2588,90 +2689,94 @@ function openAPIRegistryKeys(route) {
2588
2689
  const keys = /* @__PURE__ */ new Set();
2589
2690
  const path = (route.path || "/").replace(/^\//, "");
2590
2691
  const base = (route.basePath ?? route.path).replace(/^\//, "");
2591
- if (route.locale) keys.add(`virtual:clarify-page/${route.locale}/${base}`);
2592
- if (path) keys.add(`virtual:clarify-page/${path}`);
2593
- else keys.add("virtual:clarify-page/index");
2594
- if (!route.locale && base) keys.add(`virtual:clarify-page/${base}`);
2692
+ if (route.locale) keys.add(pageVirtualModuleId(`${route.locale}/${base}`));
2693
+ keys.add(pageVirtualModuleId(path));
2694
+ if (!route.locale && base) keys.add(pageVirtualModuleId(base));
2595
2695
  return [...keys];
2596
2696
  }
2697
+ var OpenAPIPluginState = class {
2698
+ specModules = /* @__PURE__ */ new Map();
2699
+ async enrichRoutes(routes, ctx) {
2700
+ this.specModules.clear();
2701
+ const specByFilePath = /* @__PURE__ */ new Map();
2702
+ for (const route of routes) {
2703
+ if (route.kind !== "openapi") continue;
2704
+ const specFromCache = specByFilePath.get(route.source.filePath);
2705
+ const processor = createProjectContentProcessor(ctx.plugins, ctx);
2706
+ const result = specFromCache ? {
2707
+ ok: true,
2708
+ spec: specFromCache
2709
+ } : await readOpenAPISpec(route.source.filePath, processor, ctx.generateOptions.projectRoot);
2710
+ if (!result.ok) {
2711
+ route.diagnostic = result.diagnostic;
2712
+ route.meta.title = route.meta.title || "OpenAPI parse error";
2713
+ route.meta.sections = [];
2714
+ continue;
2715
+ }
2716
+ const spec = normalizeOpenAPISpecSectionIds(result.spec);
2717
+ specByFilePath.set(route.source.filePath, spec);
2718
+ const sourceSpecId = sourceSpecIdFromPath(route.source.filePath, ctx.generateOptions.projectRoot);
2719
+ route.openapi = {
2720
+ ...route.openapi,
2721
+ sourceSpecId
2722
+ };
2723
+ route.meta.title = spec.info?.title ?? route.meta.title;
2724
+ route.meta.description = spec.info?.description ?? route.meta.description;
2725
+ route.meta.sections = extractOpenAPISections(spec, route.openapi.tagFilter);
2726
+ const pageSpec = route.openapi.tagFilter?.length ? filterSpecByTags(spec, route.openapi.tagFilter) : spec;
2727
+ const routeSpecModuleId = routeSpecModuleIdFromSourceSpecId(sourceSpecId, route.openapi.tagFilter);
2728
+ route.openapi.routeSpecModuleId = routeSpecModuleId;
2729
+ this.specModules.set(routeSpecModuleId, pageSpec);
2730
+ route.source.content = JSON.stringify(pageSpec);
2731
+ }
2732
+ return routes;
2733
+ }
2734
+ contributeModules(modules, routes) {
2735
+ const registryEntries = {};
2736
+ const serverRegistryEntries = {};
2737
+ for (const route of routes) {
2738
+ if (route.kind !== "openapi" || !route.openapi?.sourceSpecId) continue;
2739
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2740
+ const spec = this.specModules.get(routeSpecModuleId);
2741
+ if (!spec) continue;
2742
+ for (const registryKey of openAPIRegistryKeys(route)) {
2743
+ registryEntries[registryKey] = specVirtualModuleId(routeSpecModuleId);
2744
+ serverRegistryEntries[registryKey] = spec;
2745
+ }
2746
+ }
2747
+ modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
2748
+ modules.set(openApiServerRegistryModuleId, generateOpenAPIServerRegistryModule(serverRegistryEntries));
2749
+ for (const [routeSpecModuleId, spec] of this.specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2750
+ for (const route of routes) {
2751
+ if (route.kind !== "openapi") continue;
2752
+ if (route.diagnostic) {
2753
+ modules.set(route.module.pageVirtualModuleId, generateContentDiagnosticModule(route.diagnostic));
2754
+ continue;
2755
+ }
2756
+ if (!route.openapi?.sourceSpecId) continue;
2757
+ const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2758
+ if (!this.specModules.has(routeSpecModuleId)) continue;
2759
+ modules.set(route.module.pageVirtualModuleId, generateOpenAPIPageModule({ specModuleId: specVirtualModuleId(routeSpecModuleId) }));
2760
+ }
2761
+ return modules;
2762
+ }
2763
+ };
2764
+ //#endregion
2765
+ //#region source/plugins/openapi/index.ts
2597
2766
  function createOpenAPIPlugin() {
2598
- /** specs keyed by sourceSpecId (deduplicated per source file). */
2599
- const specs = /* @__PURE__ */ new Map();
2600
- const specModules = /* @__PURE__ */ new Map();
2767
+ const state = new OpenAPIPluginState();
2601
2768
  return {
2602
2769
  name: "clarify:openapi",
2603
2770
  hooks: {
2604
2771
  "routes:discover": (input, ctx) => ({
2605
2772
  ...input,
2606
- routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.locales)]
2773
+ routes: [...input.routes, ...discoverOpenAPIRoutes(input.contentRoot, ctx.projectConfig.locales)]
2607
2774
  }),
2608
2775
  "routes:discovered": async (routes, ctx) => {
2609
- specs.clear();
2610
- specModules.clear();
2611
- const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
2612
- const specByFilePath = /* @__PURE__ */ new Map();
2613
- for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
2614
- const specFromCache = specByFilePath.get(route.source.filePath);
2615
- const processor = createProjectContentProcessor(ctx.plugins, ctx);
2616
- const result = specFromCache ? {
2617
- ok: true,
2618
- spec: specFromCache
2619
- } : await readOpenAPISpec(route.source.filePath, processor, ctx.generateOptions.projectRoot);
2620
- if (!result.ok) {
2621
- route.diagnostic = result.diagnostic;
2622
- route.meta.title = route.meta.title || "OpenAPI parse error";
2623
- route.meta.sections = [];
2624
- continue;
2625
- }
2626
- const spec = normalizeOpenAPISpecSectionIds(result.spec);
2627
- specByFilePath.set(route.source.filePath, spec);
2628
- const sourceSpecId = sourceSpecIdFromPath(route.source.filePath, ctx.generateOptions.projectRoot);
2629
- route.openapi = {
2630
- ...route.openapi,
2631
- sourceSpecId
2632
- };
2633
- specs.set(sourceSpecId, {
2634
- spec,
2635
- filePath: route.source.filePath
2636
- });
2637
- route.meta.title = spec.info?.title ?? route.meta.title;
2638
- route.meta.description = spec.info?.description ?? route.meta.description;
2639
- route.meta.sections = extractOpenAPISections(spec, route.openapi?.tagFilter);
2640
- const pageSpec = route.openapi?.tagFilter?.length ? filterSpecByTags(spec, route.openapi.tagFilter) : spec;
2641
- const routeSpecModuleId = routeSpecModuleIdFromSourceSpecId(sourceSpecId, route.openapi?.tagFilter);
2642
- route.openapi.routeSpecModuleId = routeSpecModuleId;
2643
- specModules.set(routeSpecModuleId, pageSpec);
2644
- route.source.content = JSON.stringify(pageSpec);
2645
- }
2646
- return nextRoutes;
2776
+ const expandedRoutes = expandConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
2777
+ return state.enrichRoutes(expandedRoutes, ctx);
2647
2778
  },
2648
- "modules:before": (modules, ctx) => {
2649
- const registryEntries = {};
2650
- const serverRegistryEntries = {};
2651
- for (const route of ctx.routes) {
2652
- if (route.kind !== "openapi" || !route.openapi?.sourceSpecId) continue;
2653
- const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2654
- const spec = specModules.get(routeSpecModuleId) ?? specs.get(route.openapi.sourceSpecId)?.spec;
2655
- if (!spec) continue;
2656
- for (const registryKey of openAPIRegistryKeys(route)) {
2657
- registryEntries[registryKey] = specVirtualModuleId(routeSpecModuleId);
2658
- serverRegistryEntries[registryKey] = spec;
2659
- }
2660
- }
2661
- modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
2662
- modules.set(openApiServerRegistryModuleId, generateOpenAPIServerRegistryModule(serverRegistryEntries));
2663
- for (const [routeSpecModuleId, spec] of specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2664
- for (const route of ctx.routes) {
2665
- if (route.kind !== "openapi") continue;
2666
- if (route.diagnostic) modules.set(route.module.virtualModuleId, generateContentDiagnosticModule(route.diagnostic));
2667
- else if (route.openapi?.sourceSpecId) {
2668
- const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2669
- if (!specModules.has(routeSpecModuleId)) continue;
2670
- modules.set(route.module.virtualModuleId, generateOpenAPIPageModule({ specModuleId: specVirtualModuleId(routeSpecModuleId) }));
2671
- }
2672
- }
2673
- return modules;
2674
- }
2779
+ "modules:before": (modules, ctx) => state.contributeModules(modules, ctx.routes)
2675
2780
  }
2676
2781
  };
2677
2782
  }
@@ -3080,6 +3185,145 @@ async function resolveProjectContext(options = {}, env = {
3080
3185
  };
3081
3186
  }
3082
3187
  //#endregion
3188
+ //#region source/core/routing/route-analysis.ts
3189
+ function routeDescription(route) {
3190
+ return `${route.kind} ${route.source.filePath}`;
3191
+ }
3192
+ function collectConflicts(routes, keyOf) {
3193
+ const grouped = /* @__PURE__ */ new Map();
3194
+ for (const route of routes) {
3195
+ const key = keyOf(route);
3196
+ grouped.set(key, [...grouped.get(key) ?? [], route]);
3197
+ }
3198
+ return grouped;
3199
+ }
3200
+ function describeRouteConflict(route) {
3201
+ return routeDescription(route);
3202
+ }
3203
+ function findRouteConflicts(routes) {
3204
+ const conflicts = [];
3205
+ for (const [path, pathRoutes] of collectConflicts(routes, (route) => route.path)) {
3206
+ if (pathRoutes.length < 2) continue;
3207
+ conflicts.push({
3208
+ kind: "path",
3209
+ key: path,
3210
+ label: `path ${path}`,
3211
+ routes: pathRoutes
3212
+ });
3213
+ }
3214
+ for (const [pageModuleId, moduleRoutes] of collectConflicts(routes, (route) => route.module.pageVirtualModuleId)) {
3215
+ if (moduleRoutes.length < 2) continue;
3216
+ if (moduleRoutes.every((route) => route.path === moduleRoutes[0].path)) continue;
3217
+ conflicts.push({
3218
+ kind: "page-module",
3219
+ key: pageModuleId,
3220
+ label: `page module ${pageModuleId}`,
3221
+ routes: moduleRoutes
3222
+ });
3223
+ }
3224
+ return conflicts;
3225
+ }
3226
+ function formatRouteConflicts(conflicts) {
3227
+ return conflicts.map((conflict) => `- ${conflict.label}: ${conflict.routes.map(describeRouteConflict).join(", ")}`).join("\n");
3228
+ }
3229
+ //#endregion
3230
+ //#region source/core/routing/route-resolution.ts
3231
+ async function resolveRoutePages(routes, plugins, ctx) {
3232
+ const routeToken = Symbol("clarifyRouteToken");
3233
+ const resolvedPages = await runHooks(plugins, "pages:resolved", routes.map((route, routeIndex) => ({
3234
+ path: route.path,
3235
+ filePath: route.source.filePath,
3236
+ frontmatter: route.source.frontmatter ?? {},
3237
+ content: route.source.content ?? "",
3238
+ [routeToken]: routeIndex
3239
+ })), ctx);
3240
+ const pageByRouteIndex = /* @__PURE__ */ new Map();
3241
+ for (const page of resolvedPages) {
3242
+ const routeIndex = page[routeToken];
3243
+ if (!Number.isInteger(routeIndex) || routeIndex < 0 || routeIndex >= routes.length) throw new Error("[clarify] pages:resolved hooks must preserve each page route identity");
3244
+ if (pageByRouteIndex.has(routeIndex)) throw new Error("[clarify] pages:resolved hooks returned the same page route identity more than once");
3245
+ pageByRouteIndex.set(routeIndex, page);
3246
+ }
3247
+ if (pageByRouteIndex.size !== routes.length) throw new Error("[clarify] pages:resolved hooks must return exactly one page for each route");
3248
+ return routes.map((route, routeIndex) => {
3249
+ const page = pageByRouteIndex.get(routeIndex);
3250
+ return {
3251
+ ...route,
3252
+ source: {
3253
+ ...route.source,
3254
+ frontmatter: page.frontmatter,
3255
+ content: page.content
3256
+ }
3257
+ };
3258
+ });
3259
+ }
3260
+ function buildNavigationTree(routes, ctx) {
3261
+ const tabs = ctx.projectConfig.navigation?.tabs;
3262
+ if (!tabs) return {
3263
+ kind: "flat",
3264
+ nodes: buildNavigation(routes)
3265
+ };
3266
+ if (!ctx.projectConfig.locales) return buildNavigationFromTabsConfig(routes, tabs);
3267
+ return buildLocalizedNavigationFromTabsConfig(routes, tabs, ctx.projectConfig.locales) ?? {
3268
+ kind: "localized-tabbed",
3269
+ locales: {}
3270
+ };
3271
+ }
3272
+ function assertNoRouteConflicts(routes) {
3273
+ const conflicts = findRouteConflicts(routes);
3274
+ if (conflicts.length > 0) throw new Error(`[clarify] Route conflicts prevent the site from being built:\n${formatRouteConflicts(conflicts)}`);
3275
+ }
3276
+ function createRouteConflictDiagnostic(label, routes) {
3277
+ return {
3278
+ kind: "route-load",
3279
+ title: "Route conflict",
3280
+ message: `This page cannot be rendered because ${label} is used by multiple routes.`,
3281
+ details: routes.map(describeRouteConflict).join("\n")
3282
+ };
3283
+ }
3284
+ function createDevRouteConflictRoutes(routes) {
3285
+ const conflicts = findRouteConflicts(routes);
3286
+ if (conflicts.length === 0) return routes;
3287
+ const conflictedRoutes = /* @__PURE__ */ new Map();
3288
+ for (const conflict of conflicts) for (const route of conflict.routes) conflictedRoutes.set(route, conflict);
3289
+ function createConflictRoute(route, conflict) {
3290
+ return {
3291
+ ...route,
3292
+ module: {
3293
+ ...route.module,
3294
+ pageVirtualModuleId: `${route.module.pageVirtualModuleId}__conflict`
3295
+ },
3296
+ diagnostic: createRouteConflictDiagnostic(conflict.label, conflict.routes)
3297
+ };
3298
+ }
3299
+ const usedPaths = /* @__PURE__ */ new Set();
3300
+ return routes.flatMap((route) => {
3301
+ const conflict = conflictedRoutes.get(route);
3302
+ if (!conflict) return [route];
3303
+ if (usedPaths.has(route.path)) return [];
3304
+ usedPaths.add(route.path);
3305
+ return [createConflictRoute(route, conflict)];
3306
+ });
3307
+ }
3308
+ function handleRouteConflicts(routes, development) {
3309
+ if (development) return createDevRouteConflictRoutes(routes);
3310
+ assertNoRouteConflicts(routes);
3311
+ return routes;
3312
+ }
3313
+ async function resolveRouteState(routes, plugins, ctx, development) {
3314
+ const tabs = ctx.projectConfig.navigation?.tabs;
3315
+ const configuredRoutes = applyConfiguredPageRoutePaths(routes, tabs, ctx.projectConfig.locales);
3316
+ const resolved = await runHooks(plugins, "routes:resolved", {
3317
+ routes: configuredRoutes,
3318
+ navigation: buildNavigationTree(configuredRoutes, ctx)
3319
+ }, ctx);
3320
+ const finalRoutes = handleRouteConflicts(resolved.routes, development);
3321
+ return {
3322
+ routes: finalRoutes,
3323
+ navigation: finalRoutes === resolved.routes ? resolved.navigation : buildNavigationTree(finalRoutes, ctx)
3324
+ };
3325
+ }
3326
+ //#endregion
3083
3327
  //#region source/core/runtime/env-types.ts
3084
3328
  var GENERATED_FILE_NAME = "clarify-env.d.ts";
3085
3329
  /**
@@ -3620,7 +3864,7 @@ var ClarifyEngine = class {
3620
3864
  async discoverSite() {
3621
3865
  const plugins = this.ctx.plugins;
3622
3866
  const contentRoot = this.ctx.contentRoot;
3623
- const { locales, navigation: navigationConfig } = this.ctx.projectConfig;
3867
+ const { locales } = this.ctx.projectConfig;
3624
3868
  let routes = await runPhase(plugins, "site:discover", this.ctx, async () => {
3625
3869
  const options = { contentProcessor: createProjectContentProcessor(plugins, this.ctx) };
3626
3870
  const discovered = locales ? await findLocalizedContentRoutes(contentRoot, locales, options) : await findContentRoutes(contentRoot, contentRoot, options);
@@ -3628,43 +3872,10 @@ var ClarifyEngine = class {
3628
3872
  contentRoot,
3629
3873
  routes: discovered
3630
3874
  }, this.ctx);
3631
- return runHooks(plugins, "routes:discovered", result.routes, this.ctx);
3875
+ return await runHooks(plugins, "routes:discovered", result.routes, this.ctx);
3632
3876
  });
3633
- routes = await runPhase(plugins, "content:process", this.ctx, async () => {
3634
- const pages = routes.map((route) => ({
3635
- path: route.path,
3636
- filePath: route.source.filePath,
3637
- frontmatter: route.source.frontmatter ?? {},
3638
- content: route.source.content ?? ""
3639
- }));
3640
- const resolvedPages = await runHooks(plugins, "pages:resolved", pages, this.ctx);
3641
- const pageByPath = new Map(resolvedPages.map((p) => [p.path, p]));
3642
- return routes.map((route) => {
3643
- const page = pageByPath.get(route.path);
3644
- if (!page) return route;
3645
- return {
3646
- ...route,
3647
- source: {
3648
- ...route.source,
3649
- frontmatter: page.frontmatter,
3650
- content: page.content
3651
- }
3652
- };
3653
- });
3654
- });
3655
- const tabs = navigationConfig?.tabs;
3656
- routes = applyConfiguredPageRoutePaths(routes, tabs, locales);
3657
- const navigation = tabs ? locales ? buildLocalizedNavigationFromTabsConfig(routes, tabs, locales) ?? {
3658
- kind: "localized-tabbed",
3659
- locales: {}
3660
- } : buildNavigationFromTabsConfig(routes, tabs) : {
3661
- kind: "flat",
3662
- nodes: buildNavigation(routes)
3663
- };
3664
- const resolved = await runHooks(plugins, "routes:resolved", {
3665
- routes,
3666
- navigation
3667
- }, this.ctx);
3877
+ routes = await runPhase(plugins, "content:process", this.ctx, () => resolveRoutePages(routes, plugins, this.ctx));
3878
+ const resolved = await resolveRouteState(routes, plugins, this.ctx, this.runtime.command === "serve");
3668
3879
  this.ctx.routes = resolved.routes;
3669
3880
  this.ctx.navigation = resolved.navigation;
3670
3881
  }
@@ -3714,8 +3925,8 @@ var ClarifyEngine = class {
3714
3925
  return this.buildEnabled;
3715
3926
  }
3716
3927
  async configureDevServer(server) {
3717
- await runPhase(this.plugins, "dev:server", this.ctx, async () => {
3718
- await runDevConfigureServerHooks(this.plugins, server, this.ctx);
3928
+ return runPhase(this.plugins, "dev:server", this.ctx, () => {
3929
+ return runDevConfigureServerHooks(this.plugins, server, this.ctx);
3719
3930
  });
3720
3931
  }
3721
3932
  async runBuildDone() {
@@ -3786,6 +3997,6 @@ function createClarifyEngine(options = {}) {
3786
3997
  return new ClarifyEngine(options);
3787
3998
  }
3788
3999
  //#endregion
3789
- export { deserializeSearchIndex as a, cliPackageVersionWithCaret as c, createClarifyTempDir as i, createViteAdapter as l, createClarifyEngine as n, searchMcpIndex as o, createClarifyRuntimeAliases as r, cliPackageVersion as s, ClarifyEngine as t, defineConfig as u };
4000
+ export { createClarifyTempDir as a, cliPackageVersion as c, defineConfig as d, findRouteConflicts as i, cliPackageVersionWithCaret as l, createClarifyEngine as n, deserializeSearchIndex as o, createClarifyRuntimeAliases as r, searchMcpIndex as s, ClarifyEngine as t, createViteAdapter as u };
3790
4001
 
3791
- //# sourceMappingURL=engine-CrYRJRZT.js.map
4002
+ //# sourceMappingURL=engine-DuiJVdO8.js.map