blume 0.7.0 → 1.0.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.
Files changed (185) hide show
  1. package/CHANGELOG.md +666 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -0
  4. package/dist/cli/index.js +1852 -380
  5. package/dist/cli/index.js.map +98 -91
  6. package/dist/types/ai/component-markdown.d.ts +34 -0
  7. package/dist/types/components/content/youtube.d.ts +18 -0
  8. package/dist/types/core/base-path.d.ts +9 -0
  9. package/dist/types/core/config-input.d.ts +47 -2
  10. package/dist/types/core/config.d.ts +3 -2
  11. package/dist/types/core/data.d.ts +7 -0
  12. package/dist/types/core/i18n-ui.d.ts +526 -132
  13. package/dist/types/core/schema.d.ts +293 -146
  14. package/dist/types/index.d.ts +1 -0
  15. package/dist/types/openapi/references.d.ts +60 -0
  16. package/dist/types/seo/x-handle.d.ts +12 -0
  17. package/docs/01-quickstart.mdx +5 -2
  18. package/docs/02-deployment.mdx +8 -8
  19. package/docs/03-faq.mdx +46 -16
  20. package/docs/advanced/api-reference.mdx +1 -1
  21. package/docs/advanced/changelog.mdx +1 -1
  22. package/docs/advanced/custom-pages.mdx +1 -1
  23. package/docs/advanced/skills.mdx +1 -1
  24. package/docs/configuration/ai.mdx +49 -10
  25. package/docs/configuration/customization.mdx +11 -0
  26. package/docs/configuration/export.mdx +1 -1
  27. package/docs/configuration/index.mdx +27 -3
  28. package/docs/configuration/seo.mdx +35 -5
  29. package/docs/content/components.mdx +2 -2
  30. package/docs/content/i18n.mdx +1 -1
  31. package/docs/content/navigation.mdx +3 -3
  32. package/docs/content/sources.mdx +1 -1
  33. package/docs/content/syntax.mdx +6 -4
  34. package/docs/index.mdx +2 -2
  35. package/docs/reference/cli.mdx +9 -7
  36. package/docs/reference/frontmatter.mdx +1 -1
  37. package/package.json +22 -4
  38. package/skills/blume/SKILL.md +5 -3
  39. package/skills/blume-update-docs/SKILL.md +3 -2
  40. package/src/ai/agent-readability.ts +9 -8
  41. package/src/ai/ask-context.ts +7 -2
  42. package/src/ai/ask-data.ts +3 -0
  43. package/src/ai/component-markdown.ts +461 -0
  44. package/src/ai/llms.ts +135 -26
  45. package/src/ai/markdown.ts +35 -6
  46. package/src/ai/mcp/data.ts +25 -4
  47. package/src/ai/mcp/discovery.ts +10 -3
  48. package/src/ai/mcp/server.ts +21 -7
  49. package/src/ai/mcp/tools.ts +1 -1
  50. package/src/ai/visibility.ts +74 -0
  51. package/src/astro/component-slots.ts +11 -1
  52. package/src/astro/generate.ts +77 -45
  53. package/src/astro/integration.ts +1 -1
  54. package/src/astro/markdown-negotiation.ts +1 -1
  55. package/src/astro/pages.ts +81 -19
  56. package/src/astro/templates.ts +150 -19
  57. package/src/blume-modules.d.ts +8 -0
  58. package/src/cli/commands/build.ts +120 -23
  59. package/src/cli/commands/check.ts +1 -1
  60. package/src/cli/commands/dev.ts +26 -5
  61. package/src/cli/commands/eject.ts +47 -19
  62. package/src/cli/commands/init.ts +120 -180
  63. package/src/cli/commands/preview.ts +4 -1
  64. package/src/cli/commands/validate.ts +43 -2
  65. package/src/cli/dev-lock.ts +8 -4
  66. package/src/cli/eject-scripts.ts +72 -0
  67. package/src/cli/env.ts +15 -5
  68. package/src/cli/init/questions.ts +158 -0
  69. package/src/cli/init/scaffold.ts +380 -0
  70. package/src/cli/internal-error.ts +9 -4
  71. package/src/cli/prepare.ts +3 -2
  72. package/src/components/Icon.astro +2 -1
  73. package/src/components/content/AccordionItem.astro +23 -4
  74. package/src/components/content/Badge.astro +3 -1
  75. package/src/components/content/Card.astro +4 -2
  76. package/src/components/content/Step.astro +10 -1
  77. package/src/components/content/Tabs.astro +15 -3
  78. package/src/components/content/Tile.astro +2 -1
  79. package/src/components/content/Tooltip.astro +3 -1
  80. package/src/components/content/Update.astro +9 -2
  81. package/src/components/content/auto-type-table.ts +7 -1
  82. package/src/components/content/base-href.ts +33 -0
  83. package/src/components/content/changelog-element.ts +9 -2
  84. package/src/components/content/mermaid-element.ts +7 -2
  85. package/src/components/islands/AskAI.astro +5 -2
  86. package/src/components/islands/ask-ai.tsx +86 -11
  87. package/src/components/islands/hooks.ts +28 -8
  88. package/src/components/layout/Banner.astro +10 -2
  89. package/src/components/layout/Breadcrumbs.astro +11 -2
  90. package/src/components/layout/Header.astro +13 -4
  91. package/src/components/layout/Logo.astro +11 -3
  92. package/src/components/layout/NavTree.astro +19 -5
  93. package/src/components/layout/PageActions.astro +25 -10
  94. package/src/components/layout/PageLayout.astro +85 -9
  95. package/src/components/layout/Pagination.astro +10 -4
  96. package/src/components/layout/ReferenceLayout.astro +20 -2
  97. package/src/components/layout/RootLayout.astro +142 -12
  98. package/src/components/layout/Search.astro +117 -27
  99. package/src/components/layout/search/algolia.ts +11 -2
  100. package/src/components/layout/search/endpoint.ts +11 -5
  101. package/src/components/layout/search/orama-cloud.ts +8 -2
  102. package/src/components/layout/search/types.ts +5 -1
  103. package/src/components/layout/search/typesense.ts +4 -1
  104. package/src/components/layout/toc-element.ts +1 -1
  105. package/src/components/openapi/ApiTagOperations.astro +2 -1
  106. package/src/components/openapi/Operation.astro +47 -40
  107. package/src/components/openapi/RequestPanel.astro +1 -1
  108. package/src/components/openapi/helpers.ts +71 -3
  109. package/src/components/openapi/panel.ts +1 -1
  110. package/src/core/base-path.ts +24 -0
  111. package/src/core/builtin-tags.ts +2 -0
  112. package/src/core/config-input.ts +48 -2
  113. package/src/core/config.ts +3 -2
  114. package/src/core/data.ts +4 -0
  115. package/src/core/frontmatter.ts +7 -0
  116. package/src/core/graph.ts +15 -5
  117. package/src/core/i18n-ui.ts +54 -0
  118. package/src/core/i18n.ts +16 -8
  119. package/src/core/last-modified.ts +13 -6
  120. package/src/core/links.ts +32 -8
  121. package/src/core/navigation.ts +29 -4
  122. package/src/core/package-json.ts +17 -2
  123. package/src/core/project-graph.ts +15 -6
  124. package/src/core/schema.ts +71 -2
  125. package/src/core/sources/assets.ts +6 -1
  126. package/src/core/sources/filesystem.ts +4 -0
  127. package/src/core/sources/mdx-remote.ts +23 -14
  128. package/src/core/sources/normalize.ts +152 -50
  129. package/src/core/sources/notion.ts +8 -8
  130. package/src/core/ui-packs/ar.ts +8 -0
  131. package/src/core/ui-packs/bg.ts +8 -0
  132. package/src/core/ui-packs/bn.ts +8 -0
  133. package/src/core/ui-packs/ca.ts +8 -0
  134. package/src/core/ui-packs/cs.ts +8 -0
  135. package/src/core/ui-packs/da.ts +8 -0
  136. package/src/core/ui-packs/de.ts +8 -0
  137. package/src/core/ui-packs/el.ts +8 -0
  138. package/src/core/ui-packs/es.ts +8 -0
  139. package/src/core/ui-packs/fa.ts +8 -0
  140. package/src/core/ui-packs/fi.ts +8 -0
  141. package/src/core/ui-packs/fr.ts +9 -1
  142. package/src/core/ui-packs/he.ts +8 -0
  143. package/src/core/ui-packs/hi.ts +8 -0
  144. package/src/core/ui-packs/hr.ts +8 -0
  145. package/src/core/ui-packs/hu.ts +8 -0
  146. package/src/core/ui-packs/id.ts +8 -0
  147. package/src/core/ui-packs/it.ts +8 -0
  148. package/src/core/ui-packs/ja.ts +8 -0
  149. package/src/core/ui-packs/ko.ts +8 -0
  150. package/src/core/ui-packs/nl.ts +8 -0
  151. package/src/core/ui-packs/no.ts +8 -0
  152. package/src/core/ui-packs/pl.ts +8 -0
  153. package/src/core/ui-packs/pt-br.ts +8 -0
  154. package/src/core/ui-packs/pt.ts +8 -0
  155. package/src/core/ui-packs/ro.ts +8 -0
  156. package/src/core/ui-packs/ru.ts +8 -0
  157. package/src/core/ui-packs/sk.ts +8 -0
  158. package/src/core/ui-packs/sr.ts +8 -0
  159. package/src/core/ui-packs/sv.ts +8 -0
  160. package/src/core/ui-packs/th.ts +8 -0
  161. package/src/core/ui-packs/tr.ts +8 -0
  162. package/src/core/ui-packs/uk.ts +8 -0
  163. package/src/core/ui-packs/vi.ts +8 -0
  164. package/src/core/ui-packs/zh-tw.ts +8 -0
  165. package/src/core/ui-packs/zh.ts +8 -0
  166. package/src/deploy/adapter-output.ts +18 -8
  167. package/src/deploy/redirects.ts +7 -2
  168. package/src/deploy/sitemap.ts +53 -11
  169. package/src/index.ts +5 -0
  170. package/src/markdown/base-links.ts +10 -8
  171. package/src/markdown/index.ts +15 -3
  172. package/src/markdown/inline-code.ts +7 -2
  173. package/src/markdown/package-commands.ts +10 -4
  174. package/src/og/card.ts +4 -2
  175. package/src/og/dimensions.ts +12 -0
  176. package/src/openapi/model.ts +12 -4
  177. package/src/openapi/parse.ts +21 -0
  178. package/src/openapi/references.ts +38 -8
  179. package/src/openapi/render-mdx.ts +62 -1
  180. package/src/openapi/source.ts +59 -10
  181. package/src/registry/eject.ts +184 -12
  182. package/src/registry/registry.ts +0 -3
  183. package/src/search/documents.ts +34 -2
  184. package/src/seo/jsonld.ts +20 -13
  185. package/src/seo/x-handle.ts +18 -0
