blume 1.6.5 → 1.7.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 (179) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/bin/blume.mjs +3 -2
  3. package/dist/cli/chunk-0qhq7b8q.js +111 -0
  4. package/dist/cli/chunk-0qhq7b8q.js.map +11 -0
  5. package/dist/cli/chunk-18tjv4f7.js +96 -0
  6. package/dist/cli/chunk-18tjv4f7.js.map +10 -0
  7. package/dist/cli/chunk-27gtm2ym.js +69 -0
  8. package/dist/cli/chunk-27gtm2ym.js.map +11 -0
  9. package/dist/cli/chunk-2aj8ddew.js +72 -0
  10. package/dist/cli/chunk-2aj8ddew.js.map +10 -0
  11. package/dist/cli/chunk-3r94j3tc.js +221 -0
  12. package/dist/cli/chunk-3r94j3tc.js.map +10 -0
  13. package/dist/cli/chunk-4trphnvy.js +102 -0
  14. package/dist/cli/chunk-4trphnvy.js.map +11 -0
  15. package/dist/cli/chunk-4xyggvgf.js +21 -0
  16. package/dist/cli/chunk-4xyggvgf.js.map +10 -0
  17. package/dist/cli/chunk-5d4q7121.js +4064 -0
  18. package/dist/cli/chunk-5d4q7121.js.map +40 -0
  19. package/dist/cli/chunk-5hs6gb7n.js +32 -0
  20. package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
  21. package/dist/cli/chunk-6kzzpsx8.js +26 -0
  22. package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
  23. package/dist/cli/chunk-8gnpdsn1.js +952 -0
  24. package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
  25. package/dist/cli/chunk-9qs6acpw.js +176 -0
  26. package/dist/cli/chunk-9qs6acpw.js.map +10 -0
  27. package/dist/cli/chunk-agy5rzxy.js +2453 -0
  28. package/dist/cli/chunk-agy5rzxy.js.map +15 -0
  29. package/dist/cli/chunk-bcy492zc.js +16 -0
  30. package/dist/cli/chunk-bcy492zc.js.map +10 -0
  31. package/dist/cli/chunk-btfr9yvw.js +41 -0
  32. package/dist/cli/chunk-btfr9yvw.js.map +10 -0
  33. package/dist/cli/chunk-cbjnx4s8.js +73 -0
  34. package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
  35. package/dist/cli/chunk-cfw6x4rm.js +1967 -0
  36. package/dist/cli/chunk-cfw6x4rm.js.map +34 -0
  37. package/dist/cli/chunk-ckh3a410.js +277 -0
  38. package/dist/cli/chunk-ckh3a410.js.map +11 -0
  39. package/dist/cli/chunk-drke6t0h.js +259 -0
  40. package/dist/cli/chunk-drke6t0h.js.map +11 -0
  41. package/dist/cli/chunk-ev67ycx0.js +15 -0
  42. package/dist/cli/chunk-ev67ycx0.js.map +10 -0
  43. package/dist/cli/chunk-ey89bjj1.js +209 -0
  44. package/dist/cli/chunk-ey89bjj1.js.map +11 -0
  45. package/dist/cli/chunk-j6pxe0dt.js +69 -0
  46. package/dist/cli/chunk-j6pxe0dt.js.map +11 -0
  47. package/dist/cli/chunk-jk1zwka1.js +387 -0
  48. package/dist/cli/chunk-jk1zwka1.js.map +12 -0
  49. package/dist/cli/chunk-jtb45atp.js +467 -0
  50. package/dist/cli/chunk-jtb45atp.js.map +14 -0
  51. package/dist/cli/chunk-jxkxjsc1.js +76 -0
  52. package/dist/cli/chunk-jxkxjsc1.js.map +10 -0
  53. package/dist/cli/chunk-kwx90v78.js +81 -0
  54. package/dist/cli/chunk-kwx90v78.js.map +10 -0
  55. package/dist/cli/chunk-n0nyat6g.js +30 -0
  56. package/dist/cli/chunk-n0nyat6g.js.map +10 -0
  57. package/dist/cli/chunk-pxj10x8y.js +35 -0
  58. package/dist/cli/chunk-pxj10x8y.js.map +10 -0
  59. package/dist/cli/chunk-qq9nm3qd.js +1141 -0
  60. package/dist/cli/chunk-qq9nm3qd.js.map +19 -0
  61. package/dist/cli/chunk-s102bysw.js +5170 -0
  62. package/dist/cli/chunk-s102bysw.js.map +47 -0
  63. package/dist/cli/chunk-s5dsk8bj.js +769 -0
  64. package/dist/cli/chunk-s5dsk8bj.js.map +13 -0
  65. package/dist/cli/chunk-s5e5jt53.js +227 -0
  66. package/dist/cli/chunk-s5e5jt53.js.map +11 -0
  67. package/dist/cli/chunk-sbdqrjbb.js +81 -0
  68. package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
  69. package/dist/cli/chunk-tnskyrej.js +117 -0
  70. package/dist/cli/chunk-tnskyrej.js.map +10 -0
  71. package/dist/cli/chunk-v2ymm99c.js +1016 -0
  72. package/dist/cli/chunk-v2ymm99c.js.map +13 -0
  73. package/dist/cli/chunk-v5mm027v.js +185 -0
  74. package/dist/cli/chunk-v5mm027v.js.map +11 -0
  75. package/dist/cli/chunk-vt8fgygt.js +23 -0
  76. package/dist/cli/chunk-vt8fgygt.js.map +10 -0
  77. package/dist/cli/chunk-vxv4x1n8.js +17 -0
  78. package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
  79. package/dist/cli/chunk-wd27zjcz.js +60 -0
  80. package/dist/cli/chunk-wd27zjcz.js.map +10 -0
  81. package/dist/cli/chunk-x66c5yjn.js +23 -0
  82. package/dist/cli/chunk-x66c5yjn.js.map +10 -0
  83. package/dist/cli/chunk-xv91q4nm.js +5314 -0
  84. package/dist/cli/chunk-xv91q4nm.js.map +58 -0
  85. package/dist/cli/chunk-y3g15rvv.js +679 -0
  86. package/dist/cli/chunk-y3g15rvv.js.map +15 -0
  87. package/dist/cli/chunk-ye9zdkgv.js +136 -0
  88. package/dist/cli/chunk-ye9zdkgv.js.map +10 -0
  89. package/dist/cli/chunk-ynacq3ev.js +1062 -0
  90. package/dist/cli/chunk-ynacq3ev.js.map +25 -0
  91. package/dist/cli/chunk-zr3ygrq3.js +54 -0
  92. package/dist/cli/chunk-zr3ygrq3.js.map +10 -0
  93. package/dist/cli/index.js +55 -27597
  94. package/dist/cli/index.js.map +5 -243
  95. package/dist/types/ai/ask-context.d.ts +26 -0
  96. package/dist/types/components/layout/nav-utils.d.ts +33 -1
  97. package/dist/types/core/code-fences.d.ts +11 -0
  98. package/dist/types/core/package-root.d.ts +1 -1
  99. package/dist/types/core/schema.d.ts +70 -0
  100. package/dist/types/theme/fonts.d.ts +22 -22
  101. package/docs/02-deployment.mdx +22 -1
  102. package/docs/configuration/ask-ai.mdx +1 -1
  103. package/docs/configuration/customization.mdx +2 -9
  104. package/docs/content/navigation.mdx +2 -0
  105. package/docs/content/syntax.mdx +1 -1
  106. package/docs/discoverability/open-graph.mdx +4 -0
  107. package/docs/reference/cli.mdx +1 -1
  108. package/package.json +4 -2
  109. package/src/ai/api/handlers.ts +4 -7
  110. package/src/ai/api/paths.ts +8 -0
  111. package/src/ai/api/spec.ts +2 -1
  112. package/src/ai/ask-context.ts +378 -22
  113. package/src/astro/generate.ts +161 -28
  114. package/src/astro/include-hmr.ts +10 -13
  115. package/src/astro/include-refresh.ts +0 -0
  116. package/src/astro/index.ts +6 -1
  117. package/src/astro/integration.ts +280 -53
  118. package/src/astro/module-types.ts +83 -0
  119. package/src/astro/templates.ts +256 -108
  120. package/src/audit/image-size.ts +10 -8
  121. package/src/cli/command-meta.ts +77 -0
  122. package/src/cli/commands/add.ts +2 -4
  123. package/src/cli/commands/audit.ts +2 -4
  124. package/src/cli/commands/build.ts +70 -346
  125. package/src/cli/commands/check.ts +2 -4
  126. package/src/cli/commands/dev.ts +31 -42
  127. package/src/cli/commands/doctor.ts +2 -4
  128. package/src/cli/commands/eject.ts +3 -41
  129. package/src/cli/commands/eval.ts +2 -5
  130. package/src/cli/commands/init.ts +2 -4
  131. package/src/cli/commands/mcp-stdio.ts +2 -5
  132. package/src/cli/commands/preview.ts +3 -5
  133. package/src/cli/commands/sync.ts +2 -4
  134. package/src/cli/commands/translate.ts +2 -5
  135. package/src/cli/commands/validate.ts +2 -4
  136. package/src/cli/commands/version.ts +2 -4
  137. package/src/cli/eject-scripts.ts +0 -45
  138. package/src/cli/host-args.ts +16 -0
  139. package/src/cli/index.ts +84 -35
  140. package/src/cli/lazy-command.ts +47 -0
  141. package/src/components/Icon.astro +24 -0
  142. package/src/components/content/GithubInfo.astro +4 -1
  143. package/src/components/icon-sprite-middleware.ts +41 -0
  144. package/src/components/icon-sprite.ts +93 -0
  145. package/src/components/layout/IconSprite.astro +11 -0
  146. package/src/components/layout/NavTree.astro +156 -188
  147. package/src/components/layout/NavTreeCache.astro +45 -0
  148. package/src/components/layout/NavTreeScript.astro +256 -0
  149. package/src/components/layout/PageActions.astro +11 -5
  150. package/src/components/layout/PageLayout.astro +21 -3
  151. package/src/components/layout/ReferenceLayout.astro +21 -4
  152. package/src/components/layout/RootLayout.astro +44 -6
  153. package/src/components/layout/nav-cache.ts +49 -0
  154. package/src/components/layout/nav-utils.ts +69 -1
  155. package/src/components/layout/page-locale.ts +29 -0
  156. package/src/core/api-name.ts +18 -0
  157. package/src/core/code-fences.ts +48 -0
  158. package/src/core/content-assets.ts +3 -7
  159. package/src/core/includes.ts +3 -7
  160. package/src/core/package-root.ts +1 -1
  161. package/src/core/schema.ts +19 -0
  162. package/src/core/sources/normalize.ts +2 -37
  163. package/src/core/sources/obsidian.ts +3 -2
  164. package/src/core/svg-dimensions.ts +97 -0
  165. package/src/core/version-cut.ts +2 -2
  166. package/src/deploy/artifacts.ts +370 -0
  167. package/src/deploy/cloudflare-negotiation.ts +97 -32
  168. package/src/deploy/function-bundle.ts +66 -20
  169. package/src/deploy/sitemap.ts +6 -0
  170. package/src/deploy/vercel-negotiation.ts +8 -30
  171. package/src/markdown/language-icon.ts +64 -20
  172. package/src/markdown/mermaid.ts +11 -0
  173. package/src/og/cache.ts +236 -0
  174. package/src/og/card.ts +18 -16
  175. package/src/og/index.ts +8 -1
  176. package/src/openapi/render-mdx.ts +9 -5
  177. package/src/registry/eject.ts +23 -10
  178. package/src/theme/entry.ts +41 -7
  179. package/src/theme/fonts.ts +30 -23
