blume 0.7.0 → 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 +1487 -360
- package/dist/cli/index.js.map +91 -85
- 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 +9 -0
- package/dist/types/core/config-input.d.ts +36 -2
- package/dist/types/core/config.d.ts +3 -2
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/i18n-ui.d.ts +476 -132
- package/dist/types/core/schema.d.ts +216 -145
- 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 +8 -8
- 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 +27 -3
- package/docs/configuration/seo.mdx +2 -2
- package/docs/content/components.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/navigation.mdx +3 -3
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +4 -2
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +8 -6
- package/package.json +14 -4
- package/skills/blume/SKILL.md +5 -3
- package/skills/blume-update-docs/SKILL.md +3 -2
- package/src/ai/agent-readability.ts +9 -8
- package/src/ai/ask-context.ts +7 -2
- package/src/ai/ask-data.ts +3 -0
- package/src/ai/component-markdown.ts +461 -0
- package/src/ai/llms.ts +135 -26
- package/src/ai/markdown.ts +35 -6
- package/src/ai/mcp/data.ts +25 -4
- package/src/ai/mcp/discovery.ts +10 -3
- package/src/ai/mcp/server.ts +21 -7
- package/src/ai/visibility.ts +74 -0
- package/src/astro/component-slots.ts +11 -1
- package/src/astro/generate.ts +76 -45
- package/src/astro/integration.ts +1 -1
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/pages.ts +81 -19
- package/src/astro/templates.ts +99 -12
- package/src/blume-modules.d.ts +8 -0
- package/src/cli/commands/build.ts +99 -19
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/dev.ts +26 -5
- 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 +43 -2
- package/src/cli/dev-lock.ts +8 -4
- 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/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/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 +7 -1
- package/src/components/content/base-href.ts +33 -0
- package/src/components/content/changelog-element.ts +9 -2
- package/src/components/content/mermaid-element.ts +7 -2
- package/src/components/islands/AskAI.astro +5 -2
- package/src/components/islands/ask-ai.tsx +56 -6
- package/src/components/islands/hooks.ts +28 -8
- package/src/components/layout/Banner.astro +10 -2
- package/src/components/layout/Header.astro +13 -4
- package/src/components/layout/Logo.astro +11 -3
- package/src/components/layout/NavTree.astro +17 -3
- package/src/components/layout/PageActions.astro +25 -10
- package/src/components/layout/PageLayout.astro +45 -8
- package/src/components/layout/ReferenceLayout.astro +8 -1
- package/src/components/layout/RootLayout.astro +67 -9
- package/src/components/layout/Search.astro +94 -22
- 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/types.ts +5 -1
- package/src/components/layout/search/typesense.ts +4 -1
- package/src/components/layout/toc-element.ts +1 -1
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/Operation.astro +47 -40
- package/src/components/openapi/RequestPanel.astro +1 -1
- package/src/components/openapi/helpers.ts +71 -3
- package/src/components/openapi/panel.ts +1 -1
- package/src/core/base-path.ts +24 -0
- package/src/core/builtin-tags.ts +2 -0
- package/src/core/config-input.ts +37 -2
- package/src/core/config.ts +3 -2
- package/src/core/data.ts +2 -0
- package/src/core/graph.ts +15 -5
- package/src/core/i18n-ui.ts +45 -0
- package/src/core/last-modified.ts +13 -6
- package/src/core/links.ts +32 -8
- package/src/core/navigation.ts +29 -4
- package/src/core/package-json.ts +17 -2
- package/src/core/project-graph.ts +15 -6
- package/src/core/schema.ts +36 -2
- package/src/core/sources/assets.ts +6 -1
- package/src/core/sources/filesystem.ts +4 -0
- package/src/core/sources/mdx-remote.ts +23 -14
- package/src/core/sources/normalize.ts +152 -50
- package/src/core/sources/notion.ts +8 -8
- 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 +7 -2
- package/src/deploy/sitemap.ts +53 -11
- package/src/index.ts +5 -0
- package/src/markdown/base-links.ts +10 -8
- package/src/markdown/index.ts +15 -3
- package/src/markdown/inline-code.ts +7 -2
- package/src/markdown/package-commands.ts +10 -4
- package/src/openapi/model.ts +12 -4
- package/src/openapi/parse.ts +21 -0
- package/src/openapi/references.ts +38 -8
- package/src/openapi/source.ts +59 -10
- package/src/registry/eject.ts +184 -12
- package/src/registry/registry.ts +0 -3
- package/src/search/documents.ts +34 -2
- package/src/seo/jsonld.ts +13 -12
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,149 +1,73 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
|
|
1
|
+
import * as clack from "@clack/prompts";
|
|
4
2
|
import { defineCommand } from "citty";
|
|
5
|
-
import {
|
|
3
|
+
import { resolve } from "pathe";
|
|
6
4
|
|
|
7
5
|
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
8
|
-
import { blumePackageJson, toPackageName } from "../../core/package-json.ts";
|
|
9
6
|
import { eject } from "../../registry/eject.ts";
|
|
7
|
+
import { updatePackageScripts } from "../eject-scripts.ts";
|
|
8
|
+
import { collectAnswers } from "../init/questions.ts";
|
|
9
|
+
import {
|
|
10
|
+
applyPlan,
|
|
11
|
+
buildPlan,
|
|
12
|
+
commandsFor,
|
|
13
|
+
detectPackageManager,
|
|
14
|
+
nextSteps,
|
|
15
|
+
PACKAGE_MANAGERS,
|
|
16
|
+
TEMPLATES,
|
|
17
|
+
validateContentDir,
|
|
18
|
+
} from "../init/scaffold.ts";
|
|
19
|
+
import type {
|
|
20
|
+
InitAnswers,
|
|
21
|
+
PackageManager,
|
|
22
|
+
Template,
|
|
23
|
+
} from "../init/scaffold.ts";
|
|
10
24
|
import { logger } from "../log.ts";
|
|
11
25
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
sources: [
|
|
44
|
-
{
|
|
45
|
-
label: "Petstore",
|
|
46
|
-
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
},`),
|
|
50
|
-
files: (dir) => [
|
|
51
|
-
{
|
|
52
|
-
content: page(
|
|
53
|
-
"API Reference",
|
|
54
|
-
"Explore the API.",
|
|
55
|
-
"# API Reference\n\nYour OpenAPI spec renders at [`/api`](/api). Point `openapi.sources` at your own spec in `blume.config.ts`."
|
|
56
|
-
),
|
|
57
|
-
path: join(dir, "index.mdx"),
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
changelog: {
|
|
62
|
-
config: configFor(`
|
|
63
|
-
navigation: {
|
|
64
|
-
tabs: [
|
|
65
|
-
{ label: "Docs", path: "/" },
|
|
66
|
-
{ label: "Changelog", path: "/changelog" },
|
|
67
|
-
],
|
|
68
|
-
},`),
|
|
69
|
-
files: (dir) => [
|
|
70
|
-
{
|
|
71
|
-
content: page(
|
|
72
|
-
"Introduction",
|
|
73
|
-
"Welcome to your new Blume docs.",
|
|
74
|
-
"# Introduction\n\nWrite your docs here, and log releases under `changelog/`."
|
|
75
|
-
),
|
|
76
|
-
path: join(dir, "index.mdx"),
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
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`,
|
|
80
|
-
path: join(dir, "changelog", "v1-0-0.mdx"),
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
},
|
|
84
|
-
docs: {
|
|
85
|
-
config: configFor(""),
|
|
86
|
-
files: (dir) => [
|
|
87
|
-
{
|
|
88
|
-
content: page(
|
|
89
|
-
"Introduction",
|
|
90
|
-
"Welcome to your new Blume docs.",
|
|
91
|
-
`# 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\`.`
|
|
92
|
-
),
|
|
93
|
-
path: join(dir, "index.mdx"),
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
},
|
|
97
|
-
sdk: {
|
|
98
|
-
config: configFor(""),
|
|
99
|
-
files: (dir) => [
|
|
100
|
-
{
|
|
101
|
-
content: page(
|
|
102
|
-
"Introduction",
|
|
103
|
-
"Get started with the SDK.",
|
|
104
|
-
"# Introduction\n\nInstall the SDK and make your first call. See [Installation](/installation)."
|
|
105
|
-
),
|
|
106
|
-
path: join(dir, "index.mdx"),
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
content: page(
|
|
110
|
-
"Installation",
|
|
111
|
-
"Install the SDK.",
|
|
112
|
-
"# Installation\n\n```package-install\nyour-sdk\n```"
|
|
113
|
-
),
|
|
114
|
-
path: join(dir, "installation.mdx"),
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/** Install + dev commands to print for the chosen package manager. */
|
|
121
|
-
const commandsFor = (pm: PackageManager): { dev: string; install: string } => ({
|
|
122
|
-
dev: pm === "npm" ? "npm run dev" : `${pm} dev`,
|
|
123
|
-
install: `${pm} install`,
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const writeFileSafe = async (
|
|
127
|
-
path: string,
|
|
128
|
-
content: string
|
|
129
|
-
): Promise<boolean> => {
|
|
130
|
-
if (existsSync(path)) {
|
|
131
|
-
logger.info(`Skipped existing ${path}`);
|
|
132
|
-
return false;
|
|
26
|
+
/**
|
|
27
|
+
* Eject the freshly scaffolded project, or print the install-then-eject path.
|
|
28
|
+
* Eject jiti-loads the scaffolded blume.config.ts, whose `import { defineConfig }
|
|
29
|
+
* from "blume"` only resolves once dependencies are installed (or blume is
|
|
30
|
+
* hoisted from an ancestor node_modules, as in a monorepo) — so on a fresh
|
|
31
|
+
* scaffold the fallback below is the common path.
|
|
32
|
+
*/
|
|
33
|
+
const ejectScaffold = async (
|
|
34
|
+
root: string,
|
|
35
|
+
answers: InitAnswers
|
|
36
|
+
): Promise<void> => {
|
|
37
|
+
const commands = commandsFor(answers.packageManager);
|
|
38
|
+
const cd = answers.directory === "." ? [] : [`cd ${answers.directory}`];
|
|
39
|
+
try {
|
|
40
|
+
await eject(root);
|
|
41
|
+
// The scaffolded scripts point at the Blume CLI; the ejected app runs
|
|
42
|
+
// Astro directly (mirroring the standalone `blume eject` command).
|
|
43
|
+
await updatePackageScripts(root);
|
|
44
|
+
logger.success("Ejected to a standalone Astro project.");
|
|
45
|
+
const steps = [...cd, commands.install, commands.dev];
|
|
46
|
+
logger.box(`Next steps:\n\n ${steps.join("\n ")}\n`);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
logger.warn(
|
|
49
|
+
`Scaffolded, but eject needs the project's dependencies installed to load blume.config.ts: ${(error as Error).message}`
|
|
50
|
+
);
|
|
51
|
+
const steps = [
|
|
52
|
+
...cd,
|
|
53
|
+
commands.install,
|
|
54
|
+
`${commands.exec} blume eject --yes`,
|
|
55
|
+
];
|
|
56
|
+
logger.box(`Next steps:\n\n ${steps.join("\n ")}\n`);
|
|
133
57
|
}
|
|
134
|
-
await mkdir(dirname(path), { recursive: true });
|
|
135
|
-
await writeFile(path, content, "utf-8");
|
|
136
|
-
logger.success(`Created ${path}`);
|
|
137
|
-
return true;
|
|
138
58
|
};
|
|
139
59
|
|
|
140
60
|
export const initCommand = defineCommand({
|
|
141
61
|
args: {
|
|
142
62
|
"content-dir": {
|
|
143
|
-
default: "docs",
|
|
144
63
|
description: "Content directory.",
|
|
145
64
|
type: "string",
|
|
146
65
|
},
|
|
66
|
+
dir: {
|
|
67
|
+
description: "Directory to scaffold into (default: current directory).",
|
|
68
|
+
required: false,
|
|
69
|
+
type: "positional",
|
|
70
|
+
},
|
|
147
71
|
eject: {
|
|
148
72
|
description: "Eject to a standalone Astro project after scaffolding.",
|
|
149
73
|
type: "boolean",
|
|
@@ -157,85 +81,101 @@ export const initCommand = defineCommand({
|
|
|
157
81
|
description: "Starter template: docs | api | sdk | changelog.",
|
|
158
82
|
type: "string",
|
|
159
83
|
},
|
|
160
|
-
yes: {
|
|
84
|
+
yes: {
|
|
85
|
+
description: "Skip prompts and scaffold with defaults.",
|
|
86
|
+
type: "boolean",
|
|
87
|
+
},
|
|
161
88
|
},
|
|
162
89
|
meta: {
|
|
163
90
|
description: "Scaffold a minimal Blume project.",
|
|
164
91
|
name: "init",
|
|
165
92
|
},
|
|
166
93
|
async run({ args }) {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
isAbsolute(contentDir) ||
|
|
173
|
-
relative(root, join(root, contentDir)).startsWith("..")
|
|
174
|
-
) {
|
|
94
|
+
const cwd = process.cwd();
|
|
95
|
+
|
|
96
|
+
const template = args.template as Template | undefined;
|
|
97
|
+
if (template !== undefined && !TEMPLATES.includes(template)) {
|
|
175
98
|
logger.error(
|
|
176
|
-
`
|
|
99
|
+
`Unknown template "${args.template}" (use ${TEMPLATES.join(" | ")}).`
|
|
177
100
|
);
|
|
178
101
|
process.exit(1);
|
|
179
102
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (!TEMPLATES.includes(template)) {
|
|
103
|
+
const pm = args["package-manager"] as PackageManager | undefined;
|
|
104
|
+
if (pm !== undefined && !PACKAGE_MANAGERS.includes(pm)) {
|
|
183
105
|
logger.error(
|
|
184
|
-
`Unknown
|
|
106
|
+
`Unknown package manager "${args["package-manager"]}" (use ${PACKAGE_MANAGERS.join(" | ")}).`
|
|
185
107
|
);
|
|
186
108
|
process.exit(1);
|
|
187
109
|
}
|
|
188
|
-
|
|
189
|
-
|
|
110
|
+
|
|
111
|
+
const interactive =
|
|
112
|
+
!args.yes &&
|
|
113
|
+
process.stdin.isTTY === true &&
|
|
114
|
+
clack.isTTY(process.stdout) &&
|
|
115
|
+
!clack.isCI();
|
|
116
|
+
|
|
117
|
+
let answers: InitAnswers;
|
|
118
|
+
if (interactive) {
|
|
119
|
+
clack.intro("blume init");
|
|
120
|
+
const collected = await collectAnswers(
|
|
121
|
+
clack,
|
|
122
|
+
{
|
|
123
|
+
contentDir: args["content-dir"],
|
|
124
|
+
directory: args.dir,
|
|
125
|
+
packageManager: pm,
|
|
126
|
+
template,
|
|
127
|
+
},
|
|
128
|
+
{ cwd, userAgent: process.env.npm_config_user_agent }
|
|
129
|
+
);
|
|
130
|
+
if (collected === null) {
|
|
131
|
+
clack.cancel("Cancelled — nothing was written.");
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
answers = collected;
|
|
135
|
+
} else {
|
|
136
|
+
answers = {
|
|
137
|
+
contentDir: args["content-dir"] ?? "docs",
|
|
138
|
+
directory: args.dir ?? ".",
|
|
139
|
+
packageManager:
|
|
140
|
+
pm ?? detectPackageManager(process.env.npm_config_user_agent),
|
|
141
|
+
sources: ["filesystem"],
|
|
142
|
+
template: template ?? "docs",
|
|
143
|
+
title: "My Docs",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const root = resolve(cwd, answers.directory);
|
|
148
|
+
// Interactive runs validate this inline, but an explicit --content-dir
|
|
149
|
+
// flag skips that prompt, so guard here in both modes.
|
|
150
|
+
if (validateContentDir(root, answers.contentDir) !== undefined) {
|
|
190
151
|
logger.error(
|
|
191
|
-
`
|
|
152
|
+
`Invalid --content-dir "${answers.contentDir}" (must be a path inside the project).`
|
|
192
153
|
);
|
|
193
154
|
process.exit(1);
|
|
194
155
|
}
|
|
195
156
|
|
|
196
|
-
const
|
|
197
|
-
const createdPackage = await
|
|
198
|
-
join(root, "package.json"),
|
|
199
|
-
blumePackageJson(toPackageName(basename(root)))
|
|
200
|
-
);
|
|
201
|
-
await writeFileSafe(join(root, "blume.config.ts"), starter.config);
|
|
202
|
-
await Promise.all(
|
|
203
|
-
starter
|
|
204
|
-
.files(contentDir)
|
|
205
|
-
.map((file) => writeFileSafe(join(root, file.path), file.content))
|
|
206
|
-
);
|
|
157
|
+
const sink = interactive ? clack.log : logger;
|
|
158
|
+
const { createdPackage } = await applyPlan(buildPlan(root, answers), sink);
|
|
207
159
|
|
|
208
160
|
// Keep Blume's generated runtime (`.blume/`) and build output (`dist/`) out
|
|
209
161
|
// of version control. Idempotent: creates `.gitignore` when absent and skips
|
|
210
162
|
// entries already present (trailing-slash agnostic).
|
|
211
163
|
const ignored = await ensureGitignore(root, [".blume/", "dist/"]);
|
|
212
164
|
if (ignored.length > 0) {
|
|
213
|
-
|
|
165
|
+
sink.success(`Added ${ignored.join(", ")} to .gitignore`);
|
|
214
166
|
}
|
|
215
167
|
|
|
216
|
-
const commands = commandsFor(pm);
|
|
217
|
-
|
|
218
168
|
if (args.eject) {
|
|
219
|
-
|
|
220
|
-
// scaffolding. The standalone app then runs with Astro directly.
|
|
221
|
-
try {
|
|
222
|
-
await eject(root);
|
|
223
|
-
logger.success("Ejected to a standalone Astro project.");
|
|
224
|
-
logger.box(`Next steps:\n\n ${commands.install}\n npx astro dev\n`);
|
|
225
|
-
} catch (error) {
|
|
226
|
-
logger.warn(
|
|
227
|
-
`Scaffolded, but eject failed: ${(error as Error).message}`
|
|
228
|
-
);
|
|
229
|
-
logger.box(
|
|
230
|
-
`Next steps:\n\n ${commands.install}\n blume eject --yes\n`
|
|
231
|
-
);
|
|
232
|
-
}
|
|
169
|
+
await ejectScaffold(root, answers);
|
|
233
170
|
return;
|
|
234
171
|
}
|
|
235
172
|
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
173
|
+
const steps = nextSteps(answers, createdPackage);
|
|
174
|
+
if (interactive) {
|
|
175
|
+
clack.note(steps.trimEnd());
|
|
176
|
+
clack.outro("You're all set.");
|
|
177
|
+
} else {
|
|
178
|
+
logger.box(steps);
|
|
179
|
+
}
|
|
240
180
|
},
|
|
241
181
|
});
|
|
@@ -8,6 +8,7 @@ import { loadConfig } from "../../core/config.ts";
|
|
|
8
8
|
import { resolveProjectContext } from "../../core/project.ts";
|
|
9
9
|
import { parsePort } from "../args.ts";
|
|
10
10
|
import { logger } from "../log.ts";
|
|
11
|
+
import { normalizeHost } from "./dev.ts";
|
|
11
12
|
|
|
12
13
|
export const previewCommand = defineCommand({
|
|
13
14
|
args: {
|
|
@@ -32,7 +33,9 @@ export const previewCommand = defineCommand({
|
|
|
32
33
|
logLevel: "info",
|
|
33
34
|
root: context.outDir,
|
|
34
35
|
server: {
|
|
35
|
-
|
|
36
|
+
// `normalizeHost` maps a bare `--host` (citty parses it as "") to
|
|
37
|
+
// `true` so Vite binds all interfaces instead of the hostname "".
|
|
38
|
+
host: normalizeHost(args.host),
|
|
36
39
|
port: parsePort(args.port),
|
|
37
40
|
},
|
|
38
41
|
});
|
|
@@ -3,8 +3,14 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { join } from "pathe";
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
customStaticRoutes,
|
|
8
|
+
discoverPages,
|
|
9
|
+
hasGeneratedChangelog,
|
|
10
|
+
} from "../../astro/pages.ts";
|
|
6
11
|
import { BlumeError } from "../../core/diagnostics.ts";
|
|
7
12
|
import { validateLinks } from "../../core/links.ts";
|
|
13
|
+
import { buildManifest } from "../../core/manifest.ts";
|
|
8
14
|
import { scanProject } from "../../core/project-graph.ts";
|
|
9
15
|
import type { Diagnostic } from "../../core/types.ts";
|
|
10
16
|
import { reportInternalError } from "../internal-error.ts";
|
|
@@ -44,11 +50,40 @@ export const validateCommand = defineCommand({
|
|
|
44
50
|
// link-validation blind spot, so silently passing would be misleading.
|
|
45
51
|
diagnostics.push(...project.diagnostics);
|
|
46
52
|
|
|
53
|
+
// Custom `.astro` pages and the generated changelog index are servable
|
|
54
|
+
// routes the content graph can't see — without them, a docs link to e.g.
|
|
55
|
+
// a custom landing page fails as BLUME_BROKEN_LINK.
|
|
56
|
+
const userPages = project.context.pagesRoot
|
|
57
|
+
? await discoverPages(project.context.pagesRoot)
|
|
58
|
+
: [];
|
|
59
|
+
const extraRoutes = customStaticRoutes(userPages);
|
|
60
|
+
if (hasGeneratedChangelog(project, userPages)) {
|
|
61
|
+
extraRoutes.push("/changelog");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback-materialized locale routes (an untranslated page prerendered
|
|
65
|
+
// at its localized URL) are servable but absent from the content graph —
|
|
66
|
+
// without them a link to an untranslated sibling under a non-default
|
|
67
|
+
// locale fails as BLUME_BROKEN_LINK even though the built site serves it.
|
|
68
|
+
if (project.config.i18n) {
|
|
69
|
+
const manifest = buildManifest({
|
|
70
|
+
config: project.config,
|
|
71
|
+
context: project.context,
|
|
72
|
+
graph: project.graph,
|
|
73
|
+
});
|
|
74
|
+
extraRoutes.push(
|
|
75
|
+
...manifest.routes.flatMap((route) =>
|
|
76
|
+
route.fallback ? [route.path] : []
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
47
81
|
const publicDir = join(root, "public");
|
|
48
82
|
diagnostics.push(
|
|
49
83
|
...(await validateLinks(project.graph, {
|
|
50
84
|
basePath: project.config.basePath,
|
|
51
85
|
checkExternal: Boolean(args.external),
|
|
86
|
+
extraRoutes,
|
|
52
87
|
publicDir: existsSync(publicDir) ? publicDir : null,
|
|
53
88
|
redirects: project.config.redirects,
|
|
54
89
|
}))
|
|
@@ -62,12 +97,18 @@ export const validateCommand = defineCommand({
|
|
|
62
97
|
}
|
|
63
98
|
}
|
|
64
99
|
|
|
100
|
+
// `--strict` escalates warnings to failures; info-level notes (e.g.
|
|
101
|
+
// BLUME_ASSETS_UNCHECKED when there is no public/ dir) stay advisory.
|
|
102
|
+
const strictFailure =
|
|
103
|
+
Boolean(args.strict) &&
|
|
104
|
+
diagnostics.some((diagnostic) => diagnostic.severity !== "info");
|
|
105
|
+
|
|
65
106
|
if (args.json) {
|
|
66
107
|
// Drain stdout before exiting non-zero: `process.exit` would otherwise
|
|
67
108
|
// truncate the JSON payload mid-write when stdout is a pipe — exactly how
|
|
68
109
|
// `--json` is consumed in CI/editors.
|
|
69
110
|
const hadErrors = reportDiagnosticsJson(diagnostics, root);
|
|
70
|
-
if (hadErrors ||
|
|
111
|
+
if (hadErrors || strictFailure) {
|
|
71
112
|
await flushStdout();
|
|
72
113
|
process.exit(1);
|
|
73
114
|
}
|
|
@@ -78,7 +119,7 @@ export const validateCommand = defineCommand({
|
|
|
78
119
|
if (diagnostics.length === 0) {
|
|
79
120
|
logger.success("No broken links found.");
|
|
80
121
|
}
|
|
81
|
-
if (hadErrors ||
|
|
122
|
+
if (hadErrors || strictFailure) {
|
|
82
123
|
process.exit(1);
|
|
83
124
|
}
|
|
84
125
|
},
|
package/src/cli/dev-lock.ts
CHANGED
|
@@ -191,17 +191,21 @@ export const describeDevLock = (lock: DevLockInfo): string =>
|
|
|
191
191
|
* `action` names the operation being refused (e.g. "building"). `runtimeDir`
|
|
192
192
|
* relocates the checked dir: an isolated verify (`.blume-verify`) targets a dir
|
|
193
193
|
* dev never locks, so it proceeds; a default or `--runtime-dir .blume` run still
|
|
194
|
-
* refuses.
|
|
194
|
+
* refuses. Only commands that actually accept `--isolated` (build, check) should
|
|
195
|
+
* set `isolatedHint`, so the refusal never suggests a flag the command ignores.
|
|
195
196
|
*/
|
|
196
197
|
export const refuseIfDevRunning = (
|
|
197
198
|
root: string,
|
|
198
199
|
action: string,
|
|
199
|
-
runtimeDir?: string
|
|
200
|
+
options: { runtimeDir?: string; isolatedHint?: boolean } = {}
|
|
200
201
|
): void => {
|
|
201
|
-
const lock = readDevLock(resolveRuntimeDir(root, runtimeDir));
|
|
202
|
+
const lock = readDevLock(resolveRuntimeDir(root, options.runtimeDir));
|
|
202
203
|
if (lock) {
|
|
204
|
+
const remedies = options.isolatedHint
|
|
205
|
+
? "Reuse that server, stop it first, or re-run with --isolated to build/verify against .blume-verify without touching it."
|
|
206
|
+
: "Reuse that server or stop it first.";
|
|
203
207
|
logger.error(
|
|
204
|
-
`A \`blume dev\` server is running${describeDevLock(lock)}; ${action} would corrupt its .blume runtime.
|
|
208
|
+
`A \`blume dev\` server is running${describeDevLock(lock)}; ${action} would corrupt its .blume runtime. ${remedies}`
|
|
205
209
|
);
|
|
206
210
|
process.exit(1);
|
|
207
211
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { join } from "pathe";
|
|
4
|
+
|
|
5
|
+
import type { ResolvedConfig } from "../core/schema.ts";
|
|
6
|
+
import { searchProviderMeta } from "../search/providers.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The `blume build`-only artifacts this project's config actually produces, as
|
|
10
|
+
* notice lines for the eject command. After an eject the build script runs
|
|
11
|
+
* plain `astro build`, which emits none of them — printing the config-aware
|
|
12
|
+
* list makes the loss explicit instead of silent (a Pagefind site would
|
|
13
|
+
* otherwise eject into a build whose search fails at runtime). Each gate
|
|
14
|
+
* mirrors the artifact's producer in `blume build` (cli/commands/build.ts).
|
|
15
|
+
*/
|
|
16
|
+
export const droppedArtifactNotices = (config: ResolvedConfig): string[] => {
|
|
17
|
+
const notices: string[] = [];
|
|
18
|
+
if (config.search.provider === "pagefind") {
|
|
19
|
+
notices.push(
|
|
20
|
+
'the Pagefind search index — the search UI loads it from the built site, so search will break in production. Add a post-build step: `"build": "astro build && pagefind --site dist"` (with `pagefind` installed as a devDependency).'
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (searchProviderMeta(config.search.provider).syncs) {
|
|
24
|
+
notices.push(
|
|
25
|
+
`the hosted ${config.search.provider} index sync — new and updated pages stop being pushed; re-upload your search records after each build with the provider's API or CLI.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (config.ai.llmsTxt.enabled) {
|
|
29
|
+
notices.push("llms.txt and llms-full.txt");
|
|
30
|
+
}
|
|
31
|
+
if (config.deployment.site && config.seo.sitemap) {
|
|
32
|
+
notices.push(
|
|
33
|
+
"sitemap.xml — recreate it with the @astrojs/sitemap integration."
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (config.seo.robots) {
|
|
37
|
+
notices.push("robots.txt — recreate it as a public/robots.txt file.");
|
|
38
|
+
}
|
|
39
|
+
if (config.seo.agentReadability) {
|
|
40
|
+
notices.push("agent-readability.json");
|
|
41
|
+
}
|
|
42
|
+
if (config.redirects.length > 0 && config.deployment.output === "static") {
|
|
43
|
+
notices.push(
|
|
44
|
+
"the platform redirect files (_redirects, vercel.json) — your redirects still work as Astro-generated meta-refresh pages."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return notices;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Rewrite the project's package.json scripts to run Astro directly. After an
|
|
52
|
+
* eject the Blume CLI no longer manages the runtime, so scaffolded scripts like
|
|
53
|
+
* `"dev": "blume dev"` would rebuild the removed `.blume` tree instead of
|
|
54
|
+
* serving the ejected app. A missing or unreadable package.json is left alone.
|
|
55
|
+
*/
|
|
56
|
+
export const updatePackageScripts = async (root: string): Promise<void> => {
|
|
57
|
+
const pkgPath = join(root, "package.json");
|
|
58
|
+
let pkg: Record<string, unknown>;
|
|
59
|
+
try {
|
|
60
|
+
pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const scripts = (pkg.scripts ?? {}) as Record<string, string>;
|
|
65
|
+
pkg.scripts = {
|
|
66
|
+
...scripts,
|
|
67
|
+
build: "astro build",
|
|
68
|
+
dev: "astro dev",
|
|
69
|
+
preview: "astro preview",
|
|
70
|
+
};
|
|
71
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
|
|
72
|
+
};
|
package/src/cli/env.ts
CHANGED
|
@@ -13,15 +13,25 @@ const ENV_LINE =
|
|
|
13
13
|
const DOUBLE_QUOTED = /^"(?<body>[\s\S]*)"$/u;
|
|
14
14
|
const SINGLE_QUOTED = /^'(?<body>[\s\S]*)'$/u;
|
|
15
15
|
|
|
16
|
+
const ESCAPE = /\\(?<char>[\\nt"])/gu;
|
|
17
|
+
const UNESCAPED: Record<string, string> = {
|
|
18
|
+
'"': '"',
|
|
19
|
+
"\\": "\\",
|
|
20
|
+
n: "\n",
|
|
21
|
+
t: "\t",
|
|
22
|
+
};
|
|
23
|
+
|
|
16
24
|
/** Unquote a value, expanding `\n`/`\t`/escapes inside double quotes only. */
|
|
17
25
|
const unquote = (raw: string): string => {
|
|
18
26
|
const double = raw.match(DOUBLE_QUOTED)?.groups?.body;
|
|
19
27
|
if (double !== undefined) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
// A single pass so each backslash is consumed exactly once — sequential
|
|
29
|
+
// replaceAll calls would expand the `n` in `\\n` (an escaped backslash
|
|
30
|
+
// followed by a literal `n`) into a newline.
|
|
31
|
+
return double.replaceAll(
|
|
32
|
+
ESCAPE,
|
|
33
|
+
(match, char: string) => UNESCAPED[char] ?? match
|
|
34
|
+
);
|
|
25
35
|
}
|
|
26
36
|
const single = raw.match(SINGLE_QUOTED)?.groups?.body;
|
|
27
37
|
if (single !== undefined) {
|