blume 0.6.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +618 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/cli/index.js +2609 -1041
- package/dist/cli/index.js.map +110 -103
- package/dist/types/ai/component-markdown.d.ts +34 -0
- package/dist/types/components/content/youtube.d.ts +18 -0
- package/dist/types/core/base-path.d.ts +47 -0
- package/dist/types/core/config-input.d.ts +110 -12
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +4 -0
- package/dist/types/core/i18n-ui.d.ts +477 -135
- package/dist/types/core/schema.d.ts +309 -195
- package/dist/types/core/sources/types.d.ts +2 -0
- package/dist/types/core/types.d.ts +6 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/openapi/references.d.ts +60 -0
- package/docs/01-quickstart.mdx +5 -2
- package/docs/02-deployment.mdx +24 -9
- package/docs/03-faq.mdx +46 -16
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/configuration/ai.mdx +49 -10
- package/docs/configuration/customization.mdx +11 -0
- package/docs/configuration/index.mdx +33 -3
- package/docs/configuration/seo.mdx +2 -2
- package/docs/content/components.mdx +30 -3
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/islands.mdx +8 -0
- package/docs/content/navigation.mdx +3 -3
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +17 -2
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +8 -6
- package/package.json +15 -4
- package/skills/blume/SKILL.md +5 -3
- package/skills/blume-update-docs/SKILL.md +3 -2
- package/src/ai/agent-readability.ts +11 -5
- package/src/ai/ask-context.ts +7 -2
- package/src/ai/ask-data.ts +3 -0
- package/src/ai/ask.ts +12 -7
- package/src/ai/component-markdown.ts +461 -0
- package/src/ai/llms.ts +143 -23
- package/src/ai/markdown.ts +35 -6
- package/src/ai/mcp/data.ts +33 -8
- package/src/ai/mcp/discovery.ts +10 -3
- package/src/ai/mcp/server.ts +24 -7
- package/src/ai/visibility.ts +74 -0
- package/src/astro/component-slots.ts +16 -4
- package/src/astro/examples.ts +12 -7
- package/src/astro/generate.ts +393 -189
- package/src/astro/index.ts +5 -1
- package/src/astro/integration.ts +9 -5
- package/src/astro/islands.ts +11 -5
- package/src/astro/markdown-negotiation.ts +2 -2
- package/src/astro/pages.ts +89 -22
- package/src/astro/templates.ts +259 -25
- package/src/blume-modules.d.ts +8 -0
- package/src/cli/commands/build.ts +131 -38
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/dev.ts +71 -17
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/eject.ts +47 -19
- package/src/cli/commands/init.ts +120 -180
- package/src/cli/commands/preview.ts +4 -1
- package/src/cli/commands/validate.ts +44 -2
- package/src/cli/dev-lock.ts +34 -19
- package/src/cli/eject-scripts.ts +72 -0
- package/src/cli/env.ts +15 -5
- package/src/cli/init/questions.ts +158 -0
- package/src/cli/init/scaffold.ts +380 -0
- package/src/cli/required-secrets.ts +2 -1
- package/src/components/content/AccordionItem.astro +23 -4
- package/src/components/content/Badge.astro +3 -1
- package/src/components/content/Card.astro +4 -2
- package/src/components/content/CodeBlock.astro +3 -0
- package/src/components/content/Component.astro +30 -16
- package/src/components/content/Diff.astro +3 -1
- package/src/components/content/Step.astro +10 -1
- package/src/components/content/Tabs.astro +15 -3
- package/src/components/content/Tile.astro +2 -1
- package/src/components/content/Tooltip.astro +3 -1
- package/src/components/content/Update.astro +9 -2
- package/src/components/content/auto-type-table.ts +25 -9
- package/src/components/content/base-href.ts +33 -0
- package/src/components/content/changelog-element.ts +9 -2
- package/src/components/content/diff.ts +12 -6
- package/src/components/content/mermaid-element.ts +10 -2
- package/src/components/index.ts +23 -1
- package/src/components/islands/AskAI.astro +5 -2
- package/src/components/islands/ask-ai.tsx +68 -12
- package/src/components/islands/base-path.ts +28 -0
- package/src/components/islands/hooks.ts +44 -9
- package/src/components/layout/Banner.astro +12 -3
- package/src/components/layout/Breadcrumbs.astro +2 -1
- package/src/components/layout/Favicon.astro +3 -2
- package/src/components/layout/Header.astro +15 -5
- package/src/components/layout/LanguageSwitcher.astro +2 -1
- package/src/components/layout/Logo.astro +13 -4
- package/src/components/layout/NavSelector.astro +2 -1
- package/src/components/layout/NavTree.astro +22 -7
- package/src/components/layout/PageActions.astro +25 -10
- package/src/components/layout/PageFeedback.astro +4 -1
- package/src/components/layout/PageLayout.astro +51 -9
- package/src/components/layout/Pagination.astro +3 -2
- package/src/components/layout/ReferenceLayout.astro +8 -1
- package/src/components/layout/RootLayout.astro +74 -13
- package/src/components/layout/Search.astro +107 -27
- package/src/components/layout/nav-utils.ts +18 -10
- package/src/components/layout/search/algolia.ts +11 -2
- package/src/components/layout/search/endpoint.ts +11 -5
- package/src/components/layout/search/orama-cloud.ts +8 -2
- package/src/components/layout/search/pagefind.ts +3 -0
- package/src/components/layout/search/types.ts +5 -1
- package/src/components/layout/search/typesense.ts +4 -1
- package/src/components/layout/toc-element.ts +8 -2
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/Operation.astro +47 -40
- package/src/components/openapi/RequestPanel.astro +8 -2
- package/src/components/openapi/helpers.ts +71 -3
- package/src/components/openapi/panel.ts +1 -1
- package/src/components/openapi/snippets.ts +25 -11
- package/src/core/base-path.ts +94 -0
- package/src/core/builtin-tags.ts +2 -0
- package/src/core/component-overrides.ts +103 -74
- package/src/core/config-input.ts +118 -17
- package/src/core/config.ts +8 -5
- package/src/core/content.ts +2 -0
- package/src/core/data.ts +4 -0
- package/src/core/diagnostics.ts +54 -34
- package/src/core/gitignore.ts +4 -1
- package/src/core/graph.ts +166 -88
- package/src/core/i18n-ui.ts +63 -3
- package/src/core/last-modified.ts +15 -6
- package/src/core/links.ts +69 -25
- package/src/core/manifest.ts +62 -45
- package/src/core/nav-diagnostics.ts +1 -1
- package/src/core/navigation.ts +144 -58
- package/src/core/package-json.ts +17 -2
- package/src/core/project-graph.ts +25 -15
- package/src/core/schema.ts +605 -620
- package/src/core/sources/assets.ts +6 -1
- package/src/core/sources/filesystem.ts +4 -0
- package/src/core/sources/github-releases.ts +2 -1
- package/src/core/sources/mdx-remote.ts +76 -63
- package/src/core/sources/normalize.ts +236 -91
- package/src/core/sources/notion.ts +27 -18
- package/src/core/sources/types.ts +2 -0
- package/src/core/tsconfig-aliases.ts +59 -30
- package/src/core/types.ts +6 -1
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -1
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/deploy/adapter-output.ts +18 -8
- package/src/deploy/redirects.ts +25 -2
- package/src/deploy/robots.ts +6 -1
- package/src/deploy/rss.ts +10 -3
- package/src/deploy/sitemap.ts +59 -13
- package/src/index.ts +5 -0
- package/src/markdown/base-links.ts +60 -0
- package/src/markdown/code-title.ts +11 -14
- package/src/markdown/index.ts +46 -9
- package/src/markdown/inline-code.ts +14 -4
- package/src/markdown/package-commands.ts +10 -4
- package/src/markdown/themes.ts +24 -0
- package/src/openapi/model.ts +15 -5
- package/src/openapi/parse.ts +21 -0
- package/src/openapi/references.ts +75 -21
- package/src/openapi/render-mdx.ts +11 -6
- package/src/openapi/scalar.ts +32 -16
- package/src/openapi/source.ts +59 -10
- package/src/registry/eject.ts +247 -19
- package/src/registry/registry.ts +0 -3
- package/src/search/build.ts +3 -0
- package/src/search/documents.ts +36 -4
- package/src/search/sync/typesense.ts +6 -4
- package/src/seo/jsonld.ts +28 -17
- package/src/theme/entry.ts +85 -20
package/src/ai/markdown.ts
CHANGED
|
@@ -3,15 +3,37 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
4
4
|
import { readEntryText } from "../core/sources/read.ts";
|
|
5
5
|
import type { RouteManifestEntry } from "../core/types.ts";
|
|
6
|
+
import { downlevelComponents } from "./component-markdown.ts";
|
|
7
|
+
import { applyAgentVisibility } from "./visibility.ts";
|
|
8
|
+
|
|
9
|
+
/** One route's raw-Markdown variants. */
|
|
10
|
+
export interface RawMarkdownEntry {
|
|
11
|
+
/**
|
|
12
|
+
* The agent-facing Markdown served at `/<route>.md`: supported components
|
|
13
|
+
* downleveled to plain Markdown (`<TypeTable>` → table, `<Callout>` →
|
|
14
|
+
* blockquote, …). Present only when downleveling changed something, so
|
|
15
|
+
* component-free pages aren't stored twice.
|
|
16
|
+
*/
|
|
17
|
+
md?: string;
|
|
18
|
+
/** The original source, served verbatim at `/<route>.mdx`. */
|
|
19
|
+
mdx: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The Markdown an agent should read for a route. */
|
|
23
|
+
export const agentMarkdown = (entry: RawMarkdownEntry): string =>
|
|
24
|
+
entry.md ?? entry.mdx;
|
|
6
25
|
|
|
7
26
|
/**
|
|
8
27
|
* Map every route to its raw source Markdown. Powers the `<route>.md` and
|
|
9
|
-
* `<route>.mdx` endpoints
|
|
10
|
-
*
|
|
28
|
+
* `<route>.mdx` endpoints: `.mdx` serves the original source so tools can see
|
|
29
|
+
* exactly what the author wrote, while `.md` downlevels supported components
|
|
30
|
+
* to plain Markdown for consumers that can't interpret JSX. `<Visibility>`
|
|
31
|
+
* audiences are resolved for agents in both variants: web-only content is
|
|
32
|
+
* removed, agents-only unwrapped.
|
|
11
33
|
*/
|
|
12
34
|
export const buildRawMarkdown = async (
|
|
13
35
|
project: BlumeProject
|
|
14
|
-
): Promise<Record<string,
|
|
36
|
+
): Promise<Record<string, RawMarkdownEntry>> => {
|
|
15
37
|
const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
|
|
16
38
|
|
|
17
39
|
const readRoute = async (route: RouteManifestEntry): Promise<string> => {
|
|
@@ -23,9 +45,16 @@ export const buildRawMarkdown = async (
|
|
|
23
45
|
};
|
|
24
46
|
|
|
25
47
|
const entries = await Promise.all(
|
|
26
|
-
project.manifest.routes.map(
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
project.manifest.routes.map(async (route) => {
|
|
49
|
+
const source = applyAgentVisibility(await readRoute(route));
|
|
50
|
+
const md = downlevelComponents(
|
|
51
|
+
source,
|
|
52
|
+
project.config.ai.markdownComponents
|
|
53
|
+
);
|
|
54
|
+
const entry: RawMarkdownEntry =
|
|
55
|
+
md === source ? { mdx: source } : { md, mdx: source };
|
|
56
|
+
return [route.path, entry] as const;
|
|
57
|
+
})
|
|
29
58
|
);
|
|
30
59
|
return Object.fromEntries(entries);
|
|
31
60
|
};
|
package/src/ai/mcp/data.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { normalizeBasePath } from "../../core/base-path.ts";
|
|
1
2
|
import type { BlumeProject } from "../../core/project-graph.ts";
|
|
2
3
|
import type { Navigation } from "../../core/types.ts";
|
|
3
4
|
import { buildSearchDocuments } from "../../search/documents.ts";
|
|
4
5
|
import type { OramaDoc } from "../../search/orama-index.ts";
|
|
5
|
-
import { buildRawMarkdown } from "../markdown.ts";
|
|
6
|
+
import { agentMarkdown, buildRawMarkdown } from "../markdown.ts";
|
|
6
7
|
|
|
7
8
|
/** A page entry surfaced by the `list_pages` MCP tool. */
|
|
8
9
|
export interface McpRoute {
|
|
@@ -21,6 +22,12 @@ export interface McpRoute {
|
|
|
21
22
|
* access at request time. Serialized to `generated/mcp-data.json`.
|
|
22
23
|
*/
|
|
23
24
|
export interface McpData {
|
|
25
|
+
/**
|
|
26
|
+
* Normalized `deployment.base` (`""` or `/seg`), layered onto routes when
|
|
27
|
+
* emitting URLs — the site is base-less and routes are base-less manifest
|
|
28
|
+
* paths, matching the sitemap/llms.txt convention.
|
|
29
|
+
*/
|
|
30
|
+
base: string;
|
|
24
31
|
documents: OramaDoc[];
|
|
25
32
|
instructions?: string;
|
|
26
33
|
name: string;
|
|
@@ -34,29 +41,47 @@ export interface McpData {
|
|
|
34
41
|
/** Build the MCP data snapshot from a resolved project. */
|
|
35
42
|
export const buildMcpData = async (project: BlumeProject): Promise<McpData> => {
|
|
36
43
|
const { config, graph, manifest } = project;
|
|
37
|
-
const [documents,
|
|
44
|
+
const [documents, rawMarkdown] = await Promise.all([
|
|
38
45
|
// The MCP server is independent of on-page search, so index docs even when
|
|
39
|
-
// the search provider is `none`.
|
|
40
|
-
|
|
46
|
+
// the search provider is `none`. Documents are agent-facing, so
|
|
47
|
+
// `<Visibility>` resolves like `get_page`/llms-full.txt (web-only content
|
|
48
|
+
// removed, agents-only kept).
|
|
49
|
+
buildSearchDocuments(project, {
|
|
50
|
+
audience: "agents",
|
|
51
|
+
includeWhenDisabled: true,
|
|
52
|
+
}),
|
|
41
53
|
buildRawMarkdown(project),
|
|
42
54
|
]);
|
|
43
55
|
|
|
56
|
+
// `get_page` serves the agent variant: components downleveled to Markdown.
|
|
57
|
+
const pages = Object.fromEntries(
|
|
58
|
+
Object.entries(rawMarkdown).map(([route, entry]) => [
|
|
59
|
+
route,
|
|
60
|
+
agentMarkdown(entry),
|
|
61
|
+
])
|
|
62
|
+
);
|
|
63
|
+
|
|
44
64
|
const descriptionById = new Map(
|
|
45
65
|
graph.pages.map((page) => [page.id, page.description])
|
|
46
66
|
);
|
|
47
67
|
|
|
48
|
-
const routes: McpRoute[] =
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
const routes: McpRoute[] = [];
|
|
69
|
+
for (const route of manifest.routes) {
|
|
70
|
+
if (route.hidden) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
routes.push({
|
|
51
74
|
contentType: route.contentType,
|
|
52
75
|
description: descriptionById.get(route.id),
|
|
53
76
|
indexable: route.indexable,
|
|
54
77
|
lastModified: route.lastModified ?? null,
|
|
55
78
|
route: route.path,
|
|
56
79
|
title: route.title,
|
|
57
|
-
})
|
|
80
|
+
});
|
|
81
|
+
}
|
|
58
82
|
|
|
59
83
|
return {
|
|
84
|
+
base: normalizeBasePath(config.deployment.base),
|
|
60
85
|
documents: documents.map((doc) => ({
|
|
61
86
|
content: doc.content,
|
|
62
87
|
description: doc.description,
|
package/src/ai/mcp/discovery.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { withBasePath } from "../../core/base-path.ts";
|
|
1
2
|
import { MCP_TOOLS } from "./tools.ts";
|
|
2
3
|
|
|
3
4
|
/** Inputs needed to describe the MCP server in discovery documents. */
|
|
4
5
|
export interface McpDiscoveryInput {
|
|
6
|
+
/** Normalized `deployment.base` (`""` or `/seg`); the route is base-less. */
|
|
7
|
+
base: string;
|
|
5
8
|
name: string;
|
|
6
9
|
route: string;
|
|
7
10
|
site: string | null;
|
|
@@ -9,10 +12,14 @@ export interface McpDiscoveryInput {
|
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
/** The MCP server's address — absolute when a site is configured. */
|
|
12
|
-
const serverUrl = (input: McpDiscoveryInput): string =>
|
|
13
|
-
//
|
|
15
|
+
const serverUrl = (input: McpDiscoveryInput): string => {
|
|
16
|
+
// The endpoint is a generated Astro page, so it's served under
|
|
17
|
+
// `deployment.base` like every other route (the sitemap/llms.txt convention).
|
|
18
|
+
const path = withBasePath(input.base, input.route);
|
|
19
|
+
// Concatenate rather than `new URL(path, site)` — a root-absolute path
|
|
14
20
|
// would drop the base path of a subpath deployment (`acme.com/docs`).
|
|
15
|
-
input.site ? `${input.site.replace(/\/+$/u, "")}${
|
|
21
|
+
return input.site ? `${input.site.replace(/\/+$/u, "")}${path}` : path;
|
|
22
|
+
};
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* The `/.well-known/mcp.json` discovery document: the minimal pointer agents use
|
package/src/ai/mcp/server.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ListToolsRequestSchema,
|
|
6
6
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
|
|
8
|
+
import { withBasePath } from "../../core/base-path.ts";
|
|
8
9
|
import { buildOramaIndex, queryOramaIndex } from "../../search/orama-index.ts";
|
|
9
10
|
import type { OramaDoc } from "../../search/orama-index.ts";
|
|
10
11
|
import type { McpData } from "./data.ts";
|
|
@@ -89,10 +90,24 @@ const normalizeRoute = (input: string): string => {
|
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
/** Build the absolute (or root-relative) URL for a route. */
|
|
92
|
-
const urlFor = (route: string,
|
|
93
|
-
//
|
|
93
|
+
const urlFor = (route: string, data: McpData): string => {
|
|
94
|
+
// Routes are base-less manifest paths; layer `deployment.base` on top so the
|
|
95
|
+
// URL matches where the page is served (the sitemap/llms.txt convention).
|
|
96
|
+
const path = withBasePath(data.base, route);
|
|
97
|
+
// Concatenate rather than `new URL(path, site)` — a root-absolute path
|
|
94
98
|
// would drop the base path of a subpath deployment (`acme.com/docs`).
|
|
95
|
-
site ? `${site.replace(/\/+$/u, "")}${
|
|
99
|
+
return data.site ? `${data.site.replace(/\/+$/u, "")}${path}` : path;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** A hit's excerpt: its description, else the head of its content with an
|
|
103
|
+
* ellipsis only when something was actually cut off. */
|
|
104
|
+
const excerptFor = (doc: OramaDoc): string => {
|
|
105
|
+
if (doc.description) {
|
|
106
|
+
return doc.description;
|
|
107
|
+
}
|
|
108
|
+
const head = doc.content.slice(0, EXCERPT_LENGTH).trim();
|
|
109
|
+
return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;
|
|
110
|
+
};
|
|
96
111
|
|
|
97
112
|
const text = (value: string, isError = false) => ({
|
|
98
113
|
content: [{ text: value, type: "text" as const }],
|
|
@@ -127,10 +142,9 @@ const buildServer = (
|
|
|
127
142
|
asLimit(args.limit)
|
|
128
143
|
);
|
|
129
144
|
const results = hits.map((doc: OramaDoc) => ({
|
|
130
|
-
excerpt:
|
|
131
|
-
doc.description || `${doc.content.slice(0, EXCERPT_LENGTH)}…`.trim(),
|
|
145
|
+
excerpt: excerptFor(doc),
|
|
132
146
|
title: doc.title,
|
|
133
|
-
url: urlFor(doc.route, data
|
|
147
|
+
url: urlFor(doc.route, data),
|
|
134
148
|
}));
|
|
135
149
|
return text(JSON.stringify(results, null, 2));
|
|
136
150
|
}
|
|
@@ -156,7 +170,7 @@ const buildServer = (
|
|
|
156
170
|
lastModified: route.lastModified,
|
|
157
171
|
route: route.route,
|
|
158
172
|
title: route.title,
|
|
159
|
-
url: urlFor(route.route, data
|
|
173
|
+
url: urlFor(route.route, data),
|
|
160
174
|
})),
|
|
161
175
|
null,
|
|
162
176
|
2
|
|
@@ -209,6 +223,9 @@ export const createMcpFetchHandler = (
|
|
|
209
223
|
const server = buildServer(data, index);
|
|
210
224
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
211
225
|
enableJsonResponse: true,
|
|
226
|
+
// The SDK enables stateless mode only when this is `undefined`; `null` is
|
|
227
|
+
// not an accepted value for the `(() => string) | undefined` option.
|
|
228
|
+
// oxlint-disable-next-line sonarjs/no-undefined-assignment
|
|
212
229
|
sessionIdGenerator: undefined,
|
|
213
230
|
});
|
|
214
231
|
await server.connect(transport);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Mirrors the fence masking in `core/sources/assets.ts`: a fenced code sample
|
|
2
|
+
// that *shows* `<Visibility>` markup must keep showing what the author wrote.
|
|
3
|
+
const CODE_FENCE_BLOCK =
|
|
4
|
+
/^(?<fence>`{3,}|~{3,})[^\n]*\n[\s\S]*?^\k<fence>[^\n]*(?=\n|$)/gmu;
|
|
5
|
+
// NUL delimiters cannot appear in authored markdown, so tokens never collide.
|
|
6
|
+
// oxlint-disable-next-line no-control-regex -- the NUL is the collision guard.
|
|
7
|
+
const FENCE_TOKEN = /\u0000blume-fence-(?<index>\d+)\u0000/gu;
|
|
8
|
+
|
|
9
|
+
/** The audience an output surface serves — the component's two `for` values. */
|
|
10
|
+
export type VisibilityAudience = "agents" | "web";
|
|
11
|
+
|
|
12
|
+
// `<Visibility for="…">…</Visibility>` in either quote style, tolerant of
|
|
13
|
+
// whitespace around the attribute and inside the tags. Non-greedy bodies stop
|
|
14
|
+
// at the first close tag, so nesting is not supported: a nested block closes
|
|
15
|
+
// the outer match early and any remainder passes through verbatim.
|
|
16
|
+
const visibilityBlock = (audience: VisibilityAudience): RegExp =>
|
|
17
|
+
new RegExp(
|
|
18
|
+
`<Visibility\\s+for\\s*=\\s*(?:"${audience}"|'${audience}')\\s*>(?<inner>[\\s\\S]*?)</Visibility\\s*>`,
|
|
19
|
+
"gu"
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const BLOCKS: Record<VisibilityAudience, RegExp> = {
|
|
23
|
+
agents: visibilityBlock("agents"),
|
|
24
|
+
web: visibilityBlock("web"),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve `<Visibility>` blocks for one audience: blocks addressed to the
|
|
29
|
+
* other audience are removed entirely and blocks addressed to `audience` are
|
|
30
|
+
* unwrapped (tags dropped, body kept), matching what the Astro component
|
|
31
|
+
* renders on the web. Other `for` values (the component's default) are left
|
|
32
|
+
* untouched, and markdown with no matching blocks is returned byte-identical,
|
|
33
|
+
* so raw sources stay raw.
|
|
34
|
+
*/
|
|
35
|
+
export const applyAudienceVisibility = (
|
|
36
|
+
markdown: string,
|
|
37
|
+
audience: VisibilityAudience
|
|
38
|
+
): string => {
|
|
39
|
+
// Mask fenced code blocks so documentation *about* Visibility survives.
|
|
40
|
+
const fences: string[] = [];
|
|
41
|
+
const masked = markdown.replace(CODE_FENCE_BLOCK, (block) => {
|
|
42
|
+
fences.push(block);
|
|
43
|
+
return `\u0000blume-fence-${fences.length - 1}\u0000`;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let touched = false;
|
|
47
|
+
const filtered = masked
|
|
48
|
+
.replaceAll(BLOCKS[audience === "agents" ? "web" : "agents"], () => {
|
|
49
|
+
touched = true;
|
|
50
|
+
return "";
|
|
51
|
+
})
|
|
52
|
+
.replaceAll(BLOCKS[audience], (_match, inner: string) => {
|
|
53
|
+
touched = true;
|
|
54
|
+
return inner;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Removing/unwrapping block-level tags leaves runs of blank lines behind;
|
|
58
|
+
// collapse them only when something matched so untouched files round-trip
|
|
59
|
+
// exactly. Fences are masked as single-line tokens, so they are unaffected.
|
|
60
|
+
const tidied = touched ? filtered.replaceAll(/\n{3,}/gu, "\n\n") : filtered;
|
|
61
|
+
|
|
62
|
+
return tidied.replaceAll(
|
|
63
|
+
FENCE_TOKEN,
|
|
64
|
+
(token, index) => fences[Number(index)] ?? token
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve `<Visibility>` blocks for agent-facing Markdown (llms-full.txt, the
|
|
70
|
+
* `.md`/`.mdx` mirrors, MCP tools, Ask AI grounding): `for="web"` content is
|
|
71
|
+
* removed and `for="agents"` content is unwrapped.
|
|
72
|
+
*/
|
|
73
|
+
export const applyAgentVisibility = (markdown: string): string =>
|
|
74
|
+
applyAudienceVisibility(markdown, "agents");
|
|
@@ -41,6 +41,8 @@ export const layoutOverrides = {};
|
|
|
41
41
|
const attributeValue = (value: string): string =>
|
|
42
42
|
value.replaceAll(/["\n\r]/gu, " ").trim();
|
|
43
43
|
|
|
44
|
+
const CLIENT_LOAD = "client:load";
|
|
45
|
+
|
|
44
46
|
/** Astro client directive for a hydrated override. */
|
|
45
47
|
const directiveFor = (override: NormalizedOverride): string => {
|
|
46
48
|
const framework = override.source?.framework;
|
|
@@ -54,15 +56,15 @@ const directiveFor = (override: NormalizedOverride): string => {
|
|
|
54
56
|
case "media": {
|
|
55
57
|
return override.media
|
|
56
58
|
? `client:media="${attributeValue(override.media)}"`
|
|
57
|
-
:
|
|
59
|
+
: CLIENT_LOAD;
|
|
58
60
|
}
|
|
59
61
|
case "only": {
|
|
60
62
|
return framework
|
|
61
63
|
? `client:only="${attributeValue(framework)}"`
|
|
62
|
-
:
|
|
64
|
+
: CLIENT_LOAD;
|
|
63
65
|
}
|
|
64
66
|
default: {
|
|
65
|
-
return
|
|
67
|
+
return CLIENT_LOAD;
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
};
|
|
@@ -89,8 +91,18 @@ ${clause}
|
|
|
89
91
|
`;
|
|
90
92
|
};
|
|
91
93
|
|
|
94
|
+
/**
|
|
95
|
+
* A filesystem-safe, injective token for an override key. Distinct keys must
|
|
96
|
+
* never share a wrapper file ("Foo.Bar" vs "Foo_Bar" used to collide, racing
|
|
97
|
+
* the same temp file and silently rendering the wrong component), so every
|
|
98
|
+
* non-alphanumeric character is hex-escaped rather than collapsed — the same
|
|
99
|
+
* hardening as `exampleSlug` in templates.ts.
|
|
100
|
+
*/
|
|
92
101
|
const sanitize = (value: string): string =>
|
|
93
|
-
value.replaceAll(
|
|
102
|
+
value.replaceAll(
|
|
103
|
+
/[^A-Za-z0-9]/gu,
|
|
104
|
+
(char) => `_${(char.codePointAt(0) ?? 0).toString(16)}_`
|
|
105
|
+
);
|
|
94
106
|
|
|
95
107
|
export const planComponentSlots = (
|
|
96
108
|
componentsFile: string | null,
|
package/src/astro/examples.ts
CHANGED
|
@@ -59,10 +59,10 @@ const GLOB_MAGIC = /[!*?[\]{}]/u;
|
|
|
59
59
|
*/
|
|
60
60
|
const splitGlobBase = (pattern: string): { base: string; rest: string } => {
|
|
61
61
|
const segments = pattern.split("/");
|
|
62
|
+
// Only called when the pattern contains glob magic (see the caller), and `/`
|
|
63
|
+
// is never magic, so the magic char always lands in a segment — `findIndex`
|
|
64
|
+
// is never -1 here.
|
|
62
65
|
const firstMagic = segments.findIndex((segment) => GLOB_MAGIC.test(segment));
|
|
63
|
-
if (firstMagic === -1) {
|
|
64
|
-
return { base: pattern, rest: "" };
|
|
65
|
-
}
|
|
66
66
|
return {
|
|
67
67
|
base: segments.slice(0, firstMagic).join("/"),
|
|
68
68
|
rest: segments.slice(firstMagic).join("/"),
|
|
@@ -109,11 +109,13 @@ export const discoverExamples = async (
|
|
|
109
109
|
const warnings: string[] = [];
|
|
110
110
|
const seen = new Map<string, string>();
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
// Extracted so the two skip paths become early `return`s (one `continue`
|
|
113
|
+
// budget per loop under the lint rule) instead of `continue` statements.
|
|
114
|
+
const collectExample = (file: string, source: string): void => {
|
|
113
115
|
const ext = file.match(EXAMPLE_FILE)?.groups?.ext;
|
|
114
116
|
const framework = ext ? FRAMEWORK_BY_EXT[ext] : undefined;
|
|
115
117
|
if (!(ext && framework)) {
|
|
116
|
-
|
|
118
|
+
return;
|
|
117
119
|
}
|
|
118
120
|
// Strip the trailing `.<ext>` to form the `<Component path>` key.
|
|
119
121
|
const path = relative(dir, file).slice(0, -(ext.length + 1));
|
|
@@ -122,10 +124,9 @@ export const discoverExamples = async (
|
|
|
122
124
|
warnings.push(
|
|
123
125
|
`Two examples both resolve to "${path}" ("${existing}" and "${file}"); ignoring the second. Give them distinct paths.`
|
|
124
126
|
);
|
|
125
|
-
|
|
127
|
+
return;
|
|
126
128
|
}
|
|
127
129
|
seen.set(path, file);
|
|
128
|
-
const source = sources[index] ?? "";
|
|
129
130
|
examples.push({
|
|
130
131
|
client:
|
|
131
132
|
framework === "astro"
|
|
@@ -137,6 +138,10 @@ export const discoverExamples = async (
|
|
|
137
138
|
path,
|
|
138
139
|
source,
|
|
139
140
|
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
for (const [index, file] of files.entries()) {
|
|
144
|
+
collectExample(file, sources[index] ?? "");
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
return { examples, warnings };
|