@clarify-labs/cli 0.7.2 → 0.8.2

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.
@@ -8,13 +8,13 @@ import { toString } from "mdast-util-to-string";
8
8
  import shiki from "shiki";
9
9
  import { visit } from "unist-util-visit";
10
10
  import { markdownRemarkPlugins, themeBootstrapScript, themePresets } from "@clarify-labs/renderer";
11
- import { remark } from "remark";
11
+ import { stringify } from "yaml";
12
12
  import matter from "gray-matter";
13
+ import { remark } from "remark";
14
+ import { fileURLToPath, pathToFileURL } from "node:url";
13
15
  import SwaggerParser from "@apidevtools/swagger-parser";
14
16
  import { tmpdir } from "node:os";
15
- import { mkdir, writeFile } from "node:fs/promises";
16
17
  import { z } from "zod";
17
- import { pathToFileURL } from "node:url";
18
18
  import { build, loadConfigFromFile } from "vite";
19
19
  import { createRequire } from "node:module";
20
20
  //#region source/core/utils.ts
@@ -121,17 +121,16 @@ function withUtf8Signature(content) {
121
121
  }
122
122
  function shouldUseUtf8Signature(route) {
123
123
  if (route.kind === "mdx") return true;
124
- return /\.ya?ml$/i.test(route.contentArtifactUrl ?? "");
124
+ return false;
125
125
  }
126
126
  function routeToMarkdownArtifactUrl(routePath) {
127
127
  return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.md`;
128
128
  }
129
- function routeToOpenAPIArtifactUrl(routePath, filePath) {
130
- const extension = extname(filePath).toLowerCase() || ".json";
131
- return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi${extension}`;
129
+ function routeToOpenAPIArtifactUrl(routePath) {
130
+ return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi.json`;
132
131
  }
133
132
  function attachContentArtifactUrls(routes) {
134
- for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path, route.filePath) : routeToMarkdownArtifactUrl(route.path);
133
+ for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path);
135
134
  }
136
135
  function readRouteContent(route) {
137
136
  if (route.content !== void 0) return route.content;
@@ -141,40 +140,81 @@ function readRouteArtifactContent(route) {
141
140
  const content = readRouteContent(route);
142
141
  return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
143
142
  }
144
- function writeContentArtifactFiles(routes, outputDirectory) {
143
+ function isLlmsTxtRoute(route) {
144
+ return !route.path.split("/").includes("404");
145
+ }
146
+ function llmsTxtDescription(route) {
147
+ if (route.description) return route.description;
148
+ const sections = route.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
149
+ if (sections?.length) return `Covers ${sections.join(", ")}.`;
150
+ if (route.keywords?.length) return `Related topics: ${route.keywords.join(", ")}.`;
151
+ if (route.kind === "openapi") return "OpenAPI artifact for machine-readable API reference data.";
152
+ }
153
+ function llmsTxtListItem(route, basePath) {
154
+ if (!route.contentArtifactUrl) return void 0;
155
+ const description = llmsTxtDescription(route);
156
+ return description ? `- [${route.title}](${basePath}${route.contentArtifactUrl}): ${description}` : `- [${route.title}](${basePath}${route.contentArtifactUrl})`;
157
+ }
158
+ function llmsTxtLocaleLabel(locale, projectConfig) {
159
+ return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
160
+ }
161
+ function groupRoutesByLocale(routes) {
162
+ const groups = /* @__PURE__ */ new Map();
163
+ for (const route of routes) {
164
+ const key = route.locale ?? "default";
165
+ groups.set(key, [...groups.get(key) ?? [], route]);
166
+ }
167
+ return groups;
168
+ }
169
+ function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
170
+ const routesBySource = /* @__PURE__ */ new Map();
171
+ const defaultLocale = projectConfig.i18n?.defaultLocale;
145
172
  for (const route of routes) {
146
- if (!route.contentArtifactUrl) continue;
147
- const outFile = join(outputDirectory, route.contentArtifactUrl.replace(/^\//, ""));
148
- mkdirSync(dirname(outFile), { recursive: true });
149
- writeFileSync(outFile, readRouteArtifactContent(route), "utf-8");
173
+ const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.contentArtifactUrl ?? route.path}`;
174
+ const previousRoute = routesBySource.get(sourceKey);
175
+ if (!previousRoute || previousRoute.locale && !route.locale) routesBySource.set(sourceKey, route);
150
176
  }
177
+ return groupRoutesByLocale([...routesBySource.values()].map((route) => ({
178
+ ...route,
179
+ locale: route.locale ?? defaultLocale
180
+ })));
181
+ }
182
+ function llmsTxtDocsSectionTitle(locale, groupCount, projectConfig) {
183
+ if (groupCount <= 1) return "Docs";
184
+ return `Docs - ${locale === "default" ? "Default" : llmsTxtLocaleLabel(locale, projectConfig)}`;
151
185
  }
152
186
  function createLlmsTxt(routes, projectConfig) {
153
187
  const basePath = normalizeBasePath(projectConfig.routePrefix);
154
188
  const lines = [`# ${projectConfig.title}`, ""];
155
189
  if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
156
- lines.push("## Docs");
157
- for (const route of routes.filter((route) => route.kind === "mdx")) {
158
- if (!route.contentArtifactUrl) continue;
159
- lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
190
+ lines.push("This file lists the source-ready Markdown and OpenAPI artifacts for this documentation site.", "");
191
+ const mdxRoutes = routes.filter((route) => route.kind === "mdx" && isLlmsTxtRoute(route));
192
+ if (mdxRoutes.length > 0) {
193
+ const localizedGroups = groupLlmsTxtRoutesByLocale(mdxRoutes, projectConfig);
194
+ for (const [locale, localeRoutes] of localizedGroups) {
195
+ lines.push(`## ${llmsTxtDocsSectionTitle(locale, localizedGroups.size, projectConfig)}`);
196
+ for (const route of localeRoutes) {
197
+ const item = llmsTxtListItem(route, basePath);
198
+ if (item) lines.push(item);
199
+ }
200
+ lines.push("");
201
+ }
160
202
  }
161
- const openApiRoutes = routes.filter((route) => route.kind === "openapi");
203
+ const openApiRoutes = routes.filter((route) => route.kind === "openapi" && isLlmsTxtRoute(route));
162
204
  if (openApiRoutes.length > 0) {
163
- lines.push("", "## OpenAPI");
205
+ if (lines.at(-1) !== "") lines.push("");
206
+ lines.push("## OpenAPI");
164
207
  for (const route of openApiRoutes) {
165
- if (!route.contentArtifactUrl) continue;
166
- lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
208
+ const item = llmsTxtListItem(route, basePath);
209
+ if (item) lines.push(item);
167
210
  }
168
211
  }
212
+ while (lines.at(-1) === "") lines.pop();
169
213
  return `${lines.join("\n")}\n`;
170
214
  }
171
215
  function createLlmsTxtArtifact(routes, projectConfig) {
172
216
  return withUtf8Signature(createLlmsTxt(routes, projectConfig));
173
217
  }
174
- function writeLlmsTxt(routes, projectConfig, outputDirectory) {
175
- mkdirSync(outputDirectory, { recursive: true });
176
- writeFileSync(join(outputDirectory, "llms.txt"), createLlmsTxtArtifact(routes, projectConfig), "utf-8");
177
- }
178
218
  //#endregion
179
219
  //#region source/plugins/content-artifacts/server.ts
180
220
  function normalizeRoutePrefix$1(routePrefix) {
@@ -187,7 +227,6 @@ function resolveContentArtifactPath(url, projectConfig) {
187
227
  return basePath && requestPath.startsWith(basePath) ? requestPath.slice(basePath.length) || "/" : requestPath;
188
228
  }
189
229
  function resolveContentArtifactType(route) {
190
- if (route.kind === "openapi" && /\.ya?ml$/i.test(route.contentArtifactUrl ?? "")) return "text/yaml; charset=utf-8";
191
230
  return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
192
231
  }
193
232
  function serveContentArtifacts(req, res, projectConfig, routes) {
@@ -198,6 +237,18 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
198
237
  res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
199
238
  return true;
200
239
  }
240
+ if (contentPath.endsWith(".openapi.yaml")) {
241
+ const jsonPath = contentPath.replace(/\.yaml$/, ".json");
242
+ const route = routes.find((r) => r.contentArtifactUrl === jsonPath);
243
+ if (route?.kind === "openapi" && route.content) {
244
+ const spec = JSON.parse(route.content);
245
+ res.statusCode = 200;
246
+ res.setHeader("Content-Type", "text/yaml; charset=utf-8");
247
+ res.end(stringify(spec, { lineWidth: 0 }), "utf8");
248
+ return true;
249
+ }
250
+ return false;
251
+ }
201
252
  const route = routes.find((route) => route.contentArtifactUrl === contentPath);
202
253
  if (!route) return false;
203
254
  res.statusCode = 200;
@@ -221,12 +272,28 @@ function createContentArtifactsPlugin() {
221
272
  next();
222
273
  });
223
274
  },
