blume 0.2.0 → 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 +1921 -560
- package/dist/cli/index.js.map +36 -24
- 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 +26 -502
- package/dist/types/core/types.d.ts +2 -2
- package/docs/02-deployment.mdx +21 -2
- package/docs/advanced/custom-pages.mdx +63 -1
- package/docs/configuration/ai.mdx +20 -3
- package/docs/configuration/customization.mdx +103 -5
- package/docs/configuration/index.mdx +13 -0
- package/docs/configuration/seo.mdx +5 -0
- package/docs/content/islands.mdx +73 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/index.mdx +3 -12
- package/docs/reference/cli.mdx +42 -0
- 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 +132 -13
- package/src/astro/integration.ts +59 -0
- package/src/astro/pages.ts +5 -12
- package/src/astro/templates.ts +92 -44
- 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/index.ts +15 -0
- package/src/cli/internal-error.ts +63 -0
- package/src/cli/log.ts +30 -1
- package/src/cli/prepare.ts +17 -3
- package/src/cli/required-secrets.ts +44 -0
- package/src/components/BlumePage.astro +107 -0
- 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 +28 -96
- package/src/core/sources/normalize.ts +51 -0
- package/src/core/types.ts +2 -2
- package/src/deploy/redirects.ts +43 -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
|
@@ -4,16 +4,23 @@ import { BlumeError } from "../../core/diagnostics.ts";
|
|
|
4
4
|
import { scanProject } from "../../core/project-graph.ts";
|
|
5
5
|
import { serverFeatures } from "../../core/server-features.ts";
|
|
6
6
|
import type { Diagnostic } from "../../core/types.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { reportInternalError } from "../internal-error.ts";
|
|
8
|
+
import { logger, reportDiagnostics, reportDiagnosticsJson } from "../log.ts";
|
|
8
9
|
|
|
9
10
|
const MIN_NODE_MAJOR = 20;
|
|
10
11
|
|
|
11
12
|
export const doctorCommand = defineCommand({
|
|
13
|
+
args: {
|
|
14
|
+
json: {
|
|
15
|
+
description: "Emit diagnostics as JSON on stdout (for CI/editors).",
|
|
16
|
+
type: "boolean",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
12
19
|
meta: {
|
|
13
20
|
description: "Diagnose common configuration and content problems.",
|
|
14
21
|
name: "doctor",
|
|
15
22
|
},
|
|
16
|
-
async run() {
|
|
23
|
+
async run({ args }) {
|
|
17
24
|
const root = process.cwd();
|
|
18
25
|
const diagnostics: Diagnostic[] = [];
|
|
19
26
|
|
|
@@ -52,15 +59,25 @@ export const doctorCommand = defineCommand({
|
|
|
52
59
|
});
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
if (!args.json) {
|
|
63
|
+
logger.info(`Pages: ${project.graph.pages.length}`);
|
|
64
|
+
logger.info(`Output: ${config.deployment.output}`);
|
|
65
|
+
logger.info(`Search: ${config.search.provider}`);
|
|
66
|
+
}
|
|
58
67
|
} catch (error) {
|
|
59
68
|
if (error instanceof BlumeError) {
|
|
60
69
|
diagnostics.push(error.diagnostic);
|
|
61
70
|
} else {
|
|
62
|
-
|
|
71
|
+
reportInternalError(error);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (args.json) {
|
|
77
|
+
if (reportDiagnosticsJson(diagnostics, root)) {
|
|
78
|
+
process.exit(1);
|
|
63
79
|
}
|
|
80
|
+
return;
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
const hadErrors = reportDiagnostics(diagnostics, root);
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { defineCommand } from "citty";
|
|
|
5
5
|
import { basename, dirname, join } from "pathe";
|
|
6
6
|
|
|
7
7
|
import { getBlumeVersion } from "../../core/version.ts";
|
|
8
|
+
import { eject } from "../../registry/eject.ts";
|
|
8
9
|
import { logger } from "../log.ts";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -32,25 +33,119 @@ const packageTemplate = (name: string, version: string): string => `{
|
|
|
32
33
|
}
|
|
33
34
|
`;
|
|
34
35
|
|
|
35
|
-
const
|
|
36
|
+
const TEMPLATES = ["docs", "api", "sdk", "changelog"] as const;
|
|
37
|
+
type Template = (typeof TEMPLATES)[number];
|
|
38
|
+
|
|
39
|
+
const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"] as const;
|
|
40
|
+
type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
41
|
+
|
|
42
|
+
/** A starter: the config plus seed content files (path is relative to root). */
|
|
43
|
+
interface Starter {
|
|
44
|
+
config: string;
|
|
45
|
+
files: (contentDir: string) => { content: string; path: string }[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const configFor = (
|
|
49
|
+
extra: string
|
|
50
|
+
): string => `import { defineConfig } from "blume";
|
|
36
51
|
|
|
37
52
|
export default defineConfig({
|
|
38
53
|
title: "My Docs",
|
|
39
|
-
description: "Documentation powered by Blume."
|
|
54
|
+
description: "Documentation powered by Blume.",${extra}
|
|
40
55
|
});
|
|
41
56
|
`;
|
|
42
57
|
|
|
43
|
-
const
|
|
44
|
-
title:
|
|
45
|
-
description: Welcome to your new Blume docs.
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
# Introduction
|
|
58
|
+
const page = (title: string, description: string, body: string): string =>
|
|
59
|
+
`---\ntitle: ${title}\ndescription: ${description}\n---\n\n${body}\n`;
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
const STARTERS: Record<Template, Starter> = {
|
|
62
|
+
api: {
|
|
63
|
+
config: configFor(`
|
|
64
|
+
openapi: {
|
|
65
|
+
enabled: true,
|
|
66
|
+
route: "/api",
|
|
67
|
+
sources: [
|
|
68
|
+
{
|
|
69
|
+
label: "Petstore",
|
|
70
|
+
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},`),
|
|
74
|
+
files: (dir) => [
|
|
75
|
+
{
|
|
76
|
+
content: page(
|
|
77
|
+
"API Reference",
|
|
78
|
+
"Explore the API.",
|
|
79
|
+
"# API Reference\n\nYour OpenAPI spec renders at [`/api`](/api). Point `openapi.sources` at your own spec in `blume.config.ts`."
|
|
80
|
+
),
|
|
81
|
+
path: join(dir, "index.mdx"),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
changelog: {
|
|
86
|
+
config: configFor(`
|
|
87
|
+
navigation: {
|
|
88
|
+
tabs: [
|
|
89
|
+
{ label: "Docs", path: "/" },
|
|
90
|
+
{ label: "Changelog", path: "/changelog" },
|
|
91
|
+
],
|
|
92
|
+
},`),
|
|
93
|
+
files: (dir) => [
|
|
94
|
+
{
|
|
95
|
+
content: page(
|
|
96
|
+
"Introduction",
|
|
97
|
+
"Welcome to your new Blume docs.",
|
|
98
|
+
"# Introduction\n\nWrite your docs here, and log releases under `changelog/`."
|
|
99
|
+
),
|
|
100
|
+
path: join(dir, "index.mdx"),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
content: `---\ntitle: v1.0.0\ntype: changelog\ndate: 2026-01-01\n---\n\nThe first release. Edit \`${dir}/changelog/v1-0-0.mdx\` or add new entries beside it.\n`,
|
|
104
|
+
path: join(dir, "changelog", "v1-0-0.mdx"),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
docs: {
|
|
109
|
+
config: configFor(""),
|
|
110
|
+
files: (dir) => [
|
|
111
|
+
{
|
|
112
|
+
content: page(
|
|
113
|
+
"Introduction",
|
|
114
|
+
"Welcome to your new Blume docs.",
|
|
115
|
+
`# Introduction\n\nWelcome to **Blume** — markdown-first docs powered by Astro and Vite.\n\nEdit \`${dir}/index.mdx\` to get started, then run \`blume dev\`.`
|
|
116
|
+
),
|
|
117
|
+
path: join(dir, "index.mdx"),
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
sdk: {
|
|
122
|
+
config: configFor(""),
|
|
123
|
+
files: (dir) => [
|
|
124
|
+
{
|
|
125
|
+
content: page(
|
|
126
|
+
"Introduction",
|
|
127
|
+
"Get started with the SDK.",
|
|
128
|
+
"# Introduction\n\nInstall the SDK and make your first call. See [Installation](/installation)."
|
|
129
|
+
),
|
|
130
|
+
path: join(dir, "index.mdx"),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
content: page(
|
|
134
|
+
"Installation",
|
|
135
|
+
"Install the SDK.",
|
|
136
|
+
"# Installation\n\n```package-install\nyour-sdk\n```"
|
|
137
|
+
),
|
|
138
|
+
path: join(dir, "installation.mdx"),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
};
|
|
51
143
|
|
|
52
|
-
|
|
53
|
-
|
|
144
|
+
/** Install + dev commands to print for the chosen package manager. */
|
|
145
|
+
const commandsFor = (pm: PackageManager): { dev: string; install: string } => ({
|
|
146
|
+
dev: pm === "npm" ? "npm run dev" : `${pm} dev`,
|
|
147
|
+
install: `${pm} install`,
|
|
148
|
+
});
|
|
54
149
|
|
|
55
150
|
const writeFileSafe = async (
|
|
56
151
|
path: string,
|
|
@@ -73,6 +168,19 @@ export const initCommand = defineCommand({
|
|
|
73
168
|
description: "Content directory.",
|
|
74
169
|
type: "string",
|
|
75
170
|
},
|
|
171
|
+
eject: {
|
|
172
|
+
description: "Eject to a standalone Astro project after scaffolding.",
|
|
173
|
+
type: "boolean",
|
|
174
|
+
},
|
|
175
|
+
"package-manager": {
|
|
176
|
+
description:
|
|
177
|
+
"Package manager for the next-steps hint (npm|pnpm|yarn|bun).",
|
|
178
|
+
type: "string",
|
|
179
|
+
},
|
|
180
|
+
template: {
|
|
181
|
+
description: "Starter template: docs | api | sdk | changelog.",
|
|
182
|
+
type: "string",
|
|
183
|
+
},
|
|
76
184
|
yes: { description: "Skip prompts.", type: "boolean" },
|
|
77
185
|
},
|
|
78
186
|
meta: {
|
|
@@ -83,16 +191,56 @@ export const initCommand = defineCommand({
|
|
|
83
191
|
const root = process.cwd();
|
|
84
192
|
const contentDir = args["content-dir"] ?? "docs";
|
|
85
193
|
|
|
194
|
+
const template = (args.template ?? "docs") as Template;
|
|
195
|
+
if (!TEMPLATES.includes(template)) {
|
|
196
|
+
logger.error(
|
|
197
|
+
`Unknown template "${args.template}" (use ${TEMPLATES.join(" | ")}).`
|
|
198
|
+
);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
const pm = (args["package-manager"] ?? "npm") as PackageManager;
|
|
202
|
+
if (!PACKAGE_MANAGERS.includes(pm)) {
|
|
203
|
+
logger.error(
|
|
204
|
+
`Unknown package manager "${args["package-manager"]}" (use ${PACKAGE_MANAGERS.join(" | ")}).`
|
|
205
|
+
);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const starter = STARTERS[template];
|
|
86
210
|
const createdPackage = await writeFileSafe(
|
|
87
211
|
join(root, "package.json"),
|
|
88
212
|
packageTemplate(toPackageName(basename(root)), getBlumeVersion())
|
|
89
213
|
);
|
|
90
|
-
await writeFileSafe(join(root, "blume.config.ts"),
|
|
91
|
-
await
|
|
214
|
+
await writeFileSafe(join(root, "blume.config.ts"), starter.config);
|
|
215
|
+
await Promise.all(
|
|
216
|
+
starter
|
|
217
|
+
.files(contentDir)
|
|
218
|
+
.map((file) => writeFileSafe(join(root, file.path), file.content))
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const commands = commandsFor(pm);
|
|
222
|
+
|
|
223
|
+
if (args.eject) {
|
|
224
|
+
// Eject only generates files (no Astro runtime), so it works right after
|
|
225
|
+
// scaffolding. The standalone app then runs with Astro directly.
|
|
226
|
+
try {
|
|
227
|
+
await eject(root);
|
|
228
|
+
logger.success("Ejected to a standalone Astro project.");
|
|
229
|
+
logger.box(`Next steps:\n\n ${commands.install}\n npx astro dev\n`);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logger.warn(
|
|
232
|
+
`Scaffolded, but eject failed: ${(error as Error).message}`
|
|
233
|
+
);
|
|
234
|
+
logger.box(
|
|
235
|
+
`Next steps:\n\n ${commands.install}\n blume eject --yes\n`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
92
240
|
|
|
93
241
|
const nextSteps = createdPackage
|
|
94
|
-
?
|
|
95
|
-
:
|
|
242
|
+
? `Next steps:\n\n ${commands.install}\n ${commands.dev}\n`
|
|
243
|
+
: `Next steps:\n\n ${commands.dev}\n`;
|
|
96
244
|
logger.box(nextSteps);
|
|
97
245
|
},
|
|
98
246
|
});
|
|
@@ -7,7 +7,8 @@ import { BlumeError } from "../../core/diagnostics.ts";
|
|
|
7
7
|
import { validateLinks } from "../../core/links.ts";
|
|
8
8
|
import { scanProject } from "../../core/project-graph.ts";
|
|
9
9
|
import type { Diagnostic } from "../../core/types.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { reportInternalError } from "../internal-error.ts";
|
|
11
|
+
import { logger, reportDiagnostics, reportDiagnosticsJson } from "../log.ts";
|
|
11
12
|
|
|
12
13
|
export const validateCommand = defineCommand({
|
|
13
14
|
args: {
|
|
@@ -15,6 +16,10 @@ export const validateCommand = defineCommand({
|
|
|
15
16
|
description: "Check external (HTTP) links over the network.",
|
|
16
17
|
type: "boolean",
|
|
17
18
|
},
|
|
19
|
+
json: {
|
|
20
|
+
description: "Emit diagnostics as JSON on stdout (for CI/editors).",
|
|
21
|
+
type: "boolean",
|
|
22
|
+
},
|
|
18
23
|
strict: {
|
|
19
24
|
description: "Treat warnings as errors.",
|
|
20
25
|
type: "boolean",
|
|
@@ -46,8 +51,17 @@ export const validateCommand = defineCommand({
|
|
|
46
51
|
if (error instanceof BlumeError) {
|
|
47
52
|
diagnostics.push(error.diagnostic);
|
|
48
53
|
} else {
|
|
49
|
-
|
|
54
|
+
reportInternalError(error);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (args.json) {
|
|
60
|
+
const hadErrors = reportDiagnosticsJson(diagnostics, root);
|
|
61
|
+
if (hadErrors || (Boolean(args.strict) && diagnostics.length > 0)) {
|
|
62
|
+
process.exit(1);
|
|
50
63
|
}
|
|
64
|
+
return;
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
const hadErrors = reportDiagnostics(diagnostics, root);
|
package/src/cli/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { defineCommand, runMain } from "citty";
|
|
|
3
3
|
import { getBlumeVersion } from "../core/version.ts";
|
|
4
4
|
import { addCommand } from "./commands/add.ts";
|
|
5
5
|
import { buildCommand } from "./commands/build.ts";
|
|
6
|
+
import { checkCommand } from "./commands/check.ts";
|
|
6
7
|
import { devCommand } from "./commands/dev.ts";
|
|
7
8
|
import { doctorCommand } from "./commands/doctor.ts";
|
|
8
9
|
import { ejectCommand } from "./commands/eject.ts";
|
|
@@ -12,6 +13,7 @@ import { previewCommand } from "./commands/preview.ts";
|
|
|
12
13
|
import { syncCommand } from "./commands/sync.ts";
|
|
13
14
|
import { validateCommand } from "./commands/validate.ts";
|
|
14
15
|
import { loadEnvFiles } from "./env.ts";
|
|
16
|
+
import { reportInternalError } from "./internal-error.ts";
|
|
15
17
|
|
|
16
18
|
const main = defineCommand({
|
|
17
19
|
meta: {
|
|
@@ -22,6 +24,7 @@ const main = defineCommand({
|
|
|
22
24
|
subCommands: {
|
|
23
25
|
add: addCommand,
|
|
24
26
|
build: buildCommand,
|
|
27
|
+
check: checkCommand,
|
|
25
28
|
dev: devCommand,
|
|
26
29
|
doctor: doctorCommand,
|
|
27
30
|
eject: ejectCommand,
|
|
@@ -37,4 +40,16 @@ const main = defineCommand({
|
|
|
37
40
|
// can read their tokens (e.g. `GITHUB_TOKEN`) during the content scan.
|
|
38
41
|
loadEnvFiles(process.cwd());
|
|
39
42
|
|
|
43
|
+
// Backstop for unexpected async failures that escape a command's own handling
|
|
44
|
+
// (e.g. a rejected timer/watcher in `blume dev`), so even those report through
|
|
45
|
+
// the stable internal-error contract rather than a bare stack trace.
|
|
46
|
+
process.on("uncaughtException", (error) => {
|
|
47
|
+
reportInternalError(error);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
|
50
|
+
process.on("unhandledRejection", (error) => {
|
|
51
|
+
reportInternalError(error);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
40
55
|
runMain(main);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getBlumeVersion } from "../core/version.ts";
|
|
2
|
+
|
|
3
|
+
const ESC = String.fromCodePoint(27);
|
|
4
|
+
const DIM = `${ESC}[2m`;
|
|
5
|
+
const RED = `${ESC}[31m`;
|
|
6
|
+
const BOLD = `${ESC}[1m`;
|
|
7
|
+
const RESET = `${ESC}[0m`;
|
|
8
|
+
|
|
9
|
+
const ISSUES_URL = "https://github.com/haydenbleasel/blume/issues";
|
|
10
|
+
|
|
11
|
+
// Absolute paths into the hidden generated runtime (`…/.blume/…`), including any
|
|
12
|
+
// trailing `:line:col`, stopping at whitespace or a closing paren.
|
|
13
|
+
const BLUME_FRAME = /(?<abs>\/[^\s()]*\/\.blume\/[^\s()]*)/gu;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Rewrite `.blume/` frames in a stack so the generated runtime reads clearly:
|
|
17
|
+
* the machine-absolute prefix is dropped to a project-relative `.blume/…` path
|
|
18
|
+
* and tagged `(generated)`, keeping the reader oriented instead of staring at a
|
|
19
|
+
* long path into a hidden directory. Frames in the user's own source (custom
|
|
20
|
+
* pages keep their real location; wrappers import user files by their real path)
|
|
21
|
+
* are untouched, so the actionable frame stays intact.
|
|
22
|
+
*/
|
|
23
|
+
export const remapBlumeStack = (stack: string): string =>
|
|
24
|
+
stack.replaceAll(BLUME_FRAME, (match) => {
|
|
25
|
+
const marker = match.indexOf("/.blume/");
|
|
26
|
+
return `${match.slice(marker + 1)} (generated)`;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Print an unexpected (non-{@link BlumeError}) failure in a stable, reportable
|
|
31
|
+
* shape instead of a bare stack trace: a fixed `BLUME_INTERNAL` code, the
|
|
32
|
+
* message, a trimmed stack, and an environment dump for bug reports. Callers
|
|
33
|
+
* exit after this — it doesn't exit itself, so it's testable.
|
|
34
|
+
*/
|
|
35
|
+
export const reportInternalError = (error: unknown): void => {
|
|
36
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
37
|
+
const lines = [
|
|
38
|
+
`${RED}${BOLD}BLUME_INTERNAL${RESET} An unexpected error occurred.`,
|
|
39
|
+
` ${err.message}`,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// A few frames are enough to locate the fault without burying the report;
|
|
43
|
+
// `.blume/` frames are relativized so the hidden runtime reads clearly.
|
|
44
|
+
const stack = remapBlumeStack(err.stack ?? "")
|
|
45
|
+
.split("\n")
|
|
46
|
+
.slice(1, 5)
|
|
47
|
+
.map((line) => line.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
if (stack.length > 0) {
|
|
50
|
+
lines.push("", `${DIM}${stack.join("\n")}${RESET}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lines.push(
|
|
54
|
+
"",
|
|
55
|
+
"This is likely a bug in Blume. Please report it with the details below:",
|
|
56
|
+
` ${DIM}Blume: ${getBlumeVersion()}`,
|
|
57
|
+
` Node: ${process.version}`,
|
|
58
|
+
` Platform: ${process.platform} ${process.arch}${RESET}`,
|
|
59
|
+
` ${ISSUES_URL}`
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
process.stderr.write(`${lines.join("\n")}\n`);
|
|
63
|
+
};
|
package/src/cli/log.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { consola } from "consola";
|
|
2
|
+
import { relative } from "pathe";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
countBySeverity,
|
|
6
|
+
enrichDiagnostic,
|
|
5
7
|
formatDiagnostic,
|
|
6
8
|
hasErrors,
|
|
7
9
|
} from "../core/diagnostics.ts";
|
|
@@ -9,6 +11,31 @@ import type { Diagnostic } from "../core/types.ts";
|
|
|
9
11
|
|
|
10
12
|
export const logger = consola.withTag("blume");
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Print diagnostics as a JSON document on stdout for CI and editors: each is
|
|
16
|
+
* enriched with its `docsUrl` and its `file` made root-relative. Returns whether
|
|
17
|
+
* any were errors, matching {@link reportDiagnostics}.
|
|
18
|
+
*/
|
|
19
|
+
export const reportDiagnosticsJson = (
|
|
20
|
+
diagnostics: Diagnostic[],
|
|
21
|
+
root?: string
|
|
22
|
+
): boolean => {
|
|
23
|
+
const enriched = diagnostics.map((diagnostic) => {
|
|
24
|
+
const withDocs = enrichDiagnostic(diagnostic);
|
|
25
|
+
return withDocs.file && root
|
|
26
|
+
? { ...withDocs, file: relative(root, withDocs.file) }
|
|
27
|
+
: withDocs;
|
|
28
|
+
});
|
|
29
|
+
process.stdout.write(
|
|
30
|
+
`${JSON.stringify(
|
|
31
|
+
{ diagnostics: enriched, summary: countBySeverity(diagnostics) },
|
|
32
|
+
null,
|
|
33
|
+
2
|
|
34
|
+
)}\n`
|
|
35
|
+
);
|
|
36
|
+
return hasErrors(diagnostics);
|
|
37
|
+
};
|
|
38
|
+
|
|
12
39
|
/** Print a batch of diagnostics and return whether any were errors. */
|
|
13
40
|
export const reportDiagnostics = (
|
|
14
41
|
diagnostics: Diagnostic[],
|
|
@@ -19,7 +46,9 @@ export const reportDiagnostics = (
|
|
|
19
46
|
}
|
|
20
47
|
|
|
21
48
|
for (const diagnostic of diagnostics) {
|
|
22
|
-
process.stderr.write(
|
|
49
|
+
process.stderr.write(
|
|
50
|
+
`${formatDiagnostic(enrichDiagnostic(diagnostic), root)}\n`
|
|
51
|
+
);
|
|
23
52
|
}
|
|
24
53
|
|
|
25
54
|
const counts = countBySeverity(diagnostics);
|
package/src/cli/prepare.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { generateRuntime } from "../astro/generate.ts";
|
|
2
2
|
import { BlumeError, hasErrors } from "../core/diagnostics.ts";
|
|
3
3
|
import { scanProject } from "../core/project-graph.ts";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
BlumeProject,
|
|
6
|
+
BuildMode,
|
|
7
|
+
ConfigOverrides,
|
|
8
|
+
} from "../core/project-graph.ts";
|
|
5
9
|
import { serverFeatures } from "../core/server-features.ts";
|
|
6
10
|
import { loadEnvFiles } from "./env.ts";
|
|
11
|
+
import { reportInternalError } from "./internal-error.ts";
|
|
7
12
|
import { logger, reportDiagnostics } from "./log.ts";
|
|
13
|
+
import { checkRequiredSecrets } from "./required-secrets.ts";
|
|
8
14
|
|
|
9
15
|
export interface PrepareOptions {
|
|
10
16
|
root: string;
|
|
@@ -16,6 +22,8 @@ export interface PrepareOptions {
|
|
|
16
22
|
preview?: boolean;
|
|
17
23
|
/** Force remote sources to re-fetch instead of serving the cached snapshot. */
|
|
18
24
|
refresh?: boolean;
|
|
25
|
+
/** CLI config overrides (e.g. `--output`, `--content-dir`). */
|
|
26
|
+
overrides?: ConfigOverrides;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
/**
|
|
@@ -34,15 +42,17 @@ export const prepareProject = async (
|
|
|
34
42
|
project = await scanProject(options.root, {
|
|
35
43
|
devServerUrl: options.devServerUrl,
|
|
36
44
|
mode: options.mode,
|
|
45
|
+
overrides: options.overrides,
|
|
37
46
|
preview: options.preview,
|
|
38
47
|
refresh: options.refresh,
|
|
39
48
|
});
|
|
40
49
|
} catch (error) {
|
|
41
50
|
if (error instanceof BlumeError) {
|
|
42
51
|
reportDiagnostics([error.diagnostic], options.root);
|
|
43
|
-
|
|
52
|
+
} else {
|
|
53
|
+
reportInternalError(error);
|
|
44
54
|
}
|
|
45
|
-
|
|
55
|
+
process.exit(1);
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
// Hard gate: server-only features cannot ship in a static build.
|
|
@@ -81,5 +91,9 @@ export const prepareProject = async (
|
|
|
81
91
|
for (const warning of warnings) {
|
|
82
92
|
logger.warn(warning);
|
|
83
93
|
}
|
|
94
|
+
|
|
95
|
+
// Fail-fast on missing runtime secrets: warn now, not at the first request.
|
|
96
|
+
reportDiagnostics(checkRequiredSecrets(project.config), options.root);
|
|
97
|
+
|
|
84
98
|
return project;
|
|
85
99
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { resolveAskBackend } from "../ai/ask.ts";
|
|
2
|
+
import type { ResolvedConfig } from "../core/schema.ts";
|
|
3
|
+
import type { Diagnostic } from "../core/types.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Warn early when an enabled feature needs a secret env var that isn't set, so
|
|
7
|
+
* the failure surfaces at `blume dev`/`build` instead of at the first request in
|
|
8
|
+
* production. These are runtime secrets (the endpoint reads them on the server),
|
|
9
|
+
* so this warns rather than hard-fails — the value may live only in the deploy
|
|
10
|
+
* environment. Build-time secrets (search-index sync) already warn during sync.
|
|
11
|
+
*/
|
|
12
|
+
export const checkRequiredSecrets = (config: ResolvedConfig): Diagnostic[] => {
|
|
13
|
+
const diagnostics: Diagnostic[] = [];
|
|
14
|
+
const requireSecret = (feature: string, env: string, note?: string): void => {
|
|
15
|
+
if (process.env[env]) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
diagnostics.push({
|
|
19
|
+
code: "BLUME_MISSING_SECRET",
|
|
20
|
+
message: `${feature} is enabled but ${env} is not set${note ? ` (${note})` : ""}.`,
|
|
21
|
+
severity: "warning",
|
|
22
|
+
suggestion: `Set ${env} in .env.local for local dev, or in your host's environment for production.`,
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (config.ai.ask?.enabled) {
|
|
27
|
+
const backend = resolveAskBackend(config.ai.ask);
|
|
28
|
+
if (backend.kind === "gateway") {
|
|
29
|
+
requireSecret(
|
|
30
|
+
"Ask AI (AI Gateway)",
|
|
31
|
+
"AI_GATEWAY_API_KEY",
|
|
32
|
+
"on Vercel the gateway can also authenticate via OIDC"
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
requireSecret("Ask AI", backend.apiKeyEnv);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (config.search.provider === "mixedbread") {
|
|
40
|
+
requireSecret("Mixedbread search", "MIXEDBREAD_API_KEY");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return diagnostics;
|
|
44
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Render a Blume content entry's body inside a custom page — e.g. to feature a
|
|
3
|
+
// doc on a landing page, or build a bespoke index that still shows real content.
|
|
4
|
+
//
|
|
5
|
+
// import BlumePage from "blume/components/BlumePage.astro";
|
|
6
|
+
// import data from "blume:data";
|
|
7
|
+
// import { getBlumeCollection } from "blume/runtime";
|
|
8
|
+
// const [intro] = getBlumeCollection(data, { prefix: "/docs" });
|
|
9
|
+
// ---
|
|
10
|
+
// <BlumePage id={intro.entryId} />
|
|
11
|
+
//
|
|
12
|
+
// Blume's built-in MDX components are wired in so callouts, cards, steps, etc.
|
|
13
|
+
// render as they do on a normal page. Pass `components` to add your own overrides
|
|
14
|
+
// or islands (which live in the generated runtime and aren't imported here). The
|
|
15
|
+
// heavy, opt-in `<Math>` component is not included by default — pass it if the
|
|
16
|
+
// embedded content uses math.
|
|
17
|
+
import { getEntry, render } from "astro:content";
|
|
18
|
+
|
|
19
|
+
import Icon from "./Icon.astro";
|
|
20
|
+
import Accordion from "./content/Accordion.astro";
|
|
21
|
+
import AccordionItem from "./content/AccordionItem.astro";
|
|
22
|
+
import AutoTypeTable from "./content/AutoTypeTable.astro";
|
|
23
|
+
import Badge from "./content/Badge.astro";
|
|
24
|
+
import Callout from "./content/Callout.astro";
|
|
25
|
+
import Card from "./content/Card.astro";
|
|
26
|
+
import CardGroup from "./content/CardGroup.astro";
|
|
27
|
+
import CodeBlock from "./content/CodeBlock.astro";
|
|
28
|
+
import CodeGroup from "./content/CodeGroup.astro";
|
|
29
|
+
import ColorItem from "./content/ColorItem.astro";
|
|
30
|
+
import ColorRoot from "./content/Color.astro";
|
|
31
|
+
import ColorRow from "./content/ColorRow.astro";
|
|
32
|
+
import Column from "./content/Column.astro";
|
|
33
|
+
import Columns from "./content/Columns.astro";
|
|
34
|
+
import Component from "./content/Component.astro";
|
|
35
|
+
import Diff from "./content/Diff.astro";
|
|
36
|
+
import Expandable from "./content/Expandable.astro";
|
|
37
|
+
import FileTree from "./content/FileTree.astro";
|
|
38
|
+
import Frame from "./content/Frame.astro";
|
|
39
|
+
import GithubInfo from "./content/GithubInfo.astro";
|
|
40
|
+
import Panel from "./content/Panel.astro";
|
|
41
|
+
import Prompt from "./content/Prompt.astro";
|
|
42
|
+
import Step from "./content/Step.astro";
|
|
43
|
+
import Steps from "./content/Steps.astro";
|
|
44
|
+
import Tab from "./content/Tab.astro";
|
|
45
|
+
import Tabs from "./content/Tabs.astro";
|
|
46
|
+
import Tile from "./content/Tile.astro";
|
|
47
|
+
import Tooltip from "./content/Tooltip.astro";
|
|
48
|
+
import TreeRoot from "./content/Tree.astro";
|
|
49
|
+
import TreeFile from "./content/TreeFile.astro";
|
|
50
|
+
import TreeFolder from "./content/TreeFolder.astro";
|
|
51
|
+
import TypeTable from "./content/TypeTable.astro";
|
|
52
|
+
import Visibility from "./content/Visibility.astro";
|
|
53
|
+
|
|
54
|
+
interface Props {
|
|
55
|
+
/** Astro collection the entry lives in. Defaults to `"docs"`. */
|
|
56
|
+
collection?: string;
|
|
57
|
+
/** Extra MDX components merged over Blume's built-ins (overrides, islands). */
|
|
58
|
+
components?: Record<string, unknown>;
|
|
59
|
+
/** Content entry id — e.g. a `BlumeRoute.entryId` from `getBlumeCollection`. */
|
|
60
|
+
id: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { collection = "docs", components: extra = {}, id } = Astro.props;
|
|
64
|
+
|
|
65
|
+
const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
|
|
66
|
+
const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
|
|
67
|
+
|
|
68
|
+
const components = {
|
|
69
|
+
Accordion,
|
|
70
|
+
AccordionItem,
|
|
71
|
+
AutoTypeTable,
|
|
72
|
+
Badge,
|
|
73
|
+
Callout,
|
|
74
|
+
Card,
|
|
75
|
+
CardGroup,
|
|
76
|
+
CodeBlock,
|
|
77
|
+
CodeGroup,
|
|
78
|
+
Color,
|
|
79
|
+
Column,
|
|
80
|
+
Columns,
|
|
81
|
+
Component,
|
|
82
|
+
Diff,
|
|
83
|
+
Expandable,
|
|
84
|
+
FileTree,
|
|
85
|
+
Frame,
|
|
86
|
+
GithubInfo,
|
|
87
|
+
Icon,
|
|
88
|
+
Panel,
|
|
89
|
+
Prompt,
|
|
90
|
+
Step,
|
|
91
|
+
Steps,
|
|
92
|
+
Tab,
|
|
93
|
+
Tabs,
|
|
94
|
+
Tile,
|
|
95
|
+
Tooltip,
|
|
96
|
+
Tree,
|
|
97
|
+
TypeTable,
|
|
98
|
+
Visibility,
|
|
99
|
+
...extra,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const entry = await getEntry(collection, id);
|
|
103
|
+
const rendered = entry ? await render(entry) : null;
|
|
104
|
+
const Content = rendered?.Content ?? null;
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
{Content && <Content components={components} />}
|