@clarify-labs/cli 0.7.2 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/output/cli/program.d.ts +2 -0
- package/output/cli.cjs +41 -11
- package/output/cli.cjs.map +1 -1
- package/output/cli.js +41 -11
- package/output/cli.js.map +1 -1
- package/output/core/config-schema.d.ts +12 -16
- package/output/core/content.d.ts +5 -0
- package/output/core/env-types.d.ts +8 -0
- package/output/core/hooks.d.ts +3 -2
- package/output/core/options.d.ts +1 -1
- 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/content.d.ts +11 -0
- package/output/parsers/routes.d.ts +9 -3
- package/output/parsers/variables.d.ts +2 -0
- package/output/{plugin-o1rP_gts.js → plugin-D5EF3pYl.js} +620 -224
- package/output/plugin-D5EF3pYl.js.map +1 -0
- package/output/{plugin-DFpkIm9j.cjs → plugin-DyceHdXd.cjs} +620 -224
- package/output/plugin-DyceHdXd.cjs.map +1 -0
- package/output/plugins/content-artifacts/artifacts.d.ts +1 -3
- package/output/plugins/openapi/parser.d.ts +4 -1
- package/output/plugins/openapi/virtual-modules.d.ts +15 -3
- package/output/plugins/seo/artifacts.d.ts +0 -1
- package/output/plugins/variables/index.d.ts +2 -0
- package/output/types.d.ts +35 -7
- package/package.json +6 -3
- package/output/plugin-DFpkIm9j.cjs.map +0 -1
- package/output/plugin-o1rP_gts.js.map +0 -1
|
@@ -8,13 +8,13 @@ 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 {
|
|
11
|
+
import { stringify } from "yaml";
|
|
12
12
|
import matter from "gray-matter";
|
|
13
|
+
import { remark } from "remark";
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
15
|
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
14
16
|
import { tmpdir } from "node:os";
|
|
15
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
16
17
|
import { z } from "zod";
|
|
17
|
-
import { pathToFileURL } from "node:url";
|
|
18
18
|
import { build, loadConfigFromFile } from "vite";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
//#region source/core/utils.ts
|
|
@@ -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,40 +140,81 @@ function readRouteArtifactContent(route) {
|
|
|
141
140
|
const content = readRouteContent(route);
|
|
142
141
|
return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
|
|
143
142
|
}
|
|
144
|
-
function
|
|
143
|
+
function isLlmsTxtRoute(route) {
|
|
144
|
+
return !route.path.split("/").includes("404");
|
|
145
|
+
}
|
|
146
|
+
function llmsTxtDescription(route) {
|
|
147
|
+
if (route.description) return route.description;
|
|
148
|
+
const sections = route.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
|
|
149
|
+
if (sections?.length) return `Covers ${sections.join(", ")}.`;
|
|
150
|
+
if (route.keywords?.length) return `Related topics: ${route.keywords.join(", ")}.`;
|
|
151
|
+
if (route.kind === "openapi") return "OpenAPI artifact for machine-readable API reference data.";
|
|
152
|
+
}
|
|
153
|
+
function llmsTxtListItem(route, basePath) {
|
|
154
|
+
if (!route.contentArtifactUrl) return void 0;
|
|
155
|
+
const description = llmsTxtDescription(route);
|
|
156
|
+
return description ? `- [${route.title}](${basePath}${route.contentArtifactUrl}): ${description}` : `- [${route.title}](${basePath}${route.contentArtifactUrl})`;
|
|
157
|
+
}
|
|
158
|
+
function llmsTxtLocaleLabel(locale, projectConfig) {
|
|
159
|
+
return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
|
|
160
|
+
}
|
|
161
|
+
function groupRoutesByLocale(routes) {
|
|
162
|
+
const groups = /* @__PURE__ */ new Map();
|
|
163
|
+
for (const route of routes) {
|
|
164
|
+
const key = route.locale ?? "default";
|
|
165
|
+
groups.set(key, [...groups.get(key) ?? [], route]);
|
|
166
|
+
}
|
|
167
|
+
return groups;
|
|
168
|
+
}
|
|
169
|
+
function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
|
|
170
|
+
const routesBySource = /* @__PURE__ */ new Map();
|
|
171
|
+
const defaultLocale = projectConfig.i18n?.defaultLocale;
|
|
145
172
|
for (const route of routes) {
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
writeFileSync(outFile, readRouteArtifactContent(route), "utf-8");
|
|
173
|
+
const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.contentArtifactUrl ?? route.path}`;
|
|
174
|
+
const previousRoute = routesBySource.get(sourceKey);
|
|
175
|
+
if (!previousRoute || previousRoute.locale && !route.locale) routesBySource.set(sourceKey, route);
|
|
150
176
|
}
|
|
177
|
+
return groupRoutesByLocale([...routesBySource.values()].map((route) => ({
|
|
178
|
+
...route,
|
|
179
|
+
locale: route.locale ?? defaultLocale
|
|
180
|
+
})));
|
|
181
|
+
}
|
|
182
|
+
function llmsTxtDocsSectionTitle(locale, groupCount, projectConfig) {
|
|
183
|
+
if (groupCount <= 1) return "Docs";
|
|
184
|
+
return `Docs - ${locale === "default" ? "Default" : llmsTxtLocaleLabel(locale, projectConfig)}`;
|
|
151
185
|
}
|
|
152
186
|
function createLlmsTxt(routes, projectConfig) {
|
|
153
187
|
const basePath = normalizeBasePath(projectConfig.routePrefix);
|
|
154
188
|
const lines = [`# ${projectConfig.title}`, ""];
|
|
155
189
|
if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
|
|
156
|
-
lines.push("
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
190
|
+
lines.push("This file lists the source-ready Markdown and OpenAPI artifacts for this documentation site.", "");
|
|
191
|
+
const mdxRoutes = routes.filter((route) => route.kind === "mdx" && isLlmsTxtRoute(route));
|
|
192
|
+
if (mdxRoutes.length > 0) {
|
|
193
|
+
const localizedGroups = groupLlmsTxtRoutesByLocale(mdxRoutes, projectConfig);
|
|
194
|
+
for (const [locale, localeRoutes] of localizedGroups) {
|
|
195
|
+
lines.push(`## ${llmsTxtDocsSectionTitle(locale, localizedGroups.size, projectConfig)}`);
|
|
196
|
+
for (const route of localeRoutes) {
|
|
197
|
+
const item = llmsTxtListItem(route, basePath);
|
|
198
|
+
if (item) lines.push(item);
|
|
199
|
+
}
|
|
200
|
+
lines.push("");
|
|
201
|
+
}
|
|
160
202
|
}
|
|
161
|
-
const openApiRoutes = routes.filter((route) => route.kind === "openapi");
|
|
203
|
+
const openApiRoutes = routes.filter((route) => route.kind === "openapi" && isLlmsTxtRoute(route));
|
|
162
204
|
if (openApiRoutes.length > 0) {
|
|
163
|
-
lines.
|
|
205
|
+
if (lines.at(-1) !== "") lines.push("");
|
|
206
|
+
lines.push("## OpenAPI");
|
|
164
207
|
for (const route of openApiRoutes) {
|
|
165
|
-
|
|
166
|
-
lines.push(
|
|
208
|
+
const item = llmsTxtListItem(route, basePath);
|
|
209
|
+
if (item) lines.push(item);
|
|
167
210
|
}
|
|
168
211
|
}
|
|
212
|
+
while (lines.at(-1) === "") lines.pop();
|
|
169
213
|
return `${lines.join("\n")}\n`;
|
|
170
214
|
}
|
|
171
215
|
function createLlmsTxtArtifact(routes, projectConfig) {
|
|
172
216
|
return withUtf8Signature(createLlmsTxt(routes, projectConfig));
|
|
173
217
|
}
|
|
174
|
-
function writeLlmsTxt(routes, projectConfig, outputDirectory) {
|
|
175
|
-
mkdirSync(outputDirectory, { recursive: true });
|
|
176
|
-
writeFileSync(join(outputDirectory, "llms.txt"), createLlmsTxtArtifact(routes, projectConfig), "utf-8");
|
|
177
|
-
}
|
|
178
218
|
//#endregion
|
|
179
219
|
//#region source/plugins/content-artifacts/server.ts
|
|
180
220
|
function normalizeRoutePrefix$1(routePrefix) {
|
|
@@ -187,7 +227,6 @@ function resolveContentArtifactPath(url, projectConfig) {
|
|
|
187
227
|
return basePath && requestPath.startsWith(basePath) ? requestPath.slice(basePath.length) || "/" : requestPath;
|
|
188
228
|
}
|
|
189
229
|
function resolveContentArtifactType(route) {
|
|
190
|
-
if (route.kind === "openapi" && /\.ya?ml$/i.test(route.contentArtifactUrl ?? "")) return "text/yaml; charset=utf-8";
|
|
191
230
|
return route.kind === "openapi" ? "application/json; charset=utf-8" : "text/markdown; charset=utf-8";
|
|
192
231
|
}
|
|
193
232
|
function serveContentArtifacts(req, res, projectConfig, routes) {
|
|
@@ -198,6 +237,18 @@ function serveContentArtifacts(req, res, projectConfig, routes) {
|
|
|
198
237
|
res.end(createLlmsTxtArtifact(routes, projectConfig), "utf8");
|
|
199
238
|
return true;
|
|
200
239
|
}
|
|
240
|
+
if (contentPath.endsWith(".openapi.yaml")) {
|
|
241
|
+
const jsonPath = contentPath.replace(/\.yaml$/, ".json");
|
|
242
|
+
const route = routes.find((r) => r.contentArtifactUrl === jsonPath);
|
|
243
|
+
if (route?.kind === "openapi" && route.content) {
|
|
244
|
+
const spec = JSON.parse(route.content);
|
|
245
|
+
res.statusCode = 200;
|
|
246
|
+
res.setHeader("Content-Type", "text/yaml; charset=utf-8");
|
|
247
|
+
res.end(stringify(spec, { lineWidth: 0 }), "utf8");
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
201
252
|
const route = routes.find((route) => route.contentArtifactUrl === contentPath);
|
|
202
253
|
if (!route) return false;
|
|
203
254
|
res.statusCode = 200;
|
|
@@ -221,12 +272,28 @@ function createContentArtifactsPlugin() {
|
|
|
221
272
|
next();
|
|
222
273
|
});
|
|
223
274
|
},
|
|
224
|
-
"build:
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
275
|
+
"build:assets": (ctx) => {
|
|
276
|
+
const assets = [];
|
|
277
|
+
for (const route of ctx.routes) {
|
|
278
|
+
if (!route.contentArtifactUrl) continue;
|
|
279
|
+
assets.push({
|
|
280
|
+
fileName: route.contentArtifactUrl.replace(/^\//, ""),
|
|
281
|
+
source: readRouteArtifactContent(route)
|
|
282
|
+
});
|
|
283
|
+
if (route.kind === "openapi" && route.content) {
|
|
284
|
+
const yamlFileName = route.contentArtifactUrl.replace(/\.json$/, ".yaml");
|
|
285
|
+
const spec = JSON.parse(route.content);
|
|
286
|
+
assets.push({
|
|
287
|
+
fileName: yamlFileName.replace(/^\//, ""),
|
|
288
|
+
source: stringify(spec, { lineWidth: 0 })
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
assets.push({
|
|
293
|
+
fileName: "llms.txt",
|
|
294
|
+
source: createLlmsTxtArtifact(ctx.routes, ctx.projectConfig)
|
|
295
|
+
});
|
|
296
|
+
return assets;
|
|
230
297
|
}
|
|
231
298
|
}
|
|
232
299
|
};
|
|
@@ -302,8 +369,103 @@ function parseFrontmatter(content) {
|
|
|
302
369
|
};
|
|
303
370
|
}
|
|
304
371
|
//#endregion
|
|
372
|
+
//#region source/parsers/content.ts
|
|
373
|
+
async function transformContent(transform, input) {
|
|
374
|
+
return transform ? await transform(input) : input;
|
|
375
|
+
}
|
|
376
|
+
function createContentProcessor(transform) {
|
|
377
|
+
return {
|
|
378
|
+
async processMdx(source, filePath) {
|
|
379
|
+
const { frontmatter, content } = parseFrontmatter(source);
|
|
380
|
+
const transformed = await transformContent(transform, {
|
|
381
|
+
kind: "mdx",
|
|
382
|
+
source,
|
|
383
|
+
filePath,
|
|
384
|
+
frontmatter,
|
|
385
|
+
content
|
|
386
|
+
});
|
|
387
|
+
return {
|
|
388
|
+
frontmatter: transformed.frontmatter,
|
|
389
|
+
content: transformed.content
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
async processText(source, kind, filePath) {
|
|
393
|
+
return (await transformContent(transform, {
|
|
394
|
+
kind,
|
|
395
|
+
source,
|
|
396
|
+
filePath,
|
|
397
|
+
frontmatter: {},
|
|
398
|
+
content: source
|
|
399
|
+
})).content;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region source/core/hooks.ts
|
|
405
|
+
async function runHooks(plugins, hookName, input, ctx) {
|
|
406
|
+
let result = input;
|
|
407
|
+
for (const plugin of plugins) {
|
|
408
|
+
const hook = plugin.hooks?.[hookName];
|
|
409
|
+
if (!hook) continue;
|
|
410
|
+
try {
|
|
411
|
+
result = await hook(result, ctx);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
async function runDevConfigureServerHooks(plugins, server, ctx) {
|
|
419
|
+
for (const plugin of plugins) {
|
|
420
|
+
const hook = plugin.hooks?.["dev:configureServer"];
|
|
421
|
+
if (!hook) continue;
|
|
422
|
+
try {
|
|
423
|
+
await hook(server, ctx);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function runBuildAssetsHooks(plugins, ctx) {
|
|
430
|
+
const assets = [];
|
|
431
|
+
for (const plugin of plugins) {
|
|
432
|
+
const hook = plugin.hooks?.["build:assets"];
|
|
433
|
+
if (!hook) continue;
|
|
434
|
+
try {
|
|
435
|
+
const pluginAssets = await hook(ctx);
|
|
436
|
+
assets.push(...pluginAssets);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return assets;
|
|
442
|
+
}
|
|
443
|
+
async function runBuildDoneHooks(plugins, ctx) {
|
|
444
|
+
for (const plugin of plugins) {
|
|
445
|
+
const hook = plugin.hooks?.["build:done"];
|
|
446
|
+
if (!hook) continue;
|
|
447
|
+
try {
|
|
448
|
+
await hook(ctx);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
throw new Error(`[clarify] plugin "${plugin.name}" hook "build:done" failed: ${err}`, { cause: err });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region source/core/content.ts
|
|
456
|
+
var contentProcessors = /* @__PURE__ */ new WeakMap();
|
|
457
|
+
function createProjectContentProcessor(plugins, ctx) {
|
|
458
|
+
return createContentProcessor((input) => runHooks(plugins, "content:transform", input, ctx));
|
|
459
|
+
}
|
|
460
|
+
function setProjectContentProcessor(ctx, processor) {
|
|
461
|
+
contentProcessors.set(ctx, processor);
|
|
462
|
+
}
|
|
463
|
+
function getProjectContentProcessor(ctx) {
|
|
464
|
+
return contentProcessors.get(ctx);
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
305
467
|
//#region source/parsers/routes.ts
|
|
306
|
-
function kebabToTitle
|
|
468
|
+
function kebabToTitle(str) {
|
|
307
469
|
return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
308
470
|
}
|
|
309
471
|
function parseMdxTree(content) {
|
|
@@ -368,32 +530,33 @@ function routePathFromRef(ref, explicitPath) {
|
|
|
368
530
|
function virtualModuleIdFromRef(ref) {
|
|
369
531
|
return "virtual:clarify-page/" + ref.replace(/\.mdx?$/, "").replace(/\.openapi\.(json|yaml|yml)$/, "").replace(/\/+/g, "/");
|
|
370
532
|
}
|
|
371
|
-
function localizedRoutePath(basePath, locale,
|
|
533
|
+
function localizedRoutePath(basePath, locale, _i18n) {
|
|
372
534
|
const normalizedBasePath = normalizePath(basePath);
|
|
373
|
-
if (locale === i18n.defaultLocale) return normalizedBasePath;
|
|
374
535
|
const prefix = normalizePath(locale);
|
|
375
536
|
return normalizedBasePath === "/" ? prefix : normalizePath(`${prefix}${normalizedBasePath}`);
|
|
376
537
|
}
|
|
377
538
|
function resolveLocalizedText(text, locale, fallbackLocale) {
|
|
378
|
-
if (typeof text === "string"
|
|
539
|
+
if (typeof text === "string") return text;
|
|
540
|
+
if (text === void 0) return void 0;
|
|
379
541
|
return text[locale] ?? (fallbackLocale ? text[fallbackLocale] : void 0) ?? Object.values(text)[0];
|
|
380
542
|
}
|
|
381
|
-
function findContentRoutes(dir, base = dir) {
|
|
543
|
+
async function findContentRoutes(dir, base = dir, options = {}) {
|
|
382
544
|
const routes = [];
|
|
383
545
|
if (!existsSync(dir)) return routes;
|
|
384
546
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
385
547
|
for (const entry of entries) {
|
|
386
548
|
const fullPath = join(dir, entry.name);
|
|
387
|
-
if (entry.isDirectory()) routes.push(...findContentRoutes(fullPath, base));
|
|
549
|
+
if (entry.isDirectory()) routes.push(...await findContentRoutes(fullPath, base, options));
|
|
388
550
|
else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
|
|
389
551
|
const relativePath = relative(base, fullPath);
|
|
390
552
|
const pathParts = relativePath.replace(/\.mdx?$/, "").split("/");
|
|
391
553
|
const cleanPath = ("/" + pathParts.map((p) => p === "index" ? "" : p).filter(Boolean).join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
392
|
-
const
|
|
554
|
+
const source = readFileSync(fullPath, "utf-8");
|
|
555
|
+
const { frontmatter, content } = await (options.contentProcessor ?? createContentProcessor()).processMdx(source, fullPath);
|
|
393
556
|
let title = typeof frontmatter.title === "string" ? frontmatter.title : "";
|
|
394
557
|
if (!title) {
|
|
395
558
|
const lastPart = pathParts[pathParts.length - 1] ?? "";
|
|
396
|
-
title = kebabToTitle
|
|
559
|
+
title = kebabToTitle(lastPart === "index" ? pathParts.length >= 2 ? pathParts[pathParts.length - 2] : extractH1(content) : lastPart) || "Untitled";
|
|
397
560
|
}
|
|
398
561
|
routes.push({
|
|
399
562
|
path: cleanPath,
|
|
@@ -417,9 +580,9 @@ function frontmatterKeywords(frontmatter) {
|
|
|
417
580
|
const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
|
|
418
581
|
return keywords.length > 0 ? keywords : void 0;
|
|
419
582
|
}
|
|
420
|
-
function withAlternates
|
|
583
|
+
function withAlternates(route, routes, i18n) {
|
|
421
584
|
const basePath = route.basePath ?? route.path;
|
|
422
|
-
const routeByLocaleAndBase = new Map(routes.map((
|
|
585
|
+
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
586
|
const alternates = Object.fromEntries(i18n.locales.flatMap((locale) => {
|
|
424
587
|
const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
|
|
425
588
|
return alternate ? [[locale.code, alternate.path]] : [];
|
|
@@ -441,7 +604,7 @@ function buildNavigation(routes) {
|
|
|
441
604
|
if (!node) {
|
|
442
605
|
node = {
|
|
443
606
|
path: pathSoFar,
|
|
444
|
-
title: i === parts.length - 1 ? route.title : kebabToTitle
|
|
607
|
+
title: i === parts.length - 1 ? route.title : kebabToTitle(parts[i]),
|
|
445
608
|
children: []
|
|
446
609
|
};
|
|
447
610
|
current.push(node);
|
|
@@ -513,7 +676,21 @@ function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
|
|
|
513
676
|
}
|
|
514
677
|
}
|
|
515
678
|
const nextRoutes = [...routes, ...additions];
|
|
516
|
-
|
|
679
|
+
const routesWithAlternates = i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, i18n)) : nextRoutes;
|
|
680
|
+
if (!i18n) return routesWithAlternates;
|
|
681
|
+
const bareRoutes = [];
|
|
682
|
+
const seenBare = new Set(routesWithAlternates.map((r) => r.path));
|
|
683
|
+
for (const route of routesWithAlternates) {
|
|
684
|
+
if (route.locale !== i18n.defaultLocale) continue;
|
|
685
|
+
const bp = route.basePath ?? route.path;
|
|
686
|
+
if (bp === route.path || seenBare.has(bp)) continue;
|
|
687
|
+
seenBare.add(bp);
|
|
688
|
+
bareRoutes.push({
|
|
689
|
+
...route,
|
|
690
|
+
path: bp
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return [...routesWithAlternates, ...bareRoutes];
|
|
517
694
|
}
|
|
518
695
|
function buildNavigationFromPagesConfig(routes, config) {
|
|
519
696
|
if (!config || config === "FileTree") return buildNavigation(routes);
|
|
@@ -541,7 +718,7 @@ function buildNavigationFromConfig(routes, config) {
|
|
|
541
718
|
const route = routeMap.get(path);
|
|
542
719
|
return {
|
|
543
720
|
path,
|
|
544
|
-
title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle
|
|
721
|
+
title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? openapiRef),
|
|
545
722
|
icon,
|
|
546
723
|
sections: route?.sections ? navigationSections(route.sections) : void 0
|
|
547
724
|
};
|
|
@@ -551,7 +728,7 @@ function buildNavigationFromConfig(routes, config) {
|
|
|
551
728
|
const route = routeMap.get(path);
|
|
552
729
|
return {
|
|
553
730
|
path: redirect ? normalizePath(redirect) : path,
|
|
554
|
-
title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle
|
|
731
|
+
title: resolveLocalizedText(title, "", "") ?? route?.title ?? kebabToTitle(path.split("/").pop() ?? ref),
|
|
555
732
|
icon,
|
|
556
733
|
sections: route?.sections ? navigationSections(route.sections) : void 0
|
|
557
734
|
};
|
|
@@ -586,7 +763,7 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
|
|
|
586
763
|
const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
|
|
587
764
|
return {
|
|
588
765
|
path: (redirect ? localizedRoutePath(routePathFromRef(redirect), locale, i18n) : void 0) ?? path,
|
|
589
|
-
title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle
|
|
766
|
+
title: resolveLocalizedText(title, locale, i18n.defaultLocale) ?? route?.title ?? kebabToTitle(basePath.split("/").pop() ?? ref),
|
|
590
767
|
icon,
|
|
591
768
|
sections: route?.sections ? navigationSections(route.sections) : void 0
|
|
592
769
|
};
|
|
@@ -632,8 +809,12 @@ var OPENAPI_HTTP_METHODS = [
|
|
|
632
809
|
function errorMessage(error) {
|
|
633
810
|
return error instanceof Error ? error.message : String(error);
|
|
634
811
|
}
|
|
635
|
-
function
|
|
636
|
-
|
|
812
|
+
function isSameOpenAPIFile(url, filePath) {
|
|
813
|
+
try {
|
|
814
|
+
return resolve(fileURLToPath(url)) === resolve(filePath);
|
|
815
|
+
} catch {
|
|
816
|
+
return resolve(url) === resolve(filePath);
|
|
817
|
+
}
|
|
637
818
|
}
|
|
638
819
|
function findOpenAPIRoutes(dir, base = dir) {
|
|
639
820
|
const routes = [];
|
|
@@ -654,8 +835,7 @@ function findOpenAPIRoutes(dir, base = dir) {
|
|
|
654
835
|
filePath: fullPath,
|
|
655
836
|
virtualModuleId: virtualModuleIdFromRef(ref),
|
|
656
837
|
title: kebabToTitle(cleanPath.split("/").pop() ?? "API"),
|
|
657
|
-
kind: "openapi"
|
|
658
|
-
content: readFileSync(fullPath, "utf-8")
|
|
838
|
+
kind: "openapi"
|
|
659
839
|
});
|
|
660
840
|
}
|
|
661
841
|
return routes;
|
|
@@ -664,6 +844,20 @@ function operationMatchesTags(operationTags, filterTags) {
|
|
|
664
844
|
if (!filterTags?.length) return true;
|
|
665
845
|
return operationTags?.some((tag) => filterTags.includes(tag)) ?? false;
|
|
666
846
|
}
|
|
847
|
+
/** Filter an OpenAPI spec to only include paths that have at least one operation matching the given tags. */
|
|
848
|
+
function filterSpecByTags(spec, tags) {
|
|
849
|
+
const filteredPaths = {};
|
|
850
|
+
const paths = spec.paths ?? {};
|
|
851
|
+
for (const path of Object.keys(paths)) {
|
|
852
|
+
const pathItem = paths[path];
|
|
853
|
+
if (!pathItem) continue;
|
|
854
|
+
if (Object.values(pathItem).some((op) => op && typeof op === "object" && operationMatchesTags(op.tags, tags))) filteredPaths[path] = pathItem;
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
...spec,
|
|
858
|
+
paths: filteredPaths
|
|
859
|
+
};
|
|
860
|
+
}
|
|
667
861
|
/** Extract endpoint operations from an OpenAPI spec as page sections. */
|
|
668
862
|
function extractOpenAPISections(spec, filterTags) {
|
|
669
863
|
const sections = [];
|
|
@@ -685,12 +879,26 @@ function extractOpenAPISections(spec, filterTags) {
|
|
|
685
879
|
}
|
|
686
880
|
return sections;
|
|
687
881
|
}
|
|
688
|
-
async function readOpenAPISpec(filePath) {
|
|
882
|
+
async function readOpenAPISpec(filePath, contentProcessor) {
|
|
689
883
|
try {
|
|
690
|
-
return {
|
|
884
|
+
if (!contentProcessor) return {
|
|
885
|
+
ok: true,
|
|
886
|
+
spec: await SwaggerParser.dereference(filePath)
|
|
887
|
+
};
|
|
888
|
+
const source = readFileSync(filePath, "utf-8");
|
|
889
|
+
const transformedSource = await contentProcessor.processText(source, "openapi", filePath);
|
|
890
|
+
if (transformedSource === source) return {
|
|
691
891
|
ok: true,
|
|
692
892
|
spec: await SwaggerParser.dereference(filePath)
|
|
693
893
|
};
|
|
894
|
+
return {
|
|
895
|
+
ok: true,
|
|
896
|
+
spec: await SwaggerParser.dereference(filePath, { resolve: { transformedOpenAPISource: {
|
|
897
|
+
order: 1,
|
|
898
|
+
canRead: (file) => isSameOpenAPIFile(file.url, filePath),
|
|
899
|
+
read: () => transformedSource
|
|
900
|
+
} } })
|
|
901
|
+
};
|
|
694
902
|
} catch (error) {
|
|
695
903
|
return {
|
|
696
904
|
ok: false,
|
|
@@ -705,12 +913,14 @@ async function readOpenAPISpec(filePath) {
|
|
|
705
913
|
}
|
|
706
914
|
//#endregion
|
|
707
915
|
//#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
|
|
916
|
+
var VIRTUAL_CONFIG = "virtual:clarify/config";
|
|
917
|
+
var VIRTUAL_ROUTES = "virtual:clarify/routes";
|
|
918
|
+
var VIRTUAL_SERVER_ROUTES = "virtual:clarify/routes/server";
|
|
919
|
+
var VIRTUAL_OPENAPI = "virtual:clarify/openapi";
|
|
920
|
+
var VIRTUAL_SLOTS = "virtual:clarify/slots";
|
|
921
|
+
var VIRTUAL_SLOT = "virtual:clarify/slot";
|
|
922
|
+
var VIRTUAL_CLIENT_ENTRY = "virtual:clarify/entry-client";
|
|
923
|
+
var RESOLVED_CLIENT_ENTRY = "\0virtual:clarify/entry-client";
|
|
714
924
|
function resolveVirtualId(id) {
|
|
715
925
|
return "\0" + id;
|
|
716
926
|
}
|
|
@@ -751,65 +961,117 @@ function generateRoutesModule(routes, resolvedNavigation, projectConfig, mode =
|
|
|
751
961
|
const navigation = resolvedNavigation ?? (projectConfig?.tabs ? projectConfig.i18n ? buildLocalizedNavigationFromTabsConfig(routes, projectConfig.tabs, projectConfig.i18n) ?? {} : buildNavigationFromTabsConfig(routes, projectConfig.tabs) : buildNavigation(routes));
|
|
752
962
|
return `${imports}\n\nexport const routes = [\n${routesArray}\n];\n\nexport const navigation = ${JSON.stringify(navigation, null, 2)};\n`;
|
|
753
963
|
}
|
|
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
964
|
function createClientEntryModule(options = {}) {
|
|
762
965
|
return `
|
|
763
966
|
import '@clarify-labs/renderer/style.css';
|
|
764
967
|
import { render } from '@clarify-labs/renderer/client';
|
|
765
968
|
import { routes, navigation } from '${VIRTUAL_ROUTES}';
|
|
766
969
|
import { config } from '${VIRTUAL_CONFIG}';
|
|
767
|
-
import { openApis
|
|
768
|
-
|
|
970
|
+
import { openApis } from '${VIRTUAL_OPENAPI}';
|
|
971
|
+
import { runtimeSlots } from '${VIRTUAL_SLOTS}';
|
|
972
|
+
render({ config, routes, navigation, openApis, runtimeSlots, themeEditor: ${JSON.stringify(options.themeEditor ?? false)} });`;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Collect every plugin's `slots` declarations into a single runtime registry
|
|
976
|
+
* module. Component modules are exported as lazy import factories so the
|
|
977
|
+
* renderer can decide whether to code-split (client) or pre-resolve (SSR).
|
|
978
|
+
*
|
|
979
|
+
* Relative component paths are resolved against the project `root` so that
|
|
980
|
+
* the generated `import()` call uses an absolute specifier — Vite cannot
|
|
981
|
+
* resolve relative imports from virtual modules.
|
|
982
|
+
*
|
|
983
|
+
* Slot name validity is enforced at compile time by the `UISlotName`
|
|
984
|
+
* type — no runtime validation is needed.
|
|
985
|
+
*/
|
|
986
|
+
function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
|
|
987
|
+
const registrations = [];
|
|
988
|
+
for (const plugin of plugins) for (const slot of plugin.slots ?? []) registrations.push({
|
|
989
|
+
plugin: plugin.name,
|
|
990
|
+
slot
|
|
991
|
+
});
|
|
992
|
+
if (registrations.length === 0) return "export const runtimeSlots = {};\n";
|
|
993
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
994
|
+
for (const { plugin, slot } of registrations) {
|
|
995
|
+
if (slot.name.endsWith(".replace")) {
|
|
996
|
+
const existing = grouped.get(slot.name);
|
|
997
|
+
if (existing) console.warn(`[clarify] Multiple plugins register slot "${slot.name}": "${plugin}" overrides "${existing[existing.length - 1]}". Only the last registration is used.`);
|
|
998
|
+
}
|
|
999
|
+
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.`);
|
|
1000
|
+
const componentPath = root + slot.component;
|
|
1001
|
+
const entry = `{ plugin: ${JSON.stringify(plugin)}, component: () => import(${moduleSpecifier(componentPath)}) }`;
|
|
1002
|
+
const list = grouped.get(slot.name) ?? [];
|
|
1003
|
+
list.push(entry);
|
|
1004
|
+
grouped.set(slot.name, list);
|
|
1005
|
+
}
|
|
1006
|
+
return `export const runtimeSlots = {\n${[...grouped.entries()].map(([name, list]) => ` ${JSON.stringify(name)}: [\n ${list.join(",\n ")}\n ]`).join(",\n")}\n};\n`;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Re-exports the slot context hook from the renderer so plugin components can
|
|
1010
|
+
* `import { useSlot } from 'virtual:clarify/slot'` without depending on
|
|
1011
|
+
* renderer internals directly.
|
|
1012
|
+
*/
|
|
1013
|
+
function createSlotModule() {
|
|
1014
|
+
return `export { useSlot } from '@clarify-labs/renderer';\n`;
|
|
769
1015
|
}
|
|
770
1016
|
function buildVirtualModules(args) {
|
|
771
1017
|
const modules = /* @__PURE__ */ new Map();
|
|
772
1018
|
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;
|
|
1019
|
+
const allPlugins = [...args.plugins ?? []];
|
|
779
1020
|
modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.generateOptions));
|
|
780
1021
|
modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, args.projectConfig, "client"));
|
|
781
1022
|
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
|
-
}));
|
|
1023
|
+
modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
|
|
1024
|
+
modules.set(VIRTUAL_OPENAPI, generateOpenApiModule());
|
|
1025
|
+
modules.set(VIRTUAL_SLOT, createSlotModule());
|
|
790
1026
|
modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
|
|
791
1027
|
modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
|
|
792
1028
|
for (const route of args.routes) modules.set(route.virtualModuleId, `export { default } from ${moduleSpecifier(route.filePath)};`);
|
|
793
1029
|
return modules;
|
|
794
1030
|
}
|
|
1031
|
+
function generateOpenApiModule() {
|
|
1032
|
+
return `export const openApis = {};`;
|
|
1033
|
+
}
|
|
795
1034
|
//#endregion
|
|
796
1035
|
//#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
|
-
}
|
|
1036
|
+
var openApiRegistryModuleId = VIRTUAL_OPENAPI;
|
|
1037
|
+
var OPENAPI_SPEC_PREFIX = "virtual:clarify/openapi-spec/";
|
|
1038
|
+
function specVirtualModuleId(specKey) {
|
|
1039
|
+
return `${OPENAPI_SPEC_PREFIX}${specKey}`;
|
|
1040
|
+
}
|
|
1041
|
+
function generateOpenAPIRegistryModule(openApis) {
|
|
1042
|
+
return `export const openApis = ${JSON.stringify(openApis)};`;
|
|
1043
|
+
}
|
|
1044
|
+
/** Generate a virtual module that exports a single spec as default. */
|
|
1045
|
+
function generateOpenAPISpecModule(spec) {
|
|
1046
|
+
return `export default ${JSON.stringify(spec)};`;
|
|
1047
|
+
}
|
|
1048
|
+
function generateOpenAPIPageModule(opts) {
|
|
1049
|
+
const { specKey, specRegistryKey, tagFilter } = opts;
|
|
1050
|
+
return [
|
|
1051
|
+
`import { createElement, useState, useEffect, useRef } from 'react';`,
|
|
1052
|
+
`import { OpenApiDocument, useOpenApis } from '@clarify-labs/renderer';`,
|
|
1053
|
+
`const SPEC_KEY = ${JSON.stringify(specRegistryKey)};`,
|
|
1054
|
+
`const TAG_FILTER = ${JSON.stringify(tagFilter ?? void 0)};`,
|
|
1055
|
+
`let loadPromise = null;`,
|
|
1056
|
+
`function loadSpec() {`,
|
|
1057
|
+
` if (!loadPromise) loadPromise = import(${JSON.stringify(specVirtualModuleId(specKey))}).then(function(m) { return m.default; });`,
|
|
1058
|
+
` return loadPromise;`,
|
|
1059
|
+
`}`,
|
|
1060
|
+
`export default function OpenApiRoutePage() {`,
|
|
1061
|
+
` const specs = useOpenApis();`,
|
|
1062
|
+
` const serverSpec = specs[SPEC_KEY];`,
|
|
1063
|
+
` const [spec, setSpec] = useState(serverSpec || null);`,
|
|
1064
|
+
` const mountedRef = useRef(false);`,
|
|
1065
|
+
` useEffect(function() {`,
|
|
1066
|
+
` if (mountedRef.current) return;`,
|
|
1067
|
+
` mountedRef.current = true;`,
|
|
1068
|
+
` if (spec) return;`,
|
|
1069
|
+
` loadSpec().then(setSpec);`,
|
|
1070
|
+
` }, []);`,
|
|
1071
|
+
` if (!spec) return null;`,
|
|
1072
|
+
` return createElement(OpenApiDocument, { spec: spec, tagFilter: TAG_FILTER });`,
|
|
1073
|
+
`}`
|
|
1074
|
+
].join("\n");
|
|
813
1075
|
}
|
|
814
1076
|
function generateOpenAPIErrorModule(diagnostic) {
|
|
815
1077
|
return `import { createElement } from 'react';
|
|
@@ -843,18 +1105,6 @@ function collectOpenAPIPageItems(config) {
|
|
|
843
1105
|
for (const tab of config.tabs ?? []) visitPages(tab.pages);
|
|
844
1106
|
return items;
|
|
845
1107
|
}
|
|
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
1108
|
function virtualModuleIdFromPath(path) {
|
|
859
1109
|
return `virtual:clarify-page/${path.replace(/^\/+/, "") || "index"}`;
|
|
860
1110
|
}
|
|
@@ -884,10 +1134,15 @@ function createConfiguredOpenAPIRoutes(routes, config) {
|
|
|
884
1134
|
}
|
|
885
1135
|
}
|
|
886
1136
|
const nextRoutes = [...routes, ...additions];
|
|
887
|
-
return config.i18n ? nextRoutes.map((route) => withAlternates
|
|
1137
|
+
return config.i18n ? nextRoutes.map((route) => withAlternates(route, nextRoutes, config.i18n)) : nextRoutes;
|
|
1138
|
+
}
|
|
1139
|
+
/** Derive a stable, deduplicated key from an absolute spec file path. */
|
|
1140
|
+
function specFileKeyFromPath(filePath, projectRoot) {
|
|
1141
|
+
return relative(projectRoot, filePath).replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
888
1142
|
}
|
|
889
1143
|
function createOpenAPIPlugin() {
|
|
890
|
-
|
|
1144
|
+
/** specs keyed by specFileKey (deduplicated per source file). */
|
|
1145
|
+
const specs = /* @__PURE__ */ new Map();
|
|
891
1146
|
return {
|
|
892
1147
|
name: "clarify:openapi",
|
|
893
1148
|
hooks: {
|
|
@@ -896,7 +1151,7 @@ function createOpenAPIPlugin() {
|
|
|
896
1151
|
routes: [...input.routes, ...findOpenAPIRoutes(input.contentRoot)]
|
|
897
1152
|
}),
|
|
898
1153
|
"routes:discovered": async (routes, ctx) => {
|
|
899
|
-
|
|
1154
|
+
specs.clear();
|
|
900
1155
|
const nextRoutes = createConfiguredOpenAPIRoutes(routes, ctx.projectConfig);
|
|
901
1156
|
const specByFilePath = /* @__PURE__ */ new Map();
|
|
902
1157
|
for (const route of nextRoutes.filter((route) => route.kind === "openapi")) {
|
|
@@ -904,7 +1159,7 @@ function createOpenAPIPlugin() {
|
|
|
904
1159
|
const result = specFromCache ? {
|
|
905
1160
|
ok: true,
|
|
906
1161
|
spec: specFromCache
|
|
907
|
-
} : await readOpenAPISpec(route.filePath);
|
|
1162
|
+
} : await readOpenAPISpec(route.filePath, getProjectContentProcessor(ctx));
|
|
908
1163
|
if (!result.ok) {
|
|
909
1164
|
route.diagnostic = result.diagnostic;
|
|
910
1165
|
route.title = route.title || "OpenAPI parse error";
|
|
@@ -913,24 +1168,45 @@ function createOpenAPIPlugin() {
|
|
|
913
1168
|
}
|
|
914
1169
|
const spec = result.spec;
|
|
915
1170
|
specByFilePath.set(route.filePath, spec);
|
|
916
|
-
|
|
1171
|
+
const specKey = specFileKeyFromPath(route.filePath, ctx.generateOptions.projectRoot);
|
|
1172
|
+
route.specFileKey = specKey;
|
|
1173
|
+
specs.set(specKey, {
|
|
917
1174
|
spec,
|
|
918
|
-
|
|
919
|
-
};
|
|
920
|
-
specs[`virtual:clarify-page/${route.path.replace(/^\//, "")}`] = {
|
|
921
|
-
spec,
|
|
922
|
-
tagFilter: route.openapiTagFilter
|
|
923
|
-
};
|
|
1175
|
+
filePath: route.filePath
|
|
1176
|
+
});
|
|
924
1177
|
route.title = spec.info?.title ?? route.title;
|
|
925
1178
|
route.description = spec.info?.description ?? route.description;
|
|
926
1179
|
route.sections = extractOpenAPISections(spec, route.openapiTagFilter);
|
|
1180
|
+
const pageSpec = route.openapiTagFilter?.length ? filterSpecByTags(spec, route.openapiTagFilter) : spec;
|
|
1181
|
+
route.content = JSON.stringify(pageSpec);
|
|
927
1182
|
}
|
|
928
1183
|
return nextRoutes;
|
|
929
1184
|
},
|
|
930
1185
|
"modules:before": (modules, ctx) => {
|
|
931
|
-
|
|
932
|
-
for (const
|
|
933
|
-
|
|
1186
|
+
const registryEntries = {};
|
|
1187
|
+
for (const route of ctx.routes) {
|
|
1188
|
+
if (route.kind !== "openapi" || !route.specFileKey) continue;
|
|
1189
|
+
const spec = specs.get(route.specFileKey)?.spec;
|
|
1190
|
+
if (!spec) continue;
|
|
1191
|
+
const base = (route.basePath ?? route.path).replace(/^\//, "");
|
|
1192
|
+
const registryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
|
|
1193
|
+
registryEntries[registryKey] = spec;
|
|
1194
|
+
}
|
|
1195
|
+
modules.set(openApiRegistryModuleId, generateOpenAPIRegistryModule(registryEntries));
|
|
1196
|
+
for (const [specKey, entry] of specs) modules.set(specVirtualModuleId(specKey), generateOpenAPISpecModule(entry.spec));
|
|
1197
|
+
for (const route of ctx.routes) {
|
|
1198
|
+
if (route.kind !== "openapi") continue;
|
|
1199
|
+
if (route.diagnostic) modules.set(route.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
|
|
1200
|
+
else if (route.specFileKey) {
|
|
1201
|
+
const base = (route.basePath ?? route.path).replace(/^\//, "");
|
|
1202
|
+
const specRegistryKey = route.locale ? `virtual:clarify-page/${route.locale}/${base}` : `virtual:clarify-page/${base}`;
|
|
1203
|
+
modules.set(route.virtualModuleId, generateOpenAPIPageModule({
|
|
1204
|
+
specKey: route.specFileKey,
|
|
1205
|
+
specRegistryKey,
|
|
1206
|
+
tagFilter: route.openapiTagFilter
|
|
1207
|
+
}));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
934
1210
|
return modules;
|
|
935
1211
|
}
|
|
936
1212
|
}
|
|
@@ -1148,22 +1424,22 @@ function createRobotsTxt(config) {
|
|
|
1148
1424
|
""
|
|
1149
1425
|
].join("\n");
|
|
1150
1426
|
}
|
|
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
1427
|
//#endregion
|
|
1159
1428
|
//#region source/plugins/seo/index.ts
|
|
1160
1429
|
function createSeoPlugin() {
|
|
1161
1430
|
return {
|
|
1162
1431
|
name: "clarify:seo",
|
|
1163
|
-
hooks: {
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1432
|
+
hooks: { "build:assets"(ctx) {
|
|
1433
|
+
const sitemap = createSitemapXml(ctx.routes, ctx.projectConfig);
|
|
1434
|
+
const robots = createRobotsTxt(ctx.projectConfig);
|
|
1435
|
+
if (!sitemap || !robots) return [];
|
|
1436
|
+
return [{
|
|
1437
|
+
fileName: "sitemap.xml",
|
|
1438
|
+
source: sitemap
|
|
1439
|
+
}, {
|
|
1440
|
+
fileName: "robots.txt",
|
|
1441
|
+
source: robots
|
|
1442
|
+
}];
|
|
1167
1443
|
} }
|
|
1168
1444
|
};
|
|
1169
1445
|
}
|
|
@@ -1200,9 +1476,51 @@ function createSourceLinksPlugin() {
|
|
|
1200
1476
|
};
|
|
1201
1477
|
}
|
|
1202
1478
|
//#endregion
|
|
1479
|
+
//#region source/parsers/variables.ts
|
|
1480
|
+
var variablePattern = /{{\s*([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)\s*}}/g;
|
|
1481
|
+
function getVariableValue(variables, path) {
|
|
1482
|
+
let current = variables;
|
|
1483
|
+
for (const segment of path.split(".")) {
|
|
1484
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
|
|
1485
|
+
current = current[segment];
|
|
1486
|
+
}
|
|
1487
|
+
return current;
|
|
1488
|
+
}
|
|
1489
|
+
function stringifyVariableValue(variables, path, stack) {
|
|
1490
|
+
if (stack.includes(path)) throw new Error(`[clarify] variable "${path}" contains a circular reference: ${[...stack, path].join(" -> ")}`);
|
|
1491
|
+
const value = getVariableValue(variables, path);
|
|
1492
|
+
if (value === void 0 || typeof value === "object") return void 0;
|
|
1493
|
+
if (typeof value !== "string") return String(value);
|
|
1494
|
+
return value.replace(variablePattern, (match, nestedPath) => stringifyVariableValue(variables, nestedPath, [...stack, path]) ?? match);
|
|
1495
|
+
}
|
|
1496
|
+
function applyVariables(content, variables = {}) {
|
|
1497
|
+
return content.replace(variablePattern, (match, path) => stringifyVariableValue(variables, path, []) ?? match);
|
|
1498
|
+
}
|
|
1499
|
+
//#endregion
|
|
1500
|
+
//#region source/plugins/variables/index.ts
|
|
1501
|
+
function applyVariablesToFrontmatter(frontmatter, variables) {
|
|
1502
|
+
const nextFrontmatter = { ...frontmatter };
|
|
1503
|
+
for (const key of ["title", "description"]) {
|
|
1504
|
+
if (typeof nextFrontmatter[key] !== "string") continue;
|
|
1505
|
+
nextFrontmatter[key] = applyVariables(nextFrontmatter[key], variables);
|
|
1506
|
+
}
|
|
1507
|
+
return nextFrontmatter;
|
|
1508
|
+
}
|
|
1509
|
+
function createVariablesPlugin() {
|
|
1510
|
+
return {
|
|
1511
|
+
name: "clarify:variables",
|
|
1512
|
+
hooks: { "content:transform": (input, ctx) => ({
|
|
1513
|
+
...input,
|
|
1514
|
+
frontmatter: applyVariablesToFrontmatter(input.frontmatter, ctx.projectConfig.variables),
|
|
1515
|
+
content: applyVariables(input.content, ctx.projectConfig.variables)
|
|
1516
|
+
}) }
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
//#endregion
|
|
1203
1520
|
//#region source/core/builtin.ts
|
|
1204
1521
|
function createBuiltinPlugins(options = {}) {
|
|
1205
1522
|
const plugins = [
|
|
1523
|
+
createVariablesPlugin(),
|
|
1206
1524
|
createOpenAPIPlugin(),
|
|
1207
1525
|
createSourceLinksPlugin(),
|
|
1208
1526
|
createContentArtifactsPlugin(),
|
|
@@ -1261,18 +1579,23 @@ var clarifyNavbarLinkSchema = z.object({
|
|
|
1261
1579
|
href: z.string(),
|
|
1262
1580
|
external: z.boolean().optional()
|
|
1263
1581
|
});
|
|
1264
|
-
var
|
|
1582
|
+
var clarifyBannerConfigSchema = z.object({
|
|
1265
1583
|
content: clarifyLocalizedTextSchema,
|
|
1266
1584
|
dismissible: z.boolean().optional(),
|
|
1267
1585
|
link: clarifyNavbarLinkSchema.optional()
|
|
1268
1586
|
}).strict();
|
|
1269
|
-
var
|
|
1270
|
-
var clarifyFooterLinksConfigSchema = z.object({
|
|
1587
|
+
var clarifyFooterConfigSchema = z.object({
|
|
1271
1588
|
links: z.array(clarifyNavbarLinkSchema).optional(),
|
|
1272
1589
|
socials: z.record(z.string(), z.string()).optional(),
|
|
1273
1590
|
copyright: clarifyLocalizedTextSchema.optional()
|
|
1274
1591
|
}).strict();
|
|
1275
|
-
var
|
|
1592
|
+
var clarifyVariableValueSchema = z.lazy(() => z.union([
|
|
1593
|
+
z.string(),
|
|
1594
|
+
z.number(),
|
|
1595
|
+
z.boolean(),
|
|
1596
|
+
z.record(z.string(), clarifyVariableValueSchema)
|
|
1597
|
+
]));
|
|
1598
|
+
var clarifyVariablesConfigSchema = z.record(z.string(), clarifyVariableValueSchema);
|
|
1276
1599
|
var clarifySourceConfigSchema = z.object({
|
|
1277
1600
|
repository: z.string(),
|
|
1278
1601
|
branch: z.string().optional(),
|
|
@@ -1350,9 +1673,11 @@ var clarifyProjectConfigSchema = z.object({
|
|
|
1350
1673
|
favicon: clarifyFaviconConfigSchema.optional(),
|
|
1351
1674
|
theme: clarifyThemeConfigSchema.optional(),
|
|
1352
1675
|
routePrefix: z.string().optional(),
|
|
1676
|
+
assetPrefix: z.string().optional(),
|
|
1353
1677
|
navbar: z.object({ links: z.array(clarifyNavbarLinkSchema).optional() }).optional(),
|
|
1354
1678
|
banner: clarifyBannerConfigSchema.optional(),
|
|
1355
1679
|
footer: clarifyFooterConfigSchema.optional(),
|
|
1680
|
+
variables: clarifyVariablesConfigSchema.optional(),
|
|
1356
1681
|
i18n: clarifyI18nConfigSchema.optional(),
|
|
1357
1682
|
tabs: clarifyTabsConfigSchema.optional()
|
|
1358
1683
|
});
|
|
@@ -1412,7 +1737,22 @@ function resolveI18nConfig(i18n) {
|
|
|
1412
1737
|
locales: i18n.locales
|
|
1413
1738
|
};
|
|
1414
1739
|
}
|
|
1740
|
+
function resolveRoutePrefix(routePrefix) {
|
|
1741
|
+
const trimmed = routePrefix?.trim();
|
|
1742
|
+
if (!trimmed || trimmed === "/") return "/";
|
|
1743
|
+
return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
|
|
1744
|
+
}
|
|
1745
|
+
function resolveAssetPrefix(assetPrefix, routePrefix) {
|
|
1746
|
+
const trimmed = assetPrefix?.trim();
|
|
1747
|
+
if (trimmed === void 0) return routePrefix;
|
|
1748
|
+
if (trimmed === "./" || trimmed === "") return trimmed || "/";
|
|
1749
|
+
if (!trimmed || trimmed === "/") return "/";
|
|
1750
|
+
if (/^[a-z][a-z\d+.-]*:\/\//i.test(trimmed)) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
1751
|
+
if (trimmed.startsWith("./") || trimmed.startsWith("../")) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
1752
|
+
return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
|
|
1753
|
+
}
|
|
1415
1754
|
function resolveProjectConfig(config = {}) {
|
|
1755
|
+
const routePrefix = resolveRoutePrefix(config.routePrefix);
|
|
1416
1756
|
return {
|
|
1417
1757
|
title: config.title ?? "Clarify Docs",
|
|
1418
1758
|
description: config.description ?? "",
|
|
@@ -1421,51 +1761,64 @@ function resolveProjectConfig(config = {}) {
|
|
|
1421
1761
|
logo: config.logo,
|
|
1422
1762
|
homeUrl: config.homeUrl,
|
|
1423
1763
|
favicon: config.favicon,
|
|
1424
|
-
routePrefix
|
|
1764
|
+
routePrefix,
|
|
1765
|
+
assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
|
|
1425
1766
|
theme: resolveThemeConfig(config.theme),
|
|
1426
1767
|
navbar: config.navbar,
|
|
1427
1768
|
banner: config.banner,
|
|
1428
1769
|
footer: config.footer,
|
|
1770
|
+
variables: config.variables ?? {},
|
|
1429
1771
|
i18n: resolveI18nConfig(config.i18n),
|
|
1430
1772
|
tabs: config.tabs
|
|
1431
1773
|
};
|
|
1432
1774
|
}
|
|
1433
1775
|
//#endregion
|
|
1434
|
-
//#region source/core/
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1776
|
+
//#region source/core/env-types.ts
|
|
1777
|
+
var GENERATED_FILE_NAME = "clarify-env.d.ts";
|
|
1778
|
+
/**
|
|
1779
|
+
* Template for the generated `clarify-env.d.ts` file.
|
|
1780
|
+
* Edit this template directly if you need to change the type declarations.
|
|
1781
|
+
* This avoids the complexity of reading external files at runtime after bundling.
|
|
1782
|
+
*/
|
|
1783
|
+
var CLARIFY_ENV_TEMPLATE = `// This file is generated by Clarify. Do not edit it manually.
|
|
1784
|
+
// It provides ambient type declarations for Clarify virtual modules used in
|
|
1785
|
+
// plugin and slot code, so the TypeScript language service can resolve them.
|
|
1786
|
+
|
|
1787
|
+
declare module 'virtual:clarify/slot' {
|
|
1788
|
+
import type { SlotContext } from '@clarify-labs/cli'
|
|
1789
|
+
|
|
1790
|
+
export function useSlot(): SlotContext
|
|
1447
1791
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1792
|
+
|
|
1793
|
+
declare module 'virtual:clarify/slots' {
|
|
1794
|
+
import type { ComponentType } from 'react'
|
|
1795
|
+
|
|
1796
|
+
export type RuntimeSlotEntry = {
|
|
1797
|
+
plugin: string
|
|
1798
|
+
/** Lazy import factory — yields the default-exported React component. */
|
|
1799
|
+
component: () => Promise<{ default: ComponentType }>
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
export const runtimeSlots: Record<string, RuntimeSlotEntry[]>
|
|
1458
1803
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1804
|
+
`;
|
|
1805
|
+
function generateClarifyEnvDts(_plugins) {
|
|
1806
|
+
return CLARIFY_ENV_TEMPLATE;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Writes `clarify-env.d.ts` into the project root. Only rewrites when content
|
|
1810
|
+
* changes to avoid triggering unnecessary file watchers, and never overwrites a
|
|
1811
|
+
* user-authored file that does not carry the generated marker.
|
|
1812
|
+
*/
|
|
1813
|
+
function writeClarifyEnvDts(projectRoot, plugins) {
|
|
1814
|
+
const filePath = join(projectRoot, GENERATED_FILE_NAME);
|
|
1815
|
+
const next = generateClarifyEnvDts(plugins);
|
|
1816
|
+
if (existsSync(filePath)) {
|
|
1817
|
+
const current = readFileSync(filePath, "utf-8");
|
|
1818
|
+
if (current === next) return;
|
|
1819
|
+
if (!current.startsWith("// This file is generated by Clarify.")) return;
|
|
1468
1820
|
}
|
|
1821
|
+
writeFileSync(filePath, next, "utf-8");
|
|
1469
1822
|
}
|
|
1470
1823
|
//#endregion
|
|
1471
1824
|
//#region source/core/options.ts
|
|
@@ -1479,23 +1832,11 @@ function resolveBuildOptions(options = {}) {
|
|
|
1479
1832
|
}
|
|
1480
1833
|
//#endregion
|
|
1481
1834
|
//#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
1835
|
async function discoverRoutesForRoot(routeRoot, locale, plugins, ctx) {
|
|
1495
1836
|
return (await runHooks(plugins, "routes:discover", {
|
|
1496
1837
|
contentRoot: routeRoot,
|
|
1497
1838
|
locale,
|
|
1498
|
-
routes: findContentRoutes(routeRoot)
|
|
1839
|
+
routes: await findContentRoutes(routeRoot, routeRoot, { contentProcessor: getProjectContentProcessor(ctx) })
|
|
1499
1840
|
}, ctx)).routes;
|
|
1500
1841
|
}
|
|
1501
1842
|
async function discoverRoutes(root, contentRoot, plugins, ctx) {
|
|
@@ -1532,7 +1873,21 @@ async function discoverRoutes(root, contentRoot, plugins, ctx) {
|
|
|
1532
1873
|
}
|
|
1533
1874
|
}
|
|
1534
1875
|
}
|
|
1535
|
-
|
|
1876
|
+
const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, i18n));
|
|
1877
|
+
const defaultLocale = i18n.defaultLocale;
|
|
1878
|
+
const bareRoutes = [];
|
|
1879
|
+
const seenBare = new Set(routesWithAlternates.map((r) => r.path));
|
|
1880
|
+
for (const route of routesWithAlternates) {
|
|
1881
|
+
if (route.locale !== defaultLocale) continue;
|
|
1882
|
+
const bp = route.basePath ?? route.path;
|
|
1883
|
+
if (bp === route.path || seenBare.has(bp)) continue;
|
|
1884
|
+
seenBare.add(bp);
|
|
1885
|
+
bareRoutes.push({
|
|
1886
|
+
...route,
|
|
1887
|
+
path: bp
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
return [...routesWithAlternates, ...bareRoutes];
|
|
1536
1891
|
}
|
|
1537
1892
|
async function resolveClarifySite(options = {}, resolveOptions = {}) {
|
|
1538
1893
|
const root = options.projectRoot ?? process.cwd();
|
|
@@ -1546,6 +1901,7 @@ async function resolveClarifySite(options = {}, resolveOptions = {}) {
|
|
|
1546
1901
|
routes: [],
|
|
1547
1902
|
navigation: []
|
|
1548
1903
|
};
|
|
1904
|
+
setProjectContentProcessor(ctx, createProjectContentProcessor(plugins, ctx));
|
|
1549
1905
|
let routes = await discoverRoutes(root, contentRoot, plugins, ctx);
|
|
1550
1906
|
routes = await runHooks(plugins, "routes:discovered", routes, ctx);
|
|
1551
1907
|
routes = applyConfiguredPageRoutePaths(routes, projectConfig.tabs, projectConfig.i18n);
|
|
@@ -1608,13 +1964,21 @@ function createClarifyRuntimeAliases() {
|
|
|
1608
1964
|
find: /^@clarify-labs\/renderer$/,
|
|
1609
1965
|
replacement: resolvePackageExport("@clarify-labs/renderer", "")
|
|
1610
1966
|
},
|
|
1967
|
+
{
|
|
1968
|
+
find: /^react\/jsx-dev-runtime$/,
|
|
1969
|
+
replacement: resolveCliDependency("react/jsx-dev-runtime")
|
|
1970
|
+
},
|
|
1611
1971
|
{
|
|
1612
1972
|
find: /^react\/jsx-runtime$/,
|
|
1613
1973
|
replacement: resolveCliDependency("react/jsx-runtime")
|
|
1614
1974
|
},
|
|
1615
1975
|
{
|
|
1616
|
-
find: /^
|
|
1617
|
-
replacement: resolveCliDependency("
|
|
1976
|
+
find: /^mermaid$/,
|
|
1977
|
+
replacement: resolveCliDependency("mermaid")
|
|
1978
|
+
},
|
|
1979
|
+
{
|
|
1980
|
+
find: /^react-zoom-pan-pinch$/,
|
|
1981
|
+
replacement: resolveCliDependency("react-zoom-pan-pinch")
|
|
1618
1982
|
},
|
|
1619
1983
|
{
|
|
1620
1984
|
find: /^react$/,
|
|
@@ -1656,11 +2020,24 @@ function setNamedMeta(html, name, content) {
|
|
|
1656
2020
|
if (metaPattern.test(html)) return html.replace(metaPattern, meta);
|
|
1657
2021
|
return html.replace("</head>", ` ${meta}\n </head>`);
|
|
1658
2022
|
}
|
|
2023
|
+
function canonicalUrl(projectConfig, route) {
|
|
2024
|
+
if (!projectConfig.siteUrl || !route.alternates || !route.locale) return void 0;
|
|
2025
|
+
const prefixedPath = route.alternates[route.locale];
|
|
2026
|
+
if (!prefixedPath || prefixedPath === route.path) return void 0;
|
|
2027
|
+
return `${projectConfig.siteUrl.replace(/\/+$/, "")}${!projectConfig.routePrefix || projectConfig.routePrefix === "/" ? "" : `/${projectConfig.routePrefix.replace(/^\/+|\/+$/g, "")}`}${prefixedPath}`;
|
|
2028
|
+
}
|
|
2029
|
+
function injectCanonicalUrl(html, projectConfig, route) {
|
|
2030
|
+
const url = route ? canonicalUrl(projectConfig, route) : void 0;
|
|
2031
|
+
html = html.replace(/<link\b[^>]*\brel=["']canonical["'][^>]*\/?>\n?/gi, "");
|
|
2032
|
+
if (!url) return html;
|
|
2033
|
+
return html.replace("</head>", ` <link rel="canonical" href="${escapeHtml(url)}" />\n </head>`);
|
|
2034
|
+
}
|
|
1659
2035
|
function injectSSRIntoTemplate(template, appHtml, projectConfig, route) {
|
|
1660
2036
|
let html = injectHtmlLocaleAttributes(template, projectConfig, route);
|
|
1661
2037
|
html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(routeTitle(projectConfig, route))}</title>`);
|
|
1662
2038
|
html = setNamedMeta(html, "description", route?.description ?? projectConfig.description);
|
|
1663
2039
|
html = setNamedMeta(html, "keywords", route?.keywords?.join(", "));
|
|
2040
|
+
html = injectCanonicalUrl(html, projectConfig, route);
|
|
1664
2041
|
html = html.replace(/<div id="root">[\s\S]*?<\/div>/, `<div id="root">${appHtml}</div>`);
|
|
1665
2042
|
return html;
|
|
1666
2043
|
}
|
|
@@ -1673,12 +2050,20 @@ function routeOutputFiles(outputDirectory, route) {
|
|
|
1673
2050
|
return files;
|
|
1674
2051
|
}
|
|
1675
2052
|
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
|
|
2053
|
+
import { routes, navigation } from 'virtual:clarify/routes/server';
|
|
2054
|
+
import { config } from 'virtual:clarify/config';
|
|
2055
|
+
import { openApis } from 'virtual:clarify/openapi';
|
|
2056
|
+
import { runtimeSlots } from 'virtual:clarify/slots';
|
|
1679
2057
|
|
|
1680
|
-
export function render(url) {
|
|
1681
|
-
|
|
2058
|
+
export async function render(url) {
|
|
2059
|
+
// Pre-resolve every slot component factory so renderToString can use
|
|
2060
|
+
// them synchronously (renderToString has no Suspense support).
|
|
2061
|
+
const entries = Object.values(runtimeSlots).flat()
|
|
2062
|
+
await Promise.all(entries.map(async (entry) => {
|
|
2063
|
+
const mod = await entry.component()
|
|
2064
|
+
entry._resolved = mod.default
|
|
2065
|
+
}))
|
|
2066
|
+
return renderToHTML({ config, routes, navigation, openApis, runtimeSlots, url, themeEditor: config.theme.editor });
|
|
1682
2067
|
}`;
|
|
1683
2068
|
function createTempEntryFile(content) {
|
|
1684
2069
|
const entryPath = join(createClarifyTempDir("ssr"), "entry-server.ts");
|
|
@@ -1708,7 +2093,7 @@ async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundle
|
|
|
1708
2093
|
const template = readIndexHtml(outputDirectory);
|
|
1709
2094
|
if (!template) throw new Error(`[clarify] index.html not found in outputDirectory "${outputDirectory}". Make sure Vite build produces it.`);
|
|
1710
2095
|
for (const route of routes) try {
|
|
1711
|
-
const finalHtml = injectSSRIntoTemplate(template, render(route.path), projectConfig, route);
|
|
2096
|
+
const finalHtml = injectSSRIntoTemplate(template, await render(route.path), projectConfig, route);
|
|
1712
2097
|
for (const outFile of routeOutputFiles(outputDirectory, route)) {
|
|
1713
2098
|
mkdirSync(dirname(outFile), { recursive: true });
|
|
1714
2099
|
writeFileSync(outFile, finalHtml, "utf-8");
|
|
@@ -1777,23 +2162,8 @@ function clarifyPlugin(options = {}) {
|
|
|
1777
2162
|
let viteConfig;
|
|
1778
2163
|
let resolvedNavigation = [];
|
|
1779
2164
|
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);
|
|
2165
|
+
async function resolveRoutesAndSpecs(overrides) {
|
|
2166
|
+
const site = await resolveClarifySite(overrides ?? options);
|
|
1797
2167
|
projectConfig = site.projectConfig;
|
|
1798
2168
|
generateOptions = site.generateOptions;
|
|
1799
2169
|
routes = site.routes;
|
|
@@ -1810,6 +2180,7 @@ function clarifyPlugin(options = {}) {
|
|
|
1810
2180
|
generateOptions,
|
|
1811
2181
|
routes,
|
|
1812
2182
|
navigation: resolvedNavigation,
|
|
2183
|
+
plugins: clarifyPlugins,
|
|
1813
2184
|
themeEditor: viteConfig.command === "serve" || projectConfig.theme.editor
|
|
1814
2185
|
});
|
|
1815
2186
|
virtualModules = await runHooks(clarifyPlugins, "modules:before", virtualModules, ctx);
|
|
@@ -1859,7 +2230,7 @@ function clarifyPlugin(options = {}) {
|
|
|
1859
2230
|
async config() {
|
|
1860
2231
|
await resolveRoutesAndSpecs();
|
|
1861
2232
|
return {
|
|
1862
|
-
base: projectConfig.
|
|
2233
|
+
base: projectConfig.assetPrefix,
|
|
1863
2234
|
build: {
|
|
1864
2235
|
...generateOptions.outputDirectory ? { outDir: generateOptions.outputDirectory } : {},
|
|
1865
2236
|
manifest: true
|
|
@@ -1872,12 +2243,23 @@ function clarifyPlugin(options = {}) {
|
|
|
1872
2243
|
},
|
|
1873
2244
|
async buildStart() {
|
|
1874
2245
|
await rebuildVirtualModules();
|
|
2246
|
+
writeClarifyEnvDts(root, clarifyPlugins);
|
|
1875
2247
|
},
|
|
1876
2248
|
async handleHotUpdate(ctx) {
|
|
1877
2249
|
const changedFile = isAbsolute(ctx.file) ? ctx.file : join(root, ctx.file);
|
|
1878
2250
|
if (configFilePath && changedFile === configFilePath) {
|
|
1879
|
-
await
|
|
1880
|
-
|
|
2251
|
+
await resolveRoutesAndSpecs({
|
|
2252
|
+
...await loadClarifyConfig(root, {
|
|
2253
|
+
command: "serve",
|
|
2254
|
+
mode: viteConfig.mode
|
|
2255
|
+
}),
|
|
2256
|
+
projectRoot: options.projectRoot,
|
|
2257
|
+
rootDirectory: options.rootDirectory,
|
|
2258
|
+
outputDirectory: options.outputDirectory
|
|
2259
|
+
});
|
|
2260
|
+
await rebuildVirtualModules();
|
|
2261
|
+
invalidateVirtualModules(ctx.server);
|
|
2262
|
+
ctx.server.ws.send({ type: "full-reload" });
|
|
1881
2263
|
return [];
|
|
1882
2264
|
}
|
|
1883
2265
|
const relativeContentFile = relative(contentRoot, changedFile);
|
|
@@ -1886,10 +2268,13 @@ function clarifyPlugin(options = {}) {
|
|
|
1886
2268
|
return [];
|
|
1887
2269
|
},
|
|
1888
2270
|
resolveId(id) {
|
|
1889
|
-
if (id === "virtual:clarify
|
|
1890
|
-
if (id === "virtual:clarify
|
|
1891
|
-
if (id === "virtual:clarify
|
|
1892
|
-
if (id === "virtual:clarify
|
|
2271
|
+
if (id === "virtual:clarify/entry-client" || id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
|
|
2272
|
+
if (id === "virtual:clarify/config" || id === resolveVirtualId("virtual:clarify/config")) return resolveVirtualId(VIRTUAL_CONFIG);
|
|
2273
|
+
if (id === "virtual:clarify/routes" || id === resolveVirtualId("virtual:clarify/routes")) return resolveVirtualId(VIRTUAL_ROUTES);
|
|
2274
|
+
if (id === "virtual:clarify/routes/server" || id === resolveVirtualId("virtual:clarify/routes/server")) return resolveVirtualId(VIRTUAL_SERVER_ROUTES);
|
|
2275
|
+
if (id === "virtual:clarify/openapi" || id === resolveVirtualId("virtual:clarify/openapi")) return resolveVirtualId(VIRTUAL_OPENAPI);
|
|
2276
|
+
if (id === "virtual:clarify/slots" || id === resolveVirtualId("virtual:clarify/slots")) return resolveVirtualId(VIRTUAL_SLOTS);
|
|
2277
|
+
if (id === "virtual:clarify/slot" || id === resolveVirtualId("virtual:clarify/slot")) return resolveVirtualId(VIRTUAL_SLOT);
|
|
1893
2278
|
const moduleId = stripVirtualPrefix(id);
|
|
1894
2279
|
if (virtualModules.has(moduleId)) return resolveVirtualId(moduleId);
|
|
1895
2280
|
const route = routes.find((r) => r.virtualModuleId === id || r.virtualModuleId === moduleId);
|
|
@@ -1927,6 +2312,14 @@ function clarifyPlugin(options = {}) {
|
|
|
1927
2312
|
};
|
|
1928
2313
|
}
|
|
1929
2314
|
},
|
|
2315
|
+
async generateBundle(_opts, _bundle) {
|
|
2316
|
+
const assets = await runBuildAssetsHooks(clarifyPlugins, ctx);
|
|
2317
|
+
for (const asset of assets) this.emitFile({
|
|
2318
|
+
type: "asset",
|
|
2319
|
+
fileName: asset.fileName,
|
|
2320
|
+
source: asset.source
|
|
2321
|
+
});
|
|
2322
|
+
},
|
|
1930
2323
|
async closeBundle() {
|
|
1931
2324
|
if (process.env.SKIP_CLARIFY_SSG) {
|
|
1932
2325
|
await runBuildDoneHooks(clarifyPlugins, ctx);
|
|
@@ -1941,11 +2334,14 @@ function clarifyPlugin(options = {}) {
|
|
|
1941
2334
|
{
|
|
1942
2335
|
name: "clarify:virtual-ssg",
|
|
1943
2336
|
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
|
|
2337
|
+
if (id === "virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
|
|
2338
|
+
if (id === "\0virtual:clarify/entry-client") return RESOLVED_CLIENT_ENTRY;
|
|
2339
|
+
if (id === "virtual:clarify/routes/server") return id;
|
|
2340
|
+
if (id === "virtual:clarify/openapi") return id;
|
|
2341
|
+
if (id === "virtual:clarify/config") return id;
|
|
2342
|
+
if (id === "virtual:clarify/routes") return id;
|
|
2343
|
+
if (id === "virtual:clarify/slots") return id;
|
|
2344
|
+
if (id === "virtual:clarify/slot") return id;
|
|
1949
2345
|
if (virtualModules.has(stripVirtualPrefix(id))) return stripVirtualPrefix(id);
|
|
1950
2346
|
if (routes.find((r) => r.virtualModuleId === id)) return id;
|
|
1951
2347
|
return null;
|
|
@@ -1974,4 +2370,4 @@ function clarifyPlugin(options = {}) {
|
|
|
1974
2370
|
//#endregion
|
|
1975
2371
|
export { resolveClarifySite as a, createClarifyRuntimeAliases as i, defineConfig as n, createClarifyTempDir as o, loadClarifyConfig as r, clarifyPlugin as t };
|
|
1976
2372
|
|
|
1977
|
-
//# sourceMappingURL=plugin-
|
|
2373
|
+
//# sourceMappingURL=plugin-D5EF3pYl.js.map
|