@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.
@@ -35,15 +35,15 @@ let shiki = require("shiki");
35
35
  shiki = __toESM(shiki, 1);
36
36
  let unist_util_visit = require("unist-util-visit");
37
37
  let _clarify_labs_renderer = require("@clarify-labs/renderer");
38
- let remark = require("remark");
38
+ let yaml = require("yaml");
39
39
  let gray_matter = require("gray-matter");
40
40
  gray_matter = __toESM(gray_matter, 1);
41
+ let remark = require("remark");
42
+ let node_url = require("node:url");
41
43
  let _apidevtools_swagger_parser = require("@apidevtools/swagger-parser");
42
44
  _apidevtools_swagger_parser = __toESM(_apidevtools_swagger_parser, 1);
43
45
  let node_os = require("node:os");
44
- let node_fs_promises = require("node:fs/promises");
45
46
  let zod = require("zod");
46
- let node_url = require("node:url");
47
47
  let vite = require("vite");
48
48
  let node_module = require("node:module");
49
49
  //#region source/core/utils.ts
@@ -150,17 +150,16 @@ function withUtf8Signature(content) {
150
150
  }
151
151
  function shouldUseUtf8Signature(route) {
152
152
  if (route.kind === "mdx") return true;
153
- return /\.ya?ml$/i.test(route.contentArtifactUrl ?? "");
153
+ return false;
154
154
  }
155
155
  function routeToMarkdownArtifactUrl(routePath) {
156
156
  return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.md`;
157
157
  }
158
- function routeToOpenAPIArtifactUrl(routePath, filePath) {
159
- const extension = (0, node_path.extname)(filePath).toLowerCase() || ".json";
160
- return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi${extension}`;
158
+ function routeToOpenAPIArtifactUrl(routePath) {
159
+ return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi.json`;
161
160
  }
162
161
  function attachContentArtifactUrls(routes) {
163
- for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path, route.filePath) : routeToMarkdownArtifactUrl(route.path);
162
+ for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path);
164
163
  }
165
164
  function readRouteContent(route) {
166
165
  if (route.content !== void 0) return route.content;
@@ -170,40 +169,81 @@ function readRouteArtifactContent(route) {
170
169
  const content = readRouteContent(route);
171
170
  return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
172
171
  }
173
- function writeContentArtifactFiles(routes, outputDirectory) {
172
+ function isLlmsTxtRoute(route) {
173
+ return !route.path.split("/").includes("404");
174
+ }
175
+ function llmsTxtDescription(route) {
176
+ if (route.description) return route.description;
177
+ const sections = route.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
178
+ if (sections?.length) return `Covers ${sections.join(", ")}.`;
179
+ if (route.keywords?.length) return `Related topics: ${route.keywords.join(", ")}.`;
180
+ if (route.kind === "openapi") return "OpenAPI artifact for machine-readable API reference data.";
181
+ }
182
+ function llmsTxtListItem(route, basePath) {
183
+ if (!route.contentArtifactUrl) return void 0;
184
+ const description = llmsTxtDescription(route);
185
+ return description ? `- [${route.title}](${basePath}${route.contentArtifactUrl}): ${description}` : `- [${route.title}](${basePath}${route.contentArtifactUrl})`;
186
+ }
187
+ function llmsTxtLocaleLabel(locale, projectConfig) {
188
+ return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
189
+ }
190
+ function groupRoutesByLocale(routes) {
191
+ const groups = /* @__PURE__ */ new Map();
174
192
  for (const route of routes) {
175
- if (!route.contentArtifactUrl) continue;
176
- const outFile = (0, node_path.join)(outputDirectory, route.contentArtifactUrl.replace(/^\//, ""));
177
- (0, node_fs.mkdirSync)((0, node_path.dirname)(outFile), { recursive: true });
178
- (0, node_fs.writeFileSync)(outFile, readRouteArtifactContent(route), "utf-8");
193
+ const key = route.locale ?? "default";
194
+ groups.set(key, [...groups.get(key) ?? [], route]);
179
195
  }
196
+ return groups;
197
+ }
198
+ function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
199
+ const routesBySource = /* @__PURE__ */ new Map();
200
+ const defaultLocale = projectConfig.i18n?.defaultLocale;
201
+ for (const route of routes) {
202
+ const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.contentArtifactUrl ?? route.path}`;
203
+ const previousRoute = routesBySource.get(sourceKey);
204
+ if (!previousRoute || previousRoute.locale && !route.locale) routesBySource.set(sourceKey, route);
205
+ }
206
+ return groupRoutesByLocale([...routesBySource.values()].map((route) => ({
207
+ ...route,
208
+ locale: route.locale ?? defaultLocale
209
+ })));
210
+ }
211
+ function llmsTxtDocsSectionTitle(locale, groupCount, projectConfig) {
212
+ if (groupCount <= 1) return "Docs";
213
+ return `Docs - ${locale === "default" ? "Default" : llmsTxtLocaleLabel(locale, projectConfig)}`;
180
214
  }
181
215
  function createLlmsTxt(routes, projectConfig) {
182
216
  const basePath = normalizeBasePath(projectConfig.routePrefix);
183
217
  const lines = [`# ${projectConfig.title}`, ""];
184
218
  if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
185
- lines.push("## Docs");
186
- for (const route of routes.filter((route) => route.kind === "mdx")) {
187
- if (!route.contentArtifactUrl) continue;
188
- lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
219
+ lines.push("This file lists the source-ready Markdown and OpenAPI artifacts for this documentation site.", "");
220
+ const mdxRoutes = routes.filter((route) => route.kind === "mdx" && isLlmsTxtRoute(route));
221
+ if (mdxRoutes.length > 0) {
222
+ const localizedGroups = groupLlmsTxtRoutesByLocale(mdxRoutes, projectConfig);
223
+ for (const [locale, localeRoutes] of localizedGroups) {
224
+ lines.push(`## ${llmsTxtDocsSectionTitle(locale, localizedGroups.size, projectConfig)}`);
225
+ for (const route of localeRoutes) {
226
+ const item = llmsTxtListItem(route, basePath);
227
+ if (item) lines.push(item);
228
+ }
229
+ lines.push("");
230
+ }
189
231
  }
190
- const openApiRoutes = routes.filter((route) => route.kind === "openapi");
232
+ const openApiRoutes = routes.filter((route) => route.kind === "openapi" && isLlmsTxtRoute(route));
191
233
  if (openApiRoutes.length > 0) {
192
- lines.push("", "## OpenAPI");
234
+ if (lines.at(-1) !== "") lines.push("");
235
+ lines.push("## OpenAPI");
193
236
  for (const route of openApiRoutes) {
194
- if (!route.contentArtifactUrl) continue;
195
- lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
237
+ const item = llmsTxtListItem(route, basePath);
238
+ if (item) lines.push(item);
196
239
  }
197
240
  }
241
+ while (lines.at(-1) === "") lines.pop();
198
242
  return `${lines.join("\n")}\n`;
199
243
  }
200
244
  function createLlmsTxtArtifact(routes, projectConfig) {
201
245
  return withUtf8Signature(createLlmsTxt(routes, projectConfig));
202
246
  }
203
- function writeLlmsTxt(routes, projectConfig, outputDirectory) {
204
- (0, node_fs.mkdirSync)(outputDirectory, { recursive: true });
205
- (0, node_fs.writeFileSync)((0, node_path.join)(outputDirectory, "llms.txt"), createLlmsTxtArtifact(routes, projectConfig), "utf-8");
206
- }
207
247
  //#endregion
208
248
  //#region source/plugins/content-artifacts/server.ts
209
249
  function normalizeRoutePrefix$1(routePrefix) {
@@ -216,7 +256,6 @@ function resolveContentArtifactPath(url, projectConfig) {
216
256
  return basePath && requestPath.startsWith(basePath) ? requestPath.slice(basePath.length) || "/" : requestPath;
217
257
  }
218
258
  function resolveContentArtifactType(route) {
219
- if (route.kind === "openapi" && /\.ya?ml$/i.test(route.contentArtifactUrl ?? "")) return "text/yaml; charset=utf-8";
220
259
  return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
221
260
  }
222
261
  function serveContentArtifacts(req, res, projectConfig, routes) {
@@ -227,6 +266,18 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
227
266
  res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
228
267
  return true;
229
268
  }
269
+ if (contentPath.endsWith(".openapi.yaml")) {
270
+ const jsonPath = contentPath.replace(/\.yaml$/, ".json");
271
+ const route = routes.find((r) => r.contentArtifactUrl === jsonPath);
272
+ if (route?.kind === "openapi" && route.content) {
273
+ const spec = JSON.parse(route.content);
274
+ res.statusCode = 200;
275
+ res.setHeader("Content-Type", "text/yaml; charset=utf-8");
276
+ res.end((0, yaml.stringify)(spec, { lineWidth: 0 }), "utf8");
277
+ return true;
278
+ }
279
+ return false;
280
+ }
230
281
  const route = routes.find((route) => route.contentArtifactUrl === contentPath);
231
282
  if (!route) return false;
232
283
  res.statusCode = 200;
@@ -250,12 +301,28 @@ function createContentArtifactsPlugin() {
250
301
  next();
251
302
  });
252
303
  },