@@ -1,10 +1,18 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { fileURLToPath } from "node:url";
2
3
 
3
4
  import type { AstroIntegration } from "astro";
5
+ import { join, relative, resolve } from "pathe";
4
6
 
7
+ import { loadEnvFiles } from "../cli/env.ts";
5
8
  import { enrichDiagnostic } from "../core/diagnostics.ts";
9
+ import { scanProject } from "../core/project-graph.ts";
10
+ import type { BlumeProject } from "../core/project-graph.ts";
6
11
  import type { Diagnostic } from "../core/types.ts";
12
+ import { publishBuildArtifacts } from "../deploy/artifacts.ts";
13
+ import type { ArtifactLogger } from "../deploy/artifacts.ts";
7
14
  import { markdownVariantUrl, prefersMarkdown } from "./markdown-negotiation.ts";
15
+ import { runtimeModuleDeclarations } from "./module-types.ts";
8
16
 
9
17
  /** The `{ type: "error" }` payload Vite's browser overlay renders. */
10
18
  interface OverlayErrorPayload {
@@ -26,13 +34,150 @@ interface OverlayServer {
26
34
  ws?: OverlayChannel;
27
35
  }
28
36
 
29
- // Set on `astro:server:setup`; read by `showBlumeErrorOverlay` so the CLI's
30
- // regeneration can push Blume diagnostics into Vite's browser error overlay.
31
- // Same-process module singleton (dev and the integration share the instance).
32
- let overlayServer: OverlayServer | null = null;
37
+ /** The parameters Astro hands `astro:server:setup`. */
38
+ type ServerSetupParams = Parameters<
39
+ NonNullable<AstroIntegration["hooks"]["astro:server:setup"]>
40
+ >[0];
33
41
 
34
- const overlayChannel = (): OverlayChannel | undefined =>
35
- overlayServer?.ws ?? overlayServer?.hot;
42
+ /**
43
+ * Astro's `refreshContent`: re-runs every content-layer loader against the
44
+ * live store, the sanctioned way to re-sync content that changed outside
45
+ * Astro's own file watcher.
46
+ */
47
+ type RefreshContent = NonNullable<ServerSetupParams["refreshContent"]>;
48
+
49
+ /** What the dev negotiation middleware needs per request. */
50
+ interface DevNegotiation {
51
+ /** Page routes that have a raw-Markdown variant (the content manifest). */
52
+ contentRoutes: ReadonlySet<string>;
53
+ /** Homepage agent-discovery `Link` header, when the site has one. */
54
+ homeLinkHeader?: string;
55
+ }
56
+
57
+ /**
58
+ * The state shared between the CLI and the integration: the live dev server
59
+ * (for the browser error overlay), Astro's `refreshContent` (so the CLI's
60
+ * regeneration can re-sync the content store instead of restarting the
61
+ * server), the negotiation inputs the CLI publishes on every regeneration (so
62
+ * a content-route change never rewrites the generated config, which would
63
+ * restart the server in place), and the scanned project a `blume build`
64
+ * hands over so `astro:build:done` can write the deploy artifacts.
65
+ *
66
+ * Kept on `globalThis` rather than in module state, for the same reason as
67
+ * the runtime-module registry (see `runtime-modules.ts`): on a published
68
+ * install the CLI bundle (`dist/cli`) carries its own copy of this module,
69
+ * while the hooks run in the copy Vite loads from `blume/astro` for the
70
+ * generated config. A module-level variable is set in one copy and read in
71
+ * the other, so the overlay never showed anything outside this repository.
72
+ */
73
+ interface DevServerRegistry {
74
+ buildProject: BlumeProject | null;
75
+ negotiation: DevNegotiation | null;
76
+ overlay: OverlayServer | null;
77
+ refreshContent: RefreshContent | null;
78
+ }
79
+
80
+ const REGISTRY_KEY = Symbol.for("blume.integration");
81
+
82
+ type RegistryHost = typeof globalThis & {
83
+ [REGISTRY_KEY]?: DevServerRegistry;
84
+ };
85
+
86
+ const registry = (): DevServerRegistry => {
87
+ // SAFETY: the registry is stashed on globalThis under a well-known symbol so
88
+ // every copy of this module in the process shares it; the intersection only
89
+ // names that slot.
90
+ const host = globalThis as RegistryHost;
91
+ host[REGISTRY_KEY] ??= {
92
+ buildProject: null,
93
+ negotiation: null,
94
+ overlay: null,
95
+ refreshContent: null,
96
+ };
97
+ return host[REGISTRY_KEY];
98
+ };
99
+
100
+ /**
101
+ * Hand the scanned project to the integration ahead of `build()`, so its
102
+ * `astro:build:done` hook can write the deploy artifacts without scanning
103
+ * again. `blume build` publishes it for a real build and nothing for an
104
+ * isolated verify build, which produces no artifacts. `null` withdraws it.
105
+ */
106
+ export const publishBuildProject = (project: BlumeProject | null): void => {
107
+ registry().buildProject = project;
108
+ };
109
+
110
+ /**
111
+ * The project an `astro build` with no CLI in front of it (an ejected app)
112
+ * writes artifacts for: scanned from `buildArtifactsRoot`, resolved against
113
+ * the Astro root recorded on `astro:config:done`. Scan diagnostics surface as
114
+ * warnings — there is no `--strict` to honor here, and the build itself has
115
+ * already succeeded. `null` when nothing asked for a scan.
116
+ */
117
+ const scanForArtifacts = async (
118
+ astroRoot: URL | null,
119
+ artifactsRoot: string | undefined,
120
+ logger: ArtifactLogger
121
+ ): Promise<BlumeProject | null> => {
122
+ if (!(astroRoot && artifactsRoot)) {
123
+ return null;
124
+ }
125
+ const root = resolve(fileURLToPath(astroRoot), artifactsRoot);
126
+ // Remote sources read their tokens from the environment during the scan.
127
+ loadEnvFiles(root);
128
+ const project = await scanProject(root, { mode: "build" });
129
+ for (const diagnostic of project.diagnostics) {
130
+ logger.warn(`[${diagnostic.code}] ${diagnostic.message}`);
131
+ }
132
+ return project;
133
+ };
134
+
135
+ /**
136
+ * Publish the dev negotiation inputs for the running server: the content
137
+ * routes with a Markdown variant and the homepage `Link` header. Called by
138
+ * `generateRuntime` on every pass, so the middleware follows a route rename
139
+ * without the generated config changing. `null` withdraws a publication, so
140
+ * the middleware falls back to the options baked into the integration call
141
+ * (an ejected project has no CLI to publish).
142
+ */
143
+ export const publishDevNegotiation = (
144
+ negotiation: {
145
+ contentRoutes: readonly string[];
146
+ homeLinkHeader?: string;
147
+ } | null
148
+ ): void => {
149
+ registry().negotiation = negotiation
150
+ ? {
151
+ contentRoutes: new Set(negotiation.contentRoutes),
152
+ homeLinkHeader: negotiation.homeLinkHeader,
153
+ }
154
+ : null;
155
+ };
156
+
157
+ /**
158
+ * Re-run Astro's content-layer loaders against the live dev server. Returns
159
+ * `false` when no server has registered one — before the first
160
+ * `astro:server:setup`, or outside `blume dev` — so the caller can fall back.
161
+ * A route-set change (a page added, removed, or a folder renamed) is what
162
+ * needs this: Astro's glob watcher misses directory renames, and its in-place
163
+ * config restart never re-globs, so without a re-sync `getEntry` reads a
164
+ * stale store and the moved page 404s.
165
+ */
166
+ export const refreshBlumeContent = async (): Promise<boolean> => {
167
+ const { refreshContent } = registry();
168
+ if (!refreshContent) {
169
+ return false;
170
+ }
171
+ // No loader filter: every collection re-syncs (the docs glob and any
172
+ // staged collection alike).
173
+ await refreshContent({});
174
+ return true;
175
+ };
176
+
177
+ const overlayChannel = (): OverlayChannel | undefined => {
178
+ const { overlay } = registry();
179
+ return overlay?.ws ?? overlay?.hot;
180
+ };
36
181
 
37
182
  /**
38
183
  * Surface Blume's own diagnostics (config/frontmatter/content errors) in the
@@ -85,36 +230,41 @@ export interface BlumePageRoute {
85
230
 
86
231
  export interface BlumeIntegrationOptions {
87
232
  pages: BlumePageRoute[];
88
- /** Page routes that have a raw-Markdown variant (the content manifest). */
89
- contentRoutes: string[];
90
- /** Configured `deployment.base`, stripped from dev URLs before matching. */
91
- base?: string;
233
+ /**
234
+ * Page routes that have a raw-Markdown variant (the content manifest). The
235
+ * hidden runtime leaves this out: the CLI publishes the live set through
236
+ * `publishDevNegotiation` on every regeneration, so a route change never
237
+ * rewrites the generated config. An ejected project, which has no CLI,
238
+ * bakes it in here.
239
+ */
240
+ contentRoutes?: string[];
92
241
  /**
93
242
  * Homepage `Link` header value for agent discovery (see
94
243
  * `ai/link-headers.ts`); the dev-server counterpart of the `_headers` /
95
244
  * Vercel-config emission, so `curl -I` against `blume dev` shows what the
96
- * deployed site will send.
245
+ * deployed site will send. Published the same way as `contentRoutes`.
97
246
  */
98
247
  homeLinkHeader?: string;
248
+ /**
249
+ * Blume project root to scan on `astro:build:done` for the deploy artifacts
250
+ * (search index, llms.txt, sitemap, …) when no CLI has published the
251
+ * project — an ejected app running plain `astro build`. Relative to the
252
+ * Astro root. The hidden runtime leaves it unset: `blume build` publishes
253
+ * its already-scanned project instead.
254
+ */
255
+ buildArtifactsRoot?: string;
99
256
  }
