@clarify-labs/cli 0.10.3 → 0.11.1
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/commands/mcp.d.ts +27 -0
- package/output/cli/mcp/orama-loader.d.ts +56 -0
- package/output/cli/mcp/search-server.d.ts +34 -0
- package/output/cli/program.d.ts +0 -1
- package/output/cli.cjs +294 -13
- package/output/cli.cjs.map +1 -1
- package/output/cli.js +295 -14
- package/output/cli.js.map +1 -1
- package/output/core/config/config-schema.d.ts +138 -253
- package/output/core/config/config.d.ts +2 -1
- package/output/core/config/options.d.ts +0 -8
- package/output/core/config/user-config.d.ts +4 -3
- package/output/core/plugin/hooks.d.ts +0 -1
- package/output/core/project/project-info.d.ts +3 -3
- package/output/core/runtime/html-template.d.ts +0 -6
- package/output/core/runtime/ssg.d.ts +1 -1
- package/output/core/runtime/virtual-modules.d.ts +1 -5
- package/output/{engine-BDdPszs2.cjs → engine-Cbn_coqg.cjs} +569 -170
- package/output/engine-Cbn_coqg.cjs.map +1 -0
- package/output/{engine-CyFtr8Pz.js → engine-CxnKEKv8.js} +558 -171
- package/output/engine-CxnKEKv8.js.map +1 -0
- package/output/index.cjs +1 -1
- package/output/index.js +1 -1
- package/output/parsers/routes/routes.d.ts +7 -11
- package/output/parsers/routes/routes.test-utils.d.ts +2 -2
- package/output/plugins/content-artifacts/artifacts.d.ts +0 -2
- package/output/plugins/mcp-search/index.d.ts +15 -0
- package/output/plugins/mcp-search/mcp-config.d.ts +72 -0
- package/output/plugins/mcp-search/orama-index.d.ts +153 -0
- package/output/plugins/openapi/virtual-modules.d.ts +0 -1
- package/output/plugins/page-search/index.d.ts +2 -0
- package/output/plugins/source-links/source-links.d.ts +3 -3
- package/output/types.d.ts +64 -33
- package/package.json +6 -4
- package/output/engine-BDdPszs2.cjs.map +0 -1
- package/output/engine-CyFtr8Pz.js.map +0 -1
- package/output/plugins/search-index/index.d.ts +0 -2
- /package/output/plugins/{search-index → page-search}/search-language.d.ts +0 -0
|
@@ -18,6 +18,8 @@ import { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rea
|
|
|
18
18
|
import { build, loadConfigFromFile } from "vite";
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import { stringify } from "yaml";
|
|
21
|
+
import { decode, encode } from "@msgpack/msgpack";
|
|
22
|
+
import { create, insertMultiple, load, save, search } from "@orama/orama";
|
|
21
23
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
22
24
|
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
23
25
|
import { tmpdir } from "node:os";
|
|
@@ -379,10 +381,10 @@ function frontmatterKeywords(frontmatter) {
|
|
|
379
381
|
const keywords = Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" ? value.split(",").map((keyword) => keyword.trim()).filter(Boolean) : [];
|
|
380
382
|
return keywords.length > 0 ? keywords : void 0;
|
|
381
383
|
}
|
|
382
|
-
function withAlternates(route, routes,
|
|
384
|
+
function withAlternates(route, routes, locales) {
|
|
383
385
|
const basePath = route.basePath ?? route.path;
|
|
384
386
|
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]));
|
|
385
|
-
const alternates = Object.fromEntries(
|
|
387
|
+
const alternates = Object.fromEntries(locales.locales.flatMap((locale) => {
|
|
386
388
|
const alternate = routeByLocaleAndBase.get(`${locale.code}:${basePath}`);
|
|
387
389
|
return alternate ? [[locale.code, alternate.path]] : [];
|
|
388
390
|
}));
|
|
@@ -395,11 +397,11 @@ function withAlternates(route, routes, i18n) {
|
|
|
395
397
|
* 为默认语言的 locale-prefixed 路由生成无前缀的"裸路径别名",方便不带语言
|
|
396
398
|
* 前缀的 URL 也能访问。标记 `isBareAlias` 以便搜索索引生成时过滤重复索引。
|
|
397
399
|
*/
|
|
398
|
-
function generateBareAliases(routes,
|
|
400
|
+
function generateBareAliases(routes, locales) {
|
|
399
401
|
const bareRoutes = [];
|
|
400
402
|
const seenBare = new Set(routes.map((r) => r.path));
|
|
401
403
|
for (const route of routes) {
|
|
402
|
-
if (route.locale !==
|
|
404
|
+
if (route.locale !== locales.default) continue;
|
|
403
405
|
const bp = route.basePath ?? route.path;
|
|
404
406
|
if (bp === route.path || seenBare.has(bp)) continue;
|
|
405
407
|
seenBare.add(bp);
|
|
@@ -411,42 +413,42 @@ function generateBareAliases(routes, i18n) {
|
|
|
411
413
|
}
|
|
412
414
|
return bareRoutes;
|
|
413
415
|
}
|
|
414
|
-
async function findLocalizedContentRoutes(contentRoot,
|
|
415
|
-
if (!
|
|
416
|
+
async function findLocalizedContentRoutes(contentRoot, locales, options = {}) {
|
|
417
|
+
if (!locales) return findContentRoutes(contentRoot, contentRoot, options);
|
|
416
418
|
const localizedRoutes = [];
|
|
417
|
-
for (const locale of
|
|
419
|
+
for (const locale of locales.locales) {
|
|
418
420
|
const localeRoot = join(contentRoot, locale.code);
|
|
419
421
|
const discovered = await findContentRoutes(localeRoot, localeRoot, options);
|
|
420
422
|
for (const route of discovered) {
|
|
421
423
|
const basePath = route.basePath ?? route.path;
|
|
422
424
|
localizedRoutes.push({
|
|
423
425
|
...route,
|
|
424
|
-
path: localizedRoutePath(basePath, locale.code,
|
|
426
|
+
path: localizedRoutePath(basePath, locale.code, locales),
|
|
425
427
|
basePath,
|
|
426
428
|
locale: locale.code,
|
|
427
429
|
module: { virtualModuleId: virtualModuleIdForLocalizedRoute(contentRoot, route.source.filePath) }
|
|
428
430
|
});
|
|
429
431
|
}
|
|
430
432
|
}
|
|
431
|
-
if (
|
|
433
|
+
if (locales.missing === "fallback") {
|
|
432
434
|
const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
|
|
433
|
-
const defaultRoutes = localizedRoutes.filter((route) => route.locale ===
|
|
435
|
+
const defaultRoutes = localizedRoutes.filter((route) => route.locale === locales.default);
|
|
434
436
|
for (const sourceRoute of defaultRoutes) {
|
|
435
437
|
const basePath = sourceRoute.basePath ?? sourceRoute.path;
|
|
436
|
-
for (const locale of
|
|
438
|
+
for (const locale of locales.locales) {
|
|
437
439
|
const key = `${locale.code}:${basePath}`;
|
|
438
440
|
if (routeByLocaleAndBase.has(key)) continue;
|
|
439
441
|
localizedRoutes.push({
|
|
440
442
|
...sourceRoute,
|
|
441
|
-
path: localizedRoutePath(basePath, locale.code,
|
|
443
|
+
path: localizedRoutePath(basePath, locale.code, locales),
|
|
442
444
|
locale: locale.code,
|
|
443
445
|
isFallback: true
|
|
444
446
|
});
|
|
445
447
|
}
|
|
446
448
|
}
|
|
447
449
|
}
|
|
448
|
-
const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes,
|
|
449
|
-
return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates,
|
|
450
|
+
const routesWithAlternates = localizedRoutes.map((route) => withAlternates(route, localizedRoutes, locales));
|
|
451
|
+
return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, locales)];
|
|
450
452
|
}
|
|
451
453
|
function buildNavigation(routes) {
|
|
452
454
|
const root = [];
|
|
@@ -523,7 +525,7 @@ function collectExplicitPagePathIntents(tabs) {
|
|
|
523
525
|
}
|
|
524
526
|
return intents;
|
|
525
527
|
}
|
|
526
|
-
function applyConfiguredPageRoutePaths(routes, tabs,
|
|
528
|
+
function applyConfiguredPageRoutePaths(routes, tabs, locales) {
|
|
527
529
|
const pageIntents = collectExplicitPagePathIntents(tabs);
|
|
528
530
|
if (!pageIntents.length) return routes;
|
|
529
531
|
const additions = [];
|
|
@@ -539,15 +541,15 @@ function applyConfiguredPageRoutePaths(routes, tabs, i18n) {
|
|
|
539
541
|
existingKeys.add(key);
|
|
540
542
|
additions.push({
|
|
541
543
|
...route,
|
|
542
|
-
path: route.locale &&
|
|
544
|
+
path: route.locale && locales ? localizedRoutePath(targetBasePath, route.locale, locales) : targetBasePath,
|
|
543
545
|
basePath: targetBasePath
|
|
544
546
|
});
|
|
545
547
|
}
|
|
546
548
|
}
|
|
547
549
|
const nextRoutes = [...routes, ...additions];
|
|
548
|
-
const routesWithAlternates =
|
|
549
|
-
if (!
|
|
550
|
-
return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates,
|
|
550
|
+
const routesWithAlternates = locales ? nextRoutes.map((route) => withAlternates(route, nextRoutes, locales)) : nextRoutes;
|
|
551
|
+
if (!locales) return routesWithAlternates;
|
|
552
|
+
return [...routesWithAlternates, ...generateBareAliases(routesWithAlternates, locales)];
|
|
551
553
|
}
|
|
552
554
|
function buildNavigationFromPagesConfig(routes, config) {
|
|
553
555
|
if (!config || config === "FileTree") return buildNavigation(routes);
|
|
@@ -610,25 +612,25 @@ function buildNavigationFromConfig(routes, config) {
|
|
|
610
612
|
};
|
|
611
613
|
});
|
|
612
614
|
}
|
|
613
|
-
function localizeNavigationPaths(nodes, locale,
|
|
615
|
+
function localizeNavigationPaths(nodes, locale, locales) {
|
|
614
616
|
return nodes.map((node) => ({
|
|
615
617
|
...node,
|
|
616
|
-
path: localizedRoutePath(node.path, locale,
|
|
617
|
-
children: node.children ? localizeNavigationPaths(node.children, locale,
|
|
618
|
+
path: localizedRoutePath(node.path, locale, locales),
|
|
619
|
+
children: node.children ? localizeNavigationPaths(node.children, locale, locales) : void 0
|
|
618
620
|
}));
|
|
619
621
|
}
|
|
620
|
-
function buildLocalizedNavigationForLocale(routes, config, locale,
|
|
622
|
+
function buildLocalizedNavigationForLocale(routes, config, locale, locales) {
|
|
621
623
|
if (!config || config === "FileTree") return localizeNavigationPaths(buildNavigation(routes.map((route) => ({
|
|
622
624
|
...route,
|
|
623
625
|
path: route.basePath ?? route.path
|
|
624
|
-
}))), locale,
|
|
626
|
+
}))), locale, locales);
|
|
625
627
|
const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
|
|
626
628
|
const buildItem = (item) => {
|
|
627
629
|
if (typeof item !== "string" && "group" in item) {
|
|
628
630
|
const children = item.pages.map(buildItem);
|
|
629
631
|
return {
|
|
630
632
|
path: firstNavigationPath(children),
|
|
631
|
-
title: resolveLocalizedText(item.group, locale,
|
|
633
|
+
title: resolveLocalizedText(item.group, locale, locales.default) ?? "",
|
|
632
634
|
icon: item.icon,
|
|
633
635
|
children
|
|
634
636
|
};
|
|
@@ -636,10 +638,10 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
|
|
|
636
638
|
const intent = routeIntentFromPagesItem(item);
|
|
637
639
|
const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
|
|
638
640
|
const route = routeMap.get(basePath);
|
|
639
|
-
const path = route?.path ?? localizedRoutePath(basePath, locale,
|
|
641
|
+
const path = route?.path ?? localizedRoutePath(basePath, locale, locales);
|
|
640
642
|
return {
|
|
641
|
-
path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale,
|
|
642
|
-
title: resolveLocalizedText(intent.title, locale,
|
|
643
|
+
path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, locales) : void 0) ?? path,
|
|
644
|
+
title: resolveLocalizedText(intent.title, locale, locales.default) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
|
|
643
645
|
icon: intent.icon,
|
|
644
646
|
sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
|
|
645
647
|
};
|
|
@@ -647,24 +649,24 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
|
|
|
647
649
|
return config.map((group) => {
|
|
648
650
|
const children = group.pages.map(buildItem);
|
|
649
651
|
return {
|
|
650
|
-
path: children[0]?.path ?? localizedRoutePath("/", locale,
|
|
651
|
-
title: resolveLocalizedText(group.group, locale,
|
|
652
|
+
path: children[0]?.path ?? localizedRoutePath("/", locale, locales),
|
|
653
|
+
title: resolveLocalizedText(group.group, locale, locales.default) ?? "",
|
|
652
654
|
icon: group.icon,
|
|
653
655
|
children
|
|
654
656
|
};
|
|
655
657
|
});
|
|
656
658
|
}
|
|
657
|
-
function buildLocalizedNavigationFromTabsConfig(routes, tabs,
|
|
658
|
-
if (!
|
|
659
|
+
function buildLocalizedNavigationFromTabsConfig(routes, tabs, localesConfig) {
|
|
660
|
+
if (!localesConfig) return void 0;
|
|
659
661
|
const locales = {};
|
|
660
|
-
for (const locale of
|
|
662
|
+
for (const locale of localesConfig.locales) {
|
|
661
663
|
const localeRoutes = routes.filter((route) => route.locale === locale.code);
|
|
662
664
|
locales[locale.code] = { tabs: tabs.map((tab) => {
|
|
663
|
-
const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code,
|
|
665
|
+
const children = buildLocalizedNavigationForLocale(localeRoutes, tab.pages, locale.code, localesConfig);
|
|
664
666
|
return {
|
|
665
667
|
type: "tab",
|
|
666
668
|
path: firstNavigationPath(children),
|
|
667
|
-
title: resolveLocalizedText(tab.tab, locale.code,
|
|
669
|
+
title: resolveLocalizedText(tab.tab, locale.code, localesConfig.default) ?? "",
|
|
668
670
|
icon: tab.icon,
|
|
669
671
|
children
|
|
670
672
|
};
|
|
@@ -731,7 +733,7 @@ function inferRouteForFile(filePath, contentRoot, defaultLocale) {
|
|
|
731
733
|
const localeSegmentIndex = fileLocale ? segments.indexOf(fileLocale) : -1;
|
|
732
734
|
const basePath = basePathFromRef(localeSegmentIndex >= 0 ? segments.slice(localeSegmentIndex + 1).join("/") : ref);
|
|
733
735
|
const path = fileLocale && defaultLocale ? localizedRoutePath(basePath, fileLocale, {
|
|
734
|
-
defaultLocale,
|
|
736
|
+
default: defaultLocale,
|
|
735
737
|
missing: "fallback",
|
|
736
738
|
locales: []
|
|
737
739
|
}) : basePath;
|
|
@@ -784,12 +786,12 @@ async function handleDevRouteRequest(req, res, routes, context) {
|
|
|
784
786
|
}
|
|
785
787
|
const file = typeof body === "object" && body !== null ? body.file : void 0;
|
|
786
788
|
if (typeof file === "string" && file.length > 0) {
|
|
787
|
-
const route = resolveRouteForFile(file, routes, context.projectConfig.
|
|
789
|
+
const route = resolveRouteForFile(file, routes, context.projectConfig.locales?.default);
|
|
788
790
|
if (route) {
|
|
789
791
|
res.end(JSON.stringify(toDevRouteEntry(route)));
|
|
790
792
|
return;
|
|
791
793
|
}
|
|
792
|
-
const inferred = inferRouteForFile(file, context.contentRoot, context.projectConfig.
|
|
794
|
+
const inferred = inferRouteForFile(file, context.contentRoot, context.projectConfig.locales?.default);
|
|
793
795
|
res.end(JSON.stringify(inferred));
|
|
794
796
|
return;
|
|
795
797
|
}
|
|
@@ -815,8 +817,7 @@ function resolveThemeConfig(theme = {}) {
|
|
|
815
817
|
layout: {
|
|
816
818
|
...presetTheme.layout,
|
|
817
819
|
...theme.layout
|
|
818
|
-
}
|
|
819
|
-
editor: theme.editor ?? presetTheme.editor
|
|
820
|
+
}
|
|
820
821
|
};
|
|
821
822
|
}
|
|
822
823
|
//#endregion
|
|
@@ -824,26 +825,26 @@ function resolveThemeConfig(theme = {}) {
|
|
|
824
825
|
var clarifyLogoConfigSchema = z.union([z.string(), z.object({
|
|
825
826
|
light: z.string().optional(),
|
|
826
827
|
dark: z.string().optional()
|
|
827
|
-
})]);
|
|
828
|
+
}).strict()]);
|
|
828
829
|
var clarifyFaviconConfigSchema = z.union([z.string(), z.object({
|
|
829
830
|
light: z.string().optional(),
|
|
830
831
|
dark: z.string().optional()
|
|
831
|
-
})]);
|
|
832
|
+
}).strict()]);
|
|
832
833
|
var clarifyLocalizedTextSchema = z.union([z.string(), z.record(z.string(), z.string())]);
|
|
833
834
|
var clarifyLocaleConfigSchema = z.object({
|
|
834
835
|
code: z.string(),
|
|
835
836
|
label: z.string(),
|
|
836
837
|
dir: z.union([z.literal("ltr"), z.literal("rtl")]).optional()
|
|
837
|
-
});
|
|
838
|
-
var
|
|
839
|
-
|
|
838
|
+
}).strict();
|
|
839
|
+
var clarifyLocalesConfigSchema = z.object({
|
|
840
|
+
default: z.string().optional(),
|
|
840
841
|
missing: z.union([
|
|
841
842
|
z.literal("fallback"),
|
|
842
843
|
z.literal("404"),
|
|
843
844
|
z.literal("hide")
|
|
844
845
|
]).optional(),
|
|
845
846
|
locales: z.array(clarifyLocaleConfigSchema).min(1)
|
|
846
|
-
}).superRefine((config, ctx) => {
|
|
847
|
+
}).strict().superRefine((config, ctx) => {
|
|
847
848
|
const localeCodes = /* @__PURE__ */ new Set();
|
|
848
849
|
for (const [index, locale] of config.locales.entries()) {
|
|
849
850
|
if (localeCodes.has(locale.code)) ctx.addIssue({
|
|
@@ -857,17 +858,17 @@ var clarifyI18nConfigSchema = z.object({
|
|
|
857
858
|
});
|
|
858
859
|
localeCodes.add(locale.code);
|
|
859
860
|
}
|
|
860
|
-
if (config.
|
|
861
|
+
if (config.default && !localeCodes.has(config.default)) ctx.addIssue({
|
|
861
862
|
code: z.ZodIssueCode.custom,
|
|
862
|
-
message: "
|
|
863
|
-
path: ["
|
|
863
|
+
message: "default must be one of locales.locales",
|
|
864
|
+
path: ["default"]
|
|
864
865
|
});
|
|
865
866
|
});
|
|
866
867
|
var clarifyNavbarLinkSchema = z.object({
|
|
867
868
|
label: clarifyLocalizedTextSchema,
|
|
868
869
|
href: z.string(),
|
|
869
870
|
external: z.boolean().optional()
|
|
870
|
-
});
|
|
871
|
+
}).strict();
|
|
871
872
|
var clarifyBannerConfigOptionsSchema = z.object({
|
|
872
873
|
content: clarifyLocalizedTextSchema,
|
|
873
874
|
dismissible: z.boolean().optional(),
|
|
@@ -885,11 +886,11 @@ var clarifyVariableValueSchema = z.lazy(() => z.union([
|
|
|
885
886
|
z.record(z.string(), clarifyVariableValueSchema)
|
|
886
887
|
]));
|
|
887
888
|
var clarifyVariablesConfigSchema = z.record(z.string(), clarifyVariableValueSchema);
|
|
888
|
-
var
|
|
889
|
-
|
|
889
|
+
var clarifyRepositoryConfigSchema = z.object({
|
|
890
|
+
url: z.string().optional(),
|
|
890
891
|
branch: z.string().optional(),
|
|
891
892
|
directory: z.string().optional()
|
|
892
|
-
});
|
|
893
|
+
}).strict();
|
|
893
894
|
var clarifyPagesItemSchema = z.lazy(() => z.union([
|
|
894
895
|
z.string(),
|
|
895
896
|
clarifyPagesGroupSchema,
|
|
@@ -899,32 +900,32 @@ var clarifyPagesItemSchema = z.lazy(() => z.union([
|
|
|
899
900
|
title: clarifyLocalizedTextSchema.optional(),
|
|
900
901
|
icon: z.string().optional(),
|
|
901
902
|
redirect: z.string().optional()
|
|
902
|
-
}),
|
|
903
|
+
}).strict(),
|
|
903
904
|
z.object({
|
|
904
905
|
openapi: z.string(),
|
|
905
906
|
path: z.string().optional(),
|
|
906
907
|
icon: z.string().optional(),
|
|
907
908
|
title: clarifyLocalizedTextSchema.optional(),
|
|
908
|
-
filter: z.object({ tags: z.array(z.string()).optional() }).optional()
|
|
909
|
-
})
|
|
909
|
+
filter: z.object({ tags: z.array(z.string()).optional() }).strict().optional()
|
|
910
|
+
}).strict()
|
|
910
911
|
]));
|
|
911
912
|
var clarifyPagesGroupSchema = z.lazy(() => z.object({
|
|
912
913
|
group: clarifyLocalizedTextSchema,
|
|
913
914
|
icon: z.string().optional(),
|
|
914
915
|
pages: z.array(clarifyPagesItemSchema)
|
|
915
|
-
}));
|
|
916
|
+
}).strict());
|
|
916
917
|
var clarifyPagesConfigSchema = z.union([z.array(clarifyPagesGroupSchema), z.literal("FileTree")]);
|
|
917
918
|
var clarifyTabItemSchema = z.object({
|
|
918
919
|
tab: clarifyLocalizedTextSchema,
|
|
919
920
|
icon: z.string().optional(),
|
|
920
921
|
pages: clarifyPagesConfigSchema.optional()
|
|
921
|
-
});
|
|
922
|
+
}).strict();
|
|
922
923
|
var clarifyTabsConfigSchema = z.array(clarifyTabItemSchema);
|
|
923
924
|
var clarifyThemePresetSchema = z.enum(["default", "base"]);
|
|
924
925
|
var clarifyThemeModeColorValueSchema = z.object({
|
|
925
926
|
light: z.string().optional(),
|
|
926
927
|
dark: z.string().optional()
|
|
927
|
-
});
|
|
928
|
+
}).strict();
|
|
928
929
|
var clarifyThemeColorValueSchema = z.union([z.string(), clarifyThemeModeColorValueSchema]);
|
|
929
930
|
var clarifyThemeColorTokensConfigSchema = z.object({
|
|
930
931
|
primary: clarifyThemeColorValueSchema.optional(),
|
|
@@ -935,42 +936,65 @@ var clarifyThemeColorTokensConfigSchema = z.object({
|
|
|
935
936
|
muted: clarifyThemeColorValueSchema.optional(),
|
|
936
937
|
border: clarifyThemeColorValueSchema.optional(),
|
|
937
938
|
codeBackground: clarifyThemeColorValueSchema.optional()
|
|
938
|
-
});
|
|
939
|
+
}).strict();
|
|
939
940
|
var clarifyThemeRadiusTokensConfigSchema = z.object({
|
|
940
941
|
sm: z.string().optional(),
|
|
941
942
|
md: z.string().optional(),
|
|
942
943
|
lg: z.string().optional(),
|
|
943
944
|
xl: z.string().optional()
|
|
944
|
-
});
|
|
945
|
+
}).strict();
|
|
945
946
|
var clarifyThemeTokensConfigSchema = z.object({
|
|
946
947
|
colors: clarifyThemeColorTokensConfigSchema.optional(),
|
|
947
948
|
radius: clarifyThemeRadiusTokensConfigSchema.optional()
|
|
948
|
-
});
|
|
949
|
-
var clarifyThemeLayoutConfigSchema = z.object({ maxWidth: z.string().optional() });
|
|
949
|
+
}).strict();
|
|
950
|
+
var clarifyThemeLayoutConfigSchema = z.object({ maxWidth: z.string().optional() }).strict();
|
|
950
951
|
var clarifyThemeConfigSchema = z.object({
|
|
951
952
|
preset: clarifyThemePresetSchema.optional(),
|
|
952
953
|
tokens: clarifyThemeTokensConfigSchema.optional(),
|
|
953
|
-
layout: clarifyThemeLayoutConfigSchema.optional()
|
|
954
|
-
|
|
955
|
-
|
|
954
|
+
layout: clarifyThemeLayoutConfigSchema.optional()
|
|
955
|
+
}).strict();
|
|
956
|
+
function featureSchema(shape, defaults) {
|
|
957
|
+
const objectSchema = z.object({
|
|
958
|
+
enabled: z.boolean().default(true),
|
|
959
|
+
...shape
|
|
960
|
+
}).strict().default({
|
|
961
|
+
enabled: true,
|
|
962
|
+
...defaults
|
|
963
|
+
});
|
|
964
|
+
return z.union([z.boolean(), objectSchema]).default({
|
|
965
|
+
enabled: true,
|
|
966
|
+
...defaults
|
|
967
|
+
}).transform((value) => typeof value === "boolean" ? {
|
|
968
|
+
enabled: value,
|
|
969
|
+
...defaults
|
|
970
|
+
} : value);
|
|
971
|
+
}
|
|
972
|
+
var clarifyFeaturesConfigSchema = z.object({
|
|
973
|
+
search: featureSchema({ mcp: z.boolean().default(true) }, { mcp: true }),
|
|
974
|
+
repository: featureSchema(clarifyRepositoryConfigSchema.shape, {}),
|
|
975
|
+
themeEditor: featureSchema({}, {}),
|
|
976
|
+
openapi: featureSchema({ playground: z.boolean().default(true) }, { playground: true })
|
|
977
|
+
}).strict();
|
|
956
978
|
var clarifyProjectConfigSchema = z.object({
|
|
957
979
|
title: z.string().optional(),
|
|
958
980
|
description: z.string().optional(),
|
|
959
981
|
siteUrl: z.string().optional(),
|
|
960
|
-
|
|
982
|
+
routePrefix: z.string().default("/"),
|
|
983
|
+
assetPrefix: z.string().optional(),
|
|
961
984
|
logo: clarifyLogoConfigSchema.optional(),
|
|
962
985
|
homeUrl: z.string().optional(),
|
|
963
986
|
favicon: clarifyFaviconConfigSchema.optional(),
|
|
964
987
|
theme: clarifyThemeConfigSchema.optional(),
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
988
|
+
navigation: z.object({
|
|
989
|
+
links: z.array(clarifyNavbarLinkSchema).optional(),
|
|
990
|
+
tabs: clarifyTabsConfigSchema.optional()
|
|
991
|
+
}).strict().optional(),
|
|
968
992
|
banner: clarifyBannerConfigOptionsSchema.optional(),
|
|
969
993
|
footer: clarifyFooterConfigSchema.optional(),
|
|
994
|
+
locales: clarifyLocalesConfigSchema.optional(),
|
|
970
995
|
variables: clarifyVariablesConfigSchema.optional(),
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
});
|
|
996
|
+
features: clarifyFeaturesConfigSchema.optional()
|
|
997
|
+
}).strict();
|
|
974
998
|
//#endregion
|
|
975
999
|
//#region source/core/config/config.ts
|
|
976
1000
|
function formatIssuePath(path) {
|
|
@@ -992,17 +1016,20 @@ function validateProjectConfig(value) {
|
|
|
992
1016
|
if (!result.success) throw new Error(formatProjectConfigError(result.error));
|
|
993
1017
|
return result.data;
|
|
994
1018
|
}
|
|
995
|
-
function
|
|
996
|
-
if (!
|
|
997
|
-
const firstLocale =
|
|
998
|
-
const defaultLocale =
|
|
1019
|
+
function resolveLocalesConfig(locales) {
|
|
1020
|
+
if (!locales) return void 0;
|
|
1021
|
+
const firstLocale = locales.locales[0]?.code;
|
|
1022
|
+
const defaultLocale = locales.default ?? firstLocale;
|
|
999
1023
|
if (!defaultLocale) return void 0;
|
|
1000
1024
|
return {
|
|
1001
|
-
defaultLocale,
|
|
1002
|
-
missing:
|
|
1003
|
-
locales:
|
|
1025
|
+
default: defaultLocale,
|
|
1026
|
+
missing: locales.missing ?? "fallback",
|
|
1027
|
+
locales: locales.locales
|
|
1004
1028
|
};
|
|
1005
1029
|
}
|
|
1030
|
+
function resolveFeaturesConfig(features = {}) {
|
|
1031
|
+
return clarifyFeaturesConfigSchema.parse(features);
|
|
1032
|
+
}
|
|
1006
1033
|
function resolveRoutePrefix(routePrefix) {
|
|
1007
1034
|
const trimmed = routePrefix?.trim();
|
|
1008
1035
|
if (!trimmed || trimmed === "/") return "/";
|
|
@@ -1023,26 +1050,24 @@ function resolveProjectConfig(config = {}) {
|
|
|
1023
1050
|
title: config.title ?? "Clarify Docs",
|
|
1024
1051
|
description: config.description ?? "",
|
|
1025
1052
|
siteUrl: config.siteUrl,
|
|
1026
|
-
|
|
1053
|
+
routePrefix,
|
|
1054
|
+
assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
|
|
1027
1055
|
logo: config.logo,
|
|
1028
1056
|
homeUrl: config.homeUrl,
|
|
1029
1057
|
favicon: config.favicon,
|
|
1030
|
-
routePrefix,
|
|
1031
|
-
assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
|
|
1032
1058
|
theme: resolveThemeConfig(config.theme),
|
|
1033
|
-
|
|
1059
|
+
navigation: config.navigation,
|
|
1034
1060
|
banner: config.banner,
|
|
1035
1061
|
footer: config.footer,
|
|
1062
|
+
locales: resolveLocalesConfig(config.locales),
|
|
1036
1063
|
variables: config.variables ?? {},
|
|
1037
|
-
|
|
1038
|
-
tabs: config.tabs
|
|
1064
|
+
features: resolveFeaturesConfig(config.features)
|
|
1039
1065
|
};
|
|
1040
1066
|
}
|
|
1041
1067
|
//#endregion
|
|
1042
1068
|
//#region source/core/config/user-config.ts
|
|
1043
1069
|
function defineConfig(config) {
|
|
1044
|
-
|
|
1045
|
-
return config;
|
|
1070
|
+
return validateClarifyConfig(config);
|
|
1046
1071
|
}
|
|
1047
1072
|
/**
|
|
1048
1073
|
* Config filenames Clarify CLI looks for at the project root, in resolution
|
|
@@ -1066,18 +1091,23 @@ function assertConfigObject(config, configFile) {
|
|
|
1066
1091
|
}
|
|
1067
1092
|
function loadJsonConfig(configFile) {
|
|
1068
1093
|
const config = assertConfigObject(JSON.parse(readFileSync(configFile, "utf-8")), configFile);
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1094
|
+
if ("plugins" in config) throw new Error("[clarify] clarify.json does not support plugins; use clarify.ts or clarify.js");
|
|
1095
|
+
return validateClarifyConfig(config);
|
|
1096
|
+
}
|
|
1097
|
+
function validateClarifyConfig(config) {
|
|
1098
|
+
const { plugins, ...projectConfigInput } = config;
|
|
1099
|
+
if (plugins !== void 0 && !Array.isArray(plugins)) throw new Error("[clarify] config field \"plugins\" is invalid: Expected an array");
|
|
1100
|
+
const projectConfig = validateProjectConfig(projectConfigInput);
|
|
1101
|
+
return plugins ? {
|
|
1072
1102
|
...projectConfig,
|
|
1073
|
-
|
|
1074
|
-
};
|
|
1103
|
+
plugins
|
|
1104
|
+
} : projectConfig;
|
|
1075
1105
|
}
|
|
1076
1106
|
async function loadClarifyConfig(root, env) {
|
|
1077
1107
|
const configFile = findClarifyConfigFile(root);
|
|
1078
1108
|
if (!configFile) return {};
|
|
1079
1109
|
if (configFile.endsWith(".json")) return loadJsonConfig(configFile);
|
|
1080
|
-
return assertConfigObject((await loadConfigFromFile(env, configFile, root, "silent"))?.config, configFile);
|
|
1110
|
+
return validateClarifyConfig(assertConfigObject((await loadConfigFromFile(env, configFile, root, "silent"))?.config, configFile));
|
|
1081
1111
|
}
|
|
1082
1112
|
/**
|
|
1083
1113
|
* File extensions Clarify treats as content files. Kept in sync with the
|
|
@@ -1103,9 +1133,9 @@ function buildProjectInfo(context) {
|
|
|
1103
1133
|
contentRoot: (contentRoot.startsWith(projectRoot) ? contentRoot.slice(projectRoot.length).replace(/^[/\\]+/, "") : contentRoot) || "source",
|
|
1104
1134
|
projectRoot
|
|
1105
1135
|
};
|
|
1106
|
-
if (projectConfig.
|
|
1107
|
-
|
|
1108
|
-
locales: projectConfig.
|
|
1136
|
+
if (projectConfig.locales) response.locales = {
|
|
1137
|
+
default: projectConfig.locales.default,
|
|
1138
|
+
locales: projectConfig.locales.locales.map((l) => l.code)
|
|
1109
1139
|
};
|
|
1110
1140
|
return response;
|
|
1111
1141
|
}
|
|
@@ -1165,10 +1195,23 @@ function resolveVirtualModuleId(id, modules, routes) {
|
|
|
1165
1195
|
if (route) return resolveVirtualId(route.module.virtualModuleId);
|
|
1166
1196
|
return null;
|
|
1167
1197
|
}
|
|
1168
|
-
function generateConfigModule(projectConfig,
|
|
1198
|
+
function generateConfigModule(projectConfig, version) {
|
|
1169
1199
|
const runtimeConfig = {
|
|
1170
|
-
|
|
1171
|
-
|
|
1200
|
+
title: projectConfig.title,
|
|
1201
|
+
description: projectConfig.description,
|
|
1202
|
+
siteUrl: projectConfig.siteUrl,
|
|
1203
|
+
routePrefix: projectConfig.routePrefix,
|
|
1204
|
+
assetPrefix: projectConfig.assetPrefix,
|
|
1205
|
+
logo: projectConfig.logo,
|
|
1206
|
+
homeUrl: projectConfig.homeUrl,
|
|
1207
|
+
favicon: projectConfig.favicon,
|
|
1208
|
+
theme: projectConfig.theme,
|
|
1209
|
+
navigation: projectConfig.navigation,
|
|
1210
|
+
banner: projectConfig.banner,
|
|
1211
|
+
footer: projectConfig.footer,
|
|
1212
|
+
locales: projectConfig.locales,
|
|
1213
|
+
variables: projectConfig.variables,
|
|
1214
|
+
features: projectConfig.features
|
|
1172
1215
|
};
|
|
1173
1216
|
if (version) runtimeConfig.version = version;
|
|
1174
1217
|
return `export const config = ${JSON.stringify(runtimeConfig)};`;
|
|
@@ -1272,7 +1315,7 @@ function buildVirtualModules(args) {
|
|
|
1272
1315
|
const modules = /* @__PURE__ */ new Map();
|
|
1273
1316
|
const clientEntryModule = createClientEntryModule({ themeEditor: args.themeEditor });
|
|
1274
1317
|
const allPlugins = [...args.plugins ?? []];
|
|
1275
|
-
modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.
|
|
1318
|
+
modules.set(VIRTUAL_CONFIG, generateConfigModule(args.projectConfig, args.version));
|
|
1276
1319
|
modules.set(VIRTUAL_ROUTES, generateRoutesModule(args.routes, args.navigation, "client"));
|
|
1277
1320
|
modules.set(VIRTUAL_SERVER_ROUTES, generateRoutesModule(args.routes, args.navigation, "server"));
|
|
1278
1321
|
modules.set(VIRTUAL_SLOTS, createRuntimeSlotsModule(allPlugins, args.generateOptions.projectRoot));
|
|
@@ -1497,7 +1540,7 @@ function createViteAdapter(engine) {
|
|
|
1497
1540
|
const compilePolicyPlugin = {
|
|
1498
1541
|
name: "clarify:content-compile-policy",
|
|
1499
1542
|
configResolved(config) {
|
|
1500
|
-
contentCompileErrorPolicy = config.command === "build"
|
|
1543
|
+
contentCompileErrorPolicy = config.command === "build" ? "throw" : "diagnostic";
|
|
1501
1544
|
}
|
|
1502
1545
|
};
|
|
1503
1546
|
const tailwind = tailwindcss();
|
|
@@ -1513,7 +1556,7 @@ function createViteAdapter(engine) {
|
|
|
1513
1556
|
}
|
|
1514
1557
|
var cliPackageVersion = {
|
|
1515
1558
|
name: "@clarify-labs/cli",
|
|
1516
|
-
version: "0.
|
|
1559
|
+
version: "0.11.1",
|
|
1517
1560
|
license: "AGPL-3.0-only",
|
|
1518
1561
|
type: "module",
|
|
1519
1562
|
repository: {
|
|
@@ -1554,6 +1597,9 @@ var cliPackageVersion = {
|
|
|
1554
1597
|
"@clarify-labs/templates": "workspace:*",
|
|
1555
1598
|
"@mdx-js/mdx": "^3.1.1",
|
|
1556
1599
|
"@mdx-js/rollup": "^3.1.1",
|
|
1600
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
1601
|
+
"@msgpack/msgpack": "^3.1.3",
|
|
1602
|
+
"@orama/orama": "^3.1.18",
|
|
1557
1603
|
"@shikijs/rehype": "^4.3.0",
|
|
1558
1604
|
"@tailwindcss/vite": "^4.3.2",
|
|
1559
1605
|
"@vitejs/plugin-react": "^6.0.3",
|
|
@@ -1572,7 +1618,6 @@ var cliPackageVersion = {
|
|
|
1572
1618
|
"rehype-raw": "^7.0.0",
|
|
1573
1619
|
"rehype-slug": "^6.0.0",
|
|
1574
1620
|
"remark": "^15.0.1",
|
|
1575
|
-
"remark-gfm": "^4.0.1",
|
|
1576
1621
|
"shiki": "4.3.0",
|
|
1577
1622
|
"unist-util-visit": "^5.1.0",
|
|
1578
1623
|
"vite": "^8.1.4",
|
|
@@ -1593,8 +1638,7 @@ function resolveBuildOptions(options = {}) {
|
|
|
1593
1638
|
return {
|
|
1594
1639
|
projectRoot: options.projectRoot ?? process.cwd(),
|
|
1595
1640
|
rootDirectory: options.rootDirectory ?? "source",
|
|
1596
|
-
outputDirectory: options.outputDirectory
|
|
1597
|
-
ssg: { failOnError: options.ssg?.failOnError ?? true }
|
|
1641
|
+
outputDirectory: options.outputDirectory
|
|
1598
1642
|
};
|
|
1599
1643
|
}
|
|
1600
1644
|
//#endregion
|
|
@@ -1652,7 +1696,7 @@ function llmsTxtListItem(route, basePath) {
|
|
|
1652
1696
|
return description ? `- [${route.meta.title}](${basePath}${contentArtifactUrl}): ${description}` : `- [${route.meta.title}](${basePath}${contentArtifactUrl})`;
|
|
1653
1697
|
}
|
|
1654
1698
|
function llmsTxtLocaleLabel(locale, projectConfig) {
|
|
1655
|
-
return projectConfig.
|
|
1699
|
+
return projectConfig.locales?.locales.find((item) => item.code === locale)?.label ?? locale;
|
|
1656
1700
|
}
|
|
1657
1701
|
function groupRoutesByLocale(routes) {
|
|
1658
1702
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -1664,7 +1708,7 @@ function groupRoutesByLocale(routes) {
|
|
|
1664
1708
|
}
|
|
1665
1709
|
function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
|
|
1666
1710
|
const routesBySource = /* @__PURE__ */ new Map();
|
|
1667
|
-
const defaultLocale = projectConfig.
|
|
1711
|
+
const defaultLocale = projectConfig.locales?.default;
|
|
1668
1712
|
for (const route of routes) {
|
|
1669
1713
|
const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.artifacts?.contentArtifactUrl ?? route.path}`;
|
|
1670
1714
|
const previousRoute = routesBySource.get(sourceKey);
|
|
@@ -1854,6 +1898,347 @@ function createHtmlShellPlugin() {
|
|
|
1854
1898
|
};
|
|
1855
1899
|
}
|
|
1856
1900
|
//#endregion
|
|
1901
|
+
//#region source/plugins/mcp-search/mcp-config.ts
|
|
1902
|
+
/**
|
|
1903
|
+
* Normalize a route prefix into a root-relative path segment.
|
|
1904
|
+
*
|
|
1905
|
+
* `'/'` -> `''`, `'/docs'` -> `'/docs'`, `'docs/'` -> `'/docs'`.
|
|
1906
|
+
*/
|
|
1907
|
+
function normalizePrefix(routePrefix) {
|
|
1908
|
+
if (!routePrefix || routePrefix === "/") return "";
|
|
1909
|
+
return `/${routePrefix.replace(/^\/+|\/+$/g, "")}`;
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Assemble the MCP site config from build-time metadata.
|
|
1913
|
+
*
|
|
1914
|
+
* Unlike the previous pagefind-based descriptor, this no longer reads an
|
|
1915
|
+
* entry JSON file - the Orama index is a single binary blob, so the caller
|
|
1916
|
+
* supplies the document count and locale list directly from the build step
|
|
1917
|
+
* that produced the index.
|
|
1918
|
+
*/
|
|
1919
|
+
function createMcpSiteConfig(projectConfig, options) {
|
|
1920
|
+
const prefix = normalizePrefix(projectConfig.routePrefix);
|
|
1921
|
+
return {
|
|
1922
|
+
version: 3,
|
|
1923
|
+
site: {
|
|
1924
|
+
title: projectConfig.title,
|
|
1925
|
+
description: projectConfig.description,
|
|
1926
|
+
url: projectConfig.siteUrl,
|
|
1927
|
+
routePrefix: projectConfig.routePrefix ?? "/"
|
|
1928
|
+
},
|
|
1929
|
+
capabilities: { search: {
|
|
1930
|
+
type: "search",
|
|
1931
|
+
indexPath: `${prefix}/mcp-search.msp`,
|
|
1932
|
+
defaultLocale: projectConfig.locales?.default,
|
|
1933
|
+
documentCount: options.documentCount,
|
|
1934
|
+
locales: options.locales
|
|
1935
|
+
} }
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
//#endregion
|
|
1939
|
+
//#region source/plugins/mcp-search/orama-index.ts
|
|
1940
|
+
/**
|
|
1941
|
+
* Orama schema for the MCP search index.
|
|
1942
|
+
*
|
|
1943
|
+
* - `path`, `title`, `description`, `content` are full-text searchable strings.
|
|
1944
|
+
* - `locale` is an `enum` so we can use `where: { locale: { eq } }` filters at
|
|
1945
|
+
* query time. (`string` fields cannot be filtered in Orama.)
|
|
1946
|
+
* - `keywords` is a `string[]` also indexed for full-text matching.
|
|
1947
|
+
*/
|
|
1948
|
+
var mcpSearchSchema = {
|
|
1949
|
+
path: "string",
|
|
1950
|
+
title: "string",
|
|
1951
|
+
description: "string",
|
|
1952
|
+
locale: "enum",
|
|
1953
|
+
content: "string",
|
|
1954
|
+
keywords: "string[]"
|
|
1955
|
+
};
|
|
1956
|
+
/**
|
|
1957
|
+
* Intl.Segmenter cache keyed by BCP 47 tag. Segmenter construction is not
|
|
1958
|
+
* cheap, and the MCP server issues many queries against the same locale, so
|
|
1959
|
+
* we reuse instances. The cache is module-scoped: build and runtime share it
|
|
1960
|
+
* in the same process, and separate processes get their own - which is fine.
|
|
1961
|
+
*/
|
|
1962
|
+
var segmenterCache = /* @__PURE__ */ new Map();
|
|
1963
|
+
function getSegmenter(locale) {
|
|
1964
|
+
let seg = segmenterCache.get(locale);
|
|
1965
|
+
if (!seg) {
|
|
1966
|
+
seg = new Intl.Segmenter(locale, { granularity: "word" });
|
|
1967
|
+
segmenterCache.set(locale, seg);
|
|
1968
|
+
}
|
|
1969
|
+
return seg;
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Map a Clarify locale code (e.g. `zh-CN`, `en-US`, `ja-JP`) to a BCP 47 tag
|
|
1973
|
+
* suitable for `Intl.Segmenter`. Returns `null` for locales we intentionally
|
|
1974
|
+
* want to tokenize with the Latin/whitespace fallback (faster, and avoids
|
|
1975
|
+
* forcing CJK-style word segmentation on Latin scripts).
|
|
1976
|
+
*/
|
|
1977
|
+
function segmenterLocale(locale) {
|
|
1978
|
+
if (!locale) return null;
|
|
1979
|
+
const lower = locale.toLowerCase();
|
|
1980
|
+
if (lower.startsWith("zh")) return "zh-CN";
|
|
1981
|
+
if (lower.startsWith("ja")) return "ja-JP";
|
|
1982
|
+
if (lower.startsWith("ko")) return "ko-KR";
|
|
1983
|
+
return null;
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Tokenize text for Orama indexing and querying.
|
|
1987
|
+
*
|
|
1988
|
+
* CJK locales use `Intl.Segmenter` (word granularity), which splits
|
|
1989
|
+
* spaceless scripts into words and individual characters. Latin/whitespace
|
|
1990
|
+
* locales fall back to a Unicode-aware split on spaces and punctuation.
|
|
1991
|
+
*
|
|
1992
|
+
* All tokens are lowercased so queries match regardless of case.
|
|
1993
|
+
*/
|
|
1994
|
+
function tokenizeForSearch(text, locale) {
|
|
1995
|
+
if (!text) return [];
|
|
1996
|
+
const segLocale = segmenterLocale(locale);
|
|
1997
|
+
if (segLocale) {
|
|
1998
|
+
const seg = getSegmenter(segLocale);
|
|
1999
|
+
return Array.from(seg.segment(text)).filter((s) => s.isWordLike).map((s) => s.segment.toLowerCase());
|
|
2000
|
+
}
|
|
2001
|
+
return text.toLowerCase().split(/[\s\p{P}]+/u).filter(Boolean);
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Build an Orama tokenizer component bound to a specific default locale.
|
|
2005
|
+
*
|
|
2006
|
+
* The tokenizer receives the raw field value and the per-field language hint
|
|
2007
|
+
* from Orama (which we don't set per-field, so it falls back to the default
|
|
2008
|
+
* language). We resolve the segmenter locale from the default locale at
|
|
2009
|
+
* construction time so the same tokenizer handles all documents in a
|
|
2010
|
+
* predominantly single-locale index.
|
|
2011
|
+
*
|
|
2012
|
+
* For multi-locale indexes, the tokenizer is also used at query time. Since
|
|
2013
|
+
* the query locale is not known to Orama's tokenizer callback, we use the
|
|
2014
|
+
* index's default locale - this is acceptable because:
|
|
2015
|
+
* - CJK queries get CJK segmentation (correct)
|
|
2016
|
+
* - Latin queries against a CJK-default index still work because Latin
|
|
2017
|
+
* text has no word boundaries for CJK segmenters to mis-split; the
|
|
2018
|
+
* segmenter passes through Latin words as-is.
|
|
2019
|
+
*/
|
|
2020
|
+
function createMcpTokenizer(defaultLocale) {
|
|
2021
|
+
return {
|
|
2022
|
+
language: defaultLocale ?? "en",
|
|
2023
|
+
normalizationCache: /* @__PURE__ */ new Map(),
|
|
2024
|
+
tokenize(raw, language) {
|
|
2025
|
+
return tokenizeForSearch(typeof raw === "string" ? raw : String(raw ?? ""), language ?? defaultLocale);
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Compose the searchable text for a route. Shared between the Pagefind
|
|
2031
|
+
* (browser) and Orama (MCP) indexing paths so both backends see the same
|
|
2032
|
+
* field order and content.
|
|
2033
|
+
*/
|
|
2034
|
+
function routeSearchContent(route) {
|
|
2035
|
+
return [
|
|
2036
|
+
route.meta.title,
|
|
2037
|
+
route.meta.description,
|
|
2038
|
+
route.meta.keywords?.join(" "),
|
|
2039
|
+
route.meta.sections?.map((section) => section.title).join(" "),
|
|
2040
|
+
route.source.content
|
|
2041
|
+
].filter(Boolean).join("\n\n");
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Convert a `ContentRoute` into the document shape stored in the Orama index.
|
|
2045
|
+
*/
|
|
2046
|
+
function routeToSearchDocument(route, defaultLocale) {
|
|
2047
|
+
if (route.isBareAlias) return null;
|
|
2048
|
+
const content = routeSearchContent(route);
|
|
2049
|
+
if (!content.trim()) return null;
|
|
2050
|
+
return {
|
|
2051
|
+
path: route.path,
|
|
2052
|
+
title: route.meta.title ?? "",
|
|
2053
|
+
description: route.meta.description ?? "",
|
|
2054
|
+
locale: route.locale ?? defaultLocale ?? "en",
|
|
2055
|
+
content,
|
|
2056
|
+
keywords: route.meta.keywords ?? []
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Create an Orama index pre-configured with the MCP schema and CJK-aware
|
|
2061
|
+
* tokenizer. Both the build step and the runtime loader use this so the
|
|
2062
|
+
* tokenizer is guaranteed to match between indexing and querying.
|
|
2063
|
+
*
|
|
2064
|
+
* Annotated as `AnyOrama` (Orama's exported base type) rather than inferred
|
|
2065
|
+
* via `ReturnType<typeof create>` because the inference would pull in
|
|
2066
|
+
* Orama's internal component types (`DocumentsStore`, `Index`, `Sorter`)
|
|
2067
|
+
* which are not exported and break `.d.ts` generation (TS2883).
|
|
2068
|
+
*/
|
|
2069
|
+
function createMcpSearchIndex(defaultLocale) {
|
|
2070
|
+
return create({
|
|
2071
|
+
schema: mcpSearchSchema,
|
|
2072
|
+
components: { tokenizer: createMcpTokenizer(defaultLocale) }
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Insert search documents into an Orama index.
|
|
2077
|
+
*/
|
|
2078
|
+
function indexSearchDocuments(db, documents) {
|
|
2079
|
+
if (documents.length === 0) return;
|
|
2080
|
+
insertMultiple(db, documents);
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Build a ready-to-search Orama index from content routes.
|
|
2084
|
+
*
|
|
2085
|
+
* Filters out bare aliases and empty routes. Returns the populated db.
|
|
2086
|
+
*/
|
|
2087
|
+
function buildSearchIndex(routes, defaultLocale) {
|
|
2088
|
+
const db = createMcpSearchIndex(defaultLocale);
|
|
2089
|
+
const documents = [];
|
|
2090
|
+
for (const route of routes) {
|
|
2091
|
+
const doc = routeToSearchDocument(route, defaultLocale);
|
|
2092
|
+
if (doc) documents.push(doc);
|
|
2093
|
+
}
|
|
2094
|
+
indexSearchDocuments(db, documents);
|
|
2095
|
+
return {
|
|
2096
|
+
db,
|
|
2097
|
+
documentCount: documents.length
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Collect the distinct locale codes that will be present in the Orama index,
|
|
2102
|
+
* in stable insertion order. Bare aliases are skipped (they duplicate the
|
|
2103
|
+
* default-locale route). Locales with no code fall back to the project
|
|
2104
|
+
* default so single-locale sites still surface one locale.
|
|
2105
|
+
*/
|
|
2106
|
+
function collectIndexedLocales(routes, defaultLocale) {
|
|
2107
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2108
|
+
const locales = [];
|
|
2109
|
+
for (const route of routes) {
|
|
2110
|
+
if (route.isBareAlias) continue;
|
|
2111
|
+
const code = route.locale ?? defaultLocale ?? "en";
|
|
2112
|
+
if (!seen.has(code)) {
|
|
2113
|
+
seen.add(code);
|
|
2114
|
+
locales.push(code);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
return locales;
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Search an Orama MCP index.
|
|
2121
|
+
*
|
|
2122
|
+
* Applies an optional locale filter (via the `enum` field) and returns a
|
|
2123
|
+
* compact, LLM-friendly hit shape. Excerpts are derived from the matched
|
|
2124
|
+
* content: Orama does not return highlighted excerpts, so we synthesize a
|
|
2125
|
+
* short window around the first query term occurrence.
|
|
2126
|
+
*/
|
|
2127
|
+
function searchMcpIndex(db, params) {
|
|
2128
|
+
const limit = Math.max(1, Math.min(50, params.limit ?? 10));
|
|
2129
|
+
const result = search(db, {
|
|
2130
|
+
term: params.query,
|
|
2131
|
+
limit,
|
|
2132
|
+
where: params.locale ? { locale: { eq: params.locale } } : void 0
|
|
2133
|
+
});
|
|
2134
|
+
return {
|
|
2135
|
+
hits: result.hits.map((hit) => {
|
|
2136
|
+
const doc = hit.document;
|
|
2137
|
+
return {
|
|
2138
|
+
path: doc.path,
|
|
2139
|
+
title: doc.title || guessTitleFromPath(doc.path),
|
|
2140
|
+
description: doc.description,
|
|
2141
|
+
locale: doc.locale,
|
|
2142
|
+
score: hit.score,
|
|
2143
|
+
excerpt: makeExcerpt(doc.content, params.query),
|
|
2144
|
+
keywords: doc.keywords
|
|
2145
|
+
};
|
|
2146
|
+
}),
|
|
2147
|
+
count: result.count
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Build a short excerpt around the first occurrence of any query term in the
|
|
2152
|
+
* content. Falls back to the beginning of the content if no term is found.
|
|
2153
|
+
*/
|
|
2154
|
+
function makeExcerpt(content, query, maxLen = 240) {
|
|
2155
|
+
if (!content) return "";
|
|
2156
|
+
const terms = query.toLowerCase().split(/[\s\p{P}]+/u).filter(Boolean);
|
|
2157
|
+
const lower = content.toLowerCase();
|
|
2158
|
+
let pos = -1;
|
|
2159
|
+
for (const term of terms) {
|
|
2160
|
+
const i = lower.indexOf(term);
|
|
2161
|
+
if (i >= 0 && (pos < 0 || i < pos)) pos = i;
|
|
2162
|
+
}
|
|
2163
|
+
if (pos < 0) pos = 0;
|
|
2164
|
+
const half = Math.floor(maxLen / 2);
|
|
2165
|
+
const start = Math.max(0, pos - half);
|
|
2166
|
+
const end = Math.min(content.length, start + maxLen);
|
|
2167
|
+
const prefix = start > 0 ? "…" : "";
|
|
2168
|
+
const suffix = end < content.length ? "…" : "";
|
|
2169
|
+
return `${prefix}${content.slice(start, end).trim()}${suffix}`;
|
|
2170
|
+
}
|
|
2171
|
+
function guessTitleFromPath(path) {
|
|
2172
|
+
if (!path) return "(untitled)";
|
|
2173
|
+
const segments = path.split("/").filter(Boolean);
|
|
2174
|
+
if (segments.length === 0) return "Home";
|
|
2175
|
+
return segments[segments.length - 1].replace(/\.(md|mdx|html)$/, "").replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Serialize an Orama index to a msgpack binary buffer.
|
|
2179
|
+
*
|
|
2180
|
+
* Uses `save()` to extract the raw index data, then msgpack-encodes it for a
|
|
2181
|
+
* compact binary representation. The same schema + tokenizer must be supplied
|
|
2182
|
+
* when loading so query-time tokenization matches indexing-time tokenization.
|
|
2183
|
+
*/
|
|
2184
|
+
function serializeSearchIndex(db) {
|
|
2185
|
+
return encode(save(db));
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Deserialize a msgpack binary buffer into a ready-to-search Orama index.
|
|
2189
|
+
*
|
|
2190
|
+
* The caller must supply the same `defaultLocale` used when the index was
|
|
2191
|
+
* built, so the tokenizer matches. A mismatch would not corrupt data but
|
|
2192
|
+
* would cause CJK query terms to be tokenized incorrectly (e.g. "文档" left
|
|
2193
|
+
* as a single token instead of split into "文" + "档").
|
|
2194
|
+
*/
|
|
2195
|
+
function deserializeSearchIndex(buffer, defaultLocale) {
|
|
2196
|
+
const raw = decode(buffer);
|
|
2197
|
+
const db = createMcpSearchIndex(defaultLocale);
|
|
2198
|
+
load(db, raw);
|
|
2199
|
+
return db;
|
|
2200
|
+
}
|
|
2201
|
+
//#endregion
|
|
2202
|
+
//#region source/plugins/mcp-search/index.ts
|
|
2203
|
+
/**
|
|
2204
|
+
* MCP search index plugin.
|
|
2205
|
+
*
|
|
2206
|
+
* Generates the Orama binary index (`mcp-search.msp`) and the MCP site
|
|
2207
|
+
* descriptor (`mcp.json`) as build assets consumed by the `clarify mcp`
|
|
2208
|
+
* command to serve documentation search to MCP-aware clients (e.g. AI coding
|
|
2209
|
+
* assistants).
|
|
2210
|
+
*
|
|
2211
|
+
* This plugin is independent of `clarify:page-search` (Pagefind), which
|
|
2212
|
+
* powers in-browser search. The MCP server runs in Node and needs a
|
|
2213
|
+
* self-contained, compact index with CJK-aware tokenization that doesn't
|
|
2214
|
+
* depend on the browser-only Pagefind runtime.
|
|
2215
|
+
*/
|
|
2216
|
+
function createMcpSearchPlugin() {
|
|
2217
|
+
return {
|
|
2218
|
+
name: "clarify:mcp-search",
|
|
2219
|
+
hooks: { "build:assets"(ctx) {
|
|
2220
|
+
const searchConfig = ctx.projectConfig.features.search;
|
|
2221
|
+
if (!searchConfig.enabled || !searchConfig.mcp) return [];
|
|
2222
|
+
const defaultLocale = ctx.projectConfig.locales?.default;
|
|
2223
|
+
const { db, documentCount } = buildSearchIndex(ctx.routes, defaultLocale);
|
|
2224
|
+
const locales = collectIndexedLocales(ctx.routes, defaultLocale);
|
|
2225
|
+
const mcpConfig = createMcpSiteConfig(ctx.projectConfig, {
|
|
2226
|
+
documentCount,
|
|
2227
|
+
locales
|
|
2228
|
+
});
|
|
2229
|
+
const assets = [{
|
|
2230
|
+
fileName: `${mcpConfig.capabilities.search?.indexPath.replace(/\/mcp-search\.msp$/, "") ?? ""}/mcp-search.msp`.replace(/^\//, ""),
|
|
2231
|
+
source: serializeSearchIndex(db)
|
|
2232
|
+
}, {
|
|
2233
|
+
fileName: "mcp.json",
|
|
2234
|
+
source: `${JSON.stringify(mcpConfig, null, 2)}\n`
|
|
2235
|
+
}];
|
|
2236
|
+
console.log(`[clarify] MCP search index (${documentCount} docs, ${locales.length} locales) written to mcp-search.msp.`);
|
|
2237
|
+
return assets;
|
|
2238
|
+
} }
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
//#endregion
|
|
1857
2242
|
//#region source/plugins/openapi/parser.ts
|
|
1858
2243
|
var OPENAPI_HTTP_METHODS = [
|
|
1859
2244
|
"get",
|
|
@@ -2059,7 +2444,7 @@ function collectOpenAPIPageIntents(config) {
|
|
|
2059
2444
|
if (!pages || pages === "FileTree") return;
|
|
2060
2445
|
for (const group of pages) visitItems(group.pages);
|
|
2061
2446
|
}
|
|
2062
|
-
for (const tab of config.tabs ?? []) visitPages(tab.pages);
|
|
2447
|
+
for (const tab of config.navigation?.tabs ?? []) visitPages(tab.pages);
|
|
2063
2448
|
return intents;
|
|
2064
2449
|
}
|
|
2065
2450
|
function virtualModuleIdFromPath(path) {
|
|
@@ -2082,14 +2467,14 @@ function cloneOpenAPIRoute(route, overrides = {}) {
|
|
|
2082
2467
|
...overrides
|
|
2083
2468
|
};
|
|
2084
2469
|
}
|
|
2085
|
-
function findOpenAPIRoutesForContentRoot(contentRoot,
|
|
2086
|
-
if (!
|
|
2470
|
+
function findOpenAPIRoutesForContentRoot(contentRoot, locales) {
|
|
2471
|
+
if (!locales) return findOpenAPIRoutes(contentRoot);
|
|
2087
2472
|
const localizedRoutes = [];
|
|
2088
|
-
for (const locale of
|
|
2473
|
+
for (const locale of locales.locales) {
|
|
2089
2474
|
const localeRoot = join(contentRoot, locale.code);
|
|
2090
2475
|
for (const route of findOpenAPIRoutes(localeRoot, localeRoot)) {
|
|
2091
2476
|
const basePath = route.basePath ?? route.path;
|
|
2092
|
-
const path = localizedRoutePath(basePath, locale.code,
|
|
2477
|
+
const path = localizedRoutePath(basePath, locale.code, locales);
|
|
2093
2478
|
localizedRoutes.push({
|
|
2094
2479
|
...route,
|
|
2095
2480
|
path,
|
|
@@ -2099,15 +2484,15 @@ function findOpenAPIRoutesForContentRoot(contentRoot, i18n) {
|
|
|
2099
2484
|
});
|
|
2100
2485
|
}
|
|
2101
2486
|
}
|
|
2102
|
-
if (
|
|
2487
|
+
if (locales.missing === "fallback") {
|
|
2103
2488
|
const routeByLocaleAndBase = new Map(localizedRoutes.map((route) => [`${route.locale ?? ""}:${route.basePath ?? route.path}`, route]));
|
|
2104
|
-
const defaultRoutes = localizedRoutes.filter((route) => route.locale ===
|
|
2489
|
+
const defaultRoutes = localizedRoutes.filter((route) => route.locale === locales.default);
|
|
2105
2490
|
for (const sourceRoute of defaultRoutes) {
|
|
2106
2491
|
const basePath = sourceRoute.basePath ?? sourceRoute.path;
|
|
2107
|
-
for (const locale of
|
|
2492
|
+
for (const locale of locales.locales) {
|
|
2108
2493
|
const key = `${locale.code}:${basePath}`;
|
|
2109
2494
|
if (routeByLocaleAndBase.has(key)) continue;
|
|
2110
|
-
const path = localizedRoutePath(basePath, locale.code,
|
|
2495
|
+
const path = localizedRoutePath(basePath, locale.code, locales);
|
|
2111
2496
|
localizedRoutes.push(cloneOpenAPIRoute(sourceRoute, {
|
|
2112
2497
|
path,
|
|
2113
2498
|
locale: locale.code,
|
|
@@ -2134,7 +2519,7 @@ function createConfiguredOpenAPIRoutes(routes, config) {
|
|
|
2134
2519
|
const key = `${route.locale ?? ""}:${targetBasePath}`;
|
|
2135
2520
|
if (existingKeys.has(key)) continue;
|
|
2136
2521
|
existingKeys.add(key);
|
|
2137
|
-
const path = route.locale && config.
|
|
2522
|
+
const path = route.locale && config.locales ? localizedRoutePath(targetBasePath, route.locale, config.locales) : targetBasePath;
|
|
2138
2523
|
additions.push(cloneOpenAPIRoute(route, {
|
|
2139
2524
|
path,
|
|
2140
2525
|
basePath: targetBasePath,
|
|
@@ -2147,12 +2532,12 @@ function createConfiguredOpenAPIRoutes(routes, config) {
|
|
|
2147
2532
|
}
|
|
2148
2533
|
}
|
|
2149
2534
|
const nextRoutes = [...routes, ...additions];
|
|
2150
|
-
if (!config.
|
|
2151
|
-
const routesWithAlternates = nextRoutes.map((route) => withAlternates(route, nextRoutes, config.
|
|
2535
|
+
if (!config.locales) return nextRoutes;
|
|
2536
|
+
const routesWithAlternates = nextRoutes.map((route) => withAlternates(route, nextRoutes, config.locales));
|
|
2152
2537
|
const bareAliases = [];
|
|
2153
2538
|
const seenBare = new Set(routesWithAlternates.map((route) => route.path));
|
|
2154
2539
|
for (const route of routesWithAlternates) {
|
|
2155
|
-
if (route.locale !== config.
|
|
2540
|
+
if (route.locale !== config.locales.default) continue;
|
|
2156
2541
|
const basePath = route.basePath ?? route.path;
|
|
2157
2542
|
if (basePath === route.path || seenBare.has(basePath)) continue;
|
|
2158
2543
|
seenBare.add(basePath);
|
|
@@ -2191,7 +2576,7 @@ function createOpenAPIPlugin() {
|
|
|
2191
2576
|
hooks: {
|
|
2192
2577
|
"routes:discover": (input, ctx) => ({
|
|
2193
2578
|
...input,
|
|
2194
|
-
routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.
|
|
2579
|
+
routes: [...input.routes, ...findOpenAPIRoutesForContentRoot(input.contentRoot, ctx.projectConfig.locales)]
|
|
2195
2580
|
}),
|
|
2196
2581
|
"routes:discovered": async (routes, ctx) => {
|
|
2197
2582
|
specs.clear();
|
|
@@ -2276,12 +2661,12 @@ function removeClarifyTempDir(path) {
|
|
|
2276
2661
|
});
|
|
2277
2662
|
}
|
|
2278
2663
|
//#endregion
|
|
2279
|
-
//#region source/plugins/search
|
|
2664
|
+
//#region source/plugins/page-search/search-language.ts
|
|
2280
2665
|
function toPagefindLanguage(locale) {
|
|
2281
2666
|
return locale?.trim().replace(/_/g, "-").split("-").filter(Boolean).join("-").toLowerCase() || "en";
|
|
2282
2667
|
}
|
|
2283
2668
|
//#endregion
|
|
2284
|
-
//#region source/plugins/search
|
|
2669
|
+
//#region source/plugins/page-search/index.ts
|
|
2285
2670
|
var pagefindContentTypes = {
|
|
2286
2671
|
".css": "text/css; charset=utf-8",
|
|
2287
2672
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -2321,15 +2706,6 @@ async function generatePagefindIndex(options, pagefind) {
|
|
|
2321
2706
|
if (!pagefind) await runtime.close();
|
|
2322
2707
|
}
|
|
2323
2708
|
}
|
|
2324
|
-
function routeSearchContent(route) {
|
|
2325
|
-
return [
|
|
2326
|
-
route.meta.title,
|
|
2327
|
-
route.meta.description,
|
|
2328
|
-
route.meta.keywords?.join(" "),
|
|
2329
|
-
route.meta.sections?.map((section) => section.title).join(" "),
|
|
2330
|
-
route.source.content
|
|
2331
|
-
].filter(Boolean).join("\n\n");
|
|
2332
|
-
}
|
|
2333
2709
|
async function generateDevSearchIndex(ctx, root, pagefind) {
|
|
2334
2710
|
const { errors, index } = await pagefind.createIndex({
|
|
2335
2711
|
includeCharacters: "._-/#:@",
|
|
@@ -2348,7 +2724,7 @@ async function generateDevSearchIndex(ctx, root, pagefind) {
|
|
|
2348
2724
|
const result = await index.addCustomRecord({
|
|
2349
2725
|
url: route.path,
|
|
2350
2726
|
content,
|
|
2351
|
-
language: toPagefindLanguage(route.locale ?? ctx.projectConfig.
|
|
2727
|
+
language: toPagefindLanguage(route.locale ?? ctx.projectConfig.locales?.default),
|
|
2352
2728
|
meta: { title: route.meta.title }
|
|
2353
2729
|
});
|
|
2354
2730
|
assertNoPagefindErrors(`addCustomRecord ${route.path}`, result.errors);
|
|
@@ -2386,11 +2762,12 @@ function serveDevPagefind(server, getRoot, ctx) {
|
|
|
2386
2762
|
createReadStream(filePath).pipe(res);
|
|
2387
2763
|
});
|
|
2388
2764
|
}
|
|
2389
|
-
function
|
|
2765
|
+
function createPageSearchPlugin() {
|
|
2390
2766
|
return {
|
|
2391
|
-
name: "clarify:search
|
|
2767
|
+
name: "clarify:page-search",
|
|
2392
2768
|
hooks: {
|
|
2393
2769
|
async "dev:configureServer"(server, ctx) {
|
|
2770
|
+
if (!ctx.projectConfig.features.search.enabled) return;
|
|
2394
2771
|
const pagefind = await import("pagefind");
|
|
2395
2772
|
let currentRoot;
|
|
2396
2773
|
const staleRoots = /* @__PURE__ */ new Set();
|
|
@@ -2430,10 +2807,11 @@ function createSearchIndexPlugin() {
|
|
|
2430
2807
|
});
|
|
2431
2808
|
},
|
|
2432
2809
|
async "build:done"(ctx) {
|
|
2810
|
+
if (!ctx.projectConfig.features.search.enabled) return;
|
|
2433
2811
|
const outputDirectory = ctx.generateOptions.outputDirectory;
|
|
2434
2812
|
if (!outputDirectory) return;
|
|
2435
2813
|
const pageCount = await generatePagefindIndex({ outputRoot: resolve(ctx.generateOptions.projectRoot, outputDirectory) });
|
|
2436
|
-
console.log(`[clarify]
|
|
2814
|
+
console.log(`[clarify] Page search index generated for ${pageCount} pages.`);
|
|
2437
2815
|
}
|
|
2438
2816
|
}
|
|
2439
2817
|
};
|
|
@@ -2482,16 +2860,18 @@ function createSeoPlugin() {
|
|
|
2482
2860
|
return {
|
|
2483
2861
|
name: "clarify:seo",
|
|
2484
2862
|
hooks: { "build:assets"(ctx) {
|
|
2863
|
+
const assets = [];
|
|
2485
2864
|
const sitemap = createSitemapXml(ctx.routes, ctx.projectConfig);
|
|
2486
2865
|
const robots = createRobotsTxt(ctx.projectConfig);
|
|
2487
|
-
if (
|
|
2488
|
-
return [{
|
|
2866
|
+
if (sitemap) assets.push({
|
|
2489
2867
|
fileName: "sitemap.xml",
|
|
2490
2868
|
source: sitemap
|
|
2491
|
-
}
|
|
2869
|
+
});
|
|
2870
|
+
if (robots) assets.push({
|
|
2492
2871
|
fileName: "robots.txt",
|
|
2493
2872
|
source: robots
|
|
2494
|
-
}
|
|
2873
|
+
});
|
|
2874
|
+
return assets;
|
|
2495
2875
|
} }
|
|
2496
2876
|
};
|
|
2497
2877
|
}
|
|
@@ -2507,7 +2887,8 @@ function encodePath(path) {
|
|
|
2507
2887
|
return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
|
|
2508
2888
|
}
|
|
2509
2889
|
function createSourceEditUrl(filePath, contentRoot, source) {
|
|
2510
|
-
|
|
2890
|
+
if (!source.url) return "";
|
|
2891
|
+
const repository = normalizeRepositoryUrl(source.url);
|
|
2511
2892
|
const branch = source.branch ?? "main";
|
|
2512
2893
|
const sourcePath = [source.directory ? trimSlashes(source.directory) : "", relative(contentRoot, filePath).replaceAll("\\", "/")].filter(Boolean).join("/");
|
|
2513
2894
|
return `${repository}/edit/${encodeURIComponent(branch)}/${encodePath(sourcePath)}`;
|
|
@@ -2525,7 +2906,8 @@ function createSourceLinksPlugin() {
|
|
|
2525
2906
|
return {
|
|
2526
2907
|
name: "clarify:source-links",
|
|
2527
2908
|
hooks: { "routes:discovered": (routes, ctx) => {
|
|
2528
|
-
|
|
2909
|
+
if (!ctx.projectConfig.features.repository.enabled) return routes;
|
|
2910
|
+
attachSourceEditUrls(routes, join(ctx.generateOptions.projectRoot, ctx.generateOptions.rootDirectory), ctx.projectConfig.features.repository);
|
|
2529
2911
|
return routes;
|
|
2530
2912
|
} }
|
|
2531
2913
|
};
|
|
@@ -2578,7 +2960,8 @@ function createBuiltinPlugins(options = {}) {
|
|
|
2578
2960
|
createSourceLinksPlugin(),
|
|
2579
2961
|
createContentArtifactsPlugin(),
|
|
2580
2962
|
createSeoPlugin(),
|
|
2581
|
-
|
|
2963
|
+
createPageSearchPlugin(),
|
|
2964
|
+
createMcpSearchPlugin()
|
|
2582
2965
|
];
|
|
2583
2966
|
if (options.htmlShell ?? true) plugins.push(createHtmlShellPlugin());
|
|
2584
2967
|
return plugins;
|
|
@@ -2640,18 +3023,18 @@ async function resolveProjectContext(options = {}, env = {
|
|
|
2640
3023
|
}) {
|
|
2641
3024
|
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
2642
3025
|
const initialBuildOptions = resolveBuildOptions(options);
|
|
2643
|
-
const contentRoot = join(projectRoot, initialBuildOptions.rootDirectory);
|
|
2644
3026
|
const configFilePath = findClarifyConfigFile(projectRoot);
|
|
2645
3027
|
const config = configFilePath && existsSync(configFilePath) ? await loadClarifyConfig(projectRoot, env) : {};
|
|
2646
3028
|
const mergedOptions = {
|
|
2647
3029
|
...config,
|
|
2648
3030
|
...options,
|
|
2649
3031
|
projectRoot,
|
|
2650
|
-
rootDirectory: initialBuildOptions.rootDirectory,
|
|
2651
|
-
outputDirectory: initialBuildOptions.outputDirectory
|
|
3032
|
+
rootDirectory: options.rootDirectory ?? initialBuildOptions.rootDirectory,
|
|
3033
|
+
outputDirectory: options.outputDirectory ?? initialBuildOptions.outputDirectory
|
|
2652
3034
|
};
|
|
2653
3035
|
const resolvedProjectConfig = resolveProjectConfig(mergedOptions);
|
|
2654
3036
|
const resolvedBuildOptions = resolveBuildOptions(mergedOptions);
|
|
3037
|
+
const contentRoot = join(projectRoot, resolvedBuildOptions.rootDirectory);
|
|
2655
3038
|
return {
|
|
2656
3039
|
projectRoot,
|
|
2657
3040
|
contentRoot,
|
|
@@ -2741,9 +3124,9 @@ function canonicalUrl(contextOrConfig, route) {
|
|
|
2741
3124
|
/** Inject `lang` (and optional `dir`) onto the `<html>` tag for the route locale. */
|
|
2742
3125
|
function injectHtmlLocaleAttributes(html, contextOrConfig, route) {
|
|
2743
3126
|
const projectConfig = resolveProjectConfigFromContext(contextOrConfig);
|
|
2744
|
-
const localeCode = route?.locale ?? projectConfig.
|
|
3127
|
+
const localeCode = route?.locale ?? projectConfig.locales?.default;
|
|
2745
3128
|
if (!localeCode) return html;
|
|
2746
|
-
const dir = (projectConfig.
|
|
3129
|
+
const dir = (projectConfig.locales?.locales.find((locale) => locale.code === localeCode))?.dir;
|
|
2747
3130
|
return html.replace(/<html\b([^>]*)>/i, (_match, attributes) => {
|
|
2748
3131
|
let nextAttributes = attributes.replace(/\s+lang=("[^"]*"|'[^']*'|[^\s>]*)/i, "").replace(/\s+dir=("[^"]*"|'[^']*'|[^\s>]*)/i, "");
|
|
2749
3132
|
nextAttributes = `${nextAttributes} lang="${escapeHtml(localeCode)}"`;
|
|
@@ -2921,20 +3304,23 @@ async function buildSSRBundle(root, ssrEntry, ssrOutDir, plugins) {
|
|
|
2921
3304
|
}
|
|
2922
3305
|
});
|
|
2923
3306
|
}
|
|
2924
|
-
async function renderSSGRoutes(routes, contextOrConfig, outputDirectory, ssrBundlePath
|
|
3307
|
+
async function renderSSGRoutes(routes, contextOrConfig, outputDirectory, ssrBundlePath) {
|
|
2925
3308
|
const { render } = await import(pathToFileURL(ssrBundlePath).href);
|
|
2926
3309
|
const template = readIndexHtml(outputDirectory);
|
|
2927
3310
|
if (!template) throw new Error(`[clarify] index.html not found in outputDirectory "${outputDirectory}". Make sure Vite build produces it.`);
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
3311
|
+
await Promise.all(routes.map(async (route) => {
|
|
3312
|
+
try {
|
|
3313
|
+
const appHtml = await render(route.path);
|
|
3314
|
+
const finalHtml = injectSSRIntoTemplate(template, appHtml, contextOrConfig, route);
|
|
3315
|
+
for (const outFile of routeOutputFiles(outputDirectory, route)) {
|
|
3316
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
3317
|
+
writeFileSync(outFile, finalHtml, "utf-8");
|
|
3318
|
+
}
|
|
3319
|
+
} catch (err) {
|
|
3320
|
+
console.error(`[clarify] Failed to render route "${route.path}":`, err);
|
|
3321
|
+
throw err;
|
|
2933
3322
|
}
|
|
2934
|
-
}
|
|
2935
|
-
console.error(`[clarify] Failed to render route "${route.path}":`, err);
|
|
2936
|
-
if (failOnError) throw err;
|
|
2937
|
-
}
|
|
3323
|
+
}));
|
|
2938
3324
|
}
|
|
2939
3325
|
//#endregion
|
|
2940
3326
|
//#region source/core/runtime/startup.ts
|
|
@@ -3010,10 +3396,10 @@ var ClarifyContext = class {
|
|
|
3010
3396
|
this.currentPlugins = plugins;
|
|
3011
3397
|
}
|
|
3012
3398
|
get isI18n() {
|
|
3013
|
-
return Boolean(this.projectConfig.
|
|
3399
|
+
return Boolean(this.projectConfig.locales);
|
|
3014
3400
|
}
|
|
3015
3401
|
get defaultLocale() {
|
|
3016
|
-
return this.projectConfig.
|
|
3402
|
+
return this.projectConfig.locales?.default;
|
|
3017
3403
|
}
|
|
3018
3404
|
get(key) {
|
|
3019
3405
|
return this.values.get(key);
|
|
@@ -3207,10 +3593,10 @@ var ClarifyEngine = class {
|
|
|
3207
3593
|
async discoverSite() {
|
|
3208
3594
|
const plugins = this.ctx.plugins;
|
|
3209
3595
|
const contentRoot = this.ctx.contentRoot;
|
|
3210
|
-
const {
|
|
3596
|
+
const { locales, navigation: navigationConfig } = this.ctx.projectConfig;
|
|
3211
3597
|
let routes = await runPhase(plugins, "site:discover", this.ctx, async () => {
|
|
3212
3598
|
const options = { contentProcessor: createProjectContentProcessor(plugins, this.ctx) };
|
|
3213
|
-
const discovered =
|
|
3599
|
+
const discovered = locales ? await findLocalizedContentRoutes(contentRoot, locales, options) : await findContentRoutes(contentRoot, contentRoot, options);
|
|
3214
3600
|
const result = await runHooks(plugins, "routes:discover", {
|
|
3215
3601
|
contentRoot,
|
|
3216
3602
|
routes: discovered
|
|
@@ -3239,8 +3625,9 @@ var ClarifyEngine = class {
|
|
|
3239
3625
|
};
|
|
3240
3626
|
});
|
|
3241
3627
|
});
|
|
3242
|
-
|
|
3243
|
-
|
|
3628
|
+
const tabs = navigationConfig?.tabs;
|
|
3629
|
+
routes = applyConfiguredPageRoutePaths(routes, tabs, locales);
|
|
3630
|
+
const navigation = tabs ? locales ? buildLocalizedNavigationFromTabsConfig(routes, tabs, locales) ?? {
|
|
3244
3631
|
kind: "localized-tabbed",
|
|
3245
3632
|
locales: {}
|
|
3246
3633
|
} : buildNavigationFromTabsConfig(routes, tabs) : {
|
|
@@ -3270,7 +3657,7 @@ var ClarifyEngine = class {
|
|
|
3270
3657
|
routes: this.routes,
|
|
3271
3658
|
navigation: this.navigation,
|
|
3272
3659
|
plugins: this.plugins,
|
|
3273
|
-
themeEditor: this.runtime.command === "serve" || this.runtime.themeEditor === true || this.projectConfig.
|
|
3660
|
+
themeEditor: this.runtime.command === "serve" || this.runtime.themeEditor === true || this.projectConfig.features.themeEditor.enabled,
|
|
3274
3661
|
version: this.ctx.version
|
|
3275
3662
|
});
|
|
3276
3663
|
this.virtualModules = await runHooks(this.plugins, "modules:before", this.virtualModules, this.ctx);
|
|
@@ -3313,7 +3700,7 @@ var ClarifyEngine = class {
|
|
|
3313
3700
|
await runPhase(this.plugins, "ssg", this.ctx, async () => {
|
|
3314
3701
|
if (process.env.SKIP_CLARIFY_SSG) return;
|
|
3315
3702
|
if (!await runInterceptHooks(this.plugins, "ssg:shouldRun", this.ctx)) return;
|
|
3316
|
-
|
|
3703
|
+
assertNoContentDiagnostics(this.routes);
|
|
3317
3704
|
const outputDir = this.runtime.outputDirectory ?? this.generateOptions.outputDirectory;
|
|
3318
3705
|
if (!outputDir) throw new Error("[clarify] outputDirectory is required before SSG runs");
|
|
3319
3706
|
const ssrOutputDir = join(outputDir, ".ssr");
|
|
@@ -3327,10 +3714,10 @@ var ClarifyEngine = class {
|
|
|
3327
3714
|
plugins: this.runtime.ssrPlugins ?? []
|
|
3328
3715
|
});
|
|
3329
3716
|
const ssrBundlePath = join(ssrOutputDir, "entry-server.js");
|
|
3330
|
-
await renderSSGRoutes(this.routes, this.runtimeContext(), outputDir, ssrBundlePath
|
|
3717
|
+
await renderSSGRoutes(this.routes, this.runtimeContext(), outputDir, ssrBundlePath);
|
|
3331
3718
|
} catch (err) {
|
|
3332
3719
|
console.error("[clarify] SSG failed:", err);
|
|
3333
|
-
|
|
3720
|
+
throw err;
|
|
3334
3721
|
} finally {
|
|
3335
3722
|
if (tempEntryPath) try {
|
|
3336
3723
|
rmSync(tempEntryPath, { force: true });
|
|
@@ -3372,6 +3759,6 @@ function createClarifyEngine(options = {}) {
|
|
|
3372
3759
|
return new ClarifyEngine(options);
|
|
3373
3760
|
}
|
|
3374
3761
|
//#endregion
|
|
3375
|
-
export {
|
|
3762
|
+
export { deserializeSearchIndex as a, cliPackageVersionWithCaret as c, createClarifyTempDir as i, createViteAdapter as l, createClarifyEngine as n, searchMcpIndex as o, createClarifyRuntimeAliases as r, cliPackageVersion as s, ClarifyEngine as t, defineConfig as u };
|
|
3376
3763
|
|
|
3377
|
-
//# sourceMappingURL=engine-
|
|
3764
|
+
//# sourceMappingURL=engine-CxnKEKv8.js.map
|