253
- "build:done": (ctx) => {
254
- const outputDirectory = ctx.generateOptions.outputDirectory;
255
- if (!outputDirectory) return;
256
- const outputRoot = (0, node_path.join)(ctx.generateOptions.projectRoot, outputDirectory);
257
- writeContentArtifactFiles(ctx.routes, outputRoot);
258
- writeLlmsTxt(ctx.routes, ctx.projectConfig, outputRoot);
304
+ "build:assets": (ctx) => {
305
+ const assets = [];
306
+ for (const route of ctx.routes) {
307
+ if (!route.contentArtifactUrl) continue;
308
+ assets.push({
309
+ fileName: route.contentArtifactUrl.replace(/^\//, ""),
310
+ source: readRouteArtifactContent(route)
311
+ });
312
+ if (route.kind === "openapi" && route.content) {
313
+ const yamlFileName = route.contentArtifactUrl.replace(/\.json$/, ".yaml");
314
+ const spec = JSON.parse(route.content);
315
+ assets.push({
316
+ fileName: yamlFileName.replace(/^\//, ""),
317
+ source: (0, yaml.stringify)(spec, { lineWidth: 0 })
318
+ });
319
+ }
320
+ }
321
+ assets.push({
322
+ fileName: "llms.txt",
323
+ source: createLlmsTxtArtifact(ctx.routes, ctx.projectConfig)
324
+ });
325
+ return assets;
259
326
  }
260
327
  }
261
328
  };
@@ -331,8 +398,103 @@ function parseFrontmatter(content) {
331
398
  };
332
399
  }
333
400
  //#endregion
401
+ //#region source/parsers/content.ts
402
+ async function transformContent(transform, input) {
403
+ return transform ? await transform(input) : input;
404
+ }
405
+ function createContentProcessor(transform) {
406
+ return {
407
+ async processMdx(source, filePath) {
408
+ const { frontmatter, content } = parseFrontmatter(source);
409
+ const transformed = await transformContent(transform, {
410
+ kind: "mdx",
411
+ source,
412
+ filePath,
413
+ frontmatter,
414
+ content
415
+ });
416
+ return {
417
+ frontmatter: transformed.frontmatter,
418
+ content: transformed.content
419
+ };
420
+ },
421
+ async processText(source, kind, filePath) {
422
+ return (await transformContent(transform, {
423
+ kind,
424
+ source,
425
+ filePath,
426
+ frontmatter: {},
427
+ content: source
428
+ })).content;
429
+ }
430
+ };
431
+ }
432
+ //#endregion
433
+ //#region source/core/hooks.ts
434
+ async function runHooks(plugins, hookName, input, ctx) {
435
+ let result = input;
436
+ for (const plugin of plugins) {
437
+ const hook = plugin.hooks?.[hookName];
438
+ if (!hook) continue;
439
+ try {
440
+ result = await hook(result, ctx);
441
+ } catch (err) {
442
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
443
+ }
444
+ }
445
+ return result;
446
+ }
447
+ async function runDevConfigureServerHooks(plugins, server, ctx) {
448
+ for (const plugin of plugins) {
449
+ const hook = plugin.hooks?.["dev:configureServer"];
450
+ if (!hook) continue;
451
+ try {
452
+ await hook(server, ctx);
453
+ } catch (err) {
454
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
455
+ }
456
+ }
457
+ }
458
+ async function runBuildAssetsHooks(plugins, ctx) {
459
+ const assets = [];
460
+ for (const plugin of plugins) {
461
+ const hook = plugin.hooks?.["build:assets"];
462
+ if (!hook) continue;
463
+ try {
464
+ const pluginAssets = await hook(ctx);
465
+ assets.push(...pluginAssets);
466
+ } catch (err) {
467
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
468
+ }
469
+ }
470
+ return assets;
471
+ }
472
+ async function runBuildDoneHooks(plugins, ctx) {
473
+ for (const plugin of plugins) {
474
+ const hook = plugin.hooks?.["build:done"];
475
+ if (!hook) continue;
476
+ try {
477
+ await hook(ctx);
478
+ } catch (err) {
479
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:done" failed: ${err}`, { cause: err });
480
+ }
481
+ }
482
+ }
483
+ //#endregion
484
+ //#region source/core/content.ts
485
+ var contentProcessors = /* @__PURE__ */ new WeakMap();
486
+ function createProjectContentProcessor(plugins, ctx) {
487
+ return createContentProcessor((input) => runHooks(plugins, "content:transform", input, ctx));
488
+ }
489
+ function setProjectContentProcessor(ctx, processor) {
490
+ contentProcessors.set(ctx, processor);
491
+ }
492
+ function getProjectContentProcessor(ctx) {
493
+ return contentProcessors.get(ctx);
494
+ }
495
+ //#endregion
334
496
  //#region source/parsers/routes.ts