224
- "build:done": (ctx) => {
225
- const outputDirectory = ctx.generateOptions.outputDirectory;
226
- if (!outputDirectory) return;
227
- const outputRoot = join(ctx.generateOptions.projectRoot, outputDirectory);
228
- writeContentArtifactFiles(ctx.routes, outputRoot);
229
- writeLlmsTxt(ctx.routes, ctx.projectConfig, outputRoot);
275
+ "build:assets": (ctx) => {
276
+ const assets = [];
277
+ for (const route of ctx.routes) {
278
+ if (!route.contentArtifactUrl) continue;
279
+ assets.push({
280
+ fileName: route.contentArtifactUrl.replace(/^\//, ""),
281
+ source: readRouteArtifactContent(route)
282
+ });
283
+ if (route.kind === "openapi" && route.content) {
284
+ const yamlFileName = route.contentArtifactUrl.replace(/\.json$/, ".yaml");
285
+ const spec = JSON.parse(route.content);
286
+ assets.push({
287
+ fileName: yamlFileName.replace(/^\//, ""),
288
+ source: stringify(spec, { lineWidth: 0 })
289
+ });
290
+ }
291
+ }
292
+ assets.push({
293
+ fileName: "llms.txt",
294
+ source: createLlmsTxtArtifact(ctx.routes, ctx.projectConfig)
295
+ });
296
+ return assets;
230
297
  }
231
298
  }
232
299
  };
@@ -302,8 +369,103 @@ function parseFrontmatter(content) {
302
369
  };
303
370
  }
304
371
  //#endregion
372
+ //#region source/parsers/content.ts
373
+ async function transformContent(transform, input) {
374
+ return transform ? await transform(input) : input;
375
+ }
376
+ function createContentProcessor(transform) {
377
+ return {
378
+ async processMdx(source, filePath) {
379
+ const { frontmatter, content } = parseFrontmatter(source);
380
+ const transformed = await transformContent(transform, {
381
+ kind: "mdx",
382
+ source,
383
+ filePath,
384
+ frontmatter,
385
+ content
386
+ });
387
+ return {
388
+ frontmatter: transformed.frontmatter,
389
+ content: transformed.content
390
+ };
391
+ },
392
+ async processText(source, kind, filePath) {
393
+ return (await transformContent(transform, {
394
+ kind,
395
+ source,
396
+ filePath,
397
+ frontmatter: {},
398
+ content: source
399
+ })).content;
400
+ }
401
+ };
402
+ }
403
+ //#endregion
404
+ //#region source/core/hooks.ts
405
+ async function runHooks(plugins, hookName, input, ctx) {
406
+ let result = input;
407
+ for (const plugin of plugins) {
408
+ const hook = plugin.hooks?.[hookName];
409
+ if (!hook) continue;
410
+ try {
411
+ result = await hook(result, ctx);
412
+ } catch (err) {
413
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
414
+ }
415
+ }
416
+ return result;
417
+ }
418
+ async function runDevConfigureServerHooks(plugins, server, ctx) {
419
+ for (const plugin of plugins) {
420
+ const hook = plugin.hooks?.["dev:configureServer"];
421
+ if (!hook) continue;
422
+ try {
423
+ await hook(server, ctx);
424
+ } catch (err) {
425
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
426
+ }
427
+ }
428
+ }
429
+ async function runBuildAssetsHooks(plugins, ctx) {
430
+ const assets = [];
431
+ for (const plugin of plugins) {
432
+ const hook = plugin.hooks?.["build:assets"];
433
+ if (!hook) continue;
434
+ try {
435
+ const pluginAssets = await hook(ctx);
436
+ assets.push(...pluginAssets);
437
+ } catch (err) {
438
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
439
+ }
440
+ }
441
+ return assets;
442
+ }
443
+ async function runBuildDoneHooks(plugins, ctx) {
444
+ for (const plugin of plugins) {
445
+ const hook = plugin.hooks?.["build:done"];
446
+ if (!hook) continue;
447
+ try {
448
+ await hook(ctx);
449
+ } catch (err) {
450
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:done" failed: ${err}`, { cause: err });
451
+ }
452
+ }
453
+ }
454
+ //#endregion
455
+ //#region source/core/content.ts
456
+ var contentProcessors = /* @__PURE__ */ new WeakMap();
457
+ function createProjectContentProcessor(plugins, ctx) {
458
+ return createContentProcessor((input) => runHooks(plugins, "content:transform", input, ctx));
459
+ }
460
+ function setProjectContentProcessor(ctx, processor) {
461
+ contentProcessors.set(ctx, processor);
462
+ }
463
+ function getProjectContentProcessor(ctx) {
464
+ return contentProcessors.get(ctx);
465
+ }
466
+ //#endregion
305
467
  //#region source/parsers/routes.ts