100
257
 
101
258
  /**
102
259
  * Whether a dev-server request URL is the homepage: the path (query dropped,
103
- * `deployment.base` stripped, trailing slash tolerated) is the root.
260
+ * trailing slash tolerated) is the root.
104
261
  */
105
- const isHomeUrl = (rawUrl: string | undefined, base?: string): boolean => {
262
+ const isHomeUrl = (rawUrl: string | undefined): boolean => {
106
263
  if (!rawUrl) {
107
264
  return false;
108
265
  }
109
266
  const queryIndex = rawUrl.indexOf("?");
110
- let path = queryIndex === -1 ? rawUrl : rawUrl.slice(0, queryIndex);
111
- const prefix = base && base !== "/" ? base.replace(/\/$/u, "") : "";
112
- if (prefix) {
113
- if (path !== prefix && !path.startsWith(`${prefix}/`)) {
114
- return false;
115
- }
116
- path = path.slice(prefix.length);
117
- }
267
+ const path = queryIndex === -1 ? rawUrl : rawUrl.slice(0, queryIndex);
118
268
  return path === "" || path === "/";
119
269
  };
120
270
 
@@ -134,16 +284,26 @@ const isHomeUrl = (rawUrl: string | undefined, base?: string): boolean => {
134
284
  * page (see `markdownRoutePaths`). The same
135
285
  * middleware also stamps the homepage agent-discovery `Link` header, mirroring
136
286
  * what the deployed site sends via `_headers` / the Vercel routing config.
287
+ *
288
+ * Request URLs arrive base-less: Astro unshifts its own dev middlewares (base,
289
+ * trailing slash, route guard) ahead of this one from its post-`configureServer`
290
+ * hook, and its base middleware has already rewritten `/<base>/guide` to
291
+ * `/guide`. Stripping `deployment.base` here a second time would leave no
292
+ * request matching a content route.
137
293
  */
138
294
  const negotiateMarkdown =
139
- (routes: ReadonlySet<string>, base?: string, homeLinkHeader?: string) =>
295
+ (fallback: DevNegotiation) =>
140
296
  (req: IncomingMessage, res: ServerResponse, next: () => void): void => {
297
+ // Read per request: the CLI republishes on every regeneration, so a page
298
+ // renamed while the server runs negotiates under its new route.
299
+ const { contentRoutes, homeLinkHeader } =
300
+ registry().negotiation ?? fallback;
141
301
  if (req.method === "GET" || req.method === "HEAD") {
142
- if (homeLinkHeader && isHomeUrl(req.url, base)) {
302
+ if (homeLinkHeader && isHomeUrl(req.url)) {
143
303
  res.setHeader("Link", homeLinkHeader);
144
304
  }
145
305
  if (prefersMarkdown(req.headers.accept)) {
146
- const variant = markdownVariantUrl(req.url, routes, base);
306
+ const variant = markdownVariantUrl(req.url, contentRoutes);
147
307
  if (variant) {
148
308
  res.setHeader("Vary", "Accept");
149
309
  req.url = variant;
@@ -153,40 +313,107 @@ const negotiateMarkdown =
153
313
  next();
154
314
  };
155
315
 
316
+ /** The `.d.ts` the integration injects for the `blume:*` virtual modules. */
317
+ const MODULE_TYPES_FILE = "modules.d.ts";
318
+
319
+ /**
320
+ * Where Astro writes an integration's injected types when the integration
321
+ * never asked for its codegen dir (a config run whose `astro:config:setup`
322
+ * was skipped — the test fixtures). Mirrors Astro's own convention.
323
+ */
324
+ const defaultCodegenDir = (root: URL): URL =>
325
+ new URL(".astro/integrations/blume/", root);
326
+
156
327
  /**
157
328
  * Blume's Astro integration. Mounts user-authored pages from `pages/` into the
158
329
  * generated runtime via `injectRoute`, keeping each file in its original
159
- * location so relative imports and `getStaticPaths` keep working, and teaches
160
- * the dev server to honor `Accept: text/markdown`.
330
+ * location so relative imports and `getStaticPaths` keep working; declares
331
+ * the `blume:*` virtual modules' types through `injectTypes`; and teaches the
332
+ * dev server to honor `Accept: text/markdown`.
161
333
  */
162
334
  export const blumeIntegration = (
163
335
  options: BlumeIntegrationOptions
164
- ): AstroIntegration => ({
165
- hooks: {
166
- "astro:config:setup": ({ injectRoute }) => {
167
- for (const page of options.pages) {
168
- injectRoute({
169
- entrypoint: page.entrypoint,
170
- pattern: page.pattern,
171
- prerender: true,
336
+ ): AstroIntegration => {
337
+ // Astro hands out the codegen dir on `astro:config:setup`; the types are
338
+ // injected on `astro:config:done`, once `srcDir` is final, and the
339
+ // `blume:examples` declaration needs the path between the two.
340
+ let codegenDir: URL | null = null;
341
+ // The Astro root, kept for the build-artifacts scan: `astro:build:done`
342
+ // receives no config.
343
+ let astroRoot: URL | null = null;
344
+ return {
345
+ hooks: {
346
+ "astro:build:done": async ({ dir, logger }) => {
347
+ const project =
348
+ registry().buildProject ??
349
+ (await scanForArtifacts(
350
+ astroRoot,
351
+ options.buildArtifactsRoot,
352
+ logger
353
+ ));
354
+ if (!project) {
355
+ return;
356
+ }
357
+ // `dir` is what Astro reports as the client output — `dist/`, or
358
+ // `dist/client` for a server build — which is what the platform
359
+ // serves (the Vercel adapter copies it into its Build Output static
360
+ // tree in a later hook).
361
+ await publishBuildArtifacts(project, fileURLToPath(dir), logger);
362
+ },
363
+ "astro:config:done": ({ config, injectTypes }) => {
364
+ astroRoot = config.root;
365
+ const from = fileURLToPath(
366
+ codegenDir ?? defaultCodegenDir(config.root)
367
+ );
368
+ const examplesModule = relative(
369
+ from,
370
+ join(fileURLToPath(config.srcDir), "generated", "examples.ts")
371
+ );
372
+ injectTypes({
373
+ content: runtimeModuleDeclarations(examplesModule),
374
+ filename: MODULE_TYPES_FILE,
172
375
  });
173
- }
174
- },
175
- "astro:server:setup": ({ server }) => {
176
- // Keep a handle on the dev server so Blume diagnostics can be pushed to
177
- // its browser error overlay (see `showBlumeErrorOverlay`).
178
- overlayServer = server;
179
- // Prepend so the rewrite happens before Astro's own request handler,
180
- // letting the rewritten URL resolve to the `.md` endpoint.
181
- server.middlewares.stack.unshift({
182
- handle: negotiateMarkdown(
183
- new Set(options.contentRoutes),
184
- options.base,
185
- options.homeLinkHeader
186
- ),
187
- route: "",
188
- });
376
+ },
377
+ "astro:config:setup": ({
378
+ addMiddleware,
379
+ createCodegenDir,
380
+ injectRoute,
381
+ }) => {
382
+ codegenDir = createCodegenDir();
383
+ // Splices each page's icon sprite in once the page has rendered (see
384
+ // components/icon-sprite-middleware.ts). Innermost, so a project's
385
+ // own middleware sees the finished HTML.
386
+ addMiddleware({
387
+ entrypoint: "blume/components/icon-sprite-middleware.ts",
388
+ order: "post",
389
+ });
390
+ for (const page of options.pages) {
391
+ injectRoute({
392
+ entrypoint: page.entrypoint,
393
+ pattern: page.pattern,
394
+ prerender: true,
395
+ });
396
+ }
397
+ },
398
+ "astro:server:setup": ({ refreshContent, server }) => {
399
+ // Keep a handle on the dev server so Blume diagnostics can be pushed
400
+ // to its browser error overlay (see `showBlumeErrorOverlay`), and on
401
+ // Astro's content re-sync so a route-set change needs no restart
402
+ // (see `refreshBlumeContent`).
403
+ const shared = registry();
404
+ shared.overlay = server;
405
+ shared.refreshContent = refreshContent ?? null;
406
+ // Prepend so the rewrite happens before Astro's own request handler,
407
+ // letting the rewritten URL resolve to the `.md` endpoint.
408
+ server.middlewares.stack.unshift({
409
+ handle: negotiateMarkdown({
410
+ contentRoutes: new Set(options.contentRoutes),
411
+ homeLinkHeader: options.homeLinkHeader,
412
+ }),
413
+ route: "",
414
+ });
415
+ },
189
416
  },
190
- },
191
- name: "blume",
192
- });
417
+ name: "blume",
418
+ };
419
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Ambient declarations for the `blume:*` virtual modules the generated pages
3
+ * and Blume's own layouts import. The integration hands them to Astro through
4
+ * `injectTypes` on `astro:config:done`, so they land under
5
+ * `.astro/integrations/blume/` and are referenced from Astro's generated
6
+ * `types.d.ts` — the same channel `astro:content` and `astro:env` use — rather
7
+ * than a hand-written `src/env.d.ts` the generator and eject each had to
8
+ * write. Every project that mounts the integration gets the types with it.
9
+ *
10
+ * `examplesModule` is the path of the generated `examples.ts` relative to the
11
+ * declaration file: the `blume:examples` map is typed from that module's
12
+ * literal export so `<Component path>` lookups stay typed per project.
13
+ */
14
+ export const runtimeModuleDeclarations = (
15
+ examplesModule: string
16
+ ): string => `declare module "blume:ask" {
17
+ const Ask: typeof import("blume/components/islands/AskAI.astro").default;
18
+ export default Ask;
19
+ }
20
+
21
+ declare module "blume:data" {
22
+ const data: import("blume").BlumeData;
23
+ export default data;
24
+ }
25
+
26
+ declare module "blume:ask-data" {
27
+ const askData: import("blume/ai/ask-context.ts").AskData;
28
+ export default askData;
29
+ }
30
+
31
+ declare module "blume:content-assets" {
32
+ const assets: Record<string, string>;
33
+ export default assets;
34
+ }
35
+
36
+ declare module "blume:mcp-data" {
37
+ const data: import("blume/ai/mcp/data.ts").McpData;
38
+ export default data;
39
+ }
40
+
41
+ declare module "blume:raw-markdown" {
42
+ const raw: Record<string, import("blume/ai/markdown.ts").RawMarkdownEntry>;
43
+ export default raw;
44
+ }
45
+
46
+ declare module "blume:rss" {
47
+ const feeds: Record<string, string>;
48
+ export default feeds;
49
+ }
50
+
51
+ declare module "blume:search-index" {
52
+ const documents: import("blume/search/documents.ts").SearchDocument[];
53
+ export default documents;
54
+ }
55
+
56
+ declare module "blume:examples" {
57
+ type Examples = typeof import(${JSON.stringify(examplesModule)}).examples;
58
+ export const examples: Record<string, Examples[keyof Examples]>;
59
+ export const examplesBase: string;
60
+ }
61
+
62
+ declare module "blume:examples-theme";
63
+
64
+ declare module "blume:openapi" {
65
+ const specs: import("blume/openapi/model.ts").OpenApiData;
66
+ export default specs;
67
+ }
68
+
69
+ declare module "blume:features" {
70
+ /** Registers the <blume-mermaid> element; null when no page has a mermaid fence. */
71
+ export const loadMermaid: (() => Promise<unknown>) | null;
72
+ /** The EPUB generator's browser bundle; null when export.epub is off. */
73
+ export const loadEpub:
74
+ | (() => Promise<typeof import("epub-gen-memory/bundle")>)
75
+ | null;
76
+ }
77
+
78
+ declare module "blume:search-client" {
79
+ export const createSearch: () =>
80
+ | import("blume/components/layout/search/types.ts").SearchFn
81
+ | Promise<import("blume/components/layout/search/types.ts").SearchFn>;
82
+ }
83
+ `;