335
- function kebabToTitle$1(str) {
497
+ function kebabToTitle(str) {
336
498
  return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
337
499
  }
338
500
  function parseMdxTree(content) {
@@ -397,32 +559,33 @@ function routePathFromRef(ref, explicitPath) {
397
559
  function virtualModuleIdFromRef(ref) {
398
560
  return "virtual:clarify-page/" + ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
399
561
  }
400
- function localizedRoutePath(basePath, locale, i18n) {
562
+ function localizedRoutePath(basePath, locale, _i18n) {
401
563
  const normalizedBasePath = normalizePath(basePath);
402
- if (locale === i18n.defaultLocale) return normalizedBasePath;
403
564
  const prefix = normalizePath(locale);
404
565
  return normalizedBasePath === "/" ? prefix : normalizePath(`${prefix}${normalizedBasePath}`);
405
566
  }
406
567
  function resolveLocalizedText(text, locale, fallbackLocale) {
407
- if (typeof text === "string" || text === void 0) return text;
568
+ if (typeof text === "string") return text;
569
+ if (text === void 0) return void 0;
408
570
  return text[locale] ?? (fallbackLocale ? text[fallbackLocale] : void 0) ?? Object.values(text)[0];
409
571
  }
410
- function findContentRoutes(dir, base = dir) {
572
+ async function findContentRoutes(dir, base = dir, options = {}) {
411
573
  const routes = [];
412
574
  if (!(0, node_fs.existsSync)(dir)) return routes;
413
575
  const entries = (0, node_fs.readdirSync)(dir, { withFileTypes: true });
414
576
  for (const entry of entries) {
415
577
  const fullPath = (0, node_path.join)(dir, entry.name);
416
- if (entry.isDirectory()) routes.push(...findContentRoutes(fullPath, base));
578
+ if (entry.isDirectory()) routes.push(...await findContentRoutes(fullPath, base, options));
417
579
  else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
418
580
  const relativePath = (0, node_path.relative)(base, fullPath);
419
581
  const pathParts = relativePath.replace(/\.mdx?$/, "").split("/");
420
582
  const cleanPath = ("/" + pathParts.map((p) => p === "index" ? "" : p).filter(Boolean).join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
421
- const { frontmatter, content } = parseFrontmatter((0, node_fs.readFileSync)(fullPath, "utf-8"));
583
+ const source = (0, node_fs.readFileSync)(fullPath, "utf-8");
584
+ const { frontmatter, content } = await (options.contentProcessor ?? createContentProcessor()).processMdx(source, fullPath);
422
585
  let title = typeof frontmatter.title === "string" ? frontmatter.title : "";
423
586
  if (!title) {
424
587
  const lastPart = pathParts[pathParts.length - 1] ?? "";
425
- title = kebabToTitle$1(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(content) : lastPart) || "Untitled";
588
+ title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(content) : lastPart) || "Untitled";
426
589
  }
427
590
  routes.push({
428
591
  path: cleanPath,
@@ -446,9 +609,9 @@ function frontmatterKeywords(frontmatter) {
446
609
  const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
447
610
  return keywords.length > 0 ? keywords : void 0;
448
611
  }
449
- function withAlternates$2(route, routes, i18n) {
612
+ function withAlternates(route, routes, i18n) {
450
613
  const basePath = route.basePath ?? route.path;
451
- const routeByLocaleAndBase = new Map(routes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
614
+ 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]));
452
615
  const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
453
616
  const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
454
617
  return alternate ? [[locale.code, alternate.path]] : [];
@@ -470,7 +633,7 @@ function buildNavigation(routes) {
470
633
  if (!node) {
471
634
  node = {
472
635
  path: pathSoFar,
473
- title: i === parts.length - 1 ? route.title : kebabToTitle$1(parts[i]),
636
+ title: i === parts.length - 1 ? route.title : kebabToTitle(parts[i]),
474
637
  children: []
475
638
  };
476
639
  current.push(node);
@@ -542,7 +705,21 @@ function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
542
705
  }
543
706
  }
544
707
  const nextRoutes = [...routes, ...additions];
545
- return i18n ? nextRoutes.map((route) => withAlternates$2(route, nextRoutes, i18n)) : nextRoutes;
708
+ const routesWithAlternates = i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, i18n)) : nextRoutes;
709
+ if (!i18n) return routesWithAlternates;
710
+ const bareRoutes = [];
711
+ const seenBare = new Set(routesWithAlternates.map((r) => r.path));
712
+ for (const route of routesWithAlternates) {
713
+ if (route.locale !== i18n.defaultLocale) continue;
714
+ const bp = route.basePath ?? route.path;
715
+ if (bp === route.path || seenBare.has(bp)) continue;
716
+ seenBare.add(bp);
717
+ bareRoutes.push({
718
+ ...route,
719
+ path: bp
720
+ });
721
+ }
722
+ return [...routesWithAlternates, ...bareRoutes];
546
723
  }
547
724
  function buildNavigationFromPagesConfig(routes, config) {
548
725
  if (!config || config === "FileTree") return buildNavigation(routes);
@@ -570,7 +747,7 @@ function buildNavigationFromConfig(routes, config) {
570
747
  const route = routeMap.get(path);
571
748
  return {
572
749
  path,
573
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle$1(path.split("/").pop() ?? openapiRef),
750
+ title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? openapiRef),
574
751
  icon,
575
752
  sections: route?.sections ? navigationSections(route.sections) : void 0
576
753
  };
@@ -580,7 +757,7 @@ function buildNavigationFromConfig(routes, config) {
580
757
  const route = routeMap.get(path);
581
758
  return {
582
759
  path: redirect ? normalizePath(redirect) : path,
583
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle$1(path.split("/").pop() ?? ref),
760
+ title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? ref),
584
761
  icon,
585
762
  sections: route?.sections ? navigationSections(route.sections) : void 0
586
763
  };
@@ -615,7 +792,7 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
615
792
  const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
616
793
  return {
617
794
  path: (redirect ? localizedRoutePath(routePathFromRef(redirect), locale, i18n) : void 0) ?? path,
618
- title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle$1(basePath.split("/").pop() ?? ref),
795
+ title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle(basePath.split("/").pop() ?? ref),
619
796
  icon,
620
797
  sections: route?.sections ? navigationSections(route.sections) : void 0
621
798
  };
@@ -661,8 +838,12 @@ var OPENAPI_HTTP_METHODS = [
661
838
  function errorMessage(error) {
662
839
  return error instanceof Error ? error.message : String(error);
663
840
  }
664
- function kebabToTitle(str) {
665
- return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
841
+ function isSameOpenAPIFile(url, filePath) {
842
+ try {
843
+ return (0, node_path.resolve)((0, node_url.fileURLToPath)(url)) === (0, node_path.resolve)(filePath);
844
+ } catch {
845
+ return (0, node_path.resolve)(url) === (0, node_path.resolve)(filePath);
846
+ }
666
847
  }
667
848
  function findOpenAPIRoutes(dir, base = dir) {
668
849
  const routes = [];
@@ -683,8 +864,7 @@ function findOpenAPIRoutes(dir, base = dir) {
683
864
  filePath: fullPath,
684
865
  virtualModuleId: virtualModuleIdFromRef(ref),
685
866
  title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
686
- kind: "openapi",
687
- content: (0, node_fs.readFileSync)(fullPath, "utf-8")
867
+ kind: "openapi"
688
868
  });
689
869
  }
690
870
  return routes;
@@ -693,6 +873,20 @@ function operationMatchesTags(operationTags, filterTags) {
693
873
  if (!filterTags?.length) return true;
694
874
  return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
695
875
  }
876
+ /** Filter an OpenAPI spec to only include paths that have at least one operation matching the given tags. */
877
+ function filterSpecByTags(spec, tags) {
878
+ const filteredPaths = {};
879
+ const paths = spec.paths ?? {};
880
+ for (const path of Object.keys(paths)) {
881
+ const pathItem = paths[path];
882
+ if (!pathItem) continue;
883
+ if (Object.values(pathItem).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredPaths[path] = pathItem;
884
+ }
885
+ return {
886
+ ...spec,
887
+ paths: filteredPaths
888
+ };
889
+ }
696
890
  /** Extract endpoint operations from an OpenAPI spec as page sections. */
697
891
  function extractOpenAPISections(spec, filterTags) {
698
892
  const sections = [];
@@ -714,12 +908,26 @@ function extractOpenAPISections(spec, filterTags) {
714
908
  }
715
909
  return sections;
716
910
  }
717
- async function readOpenAPISpec(filePath) {
911
+ async function readOpenAPISpec(filePath, contentProcessor) {
718
912
  try {
719
- return {
913
+ if (!contentProcessor) return {
914
+ ok: true,
915
+ spec: await _apidevtools_swagger_parser.default.dereference(filePath)
916
+ };
917
+ const source = (0, node_fs.readFileSync)(filePath, "utf-8");
918
+ const transformedSource = await contentProcessor.processText(source, "openapi", filePath);
919
+ if (transformedSource === source) return {
720
920
  ok: true,
721
921
  spec: await _apidevtools_swagger_parser.default.dereference(filePath)
722
922
  };
923
+ return {
924
+ ok: true,
925
+ spec: await _apidevtools_swagger_parser.default.dereference(filePath, { resolve: { transformedOpenAPISource: {
926
+ order: 1,
927
+ canRead: (file) => isSameOpenAPIFile(file.url, filePath),
928
+ read: () => transformedSource
929
+ } } })
930
+ };
723
931
  } catch (error) {
724
932
  return {
725
933
  ok: false,
@@ -734,12 +942,14 @@ async function readOpenAPISpec(filePath) {
734
942
  }
735
943
  //#endregion
736
944
  //#region source/core/virtual-modules.ts
737
- var VIRTUAL_CONFIG = "virtual:clarify-config";
738
- var VIRTUAL_ROUTES = "virtual:clarify-routes";
739
- var VIRTUAL_SERVER_ROUTES = "virtual:clarify-routes/server";
740
- var VIRTUAL_RUNTIME = "virtual:clarify-runtime";
741
- var VIRTUAL_CLIENT_ENTRY = "virtual:clarify-entry-client";
742
- var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify-entry-client";
945
+ var VIRTUAL_CONFIG = "virtual:clarify/config";
946
+ var VIRTUAL_ROUTES = "virtual:clarify/routes";
947
+ var VIRTUAL_SERVER_ROUTES = "virtual:clarify/routes/server";
948
+ var VIRTUAL_OPENAPI = "virtual:clarify/openapi";
949
+ var VIRTUAL_SLOTS = "virtual:clarify/slots";
950
+ var VIRTUAL_SLOT = "virtual:clarify/slot";
951
+ var VIRTUAL_CLIENT_ENTRY = "virtual:clarify/entry-client";
952
+ var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify/entry-client";
743
953
  function resolveVirtualId(id) {
744
954
  return "\0" + id;
745
955
  }
@@ -780,65 +990,117 @@ function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode =
780
990
  const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
781
991
  return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
782
992
  }
783
- function createRuntimeModule(options = {}) {
784
- 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");
785
- const exports = `export const openApis = {};
786
- export const bannerComponent = ${options.bannerComponentSource === "path" ? "BannerComponent" : "undefined"};
787
- export const footerComponent = ${options.footerComponentSource === "path" ? "FooterComponent" : "undefined"};`;
788
- return imports ? `${imports}\n${exports}` : exports;
789
- }
790
993
  function createClientEntryModule(options = {}) {
791
994
  return `
792
995
  import '@clarify-labs/renderer/style.css';
793
996
  import { render } from '@clarify-labs/renderer/client';
794
997
  import { routes, navigation } from '${VIRTUAL_ROUTES}';
795
998
  import { config } from '${VIRTUAL_CONFIG}';
796
- import { openApis, bannerComponent, footerComponent } from '${VIRTUAL_RUNTIME}';
797
- render({ config, routes, navigation, openApis, bannerComponent, footerComponent, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
999
+ import { openApis } from '${VIRTUAL_OPENAPI}';
1000
+ import { runtimeSlots } from '${VIRTUAL_SLOTS}';
1001
+ render({ config, routes, navigation, openApis, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
1002
+ }
1003
+ /**
1004
+ * Collect every plugin's `slots` declarations into a single runtime registry
1005
+ * module. Component modules are exported as lazy import factories so the
1006
+ * renderer can decide whether to code-split (client) or pre-resolve (SSR).
1007
+ *
1008
+ * Relative component paths are resolved against the project `root` so that
1009
+ * the generated `import()` call uses an absolute specifier — Vite cannot
1010
+ * resolve relative imports from virtual modules.
1011
+ *
1012
+ * Slot name validity is enforced at compile time by the `UISlotName`
1013
+ * type — no runtime validation is needed.
1014
+ */
1015
+ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
1016
+ const registrations = [];
1017
+ for (const plugin of plugins) for (const slot of plugin.slots ?? []) registrations.push({
1018
+ plugin: plugin.name,
1019
+ slot
1020
+ });
1021
+ if (registrations.length === 0) return "export const runtimeSlots = {};\n";
1022
+ const grouped = /* @__PURE__ */ new Map();
1023
+ for (const { plugin, slot } of registrations) {
1024
+ if (slot.name.endsWith(".replace")) {
1025
+ const existing = grouped.get(slot.name);
1026
+ if (existing) console.warn(`[clarify] Multiple plugins register slot "${slot.name}": "${plugin}" overrides "${existing[existing.length - 1]}". Only the last registration is used.`);
1027
+ }
1028
+ 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.`);
1029
+ const componentPath = root + slot.component;
1030
+ const entry = `{ plugin: ${JSON.stringify(plugin)}, component: () => import(${moduleSpecifier(componentPath)}) }`;
1031
+ const list = grouped.get(slot.name) ?? [];
1032
+ list.push(entry);
1033
+ grouped.set(slot.name, list);
1034
+ }
1035
+ return `export const runtimeSlots = {\n${[...grouped.entries()].map(([name, list]) => ` ${JSON.stringify(name)}: [\n ${list.join(",\n ")}\n ]`).join(",\n")}\n};\n`;
1036
+ }
1037
+ /**
1038
+ * Re-exports the slot context hook from the renderer so plugin components can
1039
+ * `import { useSlot } from 'virtual:clarify/slot'` without depending on
1040
+ * renderer internals directly.
1041
+ */
1042
+ function createSlotModule() {
1043
+ return `export { useSlot } from '@clarify-labs/renderer';\n`;
798
1044
  }
799
1045
  function buildVirtualModules(args) {
800
1046
  const modules = /* @__PURE__ */ new Map();
801
1047
  const clientEntryModule = createClientEntryModule({ themeEditor: args.themeEditor });
802
- const bannerComponent = args.projectConfig.banner;
803
- const bannerComponentSource = typeof bannerComponent === "string" ? "path" : void 0;
804
- const bannerComponentImport = typeof bannerComponent === "string" ? bannerComponent : void 0;
805
- const footerComponent = args.projectConfig.footer;
806
- const footerComponentSource = typeof footerComponent === "string" ? "path" : void 0;
807
- const footerComponentImport = typeof footerComponent === "string" ? footerComponent : void 0;
1048
+ const allPlugins = [...args.plugins ?? []];
808
1049
  modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
809
1050
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
810
1051
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "server"));
811
- modules.set(VIRTUAL_RUNTIME, createRuntimeModule({
812
- bannerComponentSource,
813
- footerComponentSource,
814
- imports: {
815
- bannerComponent: bannerComponentImport,
816
- footerComponent: footerComponentImport
817
- }
818
- }));
1052
+ modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
1053
+ modules.set(VIRTUAL_OPENAPI, generateOpenApiModule());
1054
+ modules.set(VIRTUAL_SLOT, createSlotModule());
819
1055
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
820
1056
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
821
1057
  for (const route of args.routes) modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
822
1058
  return modules;
823
1059
  }
1060
+ function generateOpenApiModule() {
1061
+ return `export const openApis = {};`;
1062
+ }
824
1063
  //#endregion
825
1064
  //#region source/plugins/openapi/virtual-modules.ts
826
- var openApiRegistryModuleId = VIRTUAL_RUNTIME;
827
- function generateOpenAPIRegistryModule(openApis, runtimeModule = "") {
828
- const openApisExport = `export const openApis = ${JSON.stringify(openApis)};`;
829
- const openApisPlaceholder = /export const openApis = \{\};?/;
830
- if (!runtimeModule) return openApisExport;
831
- if (openApisPlaceholder.test(runtimeModule)) return runtimeModule.replace(openApisPlaceholder, openApisExport);
832
- return runtimeModule;
833
- }
834
- function generateOpenAPIModule(spec, tagFilter) {
835
- return `import { createElement } from 'react';
836
- import { OpenApiDocument } from '@clarify-labs/renderer';
837
- const spec = ${JSON.stringify(spec)};
838
- const tagFilter = ${JSON.stringify(tagFilter)};
839
- export default function OpenApiRoutePage() {
840
- return createElement(OpenApiDocument, { spec, tagFilter });
841
- }`;
1065
+ var openApiRegistryModuleId = VIRTUAL_OPENAPI;
1066
+ var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
1067
+ function specVirtualModuleId(specKey) {
1068
+ return `${OPENAPI_SPEC_PREFIX}${specKey}`;
1069
+ }
1070
+ function generateOpenAPIRegistryModule(openApis) {
1071
+ return `export const openApis = ${JSON.stringify(openApis)};`;
1072
+ }
1073
+ /** Generate a virtual module that exports a single spec as default. */
1074
+ function generateOpenAPISpecModule(spec) {
1075
+ return `export default ${JSON.stringify(spec)};`;
1076
+ }
1077
+ function generateOpenAPIPageModule(opts) {
1078
+ const { specKey, specRegistryKey, tagFilter } = opts;
1079
+ return [
1080
+ `import { createElement, useState, useEffect, useRef } from 'react';`,
1081
+ `import { OpenApiDocument, useOpenApis } from '@clarify-labs/renderer';`,
1082
+ `const SPEC_KEY = ${JSON.stringify(specRegistryKey)};`,
1083
+ `const TAG_FILTER = ${JSON.stringify(tagFilter ?? void 0)};`,
1084
+ `let loadPromise = null;`,
1085
+ `function loadSpec() {`,
1086
+ ` if (!loadPromise) loadPromise = import(${JSON.stringify(specVirtualModuleId(specKey))}).then(function(m) { return m.default; });`,
1087
+ ` return loadPromise;`,
1088
+ `}`,
1089
+ `export default function OpenApiRoutePage() {`,
1090
+ ` const specs = useOpenApis();`,
1091
+ ` const serverSpec = specs[SPEC_KEY];`,
1092
+ ` const [spec, setSpec] = useState(serverSpec || null);`,
1093
+ ` const mountedRef = useRef(false);`,
1094
+ ` useEffect(function() {`,
1095
+ ` if (mountedRef.current) return;`,
1096
+ ` mountedRef.current = true;`,
1097
+ ` if (spec) return;`,
1098
+ ` loadSpec().then(setSpec);`,
1099
+ ` }, []);`,
1100
+ ` if (!spec) return null;`,
1101
+ ` return createElement(OpenApiDocument, { spec: spec, tagFilter: TAG_FILTER });`,
1102
+ `}`
1103
+ ].join("\n");
842
1104
  }
843
1105
  function generateOpenAPIErrorModule(diagnostic) {
844
1106
  return `import { createElement } from 'react';
@@ -872,18 +1134,6 @@ function collectOpenAPIPageItems(config) {
872
1134
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
873
1135
  return items;
874
1136
  }
875
- function withAlternates$1(route, routes, i18n) {
876
- const basePath = route.basePath ?? route.path;
877
- const routeByLocaleAndBase = new Map(routes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
878
- const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
879
- const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
880
- return alternate ? [[locale.code, alternate.path]] : [];
881
- }));
882
- return {
883
- ...route,
884
- alternates
885
- };
886
- }
887
1137
  function virtualModuleIdFromPath(path) {
888
1138
  return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
889
1139
  }
@@ -913,10 +1163,15 @@ function createConfiguredOpenAPIRoutes(routes, config) {
913
1163
  }
914
1164
  }
915
1165
  const nextRoutes = [...routes, ...additions];
916
- return config.i18n ? nextRoutes.map((route) => withAlternates$1(route, nextRoutes, config.i18n)) : nextRoutes;
1166
+ return config.i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n)) : nextRoutes;
1167
+ }
1168
+ /** Derive a stable, deduplicated key from an absolute spec file path. */
1169
+ function specFileKeyFromPath(filePath, projectRoot) {
1170
+ return (0, node_path.relative)(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
917
1171
  }
918
1172
  function createOpenAPIPlugin() {
919
- const specs = {};
1173
+ /** specs keyed by specFileKey (deduplicated per source file). */
1174
+ const specs = /* @__PURE__ */ new Map();
920
1175
  return {
921
1176
  name: "clarify:openapi",
922
1177
  hooks: {
@@ -925,7 +1180,7 @@ function createOpenAPIPlugin() {
925
1180
  routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
926
1181
  }),
927
1182
  "routes:discovered": async (routes, ctx) => {
928
- for (const key of Object.keys(specs)) delete specs[key];
1183
+ specs.clear();
929
1184
  const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
930
1185
  const specByFilePath = /* @__PURE__ */ new Map();
931
1186
  for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
@@ -933,7 +1188,7 @@ function createOpenAPIPlugin() {
933
1188
  const result = specFromCache ? {
934
1189
  ok: true,
935
1190
  spec: specFromCache
936
- } : await readOpenAPISpec(route.filePath);
1191
+ } : await readOpenAPISpec(route.filePath, getProjectContentProcessor(ctx));
937
1192
  if (!result.ok) {
938
1193
  route.diagnostic = result.diagnostic;
939
1194
  route.title = route.title || "OpenAPI parse error";
@@ -942,24 +1197,45 @@ function createOpenAPIPlugin() {
942
1197
  }
943
1198
  const spec = result.spec;
944
1199
  specByFilePath.set(route.filePath, spec);
945
- specs[route.virtualModuleId] = {
1200
+ const specKey = specFileKeyFromPath(route.filePath, ctx.generateOptions.projectRoot);
1201
+ route.specFileKey = specKey;
1202
+ specs.set(specKey, {
946
1203
  spec,
947
- tagFilter: route.openapiTagFilter
948
- };
949
- specs[`virtual:clarify-page/${route.path.replace(/^\//, "")}`] = {
950
- spec,
951
- tagFilter: route.openapiTagFilter
952
- };
1204
+ filePath: route.filePath
1205
+ });
953
1206
  route.title = spec.info?.title ?? route.title;
954
1207
  route.description = spec.info?.description ?? route.description;
955
1208
  route.sections = extractOpenAPISections(spec, route.openapiTagFilter);
1209
+ const pageSpec = route.openapiTagFilter?.length ? filterSpecByTags(spec, route.openapiTagFilter) : spec;
1210
+ route.content = JSON.stringify(pageSpec);
956
1211
  }
957
1212
  return nextRoutes;
958
1213
  },
959
1214
  "modules:before": (modules, ctx) => {
960
- modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(Object.fromEntries(Object.entries(specs).map(([moduleId, entry]) => [moduleId, entry.spec])), modules.get(openApiRegistryModuleId)));
961
- for (const [moduleId, entry] of Object.entries(specs)) modules.set(moduleId, generateOpenAPIModule(entry.spec, entry.tagFilter));
962
- for (const route of ctx.routes.filter((route) => route.kind === "openapi" && route.diagnostic)) if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
1215
+ const registryEntries = {};
1216
+ for (const route of ctx.routes) {
1217
+ if (route.kind !== "openapi" || !route.specFileKey) continue;
1218
+ const spec = specs.get(route.specFileKey)?.spec;
1219
+ if (!spec) continue;
1220
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
1221
+ const registryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
1222
+ registryEntries[registryKey] = spec;
1223
+ }
1224
+ modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
1225
+ for (const [specKey, entry] of specs) modules.set(specVirtualModuleId(specKey), generateOpenAPISpecModule(entry.spec));
1226
+ for (const route of ctx.routes) {
1227
+ if (route.kind !== "openapi") continue;
1228
+ if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
1229
+ else if (route.specFileKey) {
1230
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
1231
+ const specRegistryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
1232
+ modules.set(route.virtualModuleId, generateOpenAPIPageModule({
1233
+ specKey: route.specFileKey,
1234
+ specRegistryKey,
1235
+ tagFilter: route.openapiTagFilter
1236
+ }));
1237
+ }
1238
+ }
963
1239
  return modules;
964
1240
  }
965
1241
  }
@@ -1177,22 +1453,22 @@ function createRobotsTxt(config) {
1177
1453
  ""
1178
1454
  ].join("\n");
1179
1455
  }
1180
- async function writeSeoFiles(outputRoot, routes, config) {
1181
- const sitemap = createSitemapXml(routes, config);
1182
- const robots = createRobotsTxt(config);
1183
- if (!sitemap || !robots) return;
1184
- await (0, node_fs_promises.mkdir)(outputRoot, { recursive: true });
1185
- await Promise.all([(0, node_fs_promises.writeFile)((0, node_path.join)(outputRoot, "sitemap.xml"), sitemap, "utf-8"), (0, node_fs_promises.writeFile)((0, node_path.join)(outputRoot, "robots.txt"), robots, "utf-8")]);
1186
- }
1187
1456
  //#endregion
1188
1457
  //#region source/plugins/seo/index.ts
1189
1458
  function createSeoPlugin() {
1190
1459
  return {
1191
1460
  name: "clarify:seo",
1192
- hooks: { async "build:done"(ctx) {
1193
- const outputDirectory = ctx.generateOptions.outputDirectory;
1194
- if (!outputDirectory) return;
1195
- await writeSeoFiles((0, node_path.join)(ctx.generateOptions.projectRoot, outputDirectory), ctx.routes, ctx.projectConfig);
1461
+ hooks: { "build:assets"(ctx) {
1462
+ const sitemap = createSitemapXml(ctx.routes, ctx.projectConfig);
1463
+ const robots = createRobotsTxt(ctx.projectConfig);
1464
+ if (!sitemap || !robots) return [];
1465
+ return [{
1466
+ fileName: "sitemap.xml",
1467
+ source: sitemap
1468
+ }, {
1469
+ fileName: "robots.txt",
1470
+ source: robots
1471
+ }];
1196
1472
  } }
1197
1473
  };
1198
1474
  }
@@ -1229,9 +1505,51 @@ function createSourceLinksPlugin() {
1229
1505
  };
1230
1506
  }
1231
1507
  //#endregion
1508
+ //#region source/parsers/variables.ts
1509
+ var variablePattern = /{{\s*([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)\s*}}/g;
1510
+ function getVariableValue(variables, path) {
1511
+ let current = variables;
1512
+ for (const segment of path.split(".")) {
1513
+ if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
1514
+ current = current[segment];
1515
+ }
1516
+ return current;
1517
+ }
1518
+ function stringifyVariableValue(variables, path, stack) {
1519
+ if (stack.includes(path)) throw new Error(`[clarify] variable "${path}" contains a circular reference: ${[...stack, path].join(" -> ")}`);
1520
+ const value = getVariableValue(variables, path);
1521
+ if (value === void 0 || typeof value === "object") return void 0;
1522
+ if (typeof value !== "string") return String(value);
1523
+ return value.replace(variablePattern, (match, nestedPath) => stringifyVariableValue(variables, nestedPath, [...stack, path]) ?? match);
1524
+ }
1525
+ function applyVariables(content, variables = {}) {
1526
+ return content.replace(variablePattern, (match, path) => stringifyVariableValue(variables, path, []) ?? match);
1527
+ }
1528
+ //#endregion
1529
+ //#region source/plugins/variables/index.ts
1530
+ function applyVariablesToFrontmatter(frontmatter, variables) {
1531
+ const nextFrontmatter = { ...frontmatter };
1532
+ for (const key of ["title", "description"]) {
1533
+ if (typeof nextFrontmatter[key] !== "string") continue;
1534
+ nextFrontmatter[key] = applyVariables(nextFrontmatter[key], variables);
1535
+ }
1536
+ return nextFrontmatter;
1537
+ }
1538
+ function createVariablesPlugin() {
1539
+ return {
1540
+ name: "clarify:variables",
1541
+ hooks: { "content:transform": (input, ctx) => ({
1542
+ ...input,
1543
+ frontmatter: applyVariablesToFrontmatter(input.frontmatter, ctx.projectConfig.variables),
1544
+ content: applyVariables(input.content, ctx.projectConfig.variables)
1545
+ }) }
1546
+ };
1547
+ }
1548
+ //#endregion
1232
1549
  //#region source/core/builtin.ts
1233
1550
  function createBuiltinPlugins(options = {}) {
1234
1551
  const plugins = [
1552
+ createVariablesPlugin(),
1235
1553
  createOpenAPIPlugin(),
1236
1554
  createSourceLinksPlugin(),
1237
1555
  createContentArtifactsPlugin(),
@@ -1290,18 +1608,23 @@ var clarifyNavbarLinkSchema = zod.z.object({
1290
1608
  href: zod.z.string(),
1291
1609
  external: zod.z.boolean().optional()
1292
1610
  });
1293
- var clarifyBannerConfigOptionsSchema = zod.z.object({
1611
+ var clarifyBannerConfigSchema = zod.z.object({
1294
1612
  content: clarifyLocalizedTextSchema,
1295
1613
  dismissible: zod.z.boolean().optional(),
1296
1614
  link: clarifyNavbarLinkSchema.optional()
1297
1615
  }).strict();
1298
- var clarifyBannerConfigSchema = zod.z.union([clarifyBannerConfigOptionsSchema, zod.z.string()]);
1299
- var clarifyFooterLinksConfigSchema = zod.z.object({
1616
+ var clarifyFooterConfigSchema = zod.z.object({
1300
1617
  links: zod.z.array(clarifyNavbarLinkSchema).optional(),
1301
1618
  socials: zod.z.record(zod.z.string(), zod.z.string()).optional(),
1302
1619
  copyright: clarifyLocalizedTextSchema.optional()
1303
1620
  }).strict();
1304
- var clarifyFooterConfigSchema = zod.z.union([clarifyFooterLinksConfigSchema, zod.z.string()]);
1621
+ var clarifyVariableValueSchema = zod.z.lazy(() => zod.z.union([
1622
+ zod.z.string(),
1623
+ zod.z.number(),
1624
+ zod.z.boolean(),
1625
+ zod.z.record(zod.z.string(), clarifyVariableValueSchema)
1626
+ ]));
1627
+ var clarifyVariablesConfigSchema = zod.z.record(zod.z.string(), clarifyVariableValueSchema);
1305
1628
  var clarifySourceConfigSchema = zod.z.object({
1306
1629
  repository: zod.z.string(),
1307
1630
  branch: zod.z.string().optional(),
@@ -1379,9 +1702,11 @@ var clarifyProjectConfigSchema = zod.z.object({
1379
1702
  favicon: clarifyFaviconConfigSchema.optional(),
1380
1703
  theme: clarifyThemeConfigSchema.optional(),
1381
1704
  routePrefix: zod.z.string().optional(),
1705
+ assetPrefix: zod.z.string().optional(),
1382
1706
  navbar: zod.z.object({ links: zod.z.array(clarifyNavbarLinkSchema).optional() }).optional(),
1383
1707
  banner: clarifyBannerConfigSchema.optional(),
1384
1708
  footer: clarifyFooterConfigSchema.optional(),
1709
+ variables: clarifyVariablesConfigSchema.optional(),
1385
1710
  i18n: clarifyI18nConfigSchema.optional(),
1386
1711
  tabs: clarifyTabsConfigSchema.optional()
1387
1712
  });
@@ -1441,7 +1766,22 @@ function resolveI18nConfig(i18n) {
1441
1766
  locales: i18n.locales
1442
1767
  };
1443
1768
  }
1769
+ function resolveRoutePrefix(routePrefix) {
1770
+ const trimmed = routePrefix?.trim();
1771
+ if (!trimmed || trimmed === "/") return "/";
1772
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
1773
+ }
1774
+ function resolveAssetPrefix(assetPrefix, routePrefix) {
1775
+ const trimmed = assetPrefix?.trim();
1776
+ if (trimmed === void 0) return routePrefix;
1777
+ if (trimmed === "./" || trimmed === "") return trimmed || "/";
1778
+ if (!trimmed || trimmed === "/") return "/";
1779
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(trimmed)) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1780
+ if (trimmed.startsWith("./") || trimmed.startsWith("../")) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1781
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
1782
+ }
1444
1783
  function resolveProjectConfig(config = {}) {
1784
+ const routePrefix = resolveRoutePrefix(config.routePrefix);
1445
1785
  return {
1446
1786
  title: config.title ?? "Clarify Docs",
1447
1787
  description: config.description ?? "",
@@ -1450,51 +1790,64 @@ function resolveProjectConfig(config = {}) {
1450
1790
  logo: config.logo,
1451
1791
  homeUrl: config.homeUrl,
1452
1792
  favicon: config.favicon,
1453
- routePrefix: config.routePrefix ?? "/",
1793
+ routePrefix,
1794
+ assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
1454
1795
  theme: resolveThemeConfig(config.theme),
1455
1796
  navbar: config.navbar,
1456
1797
  banner: config.banner,
1457
1798
  footer: config.footer,
1799
+ variables: config.variables ?? {},
1458
1800
  i18n: resolveI18nConfig(config.i18n),
1459
1801
  tabs: config.tabs
1460
1802
  };
1461
1803
  }
1462
1804
  //#endregion
1463
- //#region source/core/hooks.ts
1464
- async function runHooks(plugins, hookName, input, ctx) {
1465
- let result = input;
1466
- for (const plugin of plugins) {
1467
- const hook = plugin.hooks[hookName];
1468
- if (!hook) continue;
1469
- try {
1470
- result = await hook(result, ctx);
1471
- } catch (err) {
1472
- throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
1473
- }
1474
- }
1475
- return result;
1805
+ //#region source/core/env-types.ts
1806
+ var GENERATED_FILE_NAME = "clarify-env.d.ts";
1807
+ /**
1808
+ * Template for the generated `clarify-env.d.ts` file.
1809
+ * Edit this template directly if you need to change the type declarations.
1810
+ * This avoids the complexity of reading external files at runtime after bundling.
1811
+ */
1812
+ var CLARIFY_ENV_TEMPLATE = `// This file is generated by Clarify. Do not edit it manually.
1813
+ // It provides ambient type declarations for Clarify virtual modules used in
1814
+ // plugin and slot code, so the TypeScript language service can resolve them.
1815
+
1816
+ declare module 'virtual:clarify/slot' {
1817
+ import type { SlotContext } from '@clarify-labs/cli'
1818
+
1819
+ export function useSlot(): SlotContext
1476
1820
  }
1477
- async function runDevConfigureServerHooks(plugins, server, ctx) {
1478
- for (const plugin of plugins) {
1479
- const hook = plugin.hooks["dev:configureServer"];
1480
- if (!hook) continue;
1481
- try {
1482
- await hook(server, ctx);
1483
- } catch (err) {
1484
- throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
1485
- }
1486
- }
1821
+
1822
+ declare module 'virtual:clarify/slots' {
1823
+ import type { ComponentType } from 'react'
1824
+
1825
+ export type RuntimeSlotEntry = {
1826
+ plugin: string
1827
+ /** Lazy import factory — yields the default-exported React component. */
1828
+ component: () => Promise<{ default: ComponentType }>
1829
+ }
1830
+
1831
+ export const runtimeSlots: Record<string, RuntimeSlotEntry[]>
1487
1832
  }
1488
- async function runBuildDoneHooks(plugins, ctx) {
1489
- for (const plugin of plugins) {
1490
- const hook = plugin.hooks["build:done"];
1491
- if (!hook) continue;
1492
- try {
1493
- await hook(ctx);
1494
- } catch (err) {
1495
- throw new Error(`[clarify] plugin "${plugin.name}" hook "build:done" failed: ${err}`, { cause: err });
1496
- }
1833
+ `;
1834
+ function generateClarifyEnvDts(_plugins) {
1835
+ return CLARIFY_ENV_TEMPLATE;
1836
+ }
1837
+ /**
1838
+ * Writes `clarify-env.d.ts` into the project root. Only rewrites when content
1839
+ * changes to avoid triggering unnecessary file watchers, and never overwrites a
1840
+ * user-authored file that does not carry the generated marker.
1841
+ */
1842
+ function writeClarifyEnvDts(projectRoot, plugins) {
1843
+ const filePath = (0, node_path.join)(projectRoot, GENERATED_FILE_NAME);
1844
+ const next = generateClarifyEnvDts(plugins);
1845
+ if ((0, node_fs.existsSync)(filePath)) {
1846
+ const current = (0, node_fs.readFileSync)(filePath, "utf-8");
1847
+ if (current === next) return;
1848
+ if (!current.startsWith("// This file is generated by Clarify.")) return;
1497
1849
  }
1850
+ (0, node_fs.writeFileSync)(filePath, next, "utf-8");
1498
1851
  }
1499
1852
  //#endregion
1500
1853
  //#region source/core/options.ts
@@ -1508,23 +1861,11 @@ function resolveBuildOptions(options = {}) {
1508
1861
  }
1509
1862
  //#endregion
1510
1863
  //#region source/core/site.ts
1511
- function withAlternates(route, routeList, i18n) {
1512
- const basePath = route.basePath ?? route.path;
1513
- const routeByLocaleAndBase = new Map(routeList.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
1514
- const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
1515
- const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
1516
- return alternate ? [[locale.code, alternate.path]] : [];
1517
- }));
1518
- return {
1519
- ...route,
1520
- alternates
1521
- };
1522
- }
1523
1864
  async function discoverRoutesForRoot(routeRoot, locale, plugins, ctx) {
1524
1865
  return (await runHooks(plugins, "routes:discover", {
1525
1866
  contentRoot: routeRoot,
1526
1867
  locale,
1527
- routes: findContentRoutes(routeRoot)
1868
+ routes: await findContentRoutes(routeRoot, routeRoot, { contentProcessor: getProjectContentProcessor(ctx) })
1528
1869
  }, ctx)).routes;
1529
1870
  }
1530
1871
  async function discoverRoutes(root, contentRoot, plugins, ctx) {
@@ -1561,7 +1902,21 @@ async function discoverRoutes(root, contentRoot, plugins, ctx) {
1561
1902
  }
1562
1903
  }
1563
1904
  }
1564
- return localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
1905
+ const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
1906
+ const defaultLocale = i18n.defaultLocale;
1907
+ const bareRoutes = [];
1908
+ const seenBare = new Set(routesWithAlternates.map((r) => r.path));
1909
+ for (const route of routesWithAlternates) {
1910
+ if (route.locale !== defaultLocale) continue;
1911
+ const bp = route.basePath ?? route.path;
1912
+ if (bp === route.path || seenBare.has(bp)) continue;
1913
+ seenBare.add(bp);
1914
+ bareRoutes.push({
1915
+ ...route,
1916
+ path: bp
1917
+ });
1918
+ }
1919
+ return [...routesWithAlternates, ...bareRoutes];
1565
1920
  }
1566
1921
  async function resolveClarifySite(options = {}, resolveOptions = {}) {
1567
1922
  const root = options.projectRoot ?? process.cwd();
@@ -1575,6 +1930,7 @@ async function resolveClarifySite(options = {}, resolveOptions = {}) {
1575
1930
  routes: [],
1576
1931
  navigation: []
1577
1932
  };
1933
+ setProjectContentProcessor(ctx, createProjectContentProcessor(plugins, ctx));
1578
1934
  let routes = await discoverRoutes(root, contentRoot, plugins, ctx);
1579
1935
  routes = await runHooks(plugins, "routes:discovered", routes, ctx);
1580
1936
  routes = applyConfiguredPageRoutePaths(routes, projectConfig.tabs, projectConfig.i18n);
@@ -1637,13 +1993,21 @@ function createClarifyRuntimeAliases() {
1637
1993
  find: /^@clarify-labs\/renderer$/,
1638
1994
  replacement: resolvePackageExport("@clarify-labs/renderer", "")
1639
1995
  },
1996
+ {
1997
+ find: /^react\/jsx-dev-runtime$/,
1998
+ replacement: resolveCliDependency("react/jsx-dev-runtime")
1999
+ },
1640
2000
  {
1641
2001
  find: /^react\/jsx-runtime$/,
1642
2002
  replacement: resolveCliDependency("react/jsx-runtime")
1643
2003
  },
1644
2004
  {
1645
- find: /^react\/jsx-dev-runtime$/,
1646
- replacement: resolveCliDependency("react/jsx-dev-runtime")
2005
+ find: /^mermaid$/,
2006
+ replacement: resolveCliDependency("mermaid")
2007
+ },
2008
+ {
2009
+ find: /^react-zoom-pan-pinch$/,
2010
+ replacement: resolveCliDependency("react-zoom-pan-pinch")
1647
2011
  },
1648
2012
  {
1649
2013
  find: /^react$/,
@@ -1685,11 +2049,24 @@ function setNamedMeta(html, name, content) {
1685
2049
  if (metaPattern.test(html)) return html.replace(metaPattern, meta);
1686
2050
  return html.replace("</head>", ` ${meta}\n </head>`);
1687
2051
  }
2052
+ function canonicalUrl(projectConfig, route) {
2053
+ if (!projectConfig.siteUrl || !route.alternates || !route.locale) return void 0;
2054
+ const prefixedPath = route.alternates[route.locale];
2055
+ if (!prefixedPath || prefixedPath === route.path) return void 0;
2056
+ return `${projectConfig.siteUrl.replace(/\/+$/, "")}${!projectConfig.routePrefix || projectConfig.routePrefix === "/" ? "" : `/${projectConfig.routePrefix.replace(/^\/+|\/+$/g, "")}`}${prefixedPath}`;
2057
+ }
2058
+ function injectCanonicalUrl(html, projectConfig, route) {
2059
+ const url = route ? canonicalUrl(projectConfig, route) : void 0;
2060
+ html = html.replace(/<link\b[^>]*\brel=["']canonical["'][^>]*\/?>\n?/gi, "");
2061
+ if (!url) return html;
2062
+ return html.replace("</head>", ` <link rel="canonical" href="${escapeHtml(url)}" />\n </head>`);
2063
+ }
1688
2064
  function injectSSRIntoTemplate(template, appHtml, projectConfig, route) {
1689
2065
  let html = injectHtmlLocaleAttributes(template, projectConfig, route);
1690
2066
  html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(routeTitle(projectConfig, route))}</title>`);
1691
2067
  html = setNamedMeta(html, "description", route?.description ?? projectConfig.description);
1692
2068
  html = setNamedMeta(html, "keywords", route?.keywords?.join(", "));
2069
+ html = injectCanonicalUrl(html, projectConfig, route);
1693
2070
  html = html.replace(/<div id="root">[\s\S]*?<\/div>/, `<div id="root">${appHtml}</div>`);
1694
2071
  return html;
1695
2072
  }
@@ -1702,12 +2079,20 @@ function routeOutputFiles(outputDirectory, route) {
1702
2079
  return files;
1703
2080
  }
1704
2081
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
1705
- import { routes, navigation } from 'virtual:clarify-routes/server';
1706
- import { config } from 'virtual:clarify-config';
1707
- import { openApis, bannerComponent, footerComponent } from 'virtual:clarify-runtime';
2082
+ import { routes, navigation } from 'virtual:clarify/routes/server';
2083
+ import { config } from 'virtual:clarify/config';
2084
+ import { openApis } from 'virtual:clarify/openapi';
2085
+ import { runtimeSlots } from 'virtual:clarify/slots';
1708
2086
 
1709
- export function render(url) {
1710
- return renderToHTML({ config, routes, navigation, openApis, bannerComponent, footerComponent, url, themeEditor: config.theme.editor });
2087
+ export async function render(url) {
2088
+ // Pre-resolve every slot component factory so renderToString can use
2089
+ // them synchronously (renderToString has no Suspense support).
2090
+ const entries = Object.values(runtimeSlots).flat()
2091
+ await Promise.all(entries.map(async (entry) => {
2092
+ const mod = await entry.component()
2093
+ entry._resolved = mod.default
2094
+ }))
2095
+ return renderToHTML({ config, routes, navigation, openApis, runtimeSlots, url, themeEditor: config.theme.editor });
1711
2096
  }`;
1712
2097
  function createTempEntryFile(content) {
1713
2098
  const entryPath = (0, node_path.join)(createClarifyTempDir("ssr"), "entry-server.ts");
@@ -1737,7 +2122,7 @@ async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundle
1737
2122
  const template = readIndexHtml(outputDirectory);
1738
2123
  if (!template) throw new Error(`[clarify] index.html not found in outputDirectory "${outputDirectory}". Make sure Vite build produces it.`);
1739
2124
  for (const route of routes) try {
1740
- const finalHtml = injectSSRIntoTemplate(template, render(route.path), projectConfig, route);
2125
+ const finalHtml = injectSSRIntoTemplate(template, await render(route.path), projectConfig, route);
1741
2126
  for (const outFile of routeOutputFiles(outputDirectory, route)) {
1742
2127
  (0, node_fs.mkdirSync)((0, node_path.dirname)(outFile), { recursive: true });
1743
2128
  (0, node_fs.writeFileSync)(outFile, finalHtml, "utf-8");
@@ -1806,23 +2191,8 @@ function clarifyPlugin(options = {}) {
1806
2191
  let viteConfig;
1807
2192
  let resolvedNavigation = [];
1808
2193
  let virtualModules = /* @__PURE__ */ new Map();
1809
- async function reloadProjectConfig() {
1810
- const buildOptions = {
1811
- ...await loadClarifyConfig(root, {
1812
- command: "serve",
1813
- mode: viteConfig.mode
1814
- }),
1815
- projectRoot: options.projectRoot,
1816
- rootDirectory: options.rootDirectory,
1817
- outputDirectory: options.outputDirectory
1818
- };
1819
- projectConfig = resolveProjectConfig(buildOptions);
1820
- generateOptions = resolveBuildOptions(buildOptions);
1821
- ctx.projectConfig = projectConfig;
1822
- ctx.generateOptions = generateOptions;
1823
- }
1824
- async function resolveRoutesAndSpecs() {
1825
- const site = await resolveClarifySite(options);
2194
+ async function resolveRoutesAndSpecs(overrides) {
2195
+ const site = await resolveClarifySite(overrides ?? options);
1826
2196
  projectConfig = site.projectConfig;
1827
2197
  generateOptions = site.generateOptions;
1828
2198
  routes = site.routes;
@@ -1839,6 +2209,7 @@ function clarifyPlugin(options = {}) {
1839
2209
  generateOptions,
1840
2210
  routes,
1841
2211
  navigation: resolvedNavigation,
2212
+ plugins: clarifyPlugins,
1842
2213
  themeEditor: viteConfig.command === "serve" || projectConfig.theme.editor
1843
2214
  });
1844
2215
  virtualModules = await runHooks(clarifyPlugins, "modules:before", virtualModules, ctx);
@@ -1888,7 +2259,7 @@ function clarifyPlugin(options = {}) {
1888
2259
  async config() {
1889
2260
  await resolveRoutesAndSpecs();
1890
2261
  return {
1891
- base: projectConfig.routePrefix,
2262
+ base: projectConfig.assetPrefix,
1892
2263
  build: {
1893
2264
  ...generateOptions.outputDirectory ? { outDir: generateOptions.outputDirectory } : {},
1894
2265
  manifest: true
@@ -1901,12 +2272,23 @@ function clarifyPlugin(options = {}) {
1901
2272
  },
1902
2273
  async buildStart() {
1903
2274
  await rebuildVirtualModules();
2275
+ writeClarifyEnvDts(root, clarifyPlugins);
1904
2276
  },
1905
2277
  async handleHotUpdate(ctx) {
1906
2278
  const changedFile = (0, node_path.isAbsolute)(ctx.file) ? ctx.file : (0, node_path.join)(root, ctx.file);
1907
2279
  if (configFilePath && changedFile === configFilePath) {
1908
- await reloadProjectConfig();
1909
- await refreshDevServer(ctx.server);
2280
+ await resolveRoutesAndSpecs({
2281
+ ...await loadClarifyConfig(root, {
2282
+ command: "serve",
2283
+ mode: viteConfig.mode
2284
+ }),
2285
+ projectRoot: options.projectRoot,
2286
+ rootDirectory: options.rootDirectory,
2287
+ outputDirectory: options.outputDirectory
2288
+ });
2289
+ await rebuildVirtualModules();
2290
+ invalidateVirtualModules(ctx.server);
2291
+ ctx.server.ws.send({ type: "full-reload" });
1910
2292
  return [];
1911
2293
  }
1912
2294
  const relativeContentFile = (0, node_path.relative)(contentRoot, changedFile);
@@ -1915,10 +2297,13 @@ function clarifyPlugin(options = {}) {
1915
2297
  return [];
1916
2298
  },
1917
2299
  resolveId(id) {
1918
- if (id === "virtual:clarify-entry-client" || id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1919
- if (id === "virtual:clarify-config" || id === resolveVirtualId("virtual:clarify-config")) return resolveVirtualId(VIRTUAL_CONFIG);
1920
- if (id === "virtual:clarify-routes" || id === resolveVirtualId("virtual:clarify-routes")) return resolveVirtualId(VIRTUAL_ROUTES);
1921
- if (id === "virtual:clarify-routes/server" || id === resolveVirtualId("virtual:clarify-routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
2300
+ if (id === "virtual:clarify/entry-client" || id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2301
+ if (id === "virtual:clarify/config" || id === resolveVirtualId("virtual:clarify/config")) return resolveVirtualId(VIRTUAL_CONFIG);
2302
+ if (id === "virtual:clarify/routes" || id === resolveVirtualId("virtual:clarify/routes")) return resolveVirtualId(VIRTUAL_ROUTES);
2303
+ if (id === "virtual:clarify/routes/server" || id === resolveVirtualId("virtual:clarify/routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
2304
+ if (id === "virtual:clarify/openapi" || id === resolveVirtualId("virtual:clarify/openapi")) return resolveVirtualId(VIRTUAL_OPENAPI);
2305
+ if (id === "virtual:clarify/slots" || id === resolveVirtualId("virtual:clarify/slots")) return resolveVirtualId(VIRTUAL_SLOTS);
2306
+ if (id === "virtual:clarify/slot" || id === resolveVirtualId("virtual:clarify/slot")) return resolveVirtualId(VIRTUAL_SLOT);
1922
2307
  const moduleId = stripVirtualPrefix(id);
1923
2308
  if (virtualModules.has(moduleId)) return resolveVirtualId(moduleId);
1924
2309
  const route = routes.find((r) => r.virtualModuleId === id || r.virtualModuleId === moduleId);
@@ -1956,6 +2341,14 @@ function clarifyPlugin(options = {}) {
1956
2341
  };
1957
2342
  }
1958
2343
  },
2344
+ async generateBundle(_opts, _bundle) {
2345
+ const assets = await runBuildAssetsHooks(clarifyPlugins, ctx);
2346
+ for (const asset of assets) this.emitFile({
2347
+ type: "asset",
2348
+ fileName: asset.fileName,
2349
+ source: asset.source
2350
+ });
2351
+ },
1959
2352
  async closeBundle() {
1960
2353
  if (process.env.SKIP_CLARIFY_SSG) {
1961
2354
  await runBuildDoneHooks(clarifyPlugins, ctx);
@@ -1970,11 +2363,14 @@ function clarifyPlugin(options = {}) {
1970
2363
  {
1971
2364
  name: "clarify:virtual-ssg",
1972
2365
  resolveId(id) {
1973
- if (id === "virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1974
- if (id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1975
- if (id === "virtual:clarify-config") return id;
1976
- if (id === "virtual:clarify-routes") return id;
1977
- if (id === "virtual:clarify-routes/server") return id;
2366
+ if (id === "virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2367
+ if (id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2368
+ if (id === "virtual:clarify/routes/server") return id;
2369
+ if (id === "virtual:clarify/openapi") return id;
2370
+ if (id === "virtual:clarify/config") return id;
2371
+ if (id === "virtual:clarify/routes") return id;
2372
+ if (id === "virtual:clarify/slots") return id;
2373
+ if (id === "virtual:clarify/slot") return id;
1978
2374
  if (virtualModules.has(stripVirtualPrefix(id))) return stripVirtualPrefix(id);
1979
2375
  if (routes.find((r) => r.virtualModuleId === id)) return id;
1980
2376
  return null;
@@ -2038,4 +2434,4 @@ Object.defineProperty(exports, "resolveClarifySite", {
2038
2434
  }
2039
2435
  });
2040
2436
 
2041
- //# sourceMappingURL=plugin-DFpkIm9j.cjs.map
2437
+ //# sourceMappingURL=plugin-DyceHdXd.cjs.map