@@ -11,6 +11,7 @@ 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,
@@ -63,7 +64,10 @@ const validateBudgetFlags = (args: BudgetArgs): void => {
63
64
  /**
64
65
  * Emit platform redirect files for a static build (adapters wire redirects
65
66
  * natively). Always writes the manifest; writes `_redirects`/`vercel.json` only
66
- * when the user hasn't shipped one via public/.
67
+ * when the user hasn't shipped one via public/. Note that Vercel's
68
+ * git-integration builds read `vercel.json` from the repository root only —
69
+ * the copy emitted here takes effect when the dist folder itself is deployed
70
+ * directly via the Vercel CLI.
67
71
  */
68
72
  const emitRedirectFiles = async (
69
73
  config: ResolvedConfig,
@@ -189,11 +193,105 @@ const enforceBudget = async (
189
193
  return passed ? "pass" : "fail";
190
194
  };
191
195
 
196
+ /**
197
+ * Run the optional bundle report (`--analyze`) and performance-budget gate
198
+ * against the directory whose `_astro/` client assets the deploy serves.
199
+ * Shared by real and isolated builds — an isolated CI run passing
200
+ * `--budget-js` must still fail on an exceeded budget rather than silently
201
+ * skipping the check. Exits non-zero when a budget is exceeded.
202
+ */
203
+ export const runClientAssetChecks = async (
204
+ staticDir: string,
205
+ args: { analyze?: boolean } & BudgetArgs
206
+ ): Promise<void> => {
207
+ if (args.analyze) {
208
+ await reportBundleSizes(staticDir);
209
+ }
210
+ if ((await enforceBudget(staticDir, args)) === "fail") {
211
+ process.exit(1);
212
+ }
213
+ };
214
+
215
+ /**
216
+ * Root of an isolated build's output. The runtime-local `dist/`, except for a
217
+ * Vercel server build, whose deploy bundle lands at `<runtime>/.vercel/output`
218
+ * and is never surfaced to the project root.
219
+ */
220
+ export const isolatedOutputDir = (
221
+ config: ResolvedConfig,
222
+ context: ProjectContext
223
+ ): string => {
224
+ const { adapter, output } = config.deployment;
225
+ if (output === "server" && adapter === "vercel") {
226
+ return join(context.outDir, ".vercel", "output");
227
+ }
228
+ return context.distDir ?? join(context.outDir, "dist");
229
+ };
230
+
231
+ /**
232
+ * Directory holding an isolated build's client `_astro/` assets. Mirrors
233
+ * `deployStaticDir`, except that an isolated build never surfaces the adapter
234
+ * bundle to the project root — a Vercel server build's static output stays at
235
+ * `<runtime>/.vercel/output/static`, where `deployStaticDir` would instead
236
+ * point at the project-root copy (a previous real build's assets, or nothing).
237
+ */
238
+ export const isolatedStaticDir = (
239
+ config: ResolvedConfig,
240
+ context: ProjectContext
241
+ ): string => {
242
+ const { adapter, output } = config.deployment;
243
+ const outputDir = isolatedOutputDir(config, context);
244
+ if (output === "server" && adapter === "vercel") {
245
+ return join(outputDir, "static");
246
+ }
247
+ if (output === "server" && adapter === "node") {
248
+ return join(outputDir, "client");
249
+ }
250
+ return outputDir;
251
+ };
252
+
253
+ /**
254
+ * Generate `llms.txt`/`llms-full.txt` into the dist dir. A user's own file in
255
+ * `public/` (copied into dist by Astro before this runs, like the sitemap and
256
+ * robots.txt) wins over the generated one — each file is checked and replaced
257
+ * independently, so a custom `llms.txt` still gets a generated `llms-full.txt`.
258
+ */
259
+ const publishLlmsFiles = async (
260
+ project: BlumeProject,
261
+ distDir: string
262
+ ): Promise<void> => {
263
+ const indexPath = join(distDir, "llms.txt");
264
+ const fullPath = join(distDir, "llms-full.txt");
265
+ const writeIndex = !existsSync(indexPath);
266
+ const writeFull = !existsSync(fullPath);
267
+ if (!(writeIndex || writeFull)) {
268
+ return;
269
+ }
270
+ const { index, full } = await buildLlmsFiles(project);
271
+ const writes: Promise<void>[] = [];
272
+ if (writeIndex) {
273
+ writes.push(writeFile(indexPath, index, "utf-8"));
274
+ }
275
+ if (writeFull) {
276
+ writes.push(writeFile(fullPath, full, "utf-8"));
277
+ }
278
+ await Promise.all(writes);
279
+ logger.success(
280
+ `Generated ${[
281
+ writeIndex ? "llms.txt" : null,
282
+ writeFull ? "llms-full.txt" : null,
283
+ ]
284
+ .filter(Boolean)
285
+ .join(" and ")}`
286
+ );
287
+ };
288
+
192
289
  /**
193
290
  * Run every deploy post-step of a real (non-isolated) build: the search index +
194
291
  * hosted-provider sync, llms.txt, sitemap/robots, redirect files, the summary
195
292
  * box, and the optional bundle report / budget gate. Exits non-zero if a budget
196
- * is exceeded. Isolated verify builds skip all of this.
293
+ * is exceeded. Isolated verify builds skip all of this except the bundle
294
+ * report / budget gate, which they run against their own output.
197
295
  */
198
296
  const publishBuildArtifacts = async (
199
297
  project: BlumeProject,
@@ -214,13 +312,8 @@ const publishBuildArtifacts = async (
214
312
  warn: (message) => logger.warn(message),
215
313
  });
216
314
 
217
- if (project.config.ai.llmsTxt) {
218
- const { index, full } = await buildLlmsFiles(project);
219
- await Promise.all([
220
- writeFile(join(distDir, "llms.txt"), index, "utf-8"),
221
- writeFile(join(distDir, "llms-full.txt"), full, "utf-8"),
222
- ]);
223
- logger.success("Generated llms.txt and llms-full.txt");
315
+ if (project.config.ai.llmsTxt.enabled) {
316
+ await publishLlmsFiles(project, distDir);
224
317
  }
225
318
 
226
319
  // A user's own public/ file (copied into dist by Astro) always wins.
@@ -253,6 +346,11 @@ const publishBuildArtifacts = async (
253
346
 
254
347
  const { config } = project;
255
348
  const features = serverFeatures(config);
349
+ // `buildSitemap` returns null both when the sitemap is disabled and when no
350
+ // `site` is configured — only the latter deserves the remediation hint.
351
+ const sitemapNote = config.seo.sitemap
352
+ ? "no (set deployment.site)"
353
+ : "no (seo.sitemap is false)";
256
354
  logger.box(
257
355
  [
258
356
  `Output ${config.deployment.output}`,
@@ -260,21 +358,15 @@ const publishBuildArtifacts = async (
260
358
  `Site ${config.deployment.site ?? "not set"}`,
261
359
  `Search ${config.search.provider}`,
262
360
  `Redirects ${config.redirects.length}`,
263
- `Sitemap ${sitemap ? "yes" : "no (set deployment.site)"}`,
361
+ `Sitemap ${sitemap ? "yes" : sitemapNote}`,
264
362
  `Robots ${robots ? "yes" : "no"}`,
265
363
  `Agent JSON ${agentReadability ? "yes" : "no"}`,
266
- `LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
364
+ `LLM files ${config.ai.llmsTxt.enabled ? "yes" : "no"}`,
267
365
  `Server features ${features.length > 0 ? features.join(", ") : "none"}`,
268
366
  ].join("\n")
269
367
  );
270
368
 
271
- if (args.analyze) {
272
- await reportBundleSizes(distDir);
273
- }
274
-
275
- if ((await enforceBudget(distDir, args)) === "fail") {
276
- process.exit(1);
277
- }
369
+ await runClientAssetChecks(distDir, args);
278
370
 
279
371
  logger.success(`Built to ${distDir}`);
280
372
  };
@@ -330,7 +422,7 @@ export const buildCommand = defineCommand({
330
422
  const runtimeDir = args.isolated
331
423
  ? ".blume-verify"
332
424
  : process.env.BLUME_RUNTIME_DIR;
333
- refuseIfDevRunning(root, "building", runtimeDir);
425
+ refuseIfDevRunning(root, "building", { isolatedHint: true, runtimeDir });
334
426
  if (args.isolated) {
335
427
  await ensureGitignore(root, [".blume-verify/"]);
336
428
  }
@@ -369,15 +461,20 @@ export const buildCommand = defineCommand({
369
461
  root: project.context.outDir,
370
462
  });
371
463
 
372
- const distDir = project.context.distDir ?? join(root, "dist");
373
-
374
464
  // An isolated build is a throwaway verify: it only needs to confirm the site
375
465
  // compiles and renders. Skip the network post-steps (search sync) and
376
466
  // deploy artifacts (index/llms/sitemap/robots/redirects) that only matter
377
- // for a real publish and would push to hosted providers.
467
+ // for a real publish and would push to hosted providers. The bundle report
468
+ // and budget gate still run, though — `blume build --isolated --budget-js
469
+ // 100` exiting 0 without measuring anything would be a silent false pass
470
+ // in CI.
378
471
  if (runtimeDir) {
472
+ await runClientAssetChecks(
473
+ isolatedStaticDir(project.config, project.context),
474
+ args
475
+ );
379
476
  logger.success(
380
- `Isolated build OK — output at ${distDir} (not published).`
477
+ `Isolated build OK — output at ${isolatedOutputDir(project.config, project.context)} (not published).`
381
478
  );
382
479
  return;
383
480
  }
@@ -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
  }
@@ -15,9 +15,21 @@ 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
+
21
33
  /**
22
34
  * A fingerprint of the route set: the sorted `path entryId` pairs. It changes
23
35
  * when a page is added, removed, or renamed (a folder rename shifts many at
@@ -100,13 +112,13 @@ export const devCommand = defineCommand({
100
112
 
101
113
  // A factory so `runRegenerate` can recreate the server on a structural
102
114
  // (route-set) change: only a cold container re-globs Astro's content store,
103
- // which its in-place config restart doesn't. `open` is honoured on first
115
+ // which its in-place config restart doesn't. `open` is honored on first
104
116
  // boot only — a restart must not reopen the browser.
105
117
  const createServer = (listenPort: number | undefined, open: boolean) =>
106
118
  dev({
107
119
  logLevel: args.debug ? "debug" : "info",
108
120
  root: project.context.outDir,
109
- server: { host: args.host ?? false, open, port: listenPort },
121
+ server: { host: normalizeHost(args.host), open, port: listenPort },
110
122
  });
111
123
 
112
124
  let server = await createServer(explicitPort, args.open ?? false);
@@ -148,7 +160,6 @@ export const devCommand = defineCommand({
148
160
  });
149
161
  const nextSignature = routeSignature(next.manifest.routes);
150
162
  const structural = nextSignature !== lastSignature;
151
- lastSignature = nextSignature;
152
163
  if (structural) {
153
164
  await server.stop();
154
165
  await generateRuntime(next);
@@ -156,7 +167,17 @@ export const devCommand = defineCommand({
156
167
  } else {
157
168
  await generateRuntime(next);
158
169
  }
159
- // Surface any content/config errors in the browser overlay too.
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);
160
181
  showBlumeErrorOverlay(next.diagnostics);
161
182
  } catch (error) {
162
183
  logger.error(`Regeneration failed: ${(error as Error).message}`);
@@ -1,28 +1,32 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
-
3
1
  import { defineCommand } from "citty";
4
- import { join, relative } from "pathe";
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
- const updatePackageScripts = async (root: string): Promise<void> => {
11
- const pkgPath = join(root, "package.json");
12
- let pkg: Record<string, unknown>;
13
- try {
14
- pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
15
- } catch {
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
- const scripts = (pkg.scripts ?? {}) as Record<string, string>;
19
- pkg.scripts = {
20
- ...scripts,
21
- build: "astro build",
22
- dev: "astro dev",
23
- preview: "astro preview",
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
- "Your project is now a standalone Astro app.\n\n bun run dev\n bun run build\n\nThe blume package remains importable."
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
  });