@clarify-labs/cli 0.7.1 → 0.8.0

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,13 +35,13 @@ 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 yaml = require("yaml");
38
39
  let remark = require("remark");
39
40
  let gray_matter = require("gray-matter");
40
41
  gray_matter = __toESM(gray_matter, 1);
41
42
  let _apidevtools_swagger_parser = require("@apidevtools/swagger-parser");
42
43
  _apidevtools_swagger_parser = __toESM(_apidevtools_swagger_parser, 1);
43
44
  let node_os = require("node:os");
44
- let node_fs_promises = require("node:fs/promises");
45
45
  let zod = require("zod");
46
46
  let node_url = require("node:url");
47
47
  let vite = require("vite");
@@ -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,14 +169,6 @@ function readRouteArtifactContent(route) {
170
169
  const content = readRouteContent(route);
171
170
  return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
172
171
  }
173
- function writeContentArtifactFiles(routes, outputDirectory) {
174
- 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");
179
- }
180
- }
181
172
  function createLlmsTxt(routes, projectConfig) {
182
173
  const basePath = normalizeBasePath(projectConfig.routePrefix);
183
174
  const lines = [`# ${projectConfig.title}`, ""];
@@ -200,10 +191,6 @@ function createLlmsTxt(routes, projectConfig) {
200
191
  function createLlmsTxtArtifact(routes, projectConfig) {
201
192
  return withUtf8Signature(createLlmsTxt(routes, projectConfig));
202
193
  }
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
194
  //#endregion
208
195
  //#region source/plugins/content-artifacts/server.ts
209
196
  function normalizeRoutePrefix$1(routePrefix) {
@@ -216,7 +203,6 @@ function resolveContentArtifactPath(url, projectConfig) {
216
203
  return basePath && requestPath.startsWith(basePath) ? requestPath.slice(basePath.length) || "/" : requestPath;
217
204
  }
218
205
  function resolveContentArtifactType(route) {
219
- if (route.kind === "openapi" && /\.ya?ml$/i.test(route.contentArtifactUrl ?? "")) return "text/yaml; charset=utf-8";
220
206
  return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
221
207
  }
222
208
  function serveContentArtifacts(req, res, projectConfig, routes) {
@@ -227,6 +213,18 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
227
213
  res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
228
214
  return true;
229
215
  }
216
+ if (contentPath.endsWith(".openapi.yaml")) {
217
+ const jsonPath = contentPath.replace(/\.yaml$/, ".json");
218
+ const route = routes.find((r) => r.contentArtifactUrl === jsonPath);
219
+ if (route?.kind === "openapi" && route.content) {
220
+ const spec = JSON.parse(route.content);
221
+ res.statusCode = 200;
222
+ res.setHeader("Content-Type", "text/yaml; charset=utf-8");
223
+ res.end((0, yaml.stringify)(spec, { lineWidth: 0 }), "utf8");
224
+ return true;
225
+ }
226
+ return false;
227
+ }
230
228
  const route = routes.find((route) => route.contentArtifactUrl === contentPath);
231
229
  if (!route) return false;
232
230
  res.statusCode = 200;
@@ -250,12 +248,28 @@ function createContentArtifactsPlugin() {
250
248
  next();
251
249
  });
252
250
  },
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);
251
+ "build:assets": (ctx) => {
252
+ const assets = [];
253
+ for (const route of ctx.routes) {
254
+ if (!route.contentArtifactUrl) continue;
255
+ assets.push({
256
+ fileName: route.contentArtifactUrl.replace(/^\//, ""),
257
+ source: readRouteArtifactContent(route)
258
+ });
259
+ if (route.kind === "openapi" && route.content) {
260
+ const yamlFileName = route.contentArtifactUrl.replace(/\.json$/, ".yaml");
261
+ const spec = JSON.parse(route.content);
262
+ assets.push({
263
+ fileName: yamlFileName.replace(/^\//, ""),
264
+ source: (0, yaml.stringify)(spec, { lineWidth: 0 })
265
+ });
266
+ }
267
+ }
268
+ assets.push({
269
+ fileName: "llms.txt",
270
+ source: createLlmsTxtArtifact(ctx.routes, ctx.projectConfig)
271
+ });
272
+ return assets;
259
273
  }
260
274
  }
261
275
  };
@@ -332,7 +346,7 @@ function parseFrontmatter(content) {
332
346
  }
333
347
  //#endregion
334
348
  //#region source/parsers/routes.ts
