blume 0.1.5 → 0.3.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/dist/cli/index.js +2123 -555
- package/dist/cli/index.js.map +39 -25
- package/dist/types/core/data.d.ts +16 -0
- package/dist/types/core/define-components.d.ts +9 -2
- package/dist/types/core/diagnostics.d.ts +5 -0
- package/dist/types/core/schema.d.ts +136 -508
- package/dist/types/core/types.d.ts +2 -2
- package/docs/02-deployment.mdx +21 -2
- package/docs/advanced/changelog.mdx +28 -1
- package/docs/advanced/custom-pages.mdx +63 -2
- package/docs/configuration/ai.mdx +20 -3
- package/docs/configuration/customization.mdx +103 -5
- package/docs/configuration/index.mdx +25 -11
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +5 -0
- package/docs/configuration/theming.mdx +51 -0
- package/docs/content/components.mdx +18 -0
- package/docs/content/islands.mdx +73 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +43 -0
- package/docs/content/syntax.mdx +1 -1
- package/docs/index.mdx +3 -12
- package/docs/reference/cli.mdx +49 -1
- package/docs/reference/frontmatter.mdx +9 -1
- package/package.json +3 -1
- package/src/ai/ask-context.ts +131 -0
- package/src/ai/ask-data.ts +25 -0
- package/src/astro/component-slots.ts +165 -0
- package/src/astro/generate.ts +162 -26
- package/src/astro/integration.ts +59 -0
- package/src/astro/pages.ts +5 -12
- package/src/astro/templates.ts +102 -45
- package/src/blume-modules.d.ts +25 -0
- package/src/cli/commands/build.ts +186 -1
- package/src/cli/commands/check.ts +62 -0
- package/src/cli/commands/dev.ts +21 -1
- package/src/cli/commands/doctor.ts +23 -6
- package/src/cli/commands/init.ts +163 -15
- package/src/cli/commands/validate.ts +16 -2
- package/src/cli/env.ts +84 -0
- package/src/cli/index.ts +20 -0
- package/src/cli/internal-error.ts +63 -0
- package/src/cli/log.ts +30 -1
- package/src/cli/prepare.ts +22 -3
- package/src/cli/required-secrets.ts +44 -0
- package/src/components/BlumePage.astro +107 -0
- package/src/components/content/CodeBlock.astro +7 -2
- package/src/components/index.ts +3 -3
- package/src/components/islands/ask-ai.tsx +15 -1
- package/src/components/islands/hooks.ts +188 -0
- package/src/components/layout/Empty.astro +6 -0
- package/src/components/layout/Header.astro +24 -39
- package/src/components/layout/Logo.astro +50 -0
- package/src/components/layout/NavSelector.astro +75 -0
- package/src/components/layout/PageLayout.astro +38 -2
- package/src/components/layout/RootLayout.astro +70 -4
- package/src/components/layout/hydration-hint.ts +30 -0
- package/src/components/layout/overrides.ts +6 -4
- package/src/components/props.ts +68 -0
- package/src/core/builtin-tags.ts +39 -0
- package/src/core/component-diagnostics.ts +44 -0
- package/src/core/component-overrides.ts +478 -0
- package/src/core/config.ts +8 -0
- package/src/core/data.ts +14 -0
- package/src/core/define-components.ts +9 -2
- package/src/core/diagnostics.ts +90 -1
- package/src/core/graph.ts +7 -0
- package/src/core/nav-diagnostics.ts +205 -0
- package/src/core/project-graph.ts +40 -1
- package/src/core/schema.ts +54 -96
- package/src/core/sources/github-releases.ts +200 -0
- package/src/core/sources/normalize.ts +51 -0
- package/src/core/sources/resolve.ts +16 -0
- package/src/core/types.ts +2 -2
- package/src/deploy/redirects.ts +43 -0
- package/src/markdown/index.ts +24 -0
- package/src/migrate/mintlify/config.ts +1 -176
- package/src/migrate/starlight/config.ts +0 -4
- package/src/og/card.ts +163 -38
- package/src/registry/eject.ts +39 -9
- package/src/registry/registry.ts +166 -0
- package/src/runtime/index.ts +61 -0
- package/src/vite-env.d.ts +14 -0
- package/docs/changelog/v0-1-0.mdx +0 -12
- package/docs/changelog/v0-2-0.mdx +0 -16
package/docs/content/sources.mdx
CHANGED
|
@@ -88,6 +88,49 @@ a remote source for changes instead, set `pollInterval` (seconds) on it — the
|
|
|
88
88
|
dev server re-fetches on that interval and reloads only when the content
|
|
89
89
|
actually changes. Leave it unset to avoid hitting the API while you work.
|
|
90
90
|
|
|
91
|
+
## GitHub Releases
|
|
92
|
+
|
|
93
|
+
The built-in `github-releases` source turns a repo's releases into a changelog:
|
|
94
|
+
each release becomes a `type: changelog` entry, so your release notes _are_ your
|
|
95
|
+
changelog — nothing to write twice. Combined with the generated
|
|
96
|
+
[changelog timeline](/docs/advanced/changelog), publishing a GitHub release
|
|
97
|
+
ships a changelog entry.
|
|
98
|
+
|
|
99
|
+
```ts blume.config.ts
|
|
100
|
+
import { defineConfig } from "blume";
|
|
101
|
+
|
|
102
|
+
export default defineConfig({
|
|
103
|
+
content: {
|
|
104
|
+
sources: [
|
|
105
|
+
{ type: "filesystem", root: "content" },
|
|
106
|
+
{
|
|
107
|
+
type: "github-releases",
|
|
108
|
+
prefix: "changelog",
|
|
109
|
+
owner: "acme",
|
|
110
|
+
repo: "sdk",
|
|
111
|
+
// prereleases: false, // include prereleases (default off)
|
|
112
|
+
// drafts: false, // include drafts (needs a write token)
|
|
113
|
+
// limit: 100, // cap releases, newest-first
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Each release maps to the changelog fields automatically: its name (or tag)
|
|
121
|
+
becomes the title, its published date drives the timeline order, the tag becomes
|
|
122
|
+
`changelog.version`, and prereleases are tagged `Prerelease` (others `Release`).
|
|
123
|
+
The notes render as the entry body. Give the source a `prefix` so its release
|
|
124
|
+
pages nest under a route like `/changelog/v1-2-0`.
|
|
125
|
+
|
|
126
|
+
A private repo authenticates with the `GITHUB_TOKEN` environment variable — the
|
|
127
|
+
same token the other GitHub features use, never inlined into your config. Like
|
|
128
|
+
every remote source it's cached under `.blume/cache/<source>/` and served
|
|
129
|
+
offline if the API is unreachable. Because a changelog is supplementary, a fetch
|
|
130
|
+
failure with no cache (say a CI build without a token) degrades to an empty
|
|
131
|
+
changelog with a warning rather than failing the build — set `GITHUB_TOKEN` in
|
|
132
|
+
your CI and deploy environments to populate it.
|
|
133
|
+
|
|
91
134
|
## Sanity
|
|
92
135
|
|
|
93
136
|
The built-in `sanity` source runs a GROQ query and maps each document's fields
|
package/docs/content/syntax.mdx
CHANGED
|
@@ -11,7 +11,7 @@ each.
|
|
|
11
11
|
## Headings
|
|
12
12
|
|
|
13
13
|
Structure a page with headings. Blume renders your frontmatter `title` as the
|
|
14
|
-
page heading, so start your content at `##` — `##`
|
|
14
|
+
page heading, so start your content at `##` — `##` and `###` become entries
|
|
15
15
|
in the table of contents. Every `##`–`######` heading is also wrapped in a link
|
|
16
16
|
to its own anchor, so readers can click a heading to copy, bookmark, or share a
|
|
17
17
|
permalink straight to that section (hover to reveal the `#`). Turn this off with
|
package/docs/index.mdx
CHANGED
|
@@ -21,18 +21,9 @@ library — with no app boilerplate to write or maintain.
|
|
|
21
21
|
|
|
22
22
|
## Why Blume exists
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
should be _fast_, _AI-ready_, and require _zero configuration_ — down to not
|
|
26
|
-
needing a starter template at all.
|
|
24
|
+
Docs should be fast, AI-ready and require zero configuration — down to not needing a starter template at all. Some docs tools hand you an entire codebase to maintain before you've written a word. Others build the template around your content, but lock you in to their managed service.
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
scaffold, a framework to learn, a template to keep in sync with upstream. Blume
|
|
30
|
-
flips that around. The framework _is_ the template, so the only thing you ever
|
|
31
|
-
touch is your content. When you outgrow the defaults, you add configuration one
|
|
32
|
-
file at a time — and you can `blume eject` to a plain Astro project the day you
|
|
33
|
-
want full control.
|
|
34
|
-
|
|
35
|
-
— Hayden Bleasel
|
|
26
|
+
Blume takes the best of both worlds. The framework is the template, so the only thing you ever touch is your content. When you want to customize, you can start replacing the built-in components, modifying the single configuration file or even ejecting if you want the Astro site directly.
|
|
36
27
|
|
|
37
28
|
## What makes Blume different
|
|
38
29
|
|
|
@@ -83,7 +74,7 @@ loosely-typed YAML.
|
|
|
83
74
|
- **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD, [built in](/docs/configuration/seo).
|
|
84
75
|
- **Customization** — component overrides, React islands, custom pages, theme
|
|
85
76
|
tokens, and a source-component registry via `blume add`.
|
|
86
|
-
- **Migration** — `blume migrate mintlify | starlight | fumadocs`.
|
|
77
|
+
- **Migration** — `blume migrate mintlify | starlight | nextra | fumadocs`.
|
|
87
78
|
- **Eject** — `blume eject` produces a standalone Astro project that still uses
|
|
88
79
|
the `blume` package.
|
|
89
80
|
|
package/docs/reference/cli.mdx
CHANGED
|
@@ -17,17 +17,65 @@ blume <command> [options]
|
|
|
17
17
|
| `blume preview` | Preview the last build. |
|
|
18
18
|
| `blume add <item>` | Install a source component from the registry. |
|
|
19
19
|
| `blume migrate <tool>` | Migrate from Mintlify, Starlight, Nextra, or Fumadocs. |
|
|
20
|
+
| `blume sync` | Re-fetch remote content sources and regenerate. |
|
|
20
21
|
| `blume eject` | Promote the runtime into a standalone Astro app. |
|
|
22
|
+
| `blume check` | Type-check the site with `astro check`. |
|
|
21
23
|
| `blume doctor` | Diagnose config and content problems. |
|
|
22
24
|
| `blume validate` | Validate links across your content. |
|
|
23
25
|
|
|
24
26
|
## Common flags
|
|
25
27
|
|
|
28
|
+
- `blume init --content-dir <dir> --yes` — set the content folder (default `docs`) and skip prompts.
|
|
29
|
+
- `blume init --template docs|api|sdk|changelog` — scaffold from a starter (API reference, SDK, or changelog instead of the plain docs seed).
|
|
30
|
+
- `blume init --package-manager npm|pnpm|yarn|bun` — tailor the printed next-steps to your package manager.
|
|
31
|
+
- `blume init --eject` — scaffold, then eject to a standalone Astro project (falls back to guiding you through `blume eject` when dependencies aren't installed yet).
|
|
26
32
|
- `blume dev --host --port <n> --open`
|
|
27
|
-
- `blume
|
|
33
|
+
- `blume dev --content-dir <dir>` — scan a different content folder without editing `blume.config.ts`.
|
|
34
|
+
- `blume dev --debug` — verbose Astro/Vite logging for troubleshooting.
|
|
35
|
+
- `blume dev --preview` / `blume build --preview` — include drafts and unpublished CMS content.
|
|
36
|
+
- `blume build --strict` — fail the build on diagnostic errors (also works on `blume dev`).
|
|
37
|
+
- `blume build --output static|server --adapter vercel|node|netlify|cloudflare --base /docs` — override the deployment output, adapter, and base path from `blume.config.ts`.
|
|
38
|
+
- `blume build --analyze` — print the client JavaScript bundle sizes (largest first) after the build.
|
|
39
|
+
- `blume build --budget-js <kb> --budget-css <kb>` — fail the build when total client JavaScript/CSS exceeds the budget, turning a performance target into a CI gate.
|
|
40
|
+
- `blume preview --host --port <n>` — bind the preview server.
|
|
41
|
+
- `blume sync --force` — re-fetch remote sources, dropping the cached snapshot first.
|
|
42
|
+
- `blume add <item> --force` — overwrite files that already exist.
|
|
43
|
+
- `blume check --preview` — include drafts and unpublished CMS content when checking.
|
|
44
|
+
- `blume check --strict` — fail on content diagnostics as well as type errors.
|
|
28
45
|
- `blume eject --yes` — skip the confirmation prompt.
|
|
29
46
|
- `blume validate --external` — also check external links over the network.
|
|
30
47
|
- `blume validate --strict` — exit non-zero on warnings too.
|
|
48
|
+
- `blume validate --json` / `blume doctor --json` — emit diagnostics as JSON on
|
|
49
|
+
stdout (with `code`, `severity`, `file`, `line`/`column`, and `docsUrl`) for CI
|
|
50
|
+
and editor integrations.
|
|
51
|
+
|
|
52
|
+
## Type-checking
|
|
53
|
+
|
|
54
|
+
`blume check` runs [`astro check`](https://docs.astro.build/en/reference/cli-reference/#astro-check)
|
|
55
|
+
over your project. It regenerates the `.blume` runtime, syncs Astro's content
|
|
56
|
+
types, then reports any TypeScript errors — in your `blume.config.ts`, in custom
|
|
57
|
+
`.astro` pages, and in the components they import. It exits non-zero when there
|
|
58
|
+
are errors, so it works as a `typecheck` step in CI:
|
|
59
|
+
|
|
60
|
+
```json title="package.json"
|
|
61
|
+
{
|
|
62
|
+
"scripts": {
|
|
63
|
+
"typecheck": "blume check"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Add a `tsconfig.json` extending Astro's config to your project root so authored
|
|
69
|
+
pages resolve `blume/*` imports and virtual modules like `blume:data`:
|
|
70
|
+
|
|
71
|
+
```json title="tsconfig.json"
|
|
72
|
+
{
|
|
73
|
+
"extends": "astro/tsconfigs/strict",
|
|
74
|
+
"include": [".blume/.astro/types.d.ts", ".blume/src/env.d.ts", "**/*"]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Without a project `tsconfig.json`, only the generated runtime is checked.
|
|
31
79
|
|
|
32
80
|
## Validating links
|
|
33
81
|
|
|
@@ -24,6 +24,11 @@ Every page accepts the following frontmatter. All fields are optional.
|
|
|
24
24
|
default: "false",
|
|
25
25
|
description: "Exclude from production builds.",
|
|
26
26
|
},
|
|
27
|
+
lastModified: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description:
|
|
30
|
+
'Pin the page\'s "last updated" date (ISO or YAML date); overrides the git-derived date.',
|
|
31
|
+
},
|
|
27
32
|
}}
|
|
28
33
|
/>
|
|
29
34
|
|
|
@@ -33,7 +38,8 @@ Every page accepts the following frontmatter. All fields are optional.
|
|
|
33
38
|
sidebar:
|
|
34
39
|
label: Install
|
|
35
40
|
order: 2
|
|
36
|
-
icon:
|
|
41
|
+
icon: download
|
|
42
|
+
badge: New
|
|
37
43
|
hidden: false
|
|
38
44
|
```
|
|
39
45
|
|
|
@@ -42,7 +48,9 @@ sidebar:
|
|
|
42
48
|
```yaml lineNumbers
|
|
43
49
|
seo:
|
|
44
50
|
title: Install Blume
|
|
51
|
+
description: Install Blume and scaffold your first project.
|
|
45
52
|
image: /og/install.png
|
|
53
|
+
canonical: https://acme.com/install
|
|
46
54
|
noindex: false
|
|
47
55
|
```
|
|
48
56
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blume",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Documentation that's fast, AI-ready, and zero-config.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"default": "./src/core/schema.ts"
|
|
44
44
|
},
|
|
45
45
|
"./runtime": "./src/runtime/index.ts",
|
|
46
|
+
"./hooks": "./src/components/islands/hooks.ts",
|
|
46
47
|
"./astro": "./src/astro/index.ts",
|
|
47
48
|
"./og": "./src/og/index.ts",
|
|
48
49
|
"./markdown": "./src/markdown/index.ts",
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"typecheck": "tsgo --noEmit && tsgo -p test/tsconfig.json --noEmit"
|
|
63
64
|
},
|
|
64
65
|
"dependencies": {
|
|
66
|
+
"@astrojs/check": "^0.9.0",
|
|
65
67
|
"@astrojs/markdown-satteri": "^0.3.2",
|
|
66
68
|
"@astrojs/mdx": "^7.0.0",
|
|
67
69
|
"@astrojs/node": "^11.0.0",
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { buildOramaIndex, queryOramaIndex } from "../search/orama-index.ts";
|
|
2
|
+
import type { OramaDoc } from "../search/orama-index.ts";
|
|
3
|
+
|
|
4
|
+
/** A chat message as posted by the Ask AI island (`{ role, content }`). */
|
|
5
|
+
export interface AskMessage {
|
|
6
|
+
content: string;
|
|
7
|
+
role: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The current-page hint the island forwards so the endpoint can prioritize it. */
|
|
11
|
+
export interface AskPage {
|
|
12
|
+
path?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The self-contained snapshot the grounded Ask AI endpoint imports. Bundles the
|
|
17
|
+
* search documents so retrieval works regardless of the configured search
|
|
18
|
+
* provider and needs no filesystem access at request time. Serialized to
|
|
19
|
+
* `generated/ask-data.json` and built by {@link buildAskData}.
|
|
20
|
+
*/
|
|
21
|
+
export interface AskData {
|
|
22
|
+
documents: OramaDoc[];
|
|
23
|
+
site: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Documents retrieved per question and injected into the system prompt. */
|
|
27
|
+
const MAX_RESULTS = 6;
|
|
28
|
+
/** Characters kept per injected excerpt. */
|
|
29
|
+
const EXCERPT_CHARS = 1500;
|
|
30
|
+
/** Overall cap on injected documentation characters. */
|
|
31
|
+
const CONTEXT_BUDGET = 8000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The grounding preamble. The model is told to answer strictly from the injected
|
|
35
|
+
* excerpts and to cite the pages it used, so answers stay tied to the docs.
|
|
36
|
+
*/
|
|
37
|
+
const BASE_INSTRUCTION =
|
|
38
|
+
"You are a helpful documentation assistant for this project. Answer the user's question using ONLY the documentation excerpts below. If the answer is not covered by them, say you don't know and suggest where in the docs to look — do not invent details. Cite the page titles you drew from.";
|
|
39
|
+
|
|
40
|
+
/** Normalize a page path to a document `route` (`/`, `/a/b`, no trailing slash). */
|
|
41
|
+
const normalizeRoute = (input: string): string => {
|
|
42
|
+
const noTrailing = input.trim().replace(/\/+$/u, "");
|
|
43
|
+
const withSlash = noTrailing.startsWith("/") ? noTrailing : `/${noTrailing}`;
|
|
44
|
+
return withSlash === "" ? "/" : withSlash;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** The most recent non-empty user message, used as the retrieval query. */
|
|
48
|
+
const lastUserMessage = (messages: AskMessage[]): string => {
|
|
49
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
50
|
+
const message = messages[i];
|
|
51
|
+
if (message?.role === "user" && message.content?.trim()) {
|
|
52
|
+
return message.content.trim();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return "";
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Trim a document body to `max` characters, marking truncation with an ellipsis. */
|
|
59
|
+
const excerpt = (content: string, max: number): string => {
|
|
60
|
+
const trimmed = content.trim();
|
|
61
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the request-time grounding function for the Ask AI endpoint.
|
|
66
|
+
*
|
|
67
|
+
* Lexical retrieval over Orama (the same index/ranking the search dialog and MCP
|
|
68
|
+
* server use). The index is built once and memoized across requests. Returns a
|
|
69
|
+
* grounded system prompt — the retrieved excerpts plus the page the user is
|
|
70
|
+
* viewing — or `undefined` when there is nothing to ground on, so the endpoint
|
|
71
|
+
* can fall back to its plain prompt.
|
|
72
|
+
*/
|
|
73
|
+
export const createAskContext = (
|
|
74
|
+
data: AskData
|
|
75
|
+
): ((
|
|
76
|
+
messages: AskMessage[],
|
|
77
|
+
page?: AskPage
|
|
78
|
+
) => Promise<string | undefined>) => {
|
|
79
|
+
let dbPromise: Promise<Awaited<ReturnType<typeof buildOramaIndex>>> | null =
|
|
80
|
+
null;
|
|
81
|
+
const index = () => {
|
|
82
|
+
dbPromise ??= buildOramaIndex(data.documents);
|
|
83
|
+
return dbPromise;
|
|
84
|
+
};
|
|
85
|
+
const byRoute = new Map(data.documents.map((doc) => [doc.route, doc]));
|
|
86
|
+
|
|
87
|
+
return async (messages, page) => {
|
|
88
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
89
|
+
const query = lastUserMessage(list);
|
|
90
|
+
if (!query) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The current page anchors retrieval to its locale and is injected first.
|
|
95
|
+
const current = page?.path
|
|
96
|
+
? byRoute.get(normalizeRoute(page.path))
|
|
97
|
+
: undefined;
|
|
98
|
+
const db = await index();
|
|
99
|
+
const hits = await queryOramaIndex(
|
|
100
|
+
db,
|
|
101
|
+
query,
|
|
102
|
+
MAX_RESULTS,
|
|
103
|
+
current?.locale || undefined
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const seen = new Set<string>();
|
|
107
|
+
const sections: string[] = [];
|
|
108
|
+
let budget = CONTEXT_BUDGET;
|
|
109
|
+
const push = (doc: OramaDoc, label: string) => {
|
|
110
|
+
if (seen.has(doc.route) || budget <= 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
seen.add(doc.route);
|
|
114
|
+
const body = excerpt(doc.content, Math.min(EXCERPT_CHARS, budget));
|
|
115
|
+
budget -= body.length;
|
|
116
|
+
sections.push(`## ${doc.title} (${doc.route})${label}\n${body}`);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (current) {
|
|
120
|
+
push(current, " — the page the user is currently viewing");
|
|
121
|
+
}
|
|
122
|
+
for (const hit of hits) {
|
|
123
|
+
push(hit, "");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (sections.length === 0) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
return `${BASE_INSTRUCTION}\n\n<docs>\n${sections.join("\n\n")}\n</docs>`;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BlumeProject } from "../core/project-graph.ts";
|
|
2
|
+
import { buildSearchDocuments } from "../search/documents.ts";
|
|
3
|
+
import type { AskData } from "./ask-context.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build the grounding snapshot the Ask AI endpoint serves. Like the MCP server,
|
|
7
|
+
* Ask AI is independent of on-page search, so documents are indexed even when the
|
|
8
|
+
* search provider is `none` (`includeWhenDisabled`). `locale` is kept (unlike the
|
|
9
|
+
* MCP snapshot) so retrieval can be filtered to the current page's language.
|
|
10
|
+
*/
|
|
11
|
+
export const buildAskData = async (project: BlumeProject): Promise<AskData> => {
|
|
12
|
+
const documents = await buildSearchDocuments(project, {
|
|
13
|
+
includeWhenDisabled: true,
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
documents: documents.map((doc) => ({
|
|
17
|
+
content: doc.content,
|
|
18
|
+
description: doc.description,
|
|
19
|
+
locale: doc.locale,
|
|
20
|
+
route: doc.route,
|
|
21
|
+
title: doc.title,
|
|
22
|
+
})),
|
|
23
|
+
site: project.config.deployment.site ?? null,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentOverrideAnalysis,
|
|
3
|
+
NormalizedOverride,
|
|
4
|
+
OverrideFramework,
|
|
5
|
+
} from "../core/component-overrides.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Turn analyzed `components.ts` overrides into the generated `components.ts`
|
|
9
|
+
* module plus per-override hydration wrappers.
|
|
10
|
+
*
|
|
11
|
+
* The runtime overrides object already holds bare imported identifiers, so those
|
|
12
|
+
* (when not hydrated) ride through a plain spread. Everything else needs a static
|
|
13
|
+
* import Vite can see: path-string overrides import the file directly, while
|
|
14
|
+
* hydrated overrides get a wrapper `.astro` that applies the `client:*` directive
|
|
15
|
+
* (Astro directives must be written statically). The `islands` group is folded
|
|
16
|
+
* into the MDX component map — it is just `mdx` with a default `client: "visible"`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface ComponentSlotWrapper {
|
|
20
|
+
content: string;
|
|
21
|
+
/** File name (no extension) under `.blume/src/generated/component-slots/`. */
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ComponentSlotPlan {
|
|
26
|
+
/** Frameworks used by resolved overrides; enable the matching Astro renderer. */
|
|
27
|
+
frameworks: Set<OverrideFramework>;
|
|
28
|
+
/** Contents of `.blume/src/generated/components.ts`. */
|
|
29
|
+
module: string;
|
|
30
|
+
wrappers: ComponentSlotWrapper[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const EMPTY_MODULE = `// Generated by Blume. Do not edit.
|
|
34
|
+
export const mdxComponents = {};
|
|
35
|
+
export const layoutOverrides = {};
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
/** Astro client directive for a hydrated override. */
|
|
39
|
+
const directiveFor = (override: NormalizedOverride): string => {
|
|
40
|
+
const framework = override.source?.framework;
|
|
41
|
+
switch (override.client) {
|
|
42
|
+
case "idle": {
|
|
43
|
+
return "client:idle";
|
|
44
|
+
}
|
|
45
|
+
case "visible": {
|
|
46
|
+
return "client:visible";
|
|
47
|
+
}
|
|
48
|
+
case "media": {
|
|
49
|
+
return override.media
|
|
50
|
+
? `client:media="${override.media}"`
|
|
51
|
+
: "client:load";
|
|
52
|
+
}
|
|
53
|
+
case "only": {
|
|
54
|
+
return framework ? `client:only="${framework}"` : "client:load";
|
|
55
|
+
}
|
|
56
|
+
default: {
|
|
57
|
+
return "client:load";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const importClause = (variable: string, name: string, path: string): string =>
|
|
63
|
+
name === "default"
|
|
64
|
+
? `import ${variable} from ${JSON.stringify(path)};`
|
|
65
|
+
: `import { ${name} as ${variable} } from ${JSON.stringify(path)};`;
|
|
66
|
+
|
|
67
|
+
/** A wrapper `.astro` that statically imports a component and hydrates it. */
|
|
68
|
+
const wrapperContent = (override: NormalizedOverride): string => {
|
|
69
|
+
const { name, path } = override.source as NonNullable<
|
|
70
|
+
NormalizedOverride["source"]
|
|
71
|
+
>;
|
|
72
|
+
const clause =
|
|
73
|
+
name === "default"
|
|
74
|
+
? `import Component from ${JSON.stringify(path)};`
|
|
75
|
+
: `import { ${name} as Component } from ${JSON.stringify(path)};`;
|
|
76
|
+
return `---
|
|
77
|
+
// Generated by Blume. Do not edit.
|
|
78
|
+
${clause}
|
|
79
|
+
---
|
|
80
|
+
<Component ${directiveFor(override)} {...Astro.props}><slot /></Component>
|
|
81
|
+
`;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const sanitize = (value: string): string =>
|
|
85
|
+
value.replaceAll(/[^A-Za-z0-9]/gu, "_");
|
|
86
|
+
|
|
87
|
+
export const planComponentSlots = (
|
|
88
|
+
componentsFile: string | null,
|
|
89
|
+
analysis: ComponentOverrideAnalysis | null
|
|
90
|
+
): ComponentSlotPlan => {
|
|
91
|
+
const frameworks = new Set<OverrideFramework>();
|
|
92
|
+
if (!componentsFile) {
|
|
93
|
+
return { frameworks, module: EMPTY_MODULE, wrappers: [] };
|
|
94
|
+
}
|
|
95
|
+
if (!analysis) {
|
|
96
|
+
return {
|
|
97
|
+
frameworks,
|
|
98
|
+
module: `// Generated by Blume. Do not edit.
|
|
99
|
+
import overrides from ${JSON.stringify(componentsFile)};
|
|
100
|
+
export const mdxComponents = overrides.mdx ?? {};
|
|
101
|
+
export const layoutOverrides = overrides.layout ?? {};
|
|
102
|
+
`,
|
|
103
|
+
wrappers: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const wrappers: ComponentSlotWrapper[] = [];
|
|
108
|
+
const importLines: string[] = [];
|
|
109
|
+
// Explicit `key: Variable` map entries, per surface.
|
|
110
|
+
const mdxEntries: string[] = [];
|
|
111
|
+
const layoutEntries: string[] = [];
|
|
112
|
+
let counter = 0;
|
|
113
|
+
|
|
114
|
+
// Islands are MDX components hydrated by default, so plan them alongside `mdx`.
|
|
115
|
+
const mdxOverrides = [...analysis.mdx, ...analysis.islands];
|
|
116
|
+
|
|
117
|
+
const plan = (
|
|
118
|
+
override: NormalizedOverride,
|
|
119
|
+
group: "mdx" | "layout",
|
|
120
|
+
entries: string[]
|
|
121
|
+
): void => {
|
|
122
|
+
const { source } = override;
|
|
123
|
+
if (source?.framework) {
|
|
124
|
+
frameworks.add(source.framework);
|
|
125
|
+
}
|
|
126
|
+
// Bare identifier, not hydrated: the runtime object already has it.
|
|
127
|
+
if (override.identifier && !override.client) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!source) {
|
|
131
|
+
// Unresolved (already warned): leave it on the runtime object.
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const variable = `__blumeSlot${counter}`;
|
|
135
|
+
counter += 1;
|
|
136
|
+
if (override.client) {
|
|
137
|
+
const name = `${group}-${sanitize(override.key)}`;
|
|
138
|
+
wrappers.push({ content: wrapperContent(override), name });
|
|
139
|
+
importLines.push(
|
|
140
|
+
importClause(variable, "default", `./component-slots/${name}.astro`)
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
importLines.push(importClause(variable, source.name, source.path));
|
|
144
|
+
}
|
|
145
|
+
entries.push(`${JSON.stringify(override.key)}: ${variable}`);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
for (const override of mdxOverrides) {
|
|
149
|
+
plan(override, "mdx", mdxEntries);
|
|
150
|
+
}
|
|
151
|
+
for (const override of analysis.layout) {
|
|
152
|
+
plan(override, "layout", layoutEntries);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// `overrides.islands` is not spread: every valid island is hydrated, so it is
|
|
156
|
+
// emitted as an explicit wrapper entry above (raw spreads only carry bare,
|
|
157
|
+
// non-hydrated identifiers from `mdx`/`layout`).
|
|
158
|
+
const moduleSource = `// Generated by Blume. Do not edit.
|
|
159
|
+
import overrides from ${JSON.stringify(componentsFile)};
|
|
160
|
+
${importLines.join("\n")}${importLines.length ? "\n" : ""}export const mdxComponents = { ...(overrides.mdx ?? {})${mdxEntries.length ? `, ${mdxEntries.join(", ")}` : ""} };
|
|
161
|
+
export const layoutOverrides = { ...(overrides.layout ?? {})${layoutEntries.length ? `, ${layoutEntries.join(", ")}` : ""} };
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
return { frameworks, module: moduleSource, wrappers };
|
|
165
|
+
};
|