@clarify-labs/cli 0.7.2 → 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.
@@ -8,11 +8,11 @@ import { toString } from "mdast-util-to-string";
8
8
  import shiki from "shiki";
9
9
  import { visit } from "unist-util-visit";
10
10
  import { markdownRemarkPlugins, themeBootstrapScript, themePresets } from "@clarify-labs/renderer";
11
+ import { stringify } from "yaml";
11
12
  import { remark } from "remark";
12
13
  import matter from "gray-matter";
13
14
  import SwaggerParser from "@apidevtools/swagger-parser";
14
15
  import { tmpdir } from "node:os";
15
- import { mkdir, writeFile } from "node:fs/promises";
16
16
  import { z } from "zod";
17
17
  import { pathToFileURL } from "node:url";
18
18
  import { build, loadConfigFromFile } from "vite";
@@ -121,17 +121,16 @@ function withUtf8Signature(content) {
121
121
  }
122
122
  function shouldUseUtf8Signature(route) {
123
123
  if (route.kind === "mdx") return true;
124
- return /\.ya?ml$/i.test(route.contentArtifactUrl ?? "");
124
+ return false;
125
125
  }
126
126
  function routeToMarkdownArtifactUrl(routePath) {
127
127
  return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.md`;
128
128
  }
129
- function routeToOpenAPIArtifactUrl(routePath, filePath) {
130
- const extension = extname(filePath).toLowerCase() || ".json";
131
- return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi${extension}`;
129
+ function routeToOpenAPIArtifactUrl(routePath) {
130
+ return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.openapi.json`;
132
131
  }
133
132
  function attachContentArtifactUrls(routes) {
134
- for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path, route.filePath) : routeToMarkdownArtifactUrl(route.path);
133
+ for (const route of routes) route.contentArtifactUrl = route.kind === "openapi" ? routeToOpenAPIArtifactUrl(route.path) : routeToMarkdownArtifactUrl(route.path);
135
134
  }
136
135
  function readRouteContent(route) {
137
136
  if (route.content !== void 0) return route.content;
@@ -141,14 +140,6 @@ function readRouteArtifactContent(route) {
141
140
  const content = readRouteContent(route);
142
141
  return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
143
142
  }
144
- function writeContentArtifactFiles(routes, outputDirectory) {
145
- for (const route of routes) {
146
- if (!route.contentArtifactUrl) continue;
147
- const outFile = join(outputDirectory, route.contentArtifactUrl.replace(/^\//, ""));
148
- mkdirSync(dirname(outFile), { recursive: true });
149
- writeFileSync(outFile, readRouteArtifactContent(route), "utf-8");
150
- }
151
- }
152
143
  function createLlmsTxt(routes, projectConfig) {
153
144
  const basePath = normalizeBasePath(projectConfig.routePrefix);
154
145
  const lines = [`# ${projectConfig.title}`, ""];
@@ -171,10 +162,6 @@ function createLlmsTxt(routes, projectConfig) {
171
162
  function createLlmsTxtArtifact(routes, projectConfig) {
172
163
  return withUtf8Signature(createLlmsTxt(routes, projectConfig));
173
164
  }
174
- function writeLlmsTxt(routes, projectConfig, outputDirectory) {
175
- mkdirSync(outputDirectory, { recursive: true });
176
- writeFileSync(join(outputDirectory, "llms.txt"), createLlmsTxtArtifact(routes, projectConfig), "utf-8");
177
- }
178
165
  //#endregion
179
166
  //#region source/plugins/content-artifacts/server.ts
180
167
  function normalizeRoutePrefix$1(routePrefix) {
@@ -187,7 +174,6 @@ function resolveContentArtifactPath(url, projectConfig) {
187
174
  return basePath && requestPath.startsWith(basePath) ? requestPath.slice(basePath.length) || "/" : requestPath;
188
175
  }
189
176
  function resolveContentArtifactType(route) {
190
- if (route.kind === "openapi" && /\.ya?ml$/i.test(route.contentArtifactUrl ?? "")) return "text/yaml; charset=utf-8";
191
177
  return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
192
178
  }
193
179
  function serveContentArtifacts(req, res, projectConfig, routes) {
@@ -198,6 +184,18 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
198
184
  res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
199
185
  return true;
200
186
  }
187
+ if (contentPath.endsWith(".openapi.yaml")) {
188
+ const jsonPath = contentPath.replace(/\.yaml$/, ".json");
189
+ const route = routes.find((r) => r.contentArtifactUrl === jsonPath);
190
+ if (route?.kind === "openapi" && route.content) {
191
+ const spec = JSON.parse(route.content);
192
+ res.statusCode = 200;
193
+ res.setHeader("Content-Type", "text/yaml; charset=utf-8");
194
+ res.end(stringify(spec, { lineWidth: 0 }), "utf8");
195
+ return true;
196
+ }
197
+ return false;
198
+ }
201
199
  const route = routes.find((route) => route.contentArtifactUrl === contentPath);
202
200
  if (!route) return false;
203
201
  res.statusCode = 200;
@@ -221,12 +219,28 @@ function createContentArtifactsPlugin() {
221
219
  next();
222
220
  });
223
221
  },
224
- "build:done": (ctx) => {
225
- const outputDirectory = ctx.generateOptions.outputDirectory;
226
- if (!outputDirectory) return;
227
- const outputRoot = join(ctx.generateOptions.projectRoot, outputDirectory);
228
- writeContentArtifactFiles(ctx.routes, outputRoot);
229
- writeLlmsTxt(ctx.routes, ctx.projectConfig, outputRoot);
222
+ "build:assets": (ctx) => {
223
+ const assets = [];
224
+ for (const route of ctx.routes) {
225
+ if (!route.contentArtifactUrl) continue;
226
+ assets.push({
227
+ fileName: route.contentArtifactUrl.replace(/^\//, ""),
228
+ source: readRouteArtifactContent(route)
229
+ });
230
+ if (route.kind === "openapi" && route.content) {
231
+ const yamlFileName = route.contentArtifactUrl.replace(/\.json$/, ".yaml");
232
+ const spec = JSON.parse(route.content);
233
+ assets.push({
234
+ fileName: yamlFileName.replace(/^\//, ""),
235
+ source: stringify(spec, { lineWidth: 0 })
236
+ });
237
+ }
238
+ }
239
+ assets.push({
240
+ fileName: "llms.txt",
241
+ source: createLlmsTxtArtifact(ctx.routes, ctx.projectConfig)
242
+ });
243
+ return assets;
230
244
  }
231
245
  }
232
246
  };
@@ -303,7 +317,7 @@ function parseFrontmatter(content) {
303
317
  }
304
318
  //#endregion
305
319
  //#region source/parsers/routes.ts
306
- function kebabToTitle$1(str) {
320
+ function kebabToTitle(str) {
307
321
  return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
308
322
  }
309
323
  function parseMdxTree(content) {
@@ -368,14 +382,14 @@ function routePathFromRef(ref, explicitPath) {
368
382
  function virtualModuleIdFromRef(ref) {
369
383
  return "virtual:clarify-page/" + ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
370
384
  }
371
- function localizedRoutePath(basePath, locale, i18n) {
385
+ function localizedRoutePath(basePath, locale, _i18n) {
372
386
  const normalizedBasePath = normalizePath(basePath);
373
- if (locale === i18n.defaultLocale) return normalizedBasePath;
374
387
  const prefix = normalizePath(locale);
375
388
  return normalizedBasePath === "/" ? prefix : normalizePath(`${prefix}${normalizedBasePath}`);
376
389
  }
377
390
  function resolveLocalizedText(text, locale, fallbackLocale) {
378
- if (typeof text === "string" || text === void 0) return text;
391
+ if (typeof text === "string") return text;
392
+ if (text === void 0) return void 0;
379
393
  return text[locale] ?? (fallbackLocale ? text[fallbackLocale] : void 0) ?? Object.values(text)[0];
380
394
  }
381
395
  function findContentRoutes(dir, base = dir) {
@@ -393,7 +407,7 @@ function findContentRoutes(dir, base = dir) {
393
407
  let title = typeof frontmatter.title === "string" ? frontmatter.title : "";
394
408
  if (!title) {
395
409
  const lastPart = pathParts[pathParts.length - 1] ?? "";
396
- title = kebabToTitle$1(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(content) : lastPart) || "Untitled";
410
+ title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(content) : lastPart) || "Untitled";
397
411
  }
398
412
  routes.push({
399
413
  path: cleanPath,
@@ -417,9 +431,9 @@ function frontmatterKeywords(frontmatter) {
417
431
  const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
418
432
  return keywords.length > 0 ? keywords : void 0;
419
433
  }
420
- function withAlternates$2(route, routes, i18n) {
434
+ function withAlternates(route, routes, i18n) {
421
435
  const basePath = route.basePath ?? route.path;
422
- const routeByLocaleAndBase = new Map(routes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
436
+ const routeByLocaleAndBase = new Map(routes.filter((r) => !r.locale || r.path === `/${r.locale}` || r.path.startsWith(`/${r.locale}/`)).map((r) => [`${r.locale ?? ""}:${r.basePath ?? r.path}`, r]));
423
437
  const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
424
438
  const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
425
439
  return alternate ? [[locale.code, alternate.path]] : [];
@@ -441,7 +455,7 @@ function buildNavigation(routes) {
441
455
  if (!node) {
442
456
  node = {
443
457
  path: pathSoFar,
444
- title: i === parts.length - 1 ? route.title : kebabToTitle$1(parts[i]),
458
+ title: i === parts.length - 1 ? route.title : kebabToTitle(parts[i]),
445
459
  children: []
446
460
  };
447
461
  current.push(node);
@@ -513,7 +527,21 @@ function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
513
527
  }
514
528
  }
515
529
  const nextRoutes = [...routes, ...additions];
516
- return i18n ? nextRoutes.map((route) => withAlternates$2(route, nextRoutes, i18n)) : nextRoutes;
530
+ const routesWithAlternates = i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, i18n)) : nextRoutes;
531
+ if (!i18n) return routesWithAlternates;
532
+ const bareRoutes = [];
533
+ const seenBare = new Set(routesWithAlternates.map((r) => r.path));
534
+ for (const route of routesWithAlternates) {
535
+ if (route.locale !== i18n.defaultLocale) continue;
536
+ const bp = route.basePath ?? route.path;
537
+ if (bp === route.path || seenBare.has(bp)) continue;
538
+ seenBare.add(bp);
539
+ bareRoutes.push({
540
+ ...route,
541
+ path: bp
542
+ });
543
+ }
544
+ return [...routesWithAlternates, ...bareRoutes];
517
545
  }
518
546
  function buildNavigationFromPagesConfig(routes, config) {
519
547
  if (!config || config === "FileTree") return buildNavigation(routes);
@@ -541,7 +569,7 @@ function buildNavigationFromConfig(routes, config) {
541
569
  const route = routeMap.get(path);
542
570
  return {
543
571
  path,
544
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle$1(path.split("/").pop() ?? openapiRef),
572
+ title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? openapiRef),
545
573
  icon,
546
574
  sections: route?.sections ? navigationSections(route.sections) : void 0
547
575
  };
@@ -551,7 +579,7 @@ function buildNavigationFromConfig(routes, config) {
551
579
  const route = routeMap.get(path);
552
580
  return {
553
581
  path: redirect ? normalizePath(redirect) : path,
554
- title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle$1(path.split("/").pop() ?? ref),
582
+ title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? ref),
555
583
  icon,
556
584
  sections: route?.sections ? navigationSections(route.sections) : void 0
557
585
  };
@@ -586,7 +614,7 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
586
614
  const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
587
615
  return {
588
616
  path: (redirect ? localizedRoutePath(routePathFromRef(redirect), locale, i18n) : void 0) ?? path,
589
- title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle$1(basePath.split("/").pop() ?? ref),
617
+ title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle(basePath.split("/").pop() ?? ref),
590
618
  icon,
591
619
  sections: route?.sections ? navigationSections(route.sections) : void 0
592
620
  };
@@ -632,9 +660,6 @@ var OPENAPI_HTTP_METHODS = [
632
660
  function errorMessage(error) {
633
661
  return error instanceof Error ? error.message : String(error);
634
662
  }
635
- function kebabToTitle(str) {
636
- return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
637
- }
638
663
  function findOpenAPIRoutes(dir, base = dir) {
639
664
  const routes = [];
640
665
  if (!existsSync(dir)) return routes;
@@ -654,8 +679,7 @@ function findOpenAPIRoutes(dir, base = dir) {
654
679
  filePath: fullPath,
655
680
  virtualModuleId: virtualModuleIdFromRef(ref),
656
681
  title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
657
- kind: "openapi",
658
- content: readFileSync(fullPath, "utf-8")
682
+ kind: "openapi"
659
683
  });
660
684
  }
661
685
  return routes;
@@ -664,6 +688,20 @@ function operationMatchesTags(operationTags, filterTags) {
664
688
  if (!filterTags?.length) return true;
665
689
  return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
666
690
  }
691
+ /** Filter an OpenAPI spec to only include paths that have at least one operation matching the given tags. */
692
+ function filterSpecByTags(spec, tags) {
693
+ const filteredPaths = {};
694
+ const paths = spec.paths ?? {};
695
+ for (const path of Object.keys(paths)) {
696
+ const pathItem = paths[path];
697
+ if (!pathItem) continue;
698
+ if (Object.values(pathItem).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredPaths[path] = pathItem;
699
+ }
700
+ return {
701
+ ...spec,
702
+ paths: filteredPaths
703
+ };
704
+ }
667
705
  /** Extract endpoint operations from an OpenAPI spec as page sections. */
668
706
  function extractOpenAPISections(spec, filterTags) {
669
707
  const sections = [];
@@ -705,12 +743,14 @@ async function readOpenAPISpec(filePath) {
705
743
  }
706
744
  //#endregion
707
745
  //#region source/core/virtual-modules.ts
708
- var VIRTUAL_CONFIG = "virtual:clarify-config";
709
- var VIRTUAL_ROUTES = "virtual:clarify-routes";
710
- var VIRTUAL_SERVER_ROUTES = "virtual:clarify-routes/server";
711
- var VIRTUAL_RUNTIME = "virtual:clarify-runtime";
712
- var VIRTUAL_CLIENT_ENTRY = "virtual:clarify-entry-client";
713
- var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify-entry-client";
746
+ var VIRTUAL_CONFIG = "virtual:clarify/config";
747
+ var VIRTUAL_ROUTES = "virtual:clarify/routes";
748
+ var VIRTUAL_SERVER_ROUTES = "virtual:clarify/routes/server";
749
+ var VIRTUAL_OPENAPI = "virtual:clarify/openapi";
750
+ var VIRTUAL_SLOTS = "virtual:clarify/slots";
751
+ var VIRTUAL_SLOT = "virtual:clarify/slot";
752
+ var VIRTUAL_CLIENT_ENTRY = "virtual:clarify/entry-client";
753
+ var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify/entry-client";
714
754
  function resolveVirtualId(id) {
715
755
  return "\0" + id;
716
756
  }
@@ -751,65 +791,117 @@ function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode =
751
791
  const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
752
792
  return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
753
793
  }
754
- function createRuntimeModule(options = {}) {
755
- const imports = [options.bannerComponentSource === "path" && options.imports?.bannerComponent ? `import BannerComponent from ${moduleSpecifier(options.imports.bannerComponent)};` : void 0, options.footerComponentSource === "path" && options.imports?.footerComponent ? `import FooterComponent from ${moduleSpecifier(options.imports.footerComponent)};` : void 0].filter(Boolean).join("\n");
756
- const exports = `export const openApis = {};
757
- export const bannerComponent = ${options.bannerComponentSource === "path" ? "BannerComponent" : "undefined"};
758
- export const footerComponent = ${options.footerComponentSource === "path" ? "FooterComponent" : "undefined"};`;
759
- return imports ? `${imports}\n${exports}` : exports;
760
- }
761
794
  function createClientEntryModule(options = {}) {
762
795
  return `
763
796
  import '@clarify-labs/renderer/style.css';
764
797
  import { render } from '@clarify-labs/renderer/client';
765
798
  import { routes, navigation } from '${VIRTUAL_ROUTES}';
766
799
  import { config } from '${VIRTUAL_CONFIG}';
767
- import { openApis, bannerComponent, footerComponent } from '${VIRTUAL_RUNTIME}';
768
- render({ config, routes, navigation, openApis, bannerComponent, footerComponent, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
800
+ import { openApis } from '${VIRTUAL_OPENAPI}';
801
+ import { runtimeSlots } from '${VIRTUAL_SLOTS}';
802
+ render({ config, routes, navigation, openApis, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
803
+ }
804
+ /**
805
+ * Collect every plugin's `slots` declarations into a single runtime registry
806
+ * module. Component modules are exported as lazy import factories so the
807
+ * renderer can decide whether to code-split (client) or pre-resolve (SSR).
808
+ *
809
+ * Relative component paths are resolved against the project `root` so that
810
+ * the generated `import()` call uses an absolute specifier — Vite cannot
811
+ * resolve relative imports from virtual modules.
812
+ *
813
+ * Slot name validity is enforced at compile time by the `UISlotName`
814
+ * type — no runtime validation is needed.
815
+ */
816
+ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
817
+ const registrations = [];
818
+ for (const plugin of plugins) for (const slot of plugin.slots ?? []) registrations.push({
819
+ plugin: plugin.name,
820
+ slot
821
+ });
822
+ if (registrations.length === 0) return "export const runtimeSlots = {};\n";
823
+ const grouped = /* @__PURE__ */ new Map();
824
+ for (const { plugin, slot } of registrations) {
825
+ if (slot.name.endsWith(".replace")) {
826
+ const existing = grouped.get(slot.name);
827
+ if (existing) console.warn(`[clarify] Multiple plugins register slot "${slot.name}": "${plugin}" overrides "${existing[existing.length - 1]}". Only the last registration is used.`);
828
+ }
829
+ 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.`);
830
+ const componentPath = root + slot.component;
831
+ const entry = `{ plugin: ${JSON.stringify(plugin)}, component: () => import(${moduleSpecifier(componentPath)}) }`;
832
+ const list = grouped.get(slot.name) ?? [];
833
+ list.push(entry);
834
+ grouped.set(slot.name, list);
835
+ }
836
+ return `export const runtimeSlots = {\n${[...grouped.entries()].map(([name, list]) => ` ${JSON.stringify(name)}: [\n ${list.join(",\n ")}\n ]`).join(",\n")}\n};\n`;
837
+ }
838
+ /**
839
+ * Re-exports the slot context hook from the renderer so plugin components can
840
+ * `import { useSlot } from 'virtual:clarify/slot'` without depending on
841
+ * renderer internals directly.
842
+ */
843
+ function createSlotModule() {
844
+ return `export { useSlot } from '@clarify-labs/renderer';\n`;
769
845
  }
770
846
  function buildVirtualModules(args) {
771
847
  const modules = /* @__PURE__ */ new Map();
772
848
  const clientEntryModule = createClientEntryModule({ themeEditor: args.themeEditor });
773
- const bannerComponent = args.projectConfig.banner;
774
- const bannerComponentSource = typeof bannerComponent === "string" ? "path" : void 0;
775
- const bannerComponentImport = typeof bannerComponent === "string" ? bannerComponent : void 0;
776
- const footerComponent = args.projectConfig.footer;
777
- const footerComponentSource = typeof footerComponent === "string" ? "path" : void 0;
778
- const footerComponentImport = typeof footerComponent === "string" ? footerComponent : void 0;
849
+ const allPlugins = [...args.plugins ?? []];
779
850
  modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
780
851
  modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
781
852
  modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "server"));
782
- modules.set(VIRTUAL_RUNTIME, createRuntimeModule({
783
- bannerComponentSource,
784
- footerComponentSource,
785
- imports: {
786
- bannerComponent: bannerComponentImport,
787
- footerComponent: footerComponentImport
788
- }
789
- }));
853
+ modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
854
+ modules.set(VIRTUAL_OPENAPI, generateOpenApiModule());
855
+ modules.set(VIRTUAL_SLOT, createSlotModule());
790
856
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
791
857
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
792
858
  for (const route of args.routes) modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
793
859
  return modules;
794
860
  }
861
+ function generateOpenApiModule() {
862
+ return `export const openApis = {};`;
863
+ }
795
864
  //#endregion
796
865
  //#region source/plugins/openapi/virtual-modules.ts
797
- var openApiRegistryModuleId = VIRTUAL_RUNTIME;
798
- function generateOpenAPIRegistryModule(openApis, runtimeModule = "") {
799
- const openApisExport = `export const openApis = ${JSON.stringify(openApis)};`;
800
- const openApisPlaceholder = /export const openApis = \{\};?/;
801
- if (!runtimeModule) return openApisExport;
802
- if (openApisPlaceholder.test(runtimeModule)) return runtimeModule.replace(openApisPlaceholder, openApisExport);
803
- return runtimeModule;
804
- }
805
- function generateOpenAPIModule(spec, tagFilter) {
806
- return `import { createElement } from 'react';
807
- import { OpenApiDocument } from '@clarify-labs/renderer';
808
- const spec = ${JSON.stringify(spec)};
809
- const tagFilter = ${JSON.stringify(tagFilter)};
810
- export default function OpenApiRoutePage() {
811
- return createElement(OpenApiDocument, { spec, tagFilter });
812
- }`;
866
+ var openApiRegistryModuleId = VIRTUAL_OPENAPI;
867
+ var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
868
+ function specVirtualModuleId(specKey) {
869
+ return `${OPENAPI_SPEC_PREFIX}${specKey}`;
870
+ }
871
+ function generateOpenAPIRegistryModule(openApis) {
872
+ return `export const openApis = ${JSON.stringify(openApis)};`;
873
+ }
874
+ /** Generate a virtual module that exports a single spec as default. */
875
+ function generateOpenAPISpecModule(spec) {
876
+ return `export default ${JSON.stringify(spec)};`;
877
+ }
878
+ function generateOpenAPIPageModule(opts) {
879
+ const { specKey, specRegistryKey, tagFilter } = opts;
880
+ return [
881
+ `import { createElement, useState, useEffect, useRef } from 'react';`,
882
+ `import { OpenApiDocument, useOpenApis } from '@clarify-labs/renderer';`,
883
+ `const SPEC_KEY = ${JSON.stringify(specRegistryKey)};`,
884
+ `const TAG_FILTER = ${JSON.stringify(tagFilter ?? void 0)};`,
885
+ `let loadPromise = null;`,
886
+ `function loadSpec() {`,
887
+ ` if (!loadPromise) loadPromise = import(${JSON.stringify(specVirtualModuleId(specKey))}).then(function(m) { return m.default; });`,
888
+ ` return loadPromise;`,
889
+ `}`,
890
+ `export default function OpenApiRoutePage() {`,
891
+ ` const specs = useOpenApis();`,
892
+ ` const serverSpec = specs[SPEC_KEY];`,
893
+ ` const [spec, setSpec] = useState(serverSpec || null);`,
894
+ ` const mountedRef = useRef(false);`,
895
+ ` useEffect(function() {`,
896
+ ` if (mountedRef.current) return;`,
897
+ ` mountedRef.current = true;`,
898
+ ` if (spec) return;`,
899
+ ` loadSpec().then(setSpec);`,
900
+ ` }, []);`,
901
+ ` if (!spec) return null;`,
902
+ ` return createElement(OpenApiDocument, { spec: spec, tagFilter: TAG_FILTER });`,
903
+ `}`
904
+ ].join("\n");
813
905
  }
814
906
  function generateOpenAPIErrorModule(diagnostic) {
815
907
  return `import { createElement } from 'react';
@@ -843,18 +935,6 @@ function collectOpenAPIPageItems(config) {
843
935
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
844
936
  return items;
845
937
  }
846
- function withAlternates$1(route, routes, i18n) {
847
- const basePath = route.basePath ?? route.path;
848
- const routeByLocaleAndBase = new Map(routes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
849
- const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
850
- const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
851
- return alternate ? [[locale.code, alternate.path]] : [];
852
- }));
853
- return {
854
- ...route,
855
- alternates
856
- };
857
- }
858
938
  function virtualModuleIdFromPath(path) {
859
939
  return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
860
940
  }
@@ -884,10 +964,15 @@ function createConfiguredOpenAPIRoutes(routes, config) {
884
964
  }
885
965
  }
886
966
  const nextRoutes = [...routes, ...additions];
887
- return config.i18n ? nextRoutes.map((route) => withAlternates$1(route, nextRoutes, config.i18n)) : nextRoutes;
967
+ return config.i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n)) : nextRoutes;
968
+ }
969
+ /** Derive a stable, deduplicated key from an absolute spec file path. */
970
+ function specFileKeyFromPath(filePath, projectRoot) {
971
+ return relative(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
888
972
  }
889
973
  function createOpenAPIPlugin() {
890
- const specs = {};
974
+ /** specs keyed by specFileKey (deduplicated per source file). */
975
+ const specs = /* @__PURE__ */ new Map();
891
976
  return {
892
977
  name: "clarify:openapi",
893
978
  hooks: {
@@ -896,7 +981,7 @@ function createOpenAPIPlugin() {
896
981
  routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
897
982
  }),
898
983
  "routes:discovered": async (routes, ctx) => {
899
- for (const key of Object.keys(specs)) delete specs[key];
984
+ specs.clear();
900
985
  const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
901
986
  const specByFilePath = /* @__PURE__ */ new Map();
902
987
  for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
@@ -913,24 +998,45 @@ function createOpenAPIPlugin() {
913
998
  }
914
999
  const spec = result.spec;
915
1000
  specByFilePath.set(route.filePath, spec);
916
- specs[route.virtualModuleId] = {
917
- spec,
918
- tagFilter: route.openapiTagFilter
919
- };
920
- specs[`virtual:clarify-page/${route.path.replace(/^\//, "")}`] = {
1001
+ const specKey = specFileKeyFromPath(route.filePath, ctx.generateOptions.projectRoot);
1002
+ route.specFileKey = specKey;
1003
+ specs.set(specKey, {
921
1004
  spec,
922
- tagFilter: route.openapiTagFilter
923
- };
1005
+ filePath: route.filePath
1006
+ });
924
1007
  route.title = spec.info?.title ?? route.title;
925
1008
  route.description = spec.info?.description ?? route.description;
926
1009
  route.sections = extractOpenAPISections(spec, route.openapiTagFilter);
1010
+ const pageSpec = route.openapiTagFilter?.length ? filterSpecByTags(spec, route.openapiTagFilter) : spec;
1011
+ route.content = JSON.stringify(pageSpec);
927
1012
  }
928
1013
  return nextRoutes;
929
1014
  },
930
1015
  "modules:before": (modules, ctx) => {
931
- modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(Object.fromEntries(Object.entries(specs).map(([moduleId, entry]) => [moduleId, entry.spec])), modules.get(openApiRegistryModuleId)));
932
- for (const [moduleId, entry] of Object.entries(specs)) modules.set(moduleId, generateOpenAPIModule(entry.spec, entry.tagFilter));
933
- for (const route of ctx.routes.filter((route) => route.kind === "openapi" && route.diagnostic)) if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
1016
+ const registryEntries = {};
1017
+ for (const route of ctx.routes) {
1018
+ if (route.kind !== "openapi" || !route.specFileKey) continue;
1019
+ const spec = specs.get(route.specFileKey)?.spec;
1020
+ if (!spec) continue;
1021
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
1022
+ const registryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
1023
+ registryEntries[registryKey] = spec;
1024
+ }
1025
+ modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
1026
+ for (const [specKey, entry] of specs) modules.set(specVirtualModuleId(specKey), generateOpenAPISpecModule(entry.spec));
1027
+ for (const route of ctx.routes) {
1028
+ if (route.kind !== "openapi") continue;
1029
+ if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
1030
+ else if (route.specFileKey) {
1031
+ const base = (route.basePath ?? route.path).replace(/^\//, "");
1032
+ const specRegistryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
1033
+ modules.set(route.virtualModuleId, generateOpenAPIPageModule({
1034
+ specKey: route.specFileKey,
1035
+ specRegistryKey,
1036
+ tagFilter: route.openapiTagFilter
1037
+ }));
1038
+ }
1039
+ }
934
1040
  return modules;
935
1041
  }
936
1042
  }
@@ -1148,22 +1254,22 @@ function createRobotsTxt(config) {
1148
1254
  ""
1149
1255
  ].join("\n");
1150
1256
  }
1151
- async function writeSeoFiles(outputRoot, routes, config) {
1152
- const sitemap = createSitemapXml(routes, config);
1153
- const robots = createRobotsTxt(config);
1154
- if (!sitemap || !robots) return;
1155
- await mkdir(outputRoot, { recursive: true });
1156
- await Promise.all([writeFile(join(outputRoot, "sitemap.xml"), sitemap, "utf-8"), writeFile(join(outputRoot, "robots.txt"), robots, "utf-8")]);
1157
- }
1158
1257
  //#endregion
1159
1258
  //#region source/plugins/seo/index.ts
1160
1259
  function createSeoPlugin() {
1161
1260
  return {
1162
1261
  name: "clarify:seo",
1163
- hooks: { async "build:done"(ctx) {
1164
- const outputDirectory = ctx.generateOptions.outputDirectory;
1165
- if (!outputDirectory) return;
1166
- await writeSeoFiles(join(ctx.generateOptions.projectRoot, outputDirectory), ctx.routes, ctx.projectConfig);
1262
+ hooks: { "build:assets"(ctx) {
1263
+ const sitemap = createSitemapXml(ctx.routes, ctx.projectConfig);
1264
+ const robots = createRobotsTxt(ctx.projectConfig);
1265
+ if (!sitemap || !robots) return [];
1266
+ return [{
1267
+ fileName: "sitemap.xml",
1268
+ source: sitemap
1269
+ }, {
1270
+ fileName: "robots.txt",
1271
+ source: robots
1272
+ }];
1167
1273
  } }
1168
1274
  };
1169
1275
  }
@@ -1261,18 +1367,16 @@ var clarifyNavbarLinkSchema = z.object({
1261
1367
  href: z.string(),
1262
1368
  external: z.boolean().optional()
1263
1369
  });
1264
- var clarifyBannerConfigOptionsSchema = z.object({
1370
+ var clarifyBannerConfigSchema = z.object({
1265
1371
  content: clarifyLocalizedTextSchema,
1266
1372
  dismissible: z.boolean().optional(),
1267
1373
  link: clarifyNavbarLinkSchema.optional()
1268
1374
  }).strict();
1269
- var clarifyBannerConfigSchema = z.union([clarifyBannerConfigOptionsSchema, z.string()]);
1270
- var clarifyFooterLinksConfigSchema = z.object({
1375
+ var clarifyFooterConfigSchema = z.object({
1271
1376
  links: z.array(clarifyNavbarLinkSchema).optional(),
1272
1377
  socials: z.record(z.string(), z.string()).optional(),
1273
1378
  copyright: clarifyLocalizedTextSchema.optional()
1274
1379
  }).strict();
1275
- var clarifyFooterConfigSchema = z.union([clarifyFooterLinksConfigSchema, z.string()]);
1276
1380
  var clarifySourceConfigSchema = z.object({
1277
1381
  repository: z.string(),
1278
1382
  branch: z.string().optional(),
@@ -1431,11 +1535,59 @@ function resolveProjectConfig(config = {}) {
1431
1535
  };
1432
1536
  }
1433
1537
  //#endregion
1538
+ //#region source/core/env-types.ts
1539
+ var GENERATED_FILE_NAME = "clarify-env.d.ts";
1540
+ /**
1541
+ * Template for the generated `clarify-env.d.ts` file.
1542
+ * Edit this template directly if you need to change the type declarations.
1543
+ * This avoids the complexity of reading external files at runtime after bundling.
1544
+ */
1545
+ var CLARIFY_ENV_TEMPLATE = `// This file is generated by Clarify. Do not edit it manually.
1546
+ // It provides ambient type declarations for Clarify virtual modules used in
1547
+ // plugin and slot code, so the TypeScript language service can resolve them.
1548
+
1549
+ declare module 'virtual:clarify/slot' {
1550
+ import type { SlotContext } from '@clarify-labs/cli'
1551
+
1552
+ export function useSlot(): SlotContext
1553
+ }
1554
+
1555
+ declare module 'virtual:clarify/slots' {
1556
+ import type { ComponentType } from 'react'
1557
+
1558
+ export type RuntimeSlotEntry = {
1559
+ plugin: string
1560
+ /** Lazy import factory — yields the default-exported React component. */
1561
+ component: () => Promise<{ default: ComponentType }>
1562
+ }
1563
+
1564
+ export const runtimeSlots: Record<string, RuntimeSlotEntry[]>
1565
+ }
1566
+ `;
1567
+ function generateClarifyEnvDts(_plugins) {
1568
+ return CLARIFY_ENV_TEMPLATE;
1569
+ }
1570
+ /**
1571
+ * Writes `clarify-env.d.ts` into the project root. Only rewrites when content
1572
+ * changes to avoid triggering unnecessary file watchers, and never overwrites a
1573
+ * user-authored file that does not carry the generated marker.
1574
+ */
1575
+ function writeClarifyEnvDts(projectRoot, plugins) {
1576
+ const filePath = join(projectRoot, GENERATED_FILE_NAME);
1577
+ const next = generateClarifyEnvDts(plugins);
1578
+ if (existsSync(filePath)) {
1579
+ const current = readFileSync(filePath, "utf-8");
1580
+ if (current === next) return;
1581
+ if (!current.startsWith("// This file is generated by Clarify.")) return;
1582
+ }
1583
+ writeFileSync(filePath, next, "utf-8");
1584
+ }
1585
+ //#endregion
1434
1586
  //#region source/core/hooks.ts
1435
1587
  async function runHooks(plugins, hookName, input, ctx) {
1436
1588
  let result = input;
1437
1589
  for (const plugin of plugins) {
1438
- const hook = plugin.hooks[hookName];
1590
+ const hook = plugin.hooks?.[hookName];
1439
1591
  if (!hook) continue;
1440
1592
  try {
1441
1593
  result = await hook(result, ctx);
@@ -1447,7 +1599,7 @@ async function runHooks(plugins, hookName, input, ctx) {
1447
1599
  }
1448
1600
  async function runDevConfigureServerHooks(plugins, server, ctx) {
1449
1601
  for (const plugin of plugins) {
1450
- const hook = plugin.hooks["dev:configureServer"];
1602
+ const hook = plugin.hooks?.["dev:configureServer"];
1451
1603
  if (!hook) continue;
1452
1604
  try {
1453
1605
  await hook(server, ctx);
@@ -1456,9 +1608,23 @@ async function runDevConfigureServerHooks(plugins, server, ctx) {
1456
1608
  }
1457
1609
  }
1458
1610
  }
1611
+ async function runBuildAssetsHooks(plugins, ctx) {
1612
+ const assets = [];
1613
+ for (const plugin of plugins) {
1614
+ const hook = plugin.hooks?.["build:assets"];
1615
+ if (!hook) continue;
1616
+ try {
1617
+ const pluginAssets = await hook(ctx);
1618
+ assets.push(...pluginAssets);
1619
+ } catch (err) {
1620
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
1621
+ }
1622
+ }
1623
+ return assets;
1624
+ }
1459
1625
  async function runBuildDoneHooks(plugins, ctx) {
1460
1626
  for (const plugin of plugins) {
1461
- const hook = plugin.hooks["build:done"];
1627
+ const hook = plugin.hooks?.["build:done"];
1462
1628
  if (!hook) continue;
1463
1629
  try {
1464
1630
  await hook(ctx);
@@ -1479,18 +1645,6 @@ function resolveBuildOptions(options = {}) {
1479
1645
  }
1480
1646
  //#endregion
1481
1647
  //#region source/core/site.ts
1482
- function withAlternates(route, routeList, i18n) {
1483
- const basePath = route.basePath ?? route.path;
1484
- const routeByLocaleAndBase = new Map(routeList.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
1485
- const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
1486
- const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
1487
- return alternate ? [[locale.code, alternate.path]] : [];
1488
- }));
1489
- return {
1490
- ...route,
1491
- alternates
1492
- };
1493
- }
1494
1648
  async function discoverRoutesForRoot(routeRoot, locale, plugins, ctx) {
1495
1649
  return (await runHooks(plugins, "routes:discover", {
1496
1650
  contentRoot: routeRoot,
@@ -1532,7 +1686,21 @@ async function discoverRoutes(root, contentRoot, plugins, ctx) {
1532
1686
  }
1533
1687
  }
1534
1688
  }
1535
- return localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
1689
+ const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
1690
+ const defaultLocale = i18n.defaultLocale;
1691
+ const bareRoutes = [];
1692
+ const seenBare = new Set(routesWithAlternates.map((r) => r.path));
1693
+ for (const route of routesWithAlternates) {
1694
+ if (route.locale !== defaultLocale) continue;
1695
+ const bp = route.basePath ?? route.path;
1696
+ if (bp === route.path || seenBare.has(bp)) continue;
1697
+ seenBare.add(bp);
1698
+ bareRoutes.push({
1699
+ ...route,
1700
+ path: bp
1701
+ });
1702
+ }
1703
+ return [...routesWithAlternates, ...bareRoutes];
1536
1704
  }
1537
1705
  async function resolveClarifySite(options = {}, resolveOptions = {}) {
1538
1706
  const root = options.projectRoot ?? process.cwd();
@@ -1608,13 +1776,21 @@ function createClarifyRuntimeAliases() {
1608
1776
  find: /^@clarify-labs\/renderer$/,
1609
1777
  replacement: resolvePackageExport("@clarify-labs/renderer", "")
1610
1778
  },
1779
+ {
1780
+ find: /^react\/jsx-dev-runtime$/,
1781
+ replacement: resolveCliDependency("react/jsx-dev-runtime")
1782
+ },
1611
1783
  {
1612
1784
  find: /^react\/jsx-runtime$/,
1613
1785
  replacement: resolveCliDependency("react/jsx-runtime")
1614
1786
  },
1615
1787
  {
1616
- find: /^react\/jsx-dev-runtime$/,
1617
- replacement: resolveCliDependency("react/jsx-dev-runtime")
1788
+ find: /^mermaid$/,
1789
+ replacement: resolveCliDependency("mermaid")
1790
+ },
1791
+ {
1792
+ find: /^react-zoom-pan-pinch$/,
1793
+ replacement: resolveCliDependency("react-zoom-pan-pinch")
1618
1794
  },
1619
1795
  {
1620
1796
  find: /^react$/,
@@ -1656,11 +1832,24 @@ function setNamedMeta(html, name, content) {
1656
1832
  if (metaPattern.test(html)) return html.replace(metaPattern, meta);
1657
1833
  return html.replace("</head>", ` ${meta}\n </head>`);
1658
1834
  }
1835
+ function canonicalUrl(projectConfig, route) {
1836
+ if (!projectConfig.siteUrl || !route.alternates || !route.locale) return void 0;
1837
+ const prefixedPath = route.alternates[route.locale];
1838
+ if (!prefixedPath || prefixedPath === route.path) return void 0;
1839
+ return `${projectConfig.siteUrl.replace(/\/+$/, "")}${!projectConfig.routePrefix || projectConfig.routePrefix === "/" ? "" : `/${projectConfig.routePrefix.replace(/^\/+|\/+$/g, "")}`}${prefixedPath}`;
1840
+ }
1841
+ function injectCanonicalUrl(html, projectConfig, route) {
1842
+ const url = route ? canonicalUrl(projectConfig, route) : void 0;
1843
+ html = html.replace(/<link\b[^>]*\brel=["']canonical["'][^>]*\/?>\n?/gi, "");
1844
+ if (!url) return html;
1845
+ return html.replace("</head>", ` <link rel="canonical" href="${escapeHtml(url)}" />\n </head>`);
1846
+ }
1659
1847
  function injectSSRIntoTemplate(template, appHtml, projectConfig, route) {
1660
1848
  let html = injectHtmlLocaleAttributes(template, projectConfig, route);
1661
1849
  html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(routeTitle(projectConfig, route))}</title>`);
1662
1850
  html = setNamedMeta(html, "description", route?.description ?? projectConfig.description);
1663
1851
  html = setNamedMeta(html, "keywords", route?.keywords?.join(", "));
1852
+ html = injectCanonicalUrl(html, projectConfig, route);
1664
1853
  html = html.replace(/<div id="root">[\s\S]*?<\/div>/, `<div id="root">${appHtml}</div>`);
1665
1854
  return html;
1666
1855
  }
@@ -1673,12 +1862,20 @@ function routeOutputFiles(outputDirectory, route) {
1673
1862
  return files;
1674
1863
  }
1675
1864
  var SSR_ENTRY_CODE = `import { renderToHTML } from '@clarify-labs/renderer/server';
1676
- import { routes, navigation } from 'virtual:clarify-routes/server';
1677
- import { config } from 'virtual:clarify-config';
1678
- import { openApis, bannerComponent, footerComponent } from 'virtual:clarify-runtime';
1865
+ import { routes, navigation } from 'virtual:clarify/routes/server';
1866
+ import { config } from 'virtual:clarify/config';
1867
+ import { openApis } from 'virtual:clarify/openapi';
1868
+ import { runtimeSlots } from 'virtual:clarify/slots';
1679
1869
 
1680
- export function render(url) {
1681
- return renderToHTML({ config, routes, navigation, openApis, bannerComponent, footerComponent, url, themeEditor: config.theme.editor });
1870
+ export async function render(url) {
1871
+ // Pre-resolve every slot component factory so renderToString can use
1872
+ // them synchronously (renderToString has no Suspense support).
1873
+ const entries = Object.values(runtimeSlots).flat()
1874
+ await Promise.all(entries.map(async (entry) => {
1875
+ const mod = await entry.component()
1876
+ entry._resolved = mod.default
1877
+ }))
1878
+ return renderToHTML({ config, routes, navigation, openApis, runtimeSlots, url, themeEditor: config.theme.editor });
1682
1879
  }`;
1683
1880
  function createTempEntryFile(content) {
1684
1881
  const entryPath = join(createClarifyTempDir("ssr"), "entry-server.ts");
@@ -1708,7 +1905,7 @@ async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundle
1708
1905
  const template = readIndexHtml(outputDirectory);
1709
1906
  if (!template) throw new Error(`[clarify] index.html not found in outputDirectory "${outputDirectory}". Make sure Vite build produces it.`);
1710
1907
  for (const route of routes) try {
1711
- const finalHtml = injectSSRIntoTemplate(template, render(route.path), projectConfig, route);
1908
+ const finalHtml = injectSSRIntoTemplate(template, await render(route.path), projectConfig, route);
1712
1909
  for (const outFile of routeOutputFiles(outputDirectory, route)) {
1713
1910
  mkdirSync(dirname(outFile), { recursive: true });
1714
1911
  writeFileSync(outFile, finalHtml, "utf-8");
@@ -1777,23 +1974,8 @@ function clarifyPlugin(options = {}) {
1777
1974
  let viteConfig;
1778
1975
  let resolvedNavigation = [];
1779
1976
  let virtualModules = /* @__PURE__ */ new Map();
1780
- async function reloadProjectConfig() {
1781
- const buildOptions = {
1782
- ...await loadClarifyConfig(root, {
1783
- command: "serve",
1784
- mode: viteConfig.mode
1785
- }),
1786
- projectRoot: options.projectRoot,
1787
- rootDirectory: options.rootDirectory,
1788
- outputDirectory: options.outputDirectory
1789
- };
1790
- projectConfig = resolveProjectConfig(buildOptions);
1791
- generateOptions = resolveBuildOptions(buildOptions);
1792
- ctx.projectConfig = projectConfig;
1793
- ctx.generateOptions = generateOptions;
1794
- }
1795
- async function resolveRoutesAndSpecs() {
1796
- const site = await resolveClarifySite(options);
1977
+ async function resolveRoutesAndSpecs(overrides) {
1978
+ const site = await resolveClarifySite(overrides ?? options);
1797
1979
  projectConfig = site.projectConfig;
1798
1980
  generateOptions = site.generateOptions;
1799
1981
  routes = site.routes;
@@ -1810,6 +1992,7 @@ function clarifyPlugin(options = {}) {
1810
1992
  generateOptions,
1811
1993
  routes,
1812
1994
  navigation: resolvedNavigation,
1995
+ plugins: clarifyPlugins,
1813
1996
  themeEditor: viteConfig.command === "serve" || projectConfig.theme.editor
1814
1997
  });
1815
1998
  virtualModules = await runHooks(clarifyPlugins, "modules:before", virtualModules, ctx);
@@ -1872,12 +2055,23 @@ function clarifyPlugin(options = {}) {
1872
2055
  },
1873
2056
  async buildStart() {
1874
2057
  await rebuildVirtualModules();
2058
+ writeClarifyEnvDts(root, clarifyPlugins);
1875
2059
  },
1876
2060
  async handleHotUpdate(ctx) {
1877
2061
  const changedFile = isAbsolute(ctx.file) ? ctx.file : join(root, ctx.file);
1878
2062
  if (configFilePath && changedFile === configFilePath) {
1879
- await reloadProjectConfig();
1880
- await refreshDevServer(ctx.server);
2063
+ await resolveRoutesAndSpecs({
2064
+ ...await loadClarifyConfig(root, {
2065
+ command: "serve",
2066
+ mode: viteConfig.mode
2067
+ }),
2068
+ projectRoot: options.projectRoot,
2069
+ rootDirectory: options.rootDirectory,
2070
+ outputDirectory: options.outputDirectory
2071
+ });
2072
+ await rebuildVirtualModules();
2073
+ invalidateVirtualModules(ctx.server);
2074
+ ctx.server.ws.send({ type: "full-reload" });
1881
2075
  return [];
1882
2076
  }
1883
2077
  const relativeContentFile = relative(contentRoot, changedFile);
@@ -1886,10 +2080,13 @@ function clarifyPlugin(options = {}) {
1886
2080
  return [];
1887
2081
  },
1888
2082
  resolveId(id) {
1889
- if (id === "virtual:clarify-entry-client" || id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1890
- if (id === "virtual:clarify-config" || id === resolveVirtualId("virtual:clarify-config")) return resolveVirtualId(VIRTUAL_CONFIG);
1891
- if (id === "virtual:clarify-routes" || id === resolveVirtualId("virtual:clarify-routes")) return resolveVirtualId(VIRTUAL_ROUTES);
1892
- if (id === "virtual:clarify-routes/server" || id === resolveVirtualId("virtual:clarify-routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
2083
+ if (id === "virtual:clarify/entry-client" || id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2084
+ if (id === "virtual:clarify/config" || id === resolveVirtualId("virtual:clarify/config")) return resolveVirtualId(VIRTUAL_CONFIG);
2085
+ if (id === "virtual:clarify/routes" || id === resolveVirtualId("virtual:clarify/routes")) return resolveVirtualId(VIRTUAL_ROUTES);
2086
+ if (id === "virtual:clarify/routes/server" || id === resolveVirtualId("virtual:clarify/routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
2087
+ if (id === "virtual:clarify/openapi" || id === resolveVirtualId("virtual:clarify/openapi")) return resolveVirtualId(VIRTUAL_OPENAPI);
2088
+ if (id === "virtual:clarify/slots" || id === resolveVirtualId("virtual:clarify/slots")) return resolveVirtualId(VIRTUAL_SLOTS);
2089
+ if (id === "virtual:clarify/slot" || id === resolveVirtualId("virtual:clarify/slot")) return resolveVirtualId(VIRTUAL_SLOT);
1893
2090
  const moduleId = stripVirtualPrefix(id);
1894
2091
  if (virtualModules.has(moduleId)) return resolveVirtualId(moduleId);
1895
2092
  const route = routes.find((r) => r.virtualModuleId === id || r.virtualModuleId === moduleId);
@@ -1927,6 +2124,14 @@ function clarifyPlugin(options = {}) {
1927
2124
  };
1928
2125
  }
1929
2126
  },
2127
+ async generateBundle(_opts, _bundle) {
2128
+ const assets = await runBuildAssetsHooks(clarifyPlugins, ctx);
2129
+ for (const asset of assets) this.emitFile({
2130
+ type: "asset",
2131
+ fileName: asset.fileName,
2132
+ source: asset.source
2133
+ });
2134
+ },
1930
2135
  async closeBundle() {
1931
2136
  if (process.env.SKIP_CLARIFY_SSG) {
1932
2137
  await runBuildDoneHooks(clarifyPlugins, ctx);
@@ -1941,11 +2146,14 @@ function clarifyPlugin(options = {}) {
1941
2146
  {
1942
2147
  name: "clarify:virtual-ssg",
1943
2148
  resolveId(id) {
1944
- if (id === "virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1945
- if (id === "\0virtual:clarify-entry-client") return RESOLVED_CLIENT_ENTRY;
1946
- if (id === "virtual:clarify-config") return id;
1947
- if (id === "virtual:clarify-routes") return id;
1948
- if (id === "virtual:clarify-routes/server") return id;
2149
+ if (id === "virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2150
+ if (id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
2151
+ if (id === "virtual:clarify/routes/server") return id;
2152
+ if (id === "virtual:clarify/openapi") return id;
2153
+ if (id === "virtual:clarify/config") return id;
2154
+ if (id === "virtual:clarify/routes") return id;
2155
+ if (id === "virtual:clarify/slots") return id;
2156
+ if (id === "virtual:clarify/slot") return id;
1949
2157
  if (virtualModules.has(stripVirtualPrefix(id))) return stripVirtualPrefix(id);
1950
2158
  if (routes.find((r) => r.virtualModuleId === id)) return id;
1951
2159
  return null;
@@ -1974,4 +2182,4 @@ function clarifyPlugin(options = {}) {
1974
2182
  //#endregion
1975
2183
  export { resolveClarifySite as a, createClarifyRuntimeAliases as i, defineConfig as n, createClarifyTempDir as o, loadClarifyConfig as r, clarifyPlugin as t };
1976
2184
 
1977
- //# sourceMappingURL=plugin-o1rP_gts.js.map
2185
+ //# sourceMappingURL=plugin-CcD5HeTN.js.map