335
- function kebabToTitle$1(str) {
349
+ function kebabToTitle(str) {
336
350
  return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
337
351
  }
338
352
  function parseMdxTree(content) {
@@ -397,14 +411,14 @@ function routePathFromRef(ref, explicitPath) {
397
411
  function virtualModuleIdFromRef(ref) {
398
412
  return "virtual:clarify-page/" + ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
399
413
  }
400
- function localizedRoutePath(basePath, locale, i18n) {
414
+ function localizedRoutePath(basePath, locale, _i18n) {
401
415
  const normalizedBasePath = normalizePath(basePath);
402
- if (locale === i18n.defaultLocale) return normalizedBasePath;
403
416
  const prefix = normalizePath(locale);
404
417
  return normalizedBasePath === "/" ? prefix : normalizePath(`${prefix}${normalizedBasePath}`);
405
418
  }
406
419
  function resolveLocalizedText(text, locale, fallbackLocale) {
407
- if (typeof text === "string" || text === void 0) return text;
420
+ if (typeof text === "string") return text;
421
+ if (text === void 0) return void 0;
408
422
  return text[locale] ?? (fallbackLocale ? text[fallbackLocale] : void 0) ?? Object.values(text)[0];
409
423
  }
410
424
  function findContentRoutes(dir, base = dir) {
@@ -422,7 +436,7 @@ function findContentRoutes(dir, base = dir) {
422
436
  let title = typeof frontmatter.title === "string" ? frontmatter.title : "";
423
437
  if (!title) {
424
438
  const lastPart = pathParts[pathParts.length - 1] ?? "";
425
- title = kebabToTitle$1(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(content) : lastPart) || "Untitled";
439
+ title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(content) : lastPart) || "Untitled";
426
440
  }
427
441
  routes.push({
428
442
  path: cleanPath,
@@ -446,9 +460,9 @@ function frontmatterKeywords(frontmatter) {
446
460
  const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
447
461
  return keywords.length > 0 ? keywords : void 0;
448
462
  }
449
- function withAlternates$2(route, routes, i18n) {
463
+ function withAlternates(route, routes, i18n) {
450
464
  const basePath = route.basePath ?? route.path;
451
- const routeByLocaleAndBase = new Map(routes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
465
+ 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
466
  const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
453
467
  const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
454
468
  return alternate ? [[locale.code, alternate.path]] : [];
@@ -470,7 +484,7 @@ function buildNavigation(routes) {
470
484
  if (!node) {
471
485
  node = {
472
486
  path: pathSoFar,
473
- title: i === parts.length - 1 ? route.title : kebabToTitle$1(parts[i]),
487
+ title: i === parts.length - 1 ? route.title : kebabToTitle(parts[i]),
474
488
  children: []
475
489
  };
476
490
  current.push(node);
@@ -542,7 +556,21 @@ function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
542
556
  }
543
557
  }
544
558
  const nextRoutes = [...routes, ...additions];
545
- return i18n ? nextRoutes.map((route) => withAlternates$2(route, nextRoutes, i18n)) : nextRoutes;
559
+ const routesWithAlternates = i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, i18n)) : nextRoutes;
560
+ if (!i18n) return routesWithAlternates;
561
+ const bareRoutes = [];
562
+ const seenBare = new Set(routesWithAlternates.map((r) => r.path));
563
+ for (const route of routesWithAlternates) {
564
+ if (route.locale !== i18n.defaultLocale) continue;
565
+ const bp = route.basePath ?? route.path;
566
+ if (bp === route.path || seenBare.has(bp)) continue;
567
+ seenBare.add(bp);
568
+ bareRoutes.push({
569
+ ...route,
570
+ path: bp
571
+ });
572
+ }
573
+ return [...routesWithAlternates, ...bareRoutes];
546
574
  }
547
575
  function buildNavigationFromPagesConfig(routes, config) {
548
576
  if (!config || config === "FileTree") return buildNavigation(routes);
@@ -570,7 +598,7 @@ function buildNavigationFromConfig(routes, config) {
570
598
  const route = routeMap.get(path);
571
599
  return {
572
600
  path,
573
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle$1(path.split("/").pop() ?? openapiRef),
601
+ title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? openapiRef),
574
602
  icon,
575
603
  sections: route?.sections ? navigationSections(route.sections) : void 0
576
604
  };
@@ -580,7 +608,7 @@ function buildNavigationFromConfig(routes, config) {
580
608
  const route = routeMap.get(path);
581
609
  return {
582
610
  path: redirect ? normalizePath(redirect) : path,
583
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle$1(path.split("/").pop() ?? ref),
611
+ title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? ref),
584
612
  icon,
585
613
  sections: route?.sections ? navigationSections(route.sections) : void 0
586
614
  };
@@ -615,7 +643,7 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
615
643
  const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
616
644
  return {
617
645
  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),
646
+ title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle(basePath.split("/").pop() ?? ref),
619
647
  icon,
620
648
  sections: route?.sections ? navigationSections(route.sections) : void 0
621
649
  };
@@ -661,9 +689,6 @@ var OPENAPI_HTTP_METHODS = [
661
689
  function errorMessage(error) {
662
690
  return error instanceof Error ? error.message : String(error);
663
691
  }
664
- function kebabToTitle(str) {
665
- return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
666
- }
667
692
  function findOpenAPIRoutes(dir, base = dir) {
668
693
  const routes = [];
669
694
  if (!(0, node_fs.existsSync)(dir)) return routes;
@@ -683,8 +708,7 @@ function findOpenAPIRoutes(dir, base = dir) {
683
708
  filePath: fullPath,
684
709
  virtualModuleId: virtualModuleIdFromRef(ref),
685
710
  title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
686
- kind: "openapi",
687
- content: (0, node_fs.readFileSync)(fullPath, "utf-8")
711
+ kind: "openapi"
688
712
  });
689
713
  }
690
714
  return routes;
@@ -693,6 +717,20 @@ function operationMatchesTags(operationTags, filterTags) {
693
717
  if (!filterTags?.length) return true;
694
718
  return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
695
719
  }
720
+ /** Filter an OpenAPI spec to only include paths that have at least one operation matching the given tags. */
721
+ function filterSpecByTags(spec, tags) {
722
+ const filteredPaths = {};
723
+ const paths = spec.paths ?? {};
724
+ for (const path of Object.keys(paths)) {
725
+ const pathItem = paths[path];
726
+ if (!pathItem) continue;
727
+ if (Object.values(pathItem).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredPaths[path] = pathItem;
728
+ }
729
+ return {
730
+ ...spec,
731
+ paths: filteredPaths
732
+ };
733
+ }
696
734
  /** Extract endpoint operations from an OpenAPI spec as page sections. */
697
735
  function extractOpenAPISections(spec, filterTags) {
698
736
  const sections = [];
@@ -734,12 +772,14 @@ async function readOpenAPISpec(filePath) {
734
772
  }
735
773
  //#endregion
736
774
  //#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";
775
+ var VIRTUAL_CONFIG = "virtual:clarify/config";
776
+ var VIRTUAL_ROUTES = "virtual:clarify/routes";
777
+ var VIRTUAL_SERVER_ROUTES = "virtual:clarify/routes/server";
778
+ var VIRTUAL_OPENAPI = "virtual:clarify/openapi";
779
+ var VIRTUAL_SLOTS = "virtual:clarify/slots";
780
+ var VIRTUAL_SLOT = "virtual:clarify/slot";
781
+ var VIRTUAL_CLIENT_ENTRY = "virtual:clarify/entry-client";
782
+ var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify/entry-client";
743
783
  function resolveVirtualId(id) {
744
784
  return "\0" + id;
745
785
  }
@@ -780,65 +820,117 @@ function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode =
780
820
  const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
781
821
  return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
782
822
  }
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
823
  function createClientEntryModule(options = {}) {
791
824
  return `
792
825
  import '@clarify-labs/renderer/style.css';
793
826
  import { render } from '@clarify-labs/renderer/client';
794
827
  import { routes, navigation } from '${VIRTUAL_ROUTES}';
795
828
  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)} });`;
829
+ import { openApis } from '${VIRTUAL_OPENAPI}';
830
+ import { runtimeSlots } from '${VIRTUAL_SLOTS}';
831
+ render({ config, routes, navigation, openApis, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
832
+ }
833
+ /**
834
+ * Collect every plugin's `slots` declarations into a single runtime registry
835
+ * module. Component modules are exported as lazy import factories so the
836
+ * renderer can decide whether to code-split (client) or pre-resolve (SSR).
837
+ *
838
+ * Relative component paths are resolved against the project `root` so that
839
+ * the generated `import()` call uses an absolute specifier — Vite cannot
840
+ * resolve relative imports from virtual modules.
841
+ *
842
+ * Slot name validity is enforced at compile time by the `UISlotName`
843
+ * type — no runtime validation is needed.
844
+ */
845
+ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
846
+ const registrations = [];
847
+ for (const plugin of plugins) for (const slot of plugin.slots ?? []) registrations.push({
848
+ plugin: plugin.name,
849
+ slot
850
+ });
851
+ if (registrations.length === 0) return "export const runtimeSlots = {};\n";
852
+ const grouped = /* @__PURE__ */ new Map();
853
+ for (const { plugin, slot } of registrations) {
854
+ if (slot.name.endsWith(".replace")) {
855
+ const existing = grouped.get(slot.name);
856
+ if (existing) console.warn(`[clarify] Multiple plugins register slot "${slot.name}": "${plugin}" overrides "${existing[existing.length - 1]}". Only the last registration is used.`);
857
+ }
858
+ 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.`);
859
+ const componentPath = root + slot.component;
860
+ const entry = `{ plugin: ${JSON.stringify(plugin)}, component: () => import(${moduleSpecifier(componentPath)}) }`;
861
+ const list = grouped.get(slot.name) ?? [];
862
+ list.push(entry);
863
+ grouped.set(slot.name, list);
864
+ }
865
+ return `export const runtimeSlots = {\n${[...grouped.entries()].map(([name, list]) => ` ${JSON.stringify(name)}: [\n ${list.join(",\n ")}\n ]`).join(",\n")}\n};\n`;
866
+ }
867
+ /**
868
+ * Re-exports the slot context hook from the renderer so plugin components can
869
+ * `import { useSlot } from 'virtual:clarify/slot'` without depending on
870
+ * renderer internals directly.
871
+ */
872
+ function createSlotModule() {
873
+ return `export { useSlot } from '@clarify-labs/renderer';\n`;
798
874
  }
799
875
  function buildVirtualModules(args) {
800
876
  const modules = /* @__PURE__ */ new Map();
801
877
  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;
878
+ const allPlugins = [...args.plugins ?? []];
808
879
  modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
809
880
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
810
881
  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
- }));
882
+ modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
883
+ modules.set(VIRTUAL_OPENAPI, generateOpenApiModule());
884
+ modules.set(VIRTUAL_SLOT, createSlotModule());
819
885
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
820
886
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
821
887
  for (const route of args.routes) modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
822
888
  return modules;
823
889
  }
890
+ function generateOpenApiModule() {
891
+ return `export const openApis = {};`;
892
+ }
824
893
  //#endregion
825
894
  //#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
- }`;
895
+ var openApiRegistryModuleId = VIRTUAL_OPENAPI;
896
+ var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
897
+ function specVirtualModuleId(specKey) {
898
+ return `${OPENAPI_SPEC_PREFIX}${specKey}`;
899
+ }
900
+ function generateOpenAPIRegistryModule(openApis) {
901
+ return `export const openApis = ${JSON.stringify(openApis)};`;
902
+ }
903
+ /** Generate a virtual module that exports a single spec as default. */
904
+ function generateOpenAPISpecModule(spec) {
905
+ return `export default ${JSON.stringify(spec)};`;
906
+ }
907
+ function generateOpenAPIPageModule(opts) {
908
+ const { specKey, specRegistryKey, tagFilter } = opts;
909
+ return [
910
+ `import { createElement, useState, useEffect, useRef } from 'react';`,
911
+ `import { OpenApiDocument, useOpenApis } from '@clarify-labs/renderer';`,
912
+ `const SPEC_KEY = ${JSON.stringify(specRegistryKey)};`,
913
+ `const TAG_FILTER = ${JSON.stringify(tagFilter ?? void 0)};`,
914
+ `let loadPromise = null;`,
915
+ `function loadSpec() {`,
916
+ ` if (!loadPromise) loadPromise = import(${JSON.stringify(specVirtualModuleId(specKey))}).then(function(m) { return m.default; });`,
917
+ ` return loadPromise;`,
918
+ `}`,
919
+ `export default function OpenApiRoutePage() {`,
920
+ ` const specs = useOpenApis();`,
921
+ ` const serverSpec = specs[SPEC_KEY];`,
922
+ ` const [spec, setSpec] = useState(serverSpec || null);`,
923
+ ` const mountedRef = useRef(false);`,
924
+ ` useEffect(function() {`,
925
+ ` if (mountedRef.current) return;`,
926
+ ` mountedRef.current = true;`,
927
+ ` if (spec) return;`,
928
+ ` loadSpec().then(setSpec);`,
929
+ ` }, []);`,
930
+ ` if (!spec) return null;`,
931
+ ` return createElement(OpenApiDocument, { spec: spec, tagFilter: TAG_FILTER });`,
932
+ `}`
933
+ ].join("\n");
842
934
  }
843
935
  function generateOpenAPIErrorModule(diagnostic) {
844
936
  return `import { createElement } from 'react';
@@ -872,18 +964,6 @@ function collectOpenAPIPageItems(config) {
872
964
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
873
965
  return items;
874
966
  }
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
967
  function virtualModuleIdFromPath(path) {
888
968
  return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
889
969
  }
@@ -913,10 +993,15 @@ function createConfiguredOpenAPIRoutes(routes, config) {
913
993
  }
914
994
  }
915
995
  const nextRoutes = [...routes, ...additions];
916
- return config.i18n ? nextRoutes.map((route) => withAlternates$1(route, nextRoutes, config.i18n)) : nextRoutes;
996
+ return config.i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n)) : nextRoutes;
997
+ }
998
+ /** Derive a stable, deduplicated key from an absolute spec file path. */
999
+ function specFileKeyFromPath(filePath, projectRoot) {
1000
+ return (0, node_path.relative)(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
917
1001
  }
918
1002
  function createOpenAPIPlugin() {
919
- const specs = {};
1003
+ /** specs keyed by specFileKey (deduplicated per source file). */
1004
+ const specs = /* @__PURE__ */ new Map();
920
1005
  return {
921
1006
  name: "clarify:openapi",
922
1007
  hooks: {
@@ -925,7 +1010,7 @@ function createOpenAPIPlugin() {
925
1010
  routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
926
1011
  }),
927
1012
  "routes:discovered": async (routes, ctx) => {
928
- for (const key of Object.keys(specs)) delete specs[key];
1013
+ specs.clear();
929
1014
  const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
930
1015
  const specByFilePath = /* @__PURE__ */ new Map();
931
1016
  for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
@@ -942,24 +1027,45 @@ function createOpenAPIPlugin() {
942
1027
  }
943
1028
  const spec = result.spec;
944
1029
  specByFilePath.set(route.filePath, spec);
945
- specs[route.virtualModuleId] = {
946
- spec,
947
- tagFilter: route.openapiTagFilter
948
- };
949
- specs[`virtual:clarify-page/${route.path.replace(/^\//, "")}`] = {
1030
+ const specKey = specFileKeyFromPath(route.filePath, ctx.generateOptions.projectRoot);
1031
+ route.specFileKey = specKey;
1032
+ specs.set(specKey, {
950
1033
  spec,
951
- tagFilter: route.openapiTagFilter
952
- };
1034
+ filePath: route.filePath
1035
+ });
953
1036
  route.title = spec.info?.title ?? route.title;
954
1037
  route.description = spec.info?.description ?? route.description;
955
1038
  route.sections = extractOpenAPISections(spec, route.openapiTagFilter);
1039
+ const pageSpec = route.openapiTagFilter?.length ? filterSpecByTags(spec, route.openapiTagFilter) : spec;
1040
+ route.content = JSON.stringify(pageSpec);
956
1041
  }
957
1042
  return nextRoutes;
958
1043
  },
959
1044
  "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));
1045
+ const registryEntries = {};
1046
+ for (const route of ctx.routes) {
1047
+ if (route.kind !== "openapi" || !route.specFileKey) continue;
1048
+ const spec = specs.get(route.specFileKey)?.spec;
1049
+ if (!spec) continue;
1050
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
1051
+ const registryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
1052
+ registryEntries[registryKey] = spec;
1053
+ }
1054
+ modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
1055
+ for (const [specKey, entry] of specs) modules.set(specVirtualModuleId(specKey), generateOpenAPISpecModule(entry.spec));
1056
+ for (const route of ctx.routes) {
1057
+ if (route.kind !== "openapi") continue;
1058
+ if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
1059
+ else if (route.specFileKey) {
1060
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
1061
+ const specRegistryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
1062
+ modules.set(route.virtualModuleId, generateOpenAPIPageModule({
1063
+ specKey: route.specFileKey,
1064
+ specRegistryKey,
1065
+ tagFilter: route.openapiTagFilter
1066
+ }));
1067
+ }
1068
+ }
963
1069
  return modules;
964
1070
  }
965
1071
  }
@@ -1177,22 +1283,22 @@ function createRobotsTxt(config) {
1177
1283
  ""
1178
1284
  ].join("\n");
1179
1285
  }
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
1286
  //#endregion
1188
1287
  //#region source/plugins/seo/index.ts
1189
1288
  function createSeoPlugin() {
1190
1289
  return {
1191
1290
  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);
1291
+ hooks: { "build:assets"(ctx) {
1292
+ const sitemap = createSitemapXml(ctx.routes, ctx.projectConfig);
1293
+ const robots = createRobotsTxt(ctx.projectConfig);
1294
+ if (!sitemap || !robots) return [];
1295
+ return [{
1296
+ fileName: "sitemap.xml",
1297
+ source: sitemap
1298
+ }, {
1299
+ fileName: "robots.txt",
1300
+ source: robots
1301
+ }];
1196
1302
  } }
1197
1303
  };
1198
1304
  }
@@ -1290,18 +1396,16 @@ var clarifyNavbarLinkSchema = zod.z.object({
1290
1396
  href: zod.z.string(),
1291
1397
  external: zod.z.boolean().optional()
1292
1398
  });
1293
- var clarifyBannerConfigOptionsSchema = zod.z.object({
1399
+ var clarifyBannerConfigSchema = zod.z.object({
1294
1400
  content: clarifyLocalizedTextSchema,
1295
1401
  dismissible: zod.z.boolean().optional(),
1296
1402
  link: clarifyNavbarLinkSchema.optional()
1297
1403
  }).strict();
1298
- var clarifyBannerConfigSchema = zod.z.union([clarifyBannerConfigOptionsSchema, zod.z.string()]);
1299
- var clarifyFooterLinksConfigSchema = zod.z.object({
1404
+ var clarifyFooterConfigSchema = zod.z.object({
1300
1405
  links: zod.z.array(clarifyNavbarLinkSchema).optional(),
1301
1406
  socials: zod.z.record(zod.z.string(), zod.z.string()).optional(),
1302
1407
  copyright: clarifyLocalizedTextSchema.optional()
1303
1408
  }).strict();
1304
- var clarifyFooterConfigSchema = zod.z.union([clarifyFooterLinksConfigSchema, zod.z.string()]);
1305
1409
  var clarifySourceConfigSchema = zod.z.object({
1306
1410
  repository: zod.z.string(),
1307
1411
  branch: zod.z.string().optional(),
@@ -1460,11 +1564,59 @@ function resolveProjectConfig(config = {}) {
1460
1564
  };
1461
1565
  }
1462
1566
  //#endregion
1567
+ //#region source/core/env-types.ts
1568
+ var GENERATED_FILE_NAME = "clarify-env.d.ts";
1569
+ /**
1570
+ * Template for the generated `clarify-env.d.ts` file.
1571
+ * Edit this template directly if you need to change the type declarations.
1572
+ * This avoids the complexity of reading external files at runtime after bundling.
1573
+ */
1574
+ var CLARIFY_ENV_TEMPLATE = `// This file is generated by Clarify. Do not edit it manually.
1575
+ // It provides ambient type declarations for Clarify virtual modules used in
1576
+ // plugin and slot code, so the TypeScript language service can resolve them.
1577
+
1578
+ declare module 'virtual:clarify/slot' {
1579
+ import type { SlotContext } from '@clarify-labs/cli'
1580
+
1581
+ export function useSlot(): SlotContext
1582
+ }
1583
+
1584
+ declare module 'virtual:clarify/slots' {
1585
+ import type { ComponentType } from 'react'
1586
+
1587
+ export type RuntimeSlotEntry = {
1588
+ plugin: string
1589
+ /** Lazy import factory — yields the default-exported React component. */
1590
+ component: () => Promise<{ default: ComponentType }>
1591
+ }
1592
+
1593
+ export const runtimeSlots: Record<string, RuntimeSlotEntry[]>
1594
+ }
1595
+ `;
1596
+ function generateClarifyEnvDts(_plugins) {
1597
+ return CLARIFY_ENV_TEMPLATE;
1598
+ }
1599
+ /**
1600
+ * Writes `clarify-env.d.ts` into the project root. Only rewrites when content
1601
+ * changes to avoid triggering unnecessary file watchers, and never overwrites a
1602
+ * user-authored file that does not carry the generated marker.
1603
+ */
1604
+ function writeClarifyEnvDts(projectRoot, plugins) {
1605
+ const filePath = (0, node_path.join)(projectRoot, GENERATED_FILE_NAME);
1606
+ const next = generateClarifyEnvDts(plugins);
1607
+ if ((0, node_fs.existsSync)(filePath)) {
1608
+ const current = (0, node_fs.readFileSync)(filePath, "utf-8");
1609
+ if (current === next) return;
1610
+ if (!current.startsWith("// This file is generated by Clarify.")) return;
1611
+ }
1612
+ (0, node_fs.writeFileSync)(filePath, next, "utf-8");
1613
+ }
1614
+ //#endregion
1463
1615
  //#region source/core/hooks.ts
1464
1616
  async function runHooks(plugins, hookName, input, ctx) {
1465
1617
  let result = input;
1466
1618
  for (const plugin of plugins) {
1467
- const hook = plugin.hooks[hookName];
1619
+ const hook = plugin.hooks?.[hookName];
1468
1620
  if (!hook) continue;
1469
1621
  try {
1470
1622
  result = await hook(result, ctx);
@@ -1476,7 +1628,7 @@ async function runHooks(plugins, hookName, input, ctx) {
1476
1628
  }
1477
1629
  async function runDevConfigureServerHooks(plugins, server, ctx) {
1478
1630
  for (const plugin of plugins) {
1479
- const hook = plugin.hooks["dev:configureServer"];
1631
+ const hook = plugin.hooks?.["dev:configureServer"];
1480
1632
  if (!hook) continue;
1481
1633
  try {
1482
1634
  await hook(server, ctx);
@@ -1485,9 +1637,23 @@ async function runDevConfigureServerHooks(plugins, server, ctx) {
1485
1637
  }
1486
1638
  }
1487
1639
  }
1640
+ async function runBuildAssetsHooks(plugins, ctx) {
1641
+ const assets = [];
1642
+ for (const plugin of plugins) {
1643
+ const hook = plugin.hooks?.["build:assets"];
1644
+ if (!hook) continue;
1645
+ try {
1646
+ const pluginAssets = await hook(ctx);
1647
+ assets.push(...pluginAssets);
1648
+ } catch (err) {
1649
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
1650
+ }
1651
+ }
1652
+ return assets;
1653
+ }
1488
1654
  async function runBuildDoneHooks(plugins, ctx) {
1489
1655
  for (const plugin of plugins) {
1490
- const hook = plugin.hooks["build:done"];
1656
+ const hook = plugin.hooks?.["build:done"];
1491
1657
  if (!hook) continue;
1492
1658
  try {
1493
1659
  await hook(ctx);
@@ -1508,18 +1674,6 @@ function resolveBuildOptions(options = {}) {
1508
1674
  }
1509
1675
  //#endregion
1510
1676
  //#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
1677
  async function discoverRoutesForRoot(routeRoot, locale, plugins, ctx) {
1524
1678
  return (await runHooks(plugins, "routes:discover", {
1525
1679
  contentRoot: routeRoot,
@@ -1561,7 +1715,21 @@ async function discoverRoutes(root, contentRoot, plugins, ctx) {
1561
1715
  }
1562
1716
  }
1563
1717
  }
1564
- return localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
1718
+ const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
1719
+ const defaultLocale = i18n.defaultLocale;
1720
+ const bareRoutes = [];
1721
+ const seenBare = new Set(routesWithAlternates.map((r) => r.path));
1722
+ for (const route of routesWithAlternates) {
1723
+ if (route.locale !== defaultLocale) continue;
1724
+ const bp = route.basePath ?? route.path;
1725
+ if (bp === route.path || seenBare.has(bp)) continue;
1726
+ seenBare.add(bp);
1727
+ bareRoutes.push({
1728
+ ...route,
1729
+ path: bp
1730
+ });
1731
+ }
1732
+ return [...routesWithAlternates, ...bareRoutes];
1565
1733
  }
1566
1734
  async function resolveClarifySite(options = {}, resolveOptions = {}) {
1567
1735
  const root = options.projectRoot ?? process.cwd();
@@ -1637,13 +1805,21 @@ function createClarifyRuntimeAliases() {
1637
1805
  find: /^@clarify-labs\/renderer$/,
1638
1806
  replacement: resolvePackageExport("@clarify-labs/renderer", "")
1639
1807
  },
1808
+ {
1809
+ find: /^react\/jsx-dev-runtime$/,
1810
+ replacement: resolveCliDependency("react/jsx-dev-runtime")
1811
+ },
1640
1812
  {
1641
1813
  find: /^react\/jsx-runtime$/,
1642
1814
  replacement: resolveCliDependency("react/jsx-runtime")
1643
1815
  },
1644
1816
  {
1645
- find: /^react\/jsx-dev-runtime$/,
1646
- replacement: resolveCliDependency("react/jsx-dev-runtime")
1817
+ find: /^mermaid$/,
1818
+ replacement: resolveCliDependency("mermaid")
1819
+ },
1820
+ {
1821
+ find: /^react-zoom-pan-pinch$/,
1822
+ replacement: resolveCliDependency("react-zoom-pan-pinch")
1647
1823
  },
1648
1824
  {
1649
1825
  find: /^react$/,
@@ -1685,11 +1861,24 @@ function setNamedMeta(html, name, content) {
1685
1861
  if (metaPattern.test(html)) return html.replace(metaPattern, meta);
1686
1862
  return html.replace("</head>", ` ${meta}\n </head>`);
1687
1863
  }
1864
+ function canonicalUrl(projectConfig, route) {
1865
+ if (!projectConfig.siteUrl || !route.alternates || !route.locale) return void 0;
1866
+ const prefixedPath = route.alternates[route.locale];
1867
+ if (!prefixedPath || prefixedPath === route.path) return void 0;
1868
+ return `${projectConfig.siteUrl.replace(/\/+$/, "")}${!projectConfig.routePrefix || projectConfig.routePrefix === "/" ? "" : `/${projectConfig.routePrefix.replace(/^\/+|\/+$/g, "")}`}${prefixedPath}`;
1869
+ }
1870
+ function injectCanonicalUrl(html, projectConfig, route) {
1871
+ const url = route ? canonicalUrl(projectConfig, route) : void 0;
1872
+ html = html.replace(/<link\b[^>]*\brel=["']canonical["'][^>]*\/?>\n?/gi, "");
1873
+ if (!url) return html;
1874
+ return html.replace("</head>", ` <link rel="canonical" href="${escapeHtml(url)}" />\n </head>`);
1875
+ }
1688
1876
  function injectSSRIntoTemplate(template, appHtml, projectConfig, route) {
1689
1877
  let html = injectHtmlLocaleAttributes(template, projectConfig, route);
1690
1878
  html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(routeTitle(projectConfig, route))}</title>`);
1691
1879
  html = setNamedMeta(html, "description", route?.description ?? projectConfig.description);
1692
1880
  html = setNamedMeta(html, "keywords", route?.keywords?.join(", "));
1881
+ html = injectCanonicalUrl(html, projectConfig, route);
1693
1882
  html = html.replace(/<div id="root">[\s\S]*?<\/div>/, `<div id="root">${appHtml}</div>`);
1694
1883
  return html;
1695
1884
  }
@@ -1702,12 +1891,20 @@ function routeOutputFiles(outputDirectory, route) {
1702
1891
  return files;
1703
1892
  }
1704
1893
  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';
1894
+ import { routes, navigation } from 'virtual:clarify/routes/server';
1895
+ import { config } from 'virtual:clarify/config';
1896
+ import { openApis } from 'virtual:clarify/openapi';
1897
+ import { runtimeSlots } from 'virtual:clarify/slots';
1708
1898
 
1709
- export function render(url) {
1710
- return renderToHTML({ config, routes, navigation, openApis, bannerComponent, footerComponent, url, themeEditor: config.theme.editor });
1899
+ export async function render(url) {
1900
+ // Pre-resolve every slot component factory so renderToString can use
1901
+ // them synchronously (renderToString has no Suspense support).
1902
+ const entries = Object.values(runtimeSlots).flat()
1903
+ await Promise.all(entries.map(async (entry) => {
1904
+ const mod = await entry.component()
1905
+ entry._resolved = mod.default
1906
+ }))
1907
+ return renderToHTML({ config, routes, navigation, openApis, runtimeSlots, url, themeEditor: config.theme.editor });
1711
1908
  }`;
1712
1909
  function createTempEntryFile(content) {
1713
1910
  const entryPath = (0, node_path.join)(createClarifyTempDir("ssr"), "entry-server.ts");
@@ -1737,7 +1934,7 @@ async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundle
1737
1934
  const template = readIndexHtml(outputDirectory);
1738
1935
  if (!template) throw new Error(`[clarify] index.html not found in outputDirectory "${outputDirectory}". Make sure Vite build produces it.`);
1739
1936
  for (const route of routes) try {
1740
- const finalHtml = injectSSRIntoTemplate(template, render(route.path), projectConfig, route);
1937
+ const finalHtml = injectSSRIntoTemplate(template, await render(route.path), projectConfig, route);
1741
1938
  for (const outFile of routeOutputFiles(outputDirectory, route)) {
1742
1939
  (0, node_fs.mkdirSync)((0, node_path.dirname)(outFile), { recursive: true });
1743
1940
  (0, node_fs.writeFileSync)(outFile, finalHtml, "utf-8");
@@ -1806,23 +2003,8 @@ function clarifyPlugin(options = {}) {
1806
2003
  let viteConfig;
1807
2004
  let resolvedNavigation = [];
1808
2005
  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);
2006
+ async function resolveRoutesAndSpecs(overrides) {
2007
+ const site = await resolveClarifySite(overrides ?? options);
1826
2008
  projectConfig = site.projectConfig;
1827
2009
  generateOptions = site.generateOptions;
1828
2010
  routes = site.routes;
@@ -1839,6 +2021,7 @@ function clarifyPlugin(options = {}) {
1839
2021
  generateOptions,
1840
2022
  routes,
1841
2023
  navigation: resolvedNavigation,
2024
+ plugins: clarifyPlugins,
1842
2025
  themeEditor: viteConfig.command === "serve" || projectConfig.theme.editor
1843
2026
  });
1844
2027
  virtualModules = await runHooks(clarifyPlugins, "modules:before", virtualModules, ctx);
@@ -1901,12 +2084,23 @@ function clarifyPlugin(options = {}) {
1901
2084
  },
1902
2085
  async buildStart() {
1903
2086
  await rebuildVirtualModules();
2087
+ writeClarifyEnvDts(root, clarifyPlugins);
1904
2088
  },
1905
2089
  async handleHotUpdate(ctx) {
1906
2090
  const changedFile = (0, node_path.isAbsolute)(ctx.file) ? ctx.file : (0, node_path.join)(root, ctx.file);
1907
2091
  if (configFilePath && changedFile === configFilePath) {
1908
- await reloadProjectConfig();
1909
- await refreshDevServer(ctx.server);
2092
+ await resolveRoutesAndSpecs({
2093
+ ...await loadClarifyConfig(root, {
2094
+ command: "serve",
2095
+ mode: viteConfig.mode
2096
+ }),
2097
+ projectRoot: options.projectRoot,
2098
+ rootDirectory: options.rootDirectory,
2099
+ outputDirectory: options.outputDirectory
2100
+ });
2101
+ await rebuildVirtualModules();
2102
+ invalidateVirtualModules(ctx.server);
2103
+ ctx.server.ws.send({ type: "full-reload" });
1910
2104
  return [];
1911
2105
  }
1912
2106
  const relativeContentFile = (0, node_path.relative)(contentRoot, changedFile);
@@ -1915,10 +2109,13 @@ function clarifyPlugin(options = {}) {
1915
2109
  return [];
1916
2110
  },
1917
2111
  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);
2112
+ if (id === "virtual:clarify/entry-client" || id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2113
+ if (id === "virtual:clarify/config" || id === resolveVirtualId("virtual:clarify/config")) return resolveVirtualId(VIRTUAL_CONFIG);
2114
+ if (id === "virtual:clarify/routes" || id === resolveVirtualId("virtual:clarify/routes")) return resolveVirtualId(VIRTUAL_ROUTES);
2115
+ if (id === "virtual:clarify/routes/server" || id === resolveVirtualId("virtual:clarify/routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
2116
+ if (id === "virtual:clarify/openapi" || id === resolveVirtualId("virtual:clarify/openapi")) return resolveVirtualId(VIRTUAL_OPENAPI);
2117
+ if (id === "virtual:clarify/slots" || id === resolveVirtualId("virtual:clarify/slots")) return resolveVirtualId(VIRTUAL_SLOTS);
2118
+ if (id === "virtual:clarify/slot" || id === resolveVirtualId("virtual:clarify/slot")) return resolveVirtualId(VIRTUAL_SLOT);
1922
2119
  const moduleId = stripVirtualPrefix(id);
1923
2120
  if (virtualModules.has(moduleId)) return resolveVirtualId(moduleId);
1924
2121
  const route = routes.find((r) => r.virtualModuleId === id || r.virtualModuleId === moduleId);
@@ -1956,6 +2153,14 @@ function clarifyPlugin(options = {}) {
1956
2153
  };
1957
2154
  }
1958
2155
  },
2156
+ async generateBundle(_opts, _bundle) {
2157
+ const assets = await runBuildAssetsHooks(clarifyPlugins, ctx);
2158
+ for (const asset of assets) this.emitFile({
2159
+ type: "asset",
2160
+ fileName: asset.fileName,
2161
+ source: asset.source
2162
+ });
2163
+ },
1959
2164
  async closeBundle() {
1960
2165
  if (process.env.SKIP_CLARIFY_SSG) {
1961
2166
  await runBuildDoneHooks(clarifyPlugins, ctx);
@@ -1970,11 +2175,14 @@ function clarifyPlugin(options = {}) {
1970
2175
  {
1971
2176
  name: "clarify:virtual-ssg",
1972
2177
  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;
2178
+ if (id === "virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2179
+ if (id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2180
+ if (id === "virtual:clarify/routes/server") return id;
2181
+ if (id === "virtual:clarify/openapi") return id;
2182
+ if (id === "virtual:clarify/config") return id;
2183
+ if (id === "virtual:clarify/routes") return id;
2184
+ if (id === "virtual:clarify/slots") return id;
2185
+ if (id === "virtual:clarify/slot") return id;
1978
2186
  if (virtualModules.has(stripVirtualPrefix(id))) return stripVirtualPrefix(id);
1979
2187
  if (routes.find((r) => r.virtualModuleId === id)) return id;
1980
2188
  return null;
@@ -2038,4 +2246,4 @@ Object.defineProperty(exports, "resolveClarifySite", {
2038
2246
  }
2039
2247
  });
2040
2248
 
2041
- //# sourceMappingURL=plugin-DFpkIm9j.cjs.map
2249
+ //# sourceMappingURL=plugin-B7c0iHMy.cjs.map