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
|
@@ -11,11 +11,13 @@ import { ensureGitignore } from "../../core/gitignore.ts";
|
|
|
11
11
|
import type { BlumeProject } from "../../core/project-graph.ts";
|
|
12
12
|
import type { ResolvedConfig } from "../../core/schema.ts";
|
|
13
13
|
import { serverFeatures } from "../../core/server-features.ts";
|
|
14
|
+
import type { ProjectContext } from "../../core/types.ts";
|
|
14
15
|
import {
|
|
15
16
|
deployStaticDir,
|
|
16
17
|
surfaceAdapterOutput,
|
|
17
18
|
} from "../../deploy/adapter-output.ts";
|
|
18
19
|
import {
|
|
20
|
+
applyBaseToRedirects,
|
|
19
21
|
buildNetlifyRedirects,
|
|
20
22
|
buildRedirectManifest,
|
|
21
23
|
buildVercelConfig,
|
|
@@ -30,18 +32,27 @@ import { prepareProject } from "../prepare.ts";
|
|
|
30
32
|
|
|
31
33
|
const ADAPTERS = ["vercel", "node", "netlify", "cloudflare"] as const;
|
|
32
34
|
|
|
35
|
+
const BUDGET_JS = "budget-js";
|
|
36
|
+
const BUDGET_CSS = "budget-css";
|
|
37
|
+
|
|
38
|
+
interface BudgetArgs {
|
|
39
|
+
"budget-css"?: string;
|
|
40
|
+
"budget-js"?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
/**
|
|
34
44
|
* Reject a non-numeric performance budget. `Number("250kb")` is `NaN` and
|
|
35
45
|
* `total > NaN` is always false, so a typo'd flag would silently pass the gate;
|
|
36
46
|
* fail up front instead.
|
|
37
47
|
*/
|
|
38
|
-
const validateBudgetFlags = (args: {
|
|
39
|
-
|
|
40
|
-
"budget-js"?: string;
|
|
41
|
-
}): void => {
|
|
42
|
-
for (const flag of ["budget-js", "budget-css"] as const) {
|
|
48
|
+
const validateBudgetFlags = (args: BudgetArgs): void => {
|
|
49
|
+
for (const flag of [BUDGET_JS, BUDGET_CSS] as const) {
|
|
43
50
|
const value = args[flag];
|
|
44
|
-
|
|
51
|
+
const parsed = Number(value);
|
|
52
|
+
// Equivalent to `!(parsed > 0)` but without the inverted check: this must
|
|
53
|
+
// also reject `NaN` (a typo'd flag like "250kb"), which `parsed <= 0` alone
|
|
54
|
+
// would let through since `NaN <= 0` is false.
|
|
55
|
+
if (value !== undefined && (Number.isNaN(parsed) || parsed <= 0)) {
|
|
45
56
|
logger.error(
|
|
46
57
|
`Invalid --${flag} "${value}" (expected a positive number of kB).`
|
|
47
58
|
);
|
|
@@ -59,7 +70,7 @@ const emitRedirectFiles = async (
|
|
|
59
70
|
config: ResolvedConfig,
|
|
60
71
|
distDir: string
|
|
61
72
|
): Promise<void> => {
|
|
62
|
-
const
|
|
73
|
+
const redirects = applyBaseToRedirects(config.redirects, config.basePath);
|
|
63
74
|
if (redirects.length === 0 || config.deployment.output !== "static") {
|
|
64
75
|
return;
|
|
65
76
|
}
|
|
@@ -82,10 +93,13 @@ const emitRedirectFiles = async (
|
|
|
82
93
|
logger.success(`Emitted redirect files for ${redirects.length} redirect(s)`);
|
|
83
94
|
};
|
|
84
95
|
|
|
85
|
-
const formatBytes = (bytes: number): string =>
|
|
86
|
-
bytes < 1024
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
const formatBytes = (bytes: number): string => {
|
|
97
|
+
if (bytes < 1024) {
|
|
98
|
+
return `${bytes} B`;
|
|
99
|
+
}
|
|
100
|
+
const digits = bytes < 1024 * 100 ? 1 : 0;
|
|
101
|
+
return `${(bytes / 1024).toFixed(digits)} kB`;
|
|
102
|
+
};
|
|
89
103
|
|
|
90
104
|
/** Sizes of `dist/_astro/*.<ext>`, largest first (empty when none exist). */
|
|
91
105
|
const astroAssets = async (
|
|
@@ -144,14 +158,14 @@ const reportBundleSizes = async (distDir: string): Promise<void> => {
|
|
|
144
158
|
*/
|
|
145
159
|
const enforceBudget = async (
|
|
146
160
|
distDir: string,
|
|
147
|
-
args:
|
|
161
|
+
args: BudgetArgs
|
|
148
162
|
): Promise<"fail" | "pass" | "skip"> => {
|
|
149
163
|
const checks: { ext: string; limitKb: number; name: string }[] = [
|
|
150
|
-
...(args[
|
|
151
|
-
? [{ ext: "js", limitKb: Number(args[
|
|
164
|
+
...(args[BUDGET_JS]
|
|
165
|
+
? [{ ext: "js", limitKb: Number(args[BUDGET_JS]), name: "JavaScript" }]
|
|
152
166
|
: []),
|
|
153
|
-
...(args[
|
|
154
|
-
? [{ ext: "css", limitKb: Number(args[
|
|
167
|
+
...(args[BUDGET_CSS]
|
|
168
|
+
? [{ ext: "css", limitKb: Number(args[BUDGET_CSS]), name: "CSS" }]
|
|
155
169
|
: []),
|
|
156
170
|
];
|
|
157
171
|
if (checks.length === 0) {
|
|
@@ -176,16 +190,94 @@ const enforceBudget = async (
|
|
|
176
190
|
return passed ? "pass" : "fail";
|
|
177
191
|
};
|
|
178
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Run the optional bundle report (`--analyze`) and performance-budget gate
|
|
195
|
+
* against the directory whose `_astro/` client assets the deploy serves.
|
|
196
|
+
* Shared by real and isolated builds — an isolated CI run passing
|
|
197
|
+
* `--budget-js` must still fail on an exceeded budget rather than silently
|
|
198
|
+
* skipping the check. Exits non-zero when a budget is exceeded.
|
|
199
|
+
*/
|
|
200
|
+
export const runClientAssetChecks = async (
|
|
201
|
+
staticDir: string,
|
|
202
|
+
args: { analyze?: boolean } & BudgetArgs
|
|
203
|
+
): Promise<void> => {
|
|
204
|
+
if (args.analyze) {
|
|
205
|
+
await reportBundleSizes(staticDir);
|
|
206
|
+
}
|
|
207
|
+
if ((await enforceBudget(staticDir, args)) === "fail") {
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Directory holding an isolated build's client `_astro/` assets. Mirrors
|
|
214
|
+
* `deployStaticDir`, except that an isolated build never surfaces the adapter
|
|
215
|
+
* bundle to the project root — a Vercel server build's static output stays at
|
|
216
|
+
* `<runtime>/.vercel/output/static`, where `deployStaticDir` would instead
|
|
217
|
+
* point at the project-root copy (a previous real build's assets, or nothing).
|
|
218
|
+
*/
|
|
219
|
+
export const isolatedStaticDir = (
|
|
220
|
+
config: ResolvedConfig,
|
|
221
|
+
context: ProjectContext
|
|
222
|
+
): string => {
|
|
223
|
+
const { adapter, output } = config.deployment;
|
|
224
|
+
if (output === "server" && adapter === "vercel") {
|
|
225
|
+
return join(context.outDir, ".vercel", "output", "static");
|
|
226
|
+
}
|
|
227
|
+
const dist = context.distDir ?? join(context.outDir, "dist");
|
|
228
|
+
if (output === "server" && adapter === "node") {
|
|
229
|
+
return join(dist, "client");
|
|
230
|
+
}
|
|
231
|
+
return dist;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Generate `llms.txt`/`llms-full.txt` into the dist dir. A user's own file in
|
|
236
|
+
* `public/` (copied into dist by Astro before this runs, like the sitemap and
|
|
237
|
+
* robots.txt) wins over the generated one — each file is checked and replaced
|
|
238
|
+
* independently, so a custom `llms.txt` still gets a generated `llms-full.txt`.
|
|
239
|
+
*/
|
|
240
|
+
const publishLlmsFiles = async (
|
|
241
|
+
project: BlumeProject,
|
|
242
|
+
distDir: string
|
|
243
|
+
): Promise<void> => {
|
|
244
|
+
const indexPath = join(distDir, "llms.txt");
|
|
245
|
+
const fullPath = join(distDir, "llms-full.txt");
|
|
246
|
+
const writeIndex = !existsSync(indexPath);
|
|
247
|
+
const writeFull = !existsSync(fullPath);
|
|
248
|
+
if (!(writeIndex || writeFull)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const { index, full } = await buildLlmsFiles(project);
|
|
252
|
+
const writes: Promise<void>[] = [];
|
|
253
|
+
if (writeIndex) {
|
|
254
|
+
writes.push(writeFile(indexPath, index, "utf-8"));
|
|
255
|
+
}
|
|
256
|
+
if (writeFull) {
|
|
257
|
+
writes.push(writeFile(fullPath, full, "utf-8"));
|
|
258
|
+
}
|
|
259
|
+
await Promise.all(writes);
|
|
260
|
+
logger.success(
|
|
261
|
+
`Generated ${[
|
|
262
|
+
writeIndex ? "llms.txt" : null,
|
|
263
|
+
writeFull ? "llms-full.txt" : null,
|
|
264
|
+
]
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
.join(" and ")}`
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
|
|
179
270
|
/**
|
|
180
271
|
* Run every deploy post-step of a real (non-isolated) build: the search index +
|
|
181
272
|
* hosted-provider sync, llms.txt, sitemap/robots, redirect files, the summary
|
|
182
273
|
* box, and the optional bundle report / budget gate. Exits non-zero if a budget
|
|
183
|
-
* is exceeded. Isolated verify builds skip all of this
|
|
274
|
+
* is exceeded. Isolated verify builds skip all of this except the bundle
|
|
275
|
+
* report / budget gate, which they run against their own output.
|
|
184
276
|
*/
|
|
185
277
|
const publishBuildArtifacts = async (
|
|
186
278
|
project: BlumeProject,
|
|
187
279
|
distDir: string,
|
|
188
|
-
args: { analyze?: boolean
|
|
280
|
+
args: { analyze?: boolean } & BudgetArgs
|
|
189
281
|
): Promise<void> => {
|
|
190
282
|
if (project.config.search.provider === "pagefind") {
|
|
191
283
|
logger.start("Building search index");
|
|
@@ -201,13 +293,8 @@ const publishBuildArtifacts = async (
|
|
|
201
293
|
warn: (message) => logger.warn(message),
|
|
202
294
|
});
|
|
203
295
|
|
|
204
|
-
if (project.config.ai.llmsTxt) {
|
|
205
|
-
|
|
206
|
-
await Promise.all([
|
|
207
|
-
writeFile(join(distDir, "llms.txt"), index, "utf-8"),
|
|
208
|
-
writeFile(join(distDir, "llms-full.txt"), full, "utf-8"),
|
|
209
|
-
]);
|
|
210
|
-
logger.success("Generated llms.txt and llms-full.txt");
|
|
296
|
+
if (project.config.ai.llmsTxt.enabled) {
|
|
297
|
+
await publishLlmsFiles(project, distDir);
|
|
211
298
|
}
|
|
212
299
|
|
|
213
300
|
// A user's own public/ file (copied into dist by Astro) always wins.
|
|
@@ -240,6 +327,11 @@ const publishBuildArtifacts = async (
|
|
|
240
327
|
|
|
241
328
|
const { config } = project;
|
|
242
329
|
const features = serverFeatures(config);
|
|
330
|
+
// `buildSitemap` returns null both when the sitemap is disabled and when no
|
|
331
|
+
// `site` is configured — only the latter deserves the remediation hint.
|
|
332
|
+
const sitemapNote = config.seo.sitemap
|
|
333
|
+
? "no (set deployment.site)"
|
|
334
|
+
: "no (seo.sitemap is false)";
|
|
243
335
|
logger.box(
|
|
244
336
|
[
|
|
245
337
|
`Output ${config.deployment.output}`,
|
|
@@ -247,21 +339,15 @@ const publishBuildArtifacts = async (
|
|
|
247
339
|
`Site ${config.deployment.site ?? "not set"}`,
|
|
248
340
|
`Search ${config.search.provider}`,
|
|
249
341
|
`Redirects ${config.redirects.length}`,
|
|
250
|
-
`Sitemap ${sitemap ? "yes" :
|
|
342
|
+
`Sitemap ${sitemap ? "yes" : sitemapNote}`,
|
|
251
343
|
`Robots ${robots ? "yes" : "no"}`,
|
|
252
344
|
`Agent JSON ${agentReadability ? "yes" : "no"}`,
|
|
253
|
-
`LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
|
|
345
|
+
`LLM files ${config.ai.llmsTxt.enabled ? "yes" : "no"}`,
|
|
254
346
|
`Server features ${features.length > 0 ? features.join(", ") : "none"}`,
|
|
255
347
|
].join("\n")
|
|
256
348
|
);
|
|
257
349
|
|
|
258
|
-
|
|
259
|
-
await reportBundleSizes(distDir);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if ((await enforceBudget(distDir, args)) === "fail") {
|
|
263
|
-
process.exit(1);
|
|
264
|
-
}
|
|
350
|
+
await runClientAssetChecks(distDir, args);
|
|
265
351
|
|
|
266
352
|
logger.success(`Built to ${distDir}`);
|
|
267
353
|
};
|
|
@@ -280,11 +366,11 @@ export const buildCommand = defineCommand({
|
|
|
280
366
|
description: "Base path the site is served under (e.g. /docs).",
|
|
281
367
|
type: "string",
|
|
282
368
|
},
|
|
283
|
-
|
|
369
|
+
[BUDGET_CSS]: {
|
|
284
370
|
description: "Fail if total client CSS exceeds this many kB.",
|
|
285
371
|
type: "string",
|
|
286
372
|
},
|
|
287
|
-
|
|
373
|
+
[BUDGET_JS]: {
|
|
288
374
|
description: "Fail if total client JavaScript exceeds this many kB.",
|
|
289
375
|
type: "string",
|
|
290
376
|
},
|
|
@@ -317,7 +403,7 @@ export const buildCommand = defineCommand({
|
|
|
317
403
|
const runtimeDir = args.isolated
|
|
318
404
|
? ".blume-verify"
|
|
319
405
|
: process.env.BLUME_RUNTIME_DIR;
|
|
320
|
-
refuseIfDevRunning(root, "building", runtimeDir);
|
|
406
|
+
refuseIfDevRunning(root, "building", { isolatedHint: true, runtimeDir });
|
|
321
407
|
if (args.isolated) {
|
|
322
408
|
await ensureGitignore(root, [".blume-verify/"]);
|
|
323
409
|
}
|
|
@@ -361,8 +447,15 @@ export const buildCommand = defineCommand({
|
|
|
361
447
|
// An isolated build is a throwaway verify: it only needs to confirm the site
|
|
362
448
|
// compiles and renders. Skip the network post-steps (search sync) and
|
|
363
449
|
// deploy artifacts (index/llms/sitemap/robots/redirects) that only matter
|
|
364
|
-
// for a real publish and would push to hosted providers.
|
|
450
|
+
// for a real publish and would push to hosted providers. The bundle report
|
|
451
|
+
// and budget gate still run, though — `blume build --isolated --budget-js
|
|
452
|
+
// 100` exiting 0 without measuring anything would be a silent false pass
|
|
453
|
+
// in CI.
|
|
365
454
|
if (runtimeDir) {
|
|
455
|
+
await runClientAssetChecks(
|
|
456
|
+
isolatedStaticDir(project.config, project.context),
|
|
457
|
+
args
|
|
458
|
+
);
|
|
366
459
|
logger.success(
|
|
367
460
|
`Isolated build OK — output at ${distDir} (not published).`
|
|
368
461
|
);
|
|
@@ -39,7 +39,7 @@ export const checkCommand = defineCommand({
|
|
|
39
39
|
const runtimeDir = args.isolated
|
|
40
40
|
? ".blume-verify"
|
|
41
41
|
: process.env.BLUME_RUNTIME_DIR;
|
|
42
|
-
refuseIfDevRunning(root, "checking", runtimeDir);
|
|
42
|
+
refuseIfDevRunning(root, "checking", { isolatedHint: true, runtimeDir });
|
|
43
43
|
if (args.isolated) {
|
|
44
44
|
await ensureGitignore(root, [".blume-verify/"]);
|
|
45
45
|
}
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -15,9 +15,35 @@ import {
|
|
|
15
15
|
DevLockHeldError,
|
|
16
16
|
updateDevLockPort,
|
|
17
17
|
} from "../dev-lock.ts";
|
|
18
|
-
import { logger } from "../log.ts";
|
|
18
|
+
import { logger, reportDiagnostics } from "../log.ts";
|
|
19
19
|
import { prepareProject } from "../prepare.ts";
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a `--host` flag value into what Astro/Vite's `server.host` expects.
|
|
23
|
+
* citty (0.1) has no mixed string/boolean arg type, so `host` is declared as a
|
|
24
|
+
* string and a bare `--host` parses as `""` — Node would bind all interfaces
|
|
25
|
+
* for `""`, but Vite's `resolveHostname` treats it as a literal hostname and
|
|
26
|
+
* prints malformed URLs like `http://:4321/`. Match Astro's own `--host`
|
|
27
|
+
* semantics instead: bare flag → `true` (bind all interfaces), `--host
|
|
28
|
+
* 10.0.0.1` → that address, absent → `false` (localhost only).
|
|
29
|
+
*/
|
|
30
|
+
export const normalizeHost = (host: string | undefined): boolean | string =>
|
|
31
|
+
host === "" ? true : (host ?? false);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A fingerprint of the route set: the sorted `path entryId` pairs. It changes
|
|
35
|
+
* when a page is added, removed, or renamed (a folder rename shifts many at
|
|
36
|
+
* once) but stays equal across pure body edits — so the dev loop can tell a
|
|
37
|
+
* "structural" change (needs a cold restart) from a hot-reloadable one.
|
|
38
|
+
*/
|
|
39
|
+
const routeSignature = (
|
|
40
|
+
routes: readonly { entryId: string; path: string }[]
|
|
41
|
+
): string =>
|
|
42
|
+
routes
|
|
43
|
+
.map((route) => `${route.path} ${route.entryId}`)
|
|
44
|
+
.toSorted()
|
|
45
|
+
.join("\n");
|
|
46
|
+
|
|
21
47
|
export const devCommand = defineCommand({
|
|
22
48
|
args: {
|
|
23
49
|
"content-dir": {
|
|
@@ -84,15 +110,18 @@ export const devCommand = defineCommand({
|
|
|
84
110
|
strict: args.strict,
|
|
85
111
|
});
|
|
86
112
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
// A factory so `runRegenerate` can recreate the server on a structural
|
|
114
|
+
// (route-set) change: only a cold container re-globs Astro's content store,
|
|
115
|
+
// which its in-place config restart doesn't. `open` is honored on first
|
|
116
|
+
// boot only — a restart must not reopen the browser.
|
|
117
|
+
const createServer = (listenPort: number | undefined, open: boolean) =>
|
|
118
|
+
dev({
|
|
119
|
+
logLevel: args.debug ? "debug" : "info",
|
|
120
|
+
root: project.context.outDir,
|
|
121
|
+
server: { host: normalizeHost(args.host), open, port: listenPort },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let server = await createServer(explicitPort, args.open ?? false);
|
|
96
125
|
|
|
97
126
|
// Vite bumps to the next free port when the default is taken, so record
|
|
98
127
|
// the port the server actually bound — the lock's URL is what a refused
|
|
@@ -109,11 +138,18 @@ export const devCommand = defineCommand({
|
|
|
109
138
|
// (and its HMR channel) is up.
|
|
110
139
|
showBlumeErrorOverlay(project.diagnostics);
|
|
111
140
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
141
|
+
let lastSignature = routeSignature(project.manifest.routes);
|
|
142
|
+
|
|
143
|
+
// Watch user inputs and regenerate the runtime data on change. A body edit
|
|
144
|
+
// hot-reloads via Vite (fast path). A route-set change instead forces a cold
|
|
145
|
+
// server restart: Astro's in-place content sync never re-globs on a Blume
|
|
146
|
+
// route change (it strips `integrations` from its cache digest) and its glob
|
|
147
|
+
// watcher misses directory renames, so a renamed page 404s (`getEntry` reads
|
|
148
|
+
// a stale in-memory store) until the server is restarted. We restart it
|
|
149
|
+
// ourselves — stop, regenerate while down (no watcher races), then bring up
|
|
150
|
+
// a fresh container whose cold sync re-globs everything. `coalescedRunner`
|
|
151
|
+
// single-flights the scan so a burst of watch events can never stack
|
|
152
|
+
// overlapping regenerations (piled-up scans exhaust the heap).
|
|
117
153
|
const runRegenerate = coalescedRunner(async () => {
|
|
118
154
|
try {
|
|
119
155
|
const next = await scanProject(root, {
|
|
@@ -122,8 +158,26 @@ export const devCommand = defineCommand({
|
|
|
122
158
|
overrides,
|
|
123
159
|
preview,
|
|
124
160
|
});
|
|
125
|
-
|
|
126
|
-
|
|
161
|
+
const nextSignature = routeSignature(next.manifest.routes);
|
|
162
|
+
const structural = nextSignature !== lastSignature;
|
|
163
|
+
if (structural) {
|
|
164
|
+
await server.stop();
|
|
165
|
+
await generateRuntime(next);
|
|
166
|
+
server = await createServer(boundPort, false);
|
|
167
|
+
} else {
|
|
168
|
+
await generateRuntime(next);
|
|
169
|
+
}
|
|
170
|
+
// Commit the signature only after the (re)generation succeeded. If the
|
|
171
|
+
// restart above throws mid-sequence, the signature stays stale so the
|
|
172
|
+
// next watch event retries the structural path — committing early would
|
|
173
|
+
// route it to the non-structural branch with the server still down.
|
|
174
|
+
lastSignature = nextSignature;
|
|
175
|
+
// Surface any content/config errors in the terminal AND the browser
|
|
176
|
+
// overlay. The terminal report must not be skipped: on a published
|
|
177
|
+
// install the CLI bundle holds its own copy of the integration module,
|
|
178
|
+
// separate from the Vite module graph that registers the overlay, so
|
|
179
|
+
// the overlay call below can be a no-op there.
|
|
180
|
+
reportDiagnostics(next.diagnostics, root);
|
|
127
181
|
showBlumeErrorOverlay(next.diagnostics);
|
|
128
182
|
} catch (error) {
|
|
129
183
|
logger.error(`Regeneration failed: ${(error as Error).message}`);
|
|
@@ -34,8 +34,8 @@ const minSupportedNode = (): string => {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
const versionBelow = (current: string, minimum: string): boolean => {
|
|
37
|
-
const a = current.split(".").map((part) =>
|
|
38
|
-
const b = minimum.split(".").map((part) =>
|
|
37
|
+
const a = current.split(".").map((part) => Math.trunc(Number(part)));
|
|
38
|
+
const b = minimum.split(".").map((part) => Math.trunc(Number(part)));
|
|
39
39
|
for (let i = 0; i < 3; i += 1) {
|
|
40
40
|
const delta = (a[i] ?? 0) - (b[i] ?? 0);
|
|
41
41
|
if (delta !== 0) {
|
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
|
|
3
1
|
import { defineCommand } from "citty";
|
|
4
|
-
import {
|
|
2
|
+
import { relative } from "pathe";
|
|
5
3
|
|
|
4
|
+
import { loadConfig } from "../../core/config.ts";
|
|
6
5
|
import { eject } from "../../registry/eject.ts";
|
|
7
6
|
import { refuseIfDevRunning } from "../dev-lock.ts";
|
|
7
|
+
import {
|
|
8
|
+
droppedArtifactNotices,
|
|
9
|
+
updatePackageScripts,
|
|
10
|
+
} from "../eject-scripts.ts";
|
|
11
|
+
import { commandsFor, detectPackageManager } from "../init/scaffold.ts";
|
|
8
12
|
import { logger } from "../log.ts";
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Warn which `blume build` post-build artifacts the ejected app stops
|
|
16
|
+
* producing. Printed both at the confirmation (so the decision is informed)
|
|
17
|
+
* and after `--yes` (so a direct eject still sees it). No-op when the config
|
|
18
|
+
* activates none of them.
|
|
19
|
+
*/
|
|
20
|
+
const reportDroppedArtifacts = (notices: string[]): void => {
|
|
21
|
+
if (notices.length === 0) {
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
|
|
24
|
+
logger.warn(
|
|
25
|
+
[
|
|
26
|
+
"The ejected build script runs plain `astro build`, which stops producing these `blume build` artifacts:",
|
|
27
|
+
...notices.map((notice) => ` - ${notice}`),
|
|
28
|
+
].join("\n")
|
|
29
|
+
);
|
|
26
30
|
};
|
|
27
31
|
|
|
28
32
|
export const ejectCommand = defineCommand({
|
|
@@ -37,23 +41,47 @@ export const ejectCommand = defineCommand({
|
|
|
37
41
|
const root = process.cwd();
|
|
38
42
|
refuseIfDevRunning(root, "ejecting");
|
|
39
43
|
|
|
44
|
+
// Config-aware drop list: only the artifacts this project actually
|
|
45
|
+
// produces are mentioned (e.g. the Pagefind index only for
|
|
46
|
+
// `search.provider: "pagefind"`).
|
|
47
|
+
let notices: string[] = [];
|
|
48
|
+
try {
|
|
49
|
+
const { config } = await loadConfig(root);
|
|
50
|
+
notices = droppedArtifactNotices(config);
|
|
51
|
+
} catch {
|
|
52
|
+
// A config that fails to load can't gate the notice; the eject itself
|
|
53
|
+
// surfaces the load error.
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
if (!args.yes) {
|
|
41
57
|
logger.warn(
|
|
42
58
|
"Eject is one-way: it writes astro.config.mjs, src/, and (if absent) tsconfig.json, rewrites your package.json scripts, and removes .blume. An existing tsconfig.json is left untouched."
|
|
43
59
|
);
|
|
60
|
+
reportDroppedArtifacts(notices);
|
|
44
61
|
logger.info("Re-run with --yes to proceed.");
|
|
45
62
|
return;
|
|
46
63
|
}
|
|
47
64
|
|
|
48
|
-
const files = await eject(root);
|
|
65
|
+
const { files, warnings } = await eject(root);
|
|
49
66
|
await updatePackageScripts(root);
|
|
50
67
|
|
|
68
|
+
// The same surface as the generated-runtime path (prepare.ts): one warn
|
|
69
|
+
// per generation warning, e.g. a Scalar reference spec that wasn't found.
|
|
70
|
+
for (const warning of warnings) {
|
|
71
|
+
logger.warn(warning);
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
logger.success(`Ejected ${files.length} file(s):`);
|
|
52
75
|
for (const file of files) {
|
|
53
76
|
process.stdout.write(` ${relative(root, file)}\n`);
|
|
54
77
|
}
|
|
78
|
+
reportDroppedArtifacts(notices);
|
|
79
|
+
// Print run commands matching the user's package manager, detected the
|
|
80
|
+
// same way as `blume init`'s next-steps hint.
|
|
81
|
+
const pm = detectPackageManager(process.env.npm_config_user_agent);
|
|
82
|
+
const { build, dev } = commandsFor(pm);
|
|
55
83
|
logger.box(
|
|
56
|
-
|
|
84
|
+
`Your project is now a standalone Astro app.\n\n ${dev}\n ${build}\n\nThe blume package remains importable.`
|
|
57
85
|
);
|
|
58
86
|
},
|
|
59
87
|
});
|