@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.
- package/output/cli.cjs +11 -3
- package/output/cli.cjs.map +1 -1
- package/output/cli.js +11 -3
- package/output/cli.js.map +1 -1
- package/output/core/config-schema.d.ts +7 -16
- package/output/core/env-types.d.ts +8 -0
- package/output/core/hooks.d.ts +3 -2
- package/output/core/plugin.d.ts +0 -1
- package/output/core/ssg.d.ts +1 -1
- package/output/core/virtual-modules.d.ts +29 -16
- package/output/index.cjs +1 -1
- package/output/index.d.ts +1 -0
- package/output/index.js +1 -1
- package/output/parsers/routes.d.ts +3 -1
- package/output/{plugin-DFpkIm9j.cjs → plugin-B7c0iHMy.cjs} +388 -180
- package/output/plugin-B7c0iHMy.cjs.map +1 -0
- package/output/{plugin-o1rP_gts.js → plugin-CcD5HeTN.js} +388 -180
- package/output/plugin-CcD5HeTN.js.map +1 -0
- package/output/plugins/content-artifacts/artifacts.d.ts +1 -3
- package/output/plugins/openapi/parser.d.ts +2 -0
- package/output/plugins/openapi/virtual-modules.d.ts +15 -3
- package/output/plugins/seo/artifacts.d.ts +0 -1
- package/output/types.d.ts +15 -7
- package/package.json +6 -3
- package/output/plugin-DFpkIm9j.cjs.map +0 -1
- package/output/plugin-o1rP_gts.js.map +0 -1
|
@@ -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
|
|
153
|
+
return false;
|
|
154
154
|
}
|
|
155
155
|
function routeToMarkdownArtifactUrl(routePath) {
|
|
156
156
|
return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.md`;
|
|
157
157
|
}
|
|
158
|
-
function routeToOpenAPIArtifactUrl(routePath
|
|
159
|
-
|
|
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
|
|
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:
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
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,
|
|
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"
|
|
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
|
|
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
|
|
463
|
+
function withAlternates(route, routes, i18n) {
|
|
450
464
|
const basePath = route.basePath ?? route.path;
|
|
451
|
-
const routeByLocaleAndBase = new Map(routes.map((
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
738
|
-
var VIRTUAL_ROUTES = "virtual:clarify
|
|
739
|
-
var VIRTUAL_SERVER_ROUTES = "virtual:clarify
|
|
740
|
-
var
|
|
741
|
-
var
|
|
742
|
-
var
|
|
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
|
|
797
|
-
|
|
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
|
|
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(
|
|
812
|
-
|
|
813
|
-
|
|
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 =
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
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
|
-
|
|
961
|
-
for (const
|
|
962
|
-
|
|
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: {
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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: /^
|
|
1646
|
-
replacement: resolveCliDependency("
|
|
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
|
|
1706
|
-
import { config } from 'virtual:clarify
|
|
1707
|
-
import { openApis
|
|
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
|
-
|
|
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
|
|
1810
|
-
const
|
|
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
|
|
1909
|
-
|
|
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
|
|
1919
|
-
if (id === "virtual:clarify
|
|
1920
|
-
if (id === "virtual:clarify
|
|
1921
|
-
if (id === "virtual:clarify
|
|
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
|
|
1974
|
-
if (id === "\0virtual:clarify
|
|
1975
|
-
if (id === "virtual:clarify
|
|
1976
|
-
if (id === "virtual:clarify
|
|
1977
|
-
if (id === "virtual:clarify
|
|
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-
|
|
2249
|
+
//# sourceMappingURL=plugin-B7c0iHMy.cjs.map
|