@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
|
@@ -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
|
|
124
|
+
return false;
|
|
125
125
|
}
|
|
126
126
|
function routeToMarkdownArtifactUrl(routePath) {
|
|
127
127
|
return `${routePath === "/" ? "/index" : routePath.replace(/\/$/, "")}.md`;
|
|
128
128
|
}
|
|
129
|
-
function routeToOpenAPIArtifactUrl(routePath
|
|
130
|
-
|
|
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
|
|
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:
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
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,
|
|
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"
|
|
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
|
|
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
|
|
434
|
+
function withAlternates(route, routes, i18n) {
|
|
421
435
|
const basePath = route.basePath ?? route.path;
|
|
422
|
-
const routeByLocaleAndBase = new Map(routes.map((
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
709
|
-
var VIRTUAL_ROUTES = "virtual:clarify
|
|
710
|
-
var VIRTUAL_SERVER_ROUTES = "virtual:clarify
|
|
711
|
-
var
|
|
712
|
-
var
|
|
713
|
-
var
|
|
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
|
|
768
|
-
|
|
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
|
|
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(
|
|
783
|
-
|
|
784
|
-
|
|
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 =
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
return
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
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
|
-
|
|
932
|
-
for (const
|
|
933
|
-
|
|
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: {
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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: /^
|
|
1617
|
-
replacement: resolveCliDependency("
|
|
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
|
|
1677
|
-
import { config } from 'virtual:clarify
|
|
1678
|
-
import { openApis
|
|
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
|
-
|
|
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
|
|
1781
|
-
const
|
|
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
|
|
1880
|
-
|
|
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
|
|
1890
|
-
if (id === "virtual:clarify
|
|
1891
|
-
if (id === "virtual:clarify
|
|
1892
|
-
if (id === "virtual:clarify
|
|
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
|
|
1945
|
-
if (id === "\0virtual:clarify
|
|
1946
|
-
if (id === "virtual:clarify
|
|
1947
|
-
if (id === "virtual:clarify
|
|
1948
|
-
if (id === "virtual:clarify
|
|
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-
|
|
2185
|
+
//# sourceMappingURL=plugin-CcD5HeTN.js.map
|