306
- function kebabToTitle$1(str) {
468
+ function kebabToTitle(str) {
307
469
  return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
308
470
  }
309
471
  function parseMdxTree(content) {
@@ -368,32 +530,33 @@ function routePathFromRef(ref, explicitPath) {
368
530
  function virtualModuleIdFromRef(ref) {
369
531
  return "virtual:clarify-page/" + ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
370
532
  }
371
- function localizedRoutePath(basePath, locale, i18n) {
533
+ function localizedRoutePath(basePath, locale, _i18n) {
372
534
  const normalizedBasePath = normalizePath(basePath);
373
- if (locale === i18n.defaultLocale) return normalizedBasePath;
374
535
  const prefix = normalizePath(locale);
375
536
  return normalizedBasePath === "/" ? prefix : normalizePath(`${prefix}${normalizedBasePath}`);
376
537
  }
377
538
  function resolveLocalizedText(text, locale, fallbackLocale) {
378
- if (typeof text === "string" || text === void 0) return text;
539
+ if (typeof text === "string") return text;
540
+ if (text === void 0) return void 0;
379
541
  return text[locale] ?? (fallbackLocale ? text[fallbackLocale] : void 0) ?? Object.values(text)[0];
380
542
  }
381
- function findContentRoutes(dir, base = dir) {
543
+ async function findContentRoutes(dir, base = dir, options = {}) {
382
544
  const routes = [];
383
545
  if (!existsSync(dir)) return routes;
384
546
  const entries = readdirSync(dir, { withFileTypes: true });
385
547
  for (const entry of entries) {
386
548
  const fullPath = join(dir, entry.name);
387
- if (entry.isDirectory()) routes.push(...findContentRoutes(fullPath, base));
549
+ if (entry.isDirectory()) routes.push(...await findContentRoutes(fullPath, base, options));
388
550
  else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
389
551
  const relativePath = relative(base, fullPath);
390
552
  const pathParts = relativePath.replace(/\.mdx?$/, "").split("/");
391
553
  const cleanPath = ("/" + pathParts.map((p) => p === "index" ? "" : p).filter(Boolean).join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
392
- const { frontmatter, content } = parseFrontmatter(readFileSync(fullPath, "utf-8"));
554
+ const source = readFileSync(fullPath, "utf-8");
555
+ const { frontmatter, content } = await (options.contentProcessor ?? createContentProcessor()).processMdx(source, fullPath);
393
556
  let title = typeof frontmatter.title === "string" ? frontmatter.title : "";
394
557
  if (!title) {
395
558
  const lastPart = pathParts[pathParts.length - 1] ?? "";
396
- title = kebabToTitle$1(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(content) : lastPart) || "Untitled";
559
+ title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(content) : lastPart) || "Untitled";
397
560
  }
398
561
  routes.push({
399
562
  path: cleanPath,
@@ -417,9 +580,9 @@ function frontmatterKeywords(frontmatter) {
417
580
  const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
418
581
  return keywords.length > 0 ? keywords : void 0;
419
582
  }
420
- function withAlternates$2(route, routes, i18n) {
583
+ function withAlternates(route, routes, i18n) {
421
584
  const basePath = route.basePath ?? route.path;
422
- const routeByLocaleAndBase = new Map(routes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
585
+ 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]));
423
586
  const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
424
587
  const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
425
588
  return alternate ? [[locale.code, alternate.path]] : [];
@@ -441,7 +604,7 @@ function buildNavigation(routes) {
441
604
  if (!node) {
442
605
  node = {
443
606
  path: pathSoFar,
444
- title: i === parts.length - 1 ? route.title : kebabToTitle$1(parts[i]),
607
+ title: i === parts.length - 1 ? route.title : kebabToTitle(parts[i]),
445
608
  children: []
446
609
  };
447
610
  current.push(node);
@@ -513,7 +676,21 @@ function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
513
676
  }
514
677
  }
515
678
  const nextRoutes = [...routes, ...additions];
516
- return i18n ? nextRoutes.map((route) => withAlternates$2(route, nextRoutes, i18n)) : nextRoutes;
679
+ const routesWithAlternates = i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, i18n)) : nextRoutes;
680
+ if (!i18n) return routesWithAlternates;
681
+ const bareRoutes = [];
682
+ const seenBare = new Set(routesWithAlternates.map((r) => r.path));
683
+ for (const route of routesWithAlternates) {
684
+ if (route.locale !== i18n.defaultLocale) continue;
685
+ const bp = route.basePath ?? route.path;
686
+ if (bp === route.path || seenBare.has(bp)) continue;
687
+ seenBare.add(bp);
688
+ bareRoutes.push({
689
+ ...route,
690
+ path: bp
691
+ });
692
+ }
693
+ return [...routesWithAlternates, ...bareRoutes];
517
694
  }
518
695
  function buildNavigationFromPagesConfig(routes, config) {
519
696
  if (!config || config === "FileTree") return buildNavigation(routes);
@@ -541,7 +718,7 @@ function buildNavigationFromConfig(routes, config) {
541
718
  const route = routeMap.get(path);
542
719
  return {
543
720
  path,
544
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle$1(path.split("/").pop() ?? openapiRef),
721
+ title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? openapiRef),
545
722
  icon,
546
723
  sections: route?.sections ? navigationSections(route.sections) : void 0
547
724
  };
@@ -551,7 +728,7 @@ function buildNavigationFromConfig(routes, config) {
551
728
  const route = routeMap.get(path);
552
729
  return {
553
730
  path: redirect ? normalizePath(redirect) : path,
554
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle$1(path.split("/").pop() ?? ref),
731
+ title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? ref),
555
732
  icon,
556
733
  sections: route?.sections ? navigationSections(route.sections) : void 0
557
734
  };
@@ -586,7 +763,7 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
586
763
  const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
587
764
  return {
588
765
  path: (redirect ? localizedRoutePath(routePathFromRef(redirect), locale, i18n) : void 0) ?? path,
589
- title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle$1(basePath.split("/").pop() ?? ref),
766
+ title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle(basePath.split("/").pop() ?? ref),
590
767
  icon,
591
768
  sections: route?.sections ? navigationSections(route.sections) : void 0
592
769
  };
@@ -632,8 +809,12 @@ var OPENAPI_HTTP_METHODS = [
632
809
  function errorMessage(error) {
633
810
  return error instanceof Error ? error.message : String(error);
634
811
  }
635
- function kebabToTitle(str) {
636
- return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
812
+ function isSameOpenAPIFile(url, filePath) {
813
+ try {
814
+ return resolve(fileURLToPath(url)) === resolve(filePath);
815
+ } catch {
816
+ return resolve(url) === resolve(filePath);
817
+ }
637
818
  }
638
819
  function findOpenAPIRoutes(dir, base = dir) {
639
820
  const routes = [];
@@ -654,8 +835,7 @@ function findOpenAPIRoutes(dir, base = dir) {
654
835
  filePath: fullPath,
655
836
  virtualModuleId: virtualModuleIdFromRef(ref),
656
837
  title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
657
- kind: "openapi",
658
- content: readFileSync(fullPath, "utf-8")
838
+ kind: "openapi"
659
839
  });
660
840
  }
661
841
  return routes;
@@ -664,6 +844,20 @@ function operationMatchesTags(operationTags, filterTags) {
664
844
  if (!filterTags?.length) return true;
665
845
  return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
666
846
  }
847
+ /** Filter an OpenAPI spec to only include paths that have at least one operation matching the given tags. */
848
+ function filterSpecByTags(spec, tags) {
849
+ const filteredPaths = {};
850
+ const paths = spec.paths ?? {};
851
+ for (const path of Object.keys(paths)) {
852
+ const pathItem = paths[path];
853
+ if (!pathItem) continue;
854
+ if (Object.values(pathItem).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredPaths[path] = pathItem;
855
+ }
856
+ return {
857
+ ...spec,
858
+ paths: filteredPaths
859
+ };
860
+ }
667
861
  /** Extract endpoint operations from an OpenAPI spec as page sections. */
668
862
  function extractOpenAPISections(spec, filterTags) {
669
863
  const sections = [];
@@ -685,12 +879,26 @@ function extractOpenAPISections(spec, filterTags) {
685
879
  }
686
880
  return sections;
687
881
  }
688
- async function readOpenAPISpec(filePath) {
882
+ async function readOpenAPISpec(filePath, contentProcessor) {
689
883
  try {
690
- return {
884
+ if (!contentProcessor) return {
885
+ ok: true,
886
+ spec: await SwaggerParser.dereference(filePath)
887
+ };
888
+ const source = readFileSync(filePath, "utf-8");
889
+ const transformedSource = await contentProcessor.processText(source, "openapi", filePath);
890
+ if (transformedSource === source) return {
691
891
  ok: true,
692
892
  spec: await SwaggerParser.dereference(filePath)
693
893
  };
894
+ return {
895
+ ok: true,
896
+ spec: await SwaggerParser.dereference(filePath, { resolve: { transformedOpenAPISource: {
897
+ order: 1,
898
+ canRead: (file) => isSameOpenAPIFile(file.url, filePath),
899
+ read: () => transformedSource
900
+ } } })
901
+ };
694
902
  } catch (error) {
695
903
  return {
696
904
  ok: false,
@@ -705,12 +913,14 @@ async function readOpenAPISpec(filePath) {
705
913
  }
706
914
  //#endregion
707
915
  //#region source/core/virtual-modules.ts
708
- var VIRTUAL_CONFIG = "virtual:clarify-config";
709
- var VIRTUAL_ROUTES = "virtual:clarify-routes";
710
- var VIRTUAL_SERVER_ROUTES = "virtual:clarify-routes/server";
711
- var VIRTUAL_RUNTIME = "virtual:clarify-runtime";
712
- var VIRTUAL_CLIENT_ENTRY = "virtual:clarify-entry-client";
713
- var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify-entry-client";
916
+ var VIRTUAL_CONFIG = "virtual:clarify/config";
917
+ var VIRTUAL_ROUTES = "virtual:clarify/routes";
918
+ var VIRTUAL_SERVER_ROUTES = "virtual:clarify/routes/server";
919
+ var VIRTUAL_OPENAPI = "virtual:clarify/openapi";
920
+ var VIRTUAL_SLOTS = "virtual:clarify/slots";
921
+ var VIRTUAL_SLOT = "virtual:clarify/slot";
922
+ var VIRTUAL_CLIENT_ENTRY = "virtual:clarify/entry-client";
923
+ var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify/entry-client";
714
924
  function resolveVirtualId(id) {
715
925
  return "\0" + id;
716
926
  }
@@ -751,65 +961,117 @@ function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode =
751
961
  const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
752
962
  return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
753
963
  }
754
- function createRuntimeModule(options = {}) {
755
- const imports = [options.bannerComponentSource === "path" && options.imports?.bannerComponent ? `import BannerComponent from ${moduleSpecifier(options.imports.bannerComponent)};` : void 0, options.footerComponentSource === "path" && options.imports?.footerComponent ? `import FooterComponent from ${moduleSpecifier(options.imports.footerComponent)};` : void 0].filter(Boolean).join("\n");
756
- const exports = `export const openApis = {};
757
- export const bannerComponent = ${options.bannerComponentSource === "path" ? "BannerComponent" : "undefined"};
758
- export const footerComponent = ${options.footerComponentSource === "path" ? "FooterComponent" : "undefined"};`;
759
- return imports ? `${imports}\n${exports}` : exports;
760
- }
761
964
  function createClientEntryModule(options = {}) {
762
965
  return `
763
966
  import '@clarify-labs/renderer/style.css';
764
967
  import { render } from '@clarify-labs/renderer/client';
765
968
  import { routes, navigation } from '${VIRTUAL_ROUTES}';
766
969
  import { config } from '${VIRTUAL_CONFIG}';
767
- import { openApis, bannerComponent, footerComponent } from '${VIRTUAL_RUNTIME}';
768
- render({ config, routes, navigation, openApis, bannerComponent, footerComponent, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
970
+ import { openApis } from '${VIRTUAL_OPENAPI}';
971
+ import { runtimeSlots } from '${VIRTUAL_SLOTS}';
972
+ render({ config, routes, navigation, openApis, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
973
+ }
974
+ /**
975
+ * Collect every plugin's `slots` declarations into a single runtime registry
976
+ * module. Component modules are exported as lazy import factories so the
977
+ * renderer can decide whether to code-split (client) or pre-resolve (SSR).
978
+ *
979
+ * Relative component paths are resolved against the project `root` so that
980
+ * the generated `import()` call uses an absolute specifier — Vite cannot
981
+ * resolve relative imports from virtual modules.
982
+ *
983
+ * Slot name validity is enforced at compile time by the `UISlotName`
984
+ * type — no runtime validation is needed.
985
+ */
986
+ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
987
+ const registrations = [];
988
+ for (const plugin of plugins) for (const slot of plugin.slots ?? []) registrations.push({
989
+ plugin: plugin.name,
990
+ slot
991
+ });
992
+ if (registrations.length === 0) return "export const runtimeSlots = {};\n";
993
+ const grouped = /* @__PURE__ */ new Map();
994
+ for (const { plugin, slot } of registrations) {
995
+ if (slot.name.endsWith(".replace")) {
996
+ const existing = grouped.get(slot.name);
997
+ if (existing) console.warn(`[clarify] Multiple plugins register slot "${slot.name}": "${plugin}" overrides "${existing[existing.length - 1]}". Only the last registration is used.`);
998
+ }
999
+ if (!slot.component.startsWith("/")) throw new Error(`[clarify] Plugin "${plugin}" slot "${slot.name}" has an invalid component path "${slot.component}". Component paths must start with "/" to reference the project root.`);
1000
+ const componentPath = root + slot.component;
1001
+ const entry = `{ plugin: ${JSON.stringify(plugin)}, component: () => import(${moduleSpecifier(componentPath)}) }`;
1002
+ const list = grouped.get(slot.name) ?? [];
1003
+ list.push(entry);
1004
+ grouped.set(slot.name, list);
1005
+ }
1006
+ return `export const runtimeSlots = {\n${[...grouped.entries()].map(([name, list]) => ` ${JSON.stringify(name)}: [\n ${list.join(",\n ")}\n ]`).join(",\n")}\n};\n`;
1007
+ }
1008
+ /**
1009
+ * Re-exports the slot context hook from the renderer so plugin components can
1010
+ * `import { useSlot } from 'virtual:clarify/slot'` without depending on
1011
+ * renderer internals directly.
1012
+ */
1013
+ function createSlotModule() {
1014
+ return `export { useSlot } from '@clarify-labs/renderer';\n`;
769
1015
  }
770
1016
  function buildVirtualModules(args) {
771
1017
  const modules = /* @__PURE__ */ new Map();
772
1018
  const clientEntryModule = createClientEntryModule({ themeEditor: args.themeEditor });
773
- const bannerComponent = args.projectConfig.banner;
774
- const bannerComponentSource = typeof bannerComponent === "string" ? "path" : void 0;
775
- const bannerComponentImport = typeof bannerComponent === "string" ? bannerComponent : void 0;
776
- const footerComponent = args.projectConfig.footer;
777
- const footerComponentSource = typeof footerComponent === "string" ? "path" : void 0;
778
- const footerComponentImport = typeof footerComponent === "string" ? footerComponent : void 0;
1019
+ const allPlugins = [...args.plugins ?? []];
779
1020
  modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
780
1021
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
781
1022
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "server"));
782
- modules.set(VIRTUAL_RUNTIME, createRuntimeModule({
783
- bannerComponentSource,
784
- footerComponentSource,
785
- imports: {
786
- bannerComponent: bannerComponentImport,
787
- footerComponent: footerComponentImport
788
- }
789
- }));
1023
+ modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
1024
+ modules.set(VIRTUAL_OPENAPI, generateOpenApiModule());
1025
+ modules.set(VIRTUAL_SLOT, createSlotModule());
790
1026
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
791
1027
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
792
1028
  for (const route of args.routes) modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
793
1029
  return modules;
794
1030
  }
1031
+ function generateOpenApiModule() {
1032
+ return `export const openApis = {};`;
1033
+ }
795
1034
  //#endregion
796
1035
  //#region source/plugins/openapi/virtual-modules.ts
797
- var openApiRegistryModuleId = VIRTUAL_RUNTIME;
798
- function generateOpenAPIRegistryModule(openApis, runtimeModule = "") {
799
- const openApisExport = `export const openApis = ${JSON.stringify(openApis)};`;
800
- const openApisPlaceholder = /export const openApis = \{\};?/;
801
- if (!runtimeModule) return openApisExport;
802
- if (openApisPlaceholder.test(runtimeModule)) return runtimeModule.replace(openApisPlaceholder, openApisExport);
803
- return runtimeModule;
804
- }
805
- function generateOpenAPIModule(spec, tagFilter) {
806
- return `import { createElement } from 'react';
807
- import { OpenApiDocument } from '@clarify-labs/renderer';
808
- const spec = ${JSON.stringify(spec)};
809
- const tagFilter = ${JSON.stringify(tagFilter)};
810
- export default function OpenApiRoutePage() {
811
- return createElement(OpenApiDocument, { spec, tagFilter });
812
- }`;
1036
+ var openApiRegistryModuleId = VIRTUAL_OPENAPI;
1037
+ var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
1038
+ function specVirtualModuleId(specKey) {
1039
+ return `${OPENAPI_SPEC_PREFIX}${specKey}`;
1040
+ }
1041
+ function generateOpenAPIRegistryModule(openApis) {
1042
+ return `export const openApis = ${JSON.stringify(openApis)};`;
1043
+ }
1044
+ /** Generate a virtual module that exports a single spec as default. */
1045
+ function generateOpenAPISpecModule(spec) {
1046
+ return `export default ${JSON.stringify(spec)};`;
1047
+ }
1048
+ function generateOpenAPIPageModule(opts) {
1049
+ const { specKey, specRegistryKey, tagFilter } = opts;
1050
+ return [
1051
+ `import { createElement, useState, useEffect, useRef } from 'react';`,
1052
+ `import { OpenApiDocument, useOpenApis } from '@clarify-labs/renderer';`,
1053
+ `const SPEC_KEY = ${JSON.stringify(specRegistryKey)};`,
1054
+ `const TAG_FILTER = ${JSON.stringify(tagFilter ?? void 0)};`,
1055
+ `let loadPromise = null;`,
1056
+ `function loadSpec() {`,
1057
+ ` if (!loadPromise) loadPromise = import(${JSON.stringify(specVirtualModuleId(specKey))}).then(function(m) { return m.default; });`,
1058
+ ` return loadPromise;`,
1059
+ `}`,
1060
+ `export default function OpenApiRoutePage() {`,
1061
+ ` const specs = useOpenApis();`,
1062
+ ` const serverSpec = specs[SPEC_KEY];`,
1063
+ ` const [spec, setSpec] = useState(serverSpec || null);`,
1064
+ ` const mountedRef = useRef(false);`,
1065
+ ` useEffect(function() {`,
1066
+ ` if (mountedRef.current) return;`,
1067
+ ` mountedRef.current = true;`,
1068
+ ` if (spec) return;`,
1069
+ ` loadSpec().then(setSpec);`,
1070
+ ` }, []);`,
1071
+ ` if (!spec) return null;`,
1072
+ ` return createElement(OpenApiDocument, { spec: spec, tagFilter: TAG_FILTER });`,
1073
+ `}`
1074
+ ].join("\n");
813
1075
  }
814
1076
  function generateOpenAPIErrorModule(diagnostic) {
815
1077
  return `import { createElement } from 'react';
@@ -843,18 +1105,6 @@ function collectOpenAPIPageItems(config) {
843
1105
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
844
1106
  return items;
845
1107
  }
846
- function withAlternates$1(route, routes, i18n) {
847
- const basePath = route.basePath ?? route.path;
848
- const routeByLocaleAndBase = new Map(routes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
849
- const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
850
- const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
851
- return alternate ? [[locale.code, alternate.path]] : [];
852
- }));
853
- return {
854
- ...route,
855
- alternates
856
- };
857
- }
858
1108
  function virtualModuleIdFromPath(path) {
859
1109
  return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
860
1110
  }
@@ -884,10 +1134,15 @@ function createConfiguredOpenAPIRoutes(routes, config) {
884
1134
  }
885
1135
  }
886
1136
  const nextRoutes = [...routes, ...additions];
887
- return config.i18n ? nextRoutes.map((route) => withAlternates$1(route, nextRoutes, config.i18n)) : nextRoutes;
1137
+ return config.i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n)) : nextRoutes;
1138
+ }
1139
+ /** Derive a stable, deduplicated key from an absolute spec file path. */
1140
+ function specFileKeyFromPath(filePath, projectRoot) {
1141
+ return relative(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
888
1142
  }
889
1143
  function createOpenAPIPlugin() {
890
- const specs = {};
1144
+ /** specs keyed by specFileKey (deduplicated per source file). */
1145
+ const specs = /* @__PURE__ */ new Map();
891
1146
  return {
892
1147
  name: "clarify:openapi",
893
1148
  hooks: {
@@ -896,7 +1151,7 @@ function createOpenAPIPlugin() {
896
1151
  routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
897
1152
  }),
898
1153
  "routes:discovered": async (routes, ctx) => {
899
- for (const key of Object.keys(specs)) delete specs[key];
1154
+ specs.clear();
900
1155
  const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
901
1156
  const specByFilePath = /* @__PURE__ */ new Map();
902
1157
  for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
@@ -904,7 +1159,7 @@ function createOpenAPIPlugin() {
904
1159
  const result = specFromCache ? {
905
1160
  ok: true,
906
1161
  spec: specFromCache
907
- } : await readOpenAPISpec(route.filePath);
1162
+ } : await readOpenAPISpec(route.filePath, getProjectContentProcessor(ctx));
908
1163
  if (!result.ok) {
909
1164
  route.diagnostic = result.diagnostic;
910
1165
  route.title = route.title || "OpenAPI parse error";
@@ -913,24 +1168,45 @@ function createOpenAPIPlugin() {
913
1168
  }
914
1169
  const spec = result.spec;
915
1170
  specByFilePath.set(route.filePath, spec);
916
- specs[route.virtualModuleId] = {
1171
+ const specKey = specFileKeyFromPath(route.filePath, ctx.generateOptions.projectRoot);
1172
+ route.specFileKey = specKey;
1173
+ specs.set(specKey, {
917
1174
  spec,
918
- tagFilter: route.openapiTagFilter
919
- };
920
- specs[`virtual:clarify-page/${route.path.replace(/^\//, "")}`] = {
921
- spec,
922
- tagFilter: route.openapiTagFilter
923
- };
1175
+ filePath: route.filePath
1176
+ });
924
1177
  route.title = spec.info?.title ?? route.title;
925
1178
  route.description = spec.info?.description ?? route.description;
926
1179
  route.sections = extractOpenAPISections(spec, route.openapiTagFilter);
1180
+ const pageSpec = route.openapiTagFilter?.length ? filterSpecByTags(spec, route.openapiTagFilter) : spec;
1181
+ route.content = JSON.stringify(pageSpec);
927
1182
  }
928
1183
  return nextRoutes;
929
1184
  },
930
1185
  "modules:before": (modules, ctx) => {
931
- modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(Object.fromEntries(Object.entries(specs).map(([moduleId, entry]) => [moduleId, entry.spec])), modules.get(openApiRegistryModuleId)));
932
- for (const [moduleId, entry] of Object.entries(specs)) modules.set(moduleId, generateOpenAPIModule(entry.spec, entry.tagFilter));
933
- for (const route of ctx.routes.filter((route) => route.kind === "openapi" && route.diagnostic)) if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
1186
+ const registryEntries = {};
1187
+ for (const route of ctx.routes) {
1188
+ if (route.kind !== "openapi" || !route.specFileKey) continue;
1189
+ const spec = specs.get(route.specFileKey)?.spec;
1190
+ if (!spec) continue;
1191
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
1192
+ const registryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
1193
+ registryEntries[registryKey] = spec;
1194
+ }
1195
+ modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
1196
+ for (const [specKey, entry] of specs) modules.set(specVirtualModuleId(specKey), generateOpenAPISpecModule(entry.spec));
1197
+ for (const route of ctx.routes) {
1198
+ if (route.kind !== "openapi") continue;
1199
+ if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
1200
+ else if (route.specFileKey) {
1201
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
1202
+ const specRegistryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
1203
+ modules.set(route.virtualModuleId, generateOpenAPIPageModule({
1204
+ specKey: route.specFileKey,
1205
+ specRegistryKey,
1206
+ tagFilter: route.openapiTagFilter
1207
+ }));
1208
+ }
1209
+ }
934
1210
  return modules;
935
1211
  }
936
1212
  }
@@ -1148,22 +1424,22 @@ function createRobotsTxt(config) {
1148
1424
  ""
1149
1425
  ].join("\n");
1150
1426
  }
1151
- async function writeSeoFiles(outputRoot, routes, config) {
1152
- const sitemap = createSitemapXml(routes, config);
1153
- const robots = createRobotsTxt(config);
1154
- if (!sitemap || !robots) return;
1155
- await mkdir(outputRoot, { recursive: true });
1156
- await Promise.all([writeFile(join(outputRoot, "sitemap.xml"), sitemap, "utf-8"), writeFile(join(outputRoot, "robots.txt"), robots, "utf-8")]);
1157
- }
1158
1427
  //#endregion
1159
1428
  //#region source/plugins/seo/index.ts
1160
1429
  function createSeoPlugin() {
1161
1430
  return {
1162
1431
  name: "clarify:seo",
1163
- hooks: { async "build:done"(ctx) {
1164
- const outputDirectory = ctx.generateOptions.outputDirectory;
1165
- if (!outputDirectory) return;
1166
- await writeSeoFiles(join(ctx.generateOptions.projectRoot, outputDirectory), ctx.routes, ctx.projectConfig);
1432
+ hooks: { "build:assets"(ctx) {
1433
+ const sitemap = createSitemapXml(ctx.routes, ctx.projectConfig);
1434
+ const robots = createRobotsTxt(ctx.projectConfig);
1435
+ if (!sitemap || !robots) return [];
1436
+ return [{
1437
+ fileName: "sitemap.xml",
1438
+ source: sitemap
1439
+ }, {
1440
+ fileName: "robots.txt",
1441
+ source: robots
1442
+ }];
1167
1443
  } }
1168
1444
  };
1169
1445
  }
@@ -1200,9 +1476,51 @@ function createSourceLinksPlugin() {
1200
1476
  };
1201
1477
  }
1202
1478
  //#endregion
1479
+ //#region source/parsers/variables.ts
1480
+ var variablePattern = /{{\s*([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)\s*}}/g;
1481
+ function getVariableValue(variables, path) {
1482
+ let current = variables;
1483
+ for (const segment of path.split(".")) {
1484
+ if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
1485
+ current = current[segment];
1486
+ }
1487
+ return current;
1488
+ }
1489
+ function stringifyVariableValue(variables, path, stack) {
1490
+ if (stack.includes(path)) throw new Error(`[clarify] variable "${path}" contains a circular reference: ${[...stack, path].join(" -> ")}`);
1491
+ const value = getVariableValue(variables, path);
1492
+ if (value === void 0 || typeof value === "object") return void 0;
1493
+ if (typeof value !== "string") return String(value);
1494
+ return value.replace(variablePattern, (match, nestedPath) => stringifyVariableValue(variables, nestedPath, [...stack, path]) ?? match);
1495
+ }
1496
+ function applyVariables(content, variables = {}) {
1497
+ return content.replace(variablePattern, (match, path) => stringifyVariableValue(variables, path, []) ?? match);
1498
+ }
1499
+ //#endregion
1500
+ //#region source/plugins/variables/index.ts
1501
+ function applyVariablesToFrontmatter(frontmatter, variables) {
1502
+ const nextFrontmatter = { ...frontmatter };
1503
+ for (const key of ["title", "description"]) {
1504
+ if (typeof nextFrontmatter[key] !== "string") continue;
1505
+ nextFrontmatter[key] = applyVariables(nextFrontmatter[key], variables);
1506
+ }
1507
+ return nextFrontmatter;
1508
+ }
1509
+ function createVariablesPlugin() {
1510
+ return {
1511
+ name: "clarify:variables",
1512
+ hooks: { "content:transform": (input, ctx) => ({
1513
+ ...input,
1514
+ frontmatter: applyVariablesToFrontmatter(input.frontmatter, ctx.projectConfig.variables),
1515
+ content: applyVariables(input.content, ctx.projectConfig.variables)
1516
+ }) }
1517
+ };
1518
+ }
1519
+ //#endregion
1203
1520
  //#region source/core/builtin.ts
1204
1521
  function createBuiltinPlugins(options = {}) {
1205
1522
  const plugins = [
1523
+ createVariablesPlugin(),
1206
1524
  createOpenAPIPlugin(),
1207
1525
  createSourceLinksPlugin(),
1208
1526
  createContentArtifactsPlugin(),
@@ -1261,18 +1579,23 @@ var clarifyNavbarLinkSchema = z.object({
1261
1579
  href: z.string(),
1262
1580
  external: z.boolean().optional()
1263
1581
  });
1264
- var clarifyBannerConfigOptionsSchema = z.object({
1582
+ var clarifyBannerConfigSchema = z.object({
1265
1583
  content: clarifyLocalizedTextSchema,
1266
1584
  dismissible: z.boolean().optional(),
1267
1585
  link: clarifyNavbarLinkSchema.optional()
1268
1586
  }).strict();
1269
- var clarifyBannerConfigSchema = z.union([clarifyBannerConfigOptionsSchema, z.string()]);
1270
- var clarifyFooterLinksConfigSchema = z.object({
1587
+ var clarifyFooterConfigSchema = z.object({
1271
1588
  links: z.array(clarifyNavbarLinkSchema).optional(),
1272
1589
  socials: z.record(z.string(), z.string()).optional(),
1273
1590
  copyright: clarifyLocalizedTextSchema.optional()
1274
1591
  }).strict();
1275
- var clarifyFooterConfigSchema = z.union([clarifyFooterLinksConfigSchema, z.string()]);
1592
+ var clarifyVariableValueSchema = z.lazy(() => z.union([
1593
+ z.string(),
1594
+ z.number(),
1595
+ z.boolean(),
1596
+ z.record(z.string(), clarifyVariableValueSchema)
1597
+ ]));
1598
+ var clarifyVariablesConfigSchema = z.record(z.string(), clarifyVariableValueSchema);
1276
1599
  var clarifySourceConfigSchema = z.object({
1277
1600
  repository: z.string(),
1278
1601
  branch: z.string().optional(),
@@ -1350,9 +1673,11 @@ var clarifyProjectConfigSchema = z.object({
1350
1673
  favicon: clarifyFaviconConfigSchema.optional(),
1351
1674
  theme: clarifyThemeConfigSchema.optional(),
1352
1675
  routePrefix: z.string().optional(),
1676
+ assetPrefix: z.string().optional(),
1353
1677
  navbar: z.object({ links: z.array(clarifyNavbarLinkSchema).optional() }).optional(),
1354
1678
  banner: clarifyBannerConfigSchema.optional(),
1355
1679
  footer: clarifyFooterConfigSchema.optional(),
1680
+ variables: clarifyVariablesConfigSchema.optional(),
1356
1681
  i18n: clarifyI18nConfigSchema.optional(),
1357
1682
  tabs: clarifyTabsConfigSchema.optional()
1358
1683
  });
@@ -1412,7 +1737,22 @@ function resolveI18nConfig(i18n) {
1412
1737
  locales: i18n.locales
1413
1738
  };
1414
1739
  }
1740
+ function resolveRoutePrefix(routePrefix) {
1741
+ const trimmed = routePrefix?.trim();
1742
+ if (!trimmed || trimmed === "/") return "/";
1743
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
1744
+ }
1745
+ function resolveAssetPrefix(assetPrefix, routePrefix) {
1746
+ const trimmed = assetPrefix?.trim();
1747
+ if (trimmed === void 0) return routePrefix;
1748
+ if (trimmed === "./" || trimmed === "") return trimmed || "/";
1749
+ if (!trimmed || trimmed === "/") return "/";
1750
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(trimmed)) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1751
+ if (trimmed.startsWith("./") || trimmed.startsWith("../")) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1752
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
1753
+ }
1415
1754
  function resolveProjectConfig(config = {}) {
1755
+ const routePrefix = resolveRoutePrefix(config.routePrefix);
1416
1756
  return {
1417
1757
  title: config.title ?? "Clarify Docs",
1418
1758
  description: config.description ?? "",
@@ -1421,51 +1761,64 @@ function resolveProjectConfig(config = {}) {
1421
1761
  logo: config.logo,
1422
1762
  homeUrl: config.homeUrl,
1423
1763
  favicon: config.favicon,
1424
- routePrefix: config.routePrefix ?? "/",
1764
+ routePrefix,
1765
+ assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
1425
1766
  theme: resolveThemeConfig(config.theme),
1426
1767
  navbar: config.navbar,
1427
1768
  banner: config.banner,
1428
1769
  footer: config.footer,
1770
+ variables: config.variables ?? {},
1429
1771
  i18n: resolveI18nConfig(config.i18n),
1430
1772
  tabs: config.tabs
1431
1773
  };
1432
1774
  }
1433
1775
  //#endregion
1434
- //#region source/core/hooks.ts
1435
- async function runHooks(plugins, hookName, input, ctx) {
1436
- let result = input;
1437
- for (const plugin of plugins) {
1438
- const hook = plugin.hooks[hookName];
1439
- if (!hook) continue;
1440
- try {
1441
- result = await hook(result, ctx);
1442
- } catch (err) {
1443
- throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
1444
- }
1445
- }
1446
- return result;
1776
+ //#region source/core/env-types.ts
1777
+ var GENERATED_FILE_NAME = "clarify-env.d.ts";
1778
+ /**
1779
+ * Template for the generated `clarify-env.d.ts` file.
1780
+ * Edit this template directly if you need to change the type declarations.
1781
+ * This avoids the complexity of reading external files at runtime after bundling.
1782
+ */
1783
+ var CLARIFY_ENV_TEMPLATE = `// This file is generated by Clarify. Do not edit it manually.
1784
+ // It provides ambient type declarations for Clarify virtual modules used in
1785
+ // plugin and slot code, so the TypeScript language service can resolve them.
1786
+
1787
+ declare module 'virtual:clarify/slot' {
1788
+ import type { SlotContext } from '@clarify-labs/cli'
1789
+
1790
+ export function useSlot(): SlotContext
1447
1791
  }
1448
- async function runDevConfigureServerHooks(plugins, server, ctx) {
1449
- for (const plugin of plugins) {
1450
- const hook = plugin.hooks["dev:configureServer"];
1451
- if (!hook) continue;
1452
- try {
1453
- await hook(server, ctx);
1454
- } catch (err) {
1455
- throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
1456
- }
1457
- }
1792
+
1793
+ declare module 'virtual:clarify/slots' {
1794
+ import type { ComponentType } from 'react'
1795
+
1796
+ export type RuntimeSlotEntry = {
1797
+ plugin: string
1798
+ /** Lazy import factory — yields the default-exported React component. */
1799
+ component: () => Promise<{ default: ComponentType }>
1800
+ }
1801
+
1802
+ export const runtimeSlots: Record<string, RuntimeSlotEntry[]>
1458
1803
  }
1459
- async function runBuildDoneHooks(plugins, ctx) {
1460
- for (const plugin of plugins) {
1461
- const hook = plugin.hooks["build:done"];
1462
- if (!hook) continue;
1463
- try {
1464
- await hook(ctx);
1465
- } catch (err) {
1466
- throw new Error(`[clarify] plugin "${plugin.name}" hook "build:done" failed: ${err}`, { cause: err });
1467
- }
1804
+ `;
1805
+ function generateClarifyEnvDts(_plugins) {
1806
+ return CLARIFY_ENV_TEMPLATE;
1807
+ }
1808
+ /**
1809
+ * Writes `clarify-env.d.ts` into the project root. Only rewrites when content
1810
+ * changes to avoid triggering unnecessary file watchers, and never overwrites a
1811
+ * user-authored file that does not carry the generated marker.
1812
+ */
1813
+ function writeClarifyEnvDts(projectRoot, plugins) {
1814
+ const filePath = join(projectRoot, GENERATED_FILE_NAME);
1815
+ const next = generateClarifyEnvDts(plugins);
1816
+ if (existsSync(filePath)) {
1817
+ const current = readFileSync(filePath, "utf-8");
1818
+ if (current === next) return;
1819
+ if (!current.startsWith("// This file is generated by Clarify.")) return;
1468
1820
  }
1821
+ writeFileSync(filePath, next, "utf-8");
1469
1822
  }
1470
1823
  //#endregion
1471
1824
  //#region source/core/options.ts
@@ -1479,23 +1832,11 @@ function resolveBuildOptions(options = {}) {
1479
1832
  }
1480
1833
  //#endregion
1481
1834
  //#region source/core/site.ts
1482
- function withAlternates(route, routeList, i18n) {
1483
- const basePath = route.basePath ?? route.path;
1484
- const routeByLocaleAndBase = new Map(routeList.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
1485
- const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
1486
- const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
1487
- return alternate ? [[locale.code, alternate.path]] : [];
1488
- }));
1489
- return {
1490
- ...route,
1491
- alternates
1492
- };
1493
- }
1494
1835
  async function discoverRoutesForRoot(routeRoot, locale, plugins, ctx) {
1495
1836
  return (await runHooks(plugins, "routes:discover", {
1496
1837
  contentRoot: routeRoot,
1497
1838
  locale,
1498
- routes: findContentRoutes(routeRoot)
1839
+ routes: await findContentRoutes(routeRoot, routeRoot, { contentProcessor: getProjectContentProcessor(ctx) })
1499
1840
  }, ctx)).routes;
1500
1841
  }
1501
1842
  async function discoverRoutes(root, contentRoot, plugins, ctx) {
@@ -1532,7 +1873,21 @@ async function discoverRoutes(root, contentRoot, plugins, ctx) {
1532
1873
  }
1533
1874
  }
1534
1875
  }
1535
- return localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
1876
+ const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
1877
+ const defaultLocale = i18n.defaultLocale;
1878
+ const bareRoutes = [];
1879
+ const seenBare = new Set(routesWithAlternates.map((r) => r.path));
1880
+ for (const route of routesWithAlternates) {
1881
+ if (route.locale !== defaultLocale) continue;
1882
+ const bp = route.basePath ?? route.path;
1883
+ if (bp === route.path || seenBare.has(bp)) continue;
1884
+ seenBare.add(bp);
1885
+ bareRoutes.push({
1886
+ ...route,
1887
+ path: bp
1888
+ });
1889
+ }
1890
+ return [...routesWithAlternates, ...bareRoutes];
1536
1891
  }
1537
1892
  async function resolveClarifySite(options = {}, resolveOptions = {}) {
1538
1893
  const root = options.projectRoot ?? process.cwd();
@@ -1546,6 +1901,7 @@ async function resolveClarifySite(options = {}, resolveOptions = {}) {
1546
1901
  routes: [],
1547
1902
  navigation: []
1548
1903
  };
1904
+ setProjectContentProcessor(ctx, createProjectContentProcessor(plugins, ctx));
1549
1905
  let routes = await discoverRoutes(root, contentRoot, plugins, ctx);
1550
1906
  routes = await runHooks(plugins, "routes:discovered", routes, ctx);
1551
1907
  routes = applyConfiguredPageRoutePaths(routes, projectConfig.tabs, projectConfig.i18n);
@@ -1608,13 +1964,21 @@ function createClarifyRuntimeAliases() {
1608
1964
  find: /^@clarify-labs\/renderer$/,
1609
1965
  replacement: resolvePackageExport("@clarify-labs/renderer", "")
1610
1966
  },
1967
+ {
1968
+ find: /^react\/jsx-dev-runtime$/,
1969
+ replacement: resolveCliDependency("react/jsx-dev-runtime")
1970
+ },
1611
1971
  {
1612
1972
  find: /^react\/jsx-runtime$/,
1613
1973
  replacement: resolveCliDependency("react/jsx-runtime")
1614
1974
  },
1615
1975
  {
1616
- find: /^react\/jsx-dev-runtime$/,
1617
- replacement: resolveCliDependency("react/jsx-dev-runtime")
1976
+ find: /^mermaid$/,
1977
+ replacement: resolveCliDependency("mermaid")
1978
+ },
1979
+ {
1980
+ find: /^react-zoom-pan-pinch$/,
1981
+ replacement: resolveCliDependency("react-zoom-pan-pinch")
1618
1982
  },
1619
1983
  {
1620
1984
  find: /^react$/,
@@ -1656,11 +2020,24 @@ function setNamedMeta(html, name, content) {
1656
2020
  if (metaPattern.test(html)) return html.replace(metaPattern, meta);
1657
2021
  return html.replace("</head>", ` ${meta}\n </head>`);
1658
2022
  }
2023
+ function canonicalUrl(projectConfig, route) {
2024
+ if (!projectConfig.siteUrl || !route.alternates || !route.locale) return void 0;
2025
+ const prefixedPath = route.alternates[route.locale];
2026
+ if (!prefixedPath || prefixedPath === route.path) return void 0;
2027
+ return `${projectConfig.siteUrl.replace(/\/+$/, "")}${!projectConfig.routePrefix || projectConfig.routePrefix === "/" ? "" : `/${projectConfig.routePrefix.replace(/^\/+|\/+$/g, "")}`}${prefixedPath}`;
2028
+ }
2029
+ function injectCanonicalUrl(html, projectConfig, route) {
2030
+ const url = route ? canonicalUrl(projectConfig, route) : void 0;
2031
+ html = html.replace(/<link\b[^>]*\brel=["']canonical["'][^>]*\/?>\n?/gi, "");
2032
+ if (!url) return html;
2033
+ return html.replace("</head>", ` <link rel="canonical" href="${escapeHtml(url)}" />\n </head>`);
2034
+ }
1659
2035
  function injectSSRIntoTemplate(template, appHtml, projectConfig, route) {
1660
2036
  let html = injectHtmlLocaleAttributes(template, projectConfig, route);
1661
2037
  html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(routeTitle(projectConfig, route))}</title>`);
1662
2038
  html = setNamedMeta(html, "description", route?.description ?? projectConfig.description);
1663
2039
  html = setNamedMeta(html, "keywords", route?.keywords?.join(", "));
2040
+ html = injectCanonicalUrl(html, projectConfig, route);
1664
2041
  html = html.replace(/<div id="root">[\s\S]*?<\/div>/, `<div id="root">${appHtml}</div>`);
1665
2042
  return html;
1666
2043
  }
@@ -1673,12 +2050,20 @@ function routeOutputFiles(outputDirectory, route) {
1673
2050
  return files;
1674
2051
  }
1675
2052
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
1676
- import { routes, navigation } from 'virtual:clarify-routes/server';
1677
- import { config } from 'virtual:clarify-config';
1678
- import { openApis, bannerComponent, footerComponent } from 'virtual:clarify-runtime';
2053
+ import { routes, navigation } from 'virtual:clarify/routes/server';
2054
+ import { config } from 'virtual:clarify/config';
2055
+ import { openApis } from 'virtual:clarify/openapi';
2056
+ import { runtimeSlots } from 'virtual:clarify/slots';
1679
2057
 
1680
- export function render(url) {
1681
- return renderToHTML({ config, routes, navigation, openApis, bannerComponent, footerComponent, url, themeEditor: config.theme.editor });
2058
+ export async function render(url) {
2059
+ // Pre-resolve every slot component factory so renderToString can use
2060
+ // them synchronously (renderToString has no Suspense support).
2061
+ const entries = Object.values(runtimeSlots).flat()
2062
+ await Promise.all(entries.map(async (entry) => {
2063
+ const mod = await entry.component()
2064
+ entry._resolved = mod.default
2065
+ }))
2066
+ return renderToHTML({ config, routes, navigation, openApis, runtimeSlots, url, themeEditor: config.theme.editor });
1682
2067
  }`;
1683
2068
  function createTempEntryFile(content) {
1684
2069
  const entryPath = join(createClarifyTempDir("ssr"), "entry-server.ts");
@@ -1708,7 +2093,7 @@ async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundle
1708
2093
  const template = readIndexHtml(outputDirectory);
1709
2094
  if (!template) throw new Error(`[clarify] index.html not found in outputDirectory "${outputDirectory}". Make sure Vite build produces it.`);
1710
2095
  for (const route of routes) try {
1711
- const finalHtml = injectSSRIntoTemplate(template, render(route.path), projectConfig, route);
2096
+ const finalHtml = injectSSRIntoTemplate(template, await render(route.path), projectConfig, route);
1712
2097
  for (const outFile of routeOutputFiles(outputDirectory, route)) {
1713
2098
  mkdirSync(dirname(outFile), { recursive: true });
1714
2099
  writeFileSync(outFile, finalHtml, "utf-8");
@@ -1777,23 +2162,8 @@ function clarifyPlugin(options = {}) {
1777
2162
  let viteConfig;
1778
2163
  let resolvedNavigation = [];
1779
2164
  let virtualModules = /* @__PURE__ */ new Map();
1780
- async function reloadProjectConfig() {
1781
- const buildOptions = {
1782
- ...await loadClarifyConfig(root, {
1783
- command: "serve",
1784
- mode: viteConfig.mode
1785
- }),
1786
- projectRoot: options.projectRoot,
1787
- rootDirectory: options.rootDirectory,
1788
- outputDirectory: options.outputDirectory
1789
- };
1790
- projectConfig = resolveProjectConfig(buildOptions);
1791
- generateOptions = resolveBuildOptions(buildOptions);
1792
- ctx.projectConfig = projectConfig;
1793
- ctx.generateOptions = generateOptions;
1794
- }
1795
- async function resolveRoutesAndSpecs() {
1796
- const site = await resolveClarifySite(options);
2165
+ async function resolveRoutesAndSpecs(overrides) {
2166
+ const site = await resolveClarifySite(overrides ?? options);
1797
2167
  projectConfig = site.projectConfig;
1798
2168
  generateOptions = site.generateOptions;
1799
2169
  routes = site.routes;
@@ -1810,6 +2180,7 @@ function clarifyPlugin(options = {}) {
1810
2180
  generateOptions,
1811
2181
  routes,
1812
2182
  navigation: resolvedNavigation,
2183
+ plugins: clarifyPlugins,
1813
2184
  themeEditor: viteConfig.command === "serve" || projectConfig.theme.editor
1814
2185
  });
1815
2186
  virtualModules = await runHooks(clarifyPlugins, "modules:before", virtualModules, ctx);
@@ -1859,7 +2230,7 @@ function clarifyPlugin(options = {}) {
1859
2230
  async config() {
1860
2231
  await resolveRoutesAndSpecs();
1861
2232
  return {
1862
- base: projectConfig.routePrefix,
2233
+ base: projectConfig.assetPrefix,
1863
2234
  build: {
1864
2235
  ...generateOptions.outputDirectory ? { outDir: generateOptions.outputDirectory } : {},
1865
2236
  manifest: true
@@ -1872,12 +2243,23 @@ function clarifyPlugin(options = {}) {
1872
2243
  },
1873
2244
  async buildStart() {
1874
2245
  await rebuildVirtualModules();
2246
+ writeClarifyEnvDts(root, clarifyPlugins);
1875
2247
  },
1876
2248
  async handleHotUpdate(ctx) {
1877
2249
  const changedFile = isAbsolute(ctx.file) ? ctx.file : join(root, ctx.file);
1878
2250
  if (configFilePath && changedFile === configFilePath) {
1879
- await reloadProjectConfig();
1880
- await refreshDevServer(ctx.server);
2251
+ await resolveRoutesAndSpecs({
2252
+ ...await loadClarifyConfig(root, {
2253
+ command: "serve",
2254
+ mode: viteConfig.mode
2255
+ }),
2256
+ projectRoot: options.projectRoot,
2257
+ rootDirectory: options.rootDirectory,
2258
+ outputDirectory: options.outputDirectory
2259
+ });
2260
+ await rebuildVirtualModules();
2261
+ invalidateVirtualModules(ctx.server);
2262
+ ctx.server.ws.send({ type: "full-reload" });
1881
2263
  return [];
1882
2264
  }
1883
2265
  const relativeContentFile = relative(contentRoot, changedFile);
@@ -1886,10 +2268,13 @@ function clarifyPlugin(options = {}) {
1886
2268
  return [];
1887
2269
  },
1888
2270
  resolveId(id) {
1889
- if (id === "virtual:clarify-entry-client" || id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1890
- if (id === "virtual:clarify-config" || id === resolveVirtualId("virtual:clarify-config")) return resolveVirtualId(VIRTUAL_CONFIG);
1891
- if (id === "virtual:clarify-routes" || id === resolveVirtualId("virtual:clarify-routes")) return resolveVirtualId(VIRTUAL_ROUTES);
1892
- if (id === "virtual:clarify-routes/server" || id === resolveVirtualId("virtual:clarify-routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
2271
+ if (id === "virtual:clarify/entry-client" || id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2272
+ if (id === "virtual:clarify/config" || id === resolveVirtualId("virtual:clarify/config")) return resolveVirtualId(VIRTUAL_CONFIG);
2273
+ if (id === "virtual:clarify/routes" || id === resolveVirtualId("virtual:clarify/routes")) return resolveVirtualId(VIRTUAL_ROUTES);
2274
+ if (id === "virtual:clarify/routes/server" || id === resolveVirtualId("virtual:clarify/routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
2275
+ if (id === "virtual:clarify/openapi" || id === resolveVirtualId("virtual:clarify/openapi")) return resolveVirtualId(VIRTUAL_OPENAPI);
2276
+ if (id === "virtual:clarify/slots" || id === resolveVirtualId("virtual:clarify/slots")) return resolveVirtualId(VIRTUAL_SLOTS);
2277
+ if (id === "virtual:clarify/slot" || id === resolveVirtualId("virtual:clarify/slot")) return resolveVirtualId(VIRTUAL_SLOT);
1893
2278
  const moduleId = stripVirtualPrefix(id);
1894
2279
  if (virtualModules.has(moduleId)) return resolveVirtualId(moduleId);
1895
2280
  const route = routes.find((r) => r.virtualModuleId === id || r.virtualModuleId === moduleId);
@@ -1927,6 +2312,14 @@ function clarifyPlugin(options = {}) {
1927
2312
  };
1928
2313
  }
1929
2314
  },
2315
+ async generateBundle(_opts, _bundle) {
2316
+ const assets = await runBuildAssetsHooks(clarifyPlugins, ctx);
2317
+ for (const asset of assets) this.emitFile({
2318
+ type: "asset",
2319
+ fileName: asset.fileName,
2320
+ source: asset.source
2321
+ });
2322
+ },
1930
2323
  async closeBundle() {
1931
2324
  if (process.env.SKIP_CLARIFY_SSG) {
1932
2325
  await runBuildDoneHooks(clarifyPlugins, ctx);
@@ -1941,11 +2334,14 @@ function clarifyPlugin(options = {}) {
1941
2334
  {
1942
2335
  name: "clarify:virtual-ssg",
1943
2336
  resolveId(id) {
1944
- if (id === "virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1945
- if (id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1946
- if (id === "virtual:clarify-config") return id;
1947
- if (id === "virtual:clarify-routes") return id;
1948
- if (id === "virtual:clarify-routes/server") return id;
2337
+ if (id === "virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2338
+ if (id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2339
+ if (id === "virtual:clarify/routes/server") return id;
2340
+ if (id === "virtual:clarify/openapi") return id;
2341
+ if (id === "virtual:clarify/config") return id;
2342
+ if (id === "virtual:clarify/routes") return id;
2343
+ if (id === "virtual:clarify/slots") return id;
2344
+ if (id === "virtual:clarify/slot") return id;
1949
2345
  if (virtualModules.has(stripVirtualPrefix(id))) return stripVirtualPrefix(id);
1950
2346
  if (routes.find((r) => r.virtualModuleId === id)) return id;
1951
2347
  return null;
@@ -1974,4 +2370,4 @@ function clarifyPlugin(options = {}) {
1974
2370
  //#endregion
1975
2371
  export { resolveClarifySite as a, createClarifyRuntimeAliases as i, defineConfig as n, createClarifyTempDir as o, loadClarifyConfig as r, clarifyPlugin as t };
1976
2372
 
1977
- //# sourceMappingURL=plugin-o1rP_gts.js.map
2373
+ //# sourceMappingURL=plugin-D5EF3pYl.js.map