blume 1.4.3 → 1.5.1

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 (204) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +16 -12
  3. package/dist/cli/index.js +1784 -633
  4. package/dist/cli/index.js.map +111 -106
  5. package/dist/types/ai/component-markdown.d.ts +14 -4
  6. package/dist/types/core/config-input.d.ts +80 -28
  7. package/dist/types/core/config.d.ts +2 -1
  8. package/dist/types/core/data.d.ts +19 -3
  9. package/dist/types/core/diagnostics.d.ts +5 -1
  10. package/dist/types/core/i18n-ui.d.ts +12 -0
  11. package/dist/types/core/schema.d.ts +112 -15
  12. package/dist/types/core/sources/types.d.ts +3 -1
  13. package/dist/types/core/standard-schema.d.ts +7 -3
  14. package/dist/types/core/types.d.ts +43 -2
  15. package/dist/types/core/ui-packs/index.d.ts +9 -1
  16. package/dist/types/openapi/references.d.ts +6 -5
  17. package/dist/types/seo/x-handle.d.ts +3 -2
  18. package/dist/types/theme/fonts.d.ts +11 -2
  19. package/docs/advanced/api-reference.mdx +8 -6
  20. package/docs/advanced/custom-pages.mdx +5 -1
  21. package/docs/configuration/index.mdx +1 -1
  22. package/docs/configuration/search.mdx +2 -0
  23. package/docs/configuration/seo.mdx +1 -1
  24. package/docs/configuration/theming.mdx +4 -2
  25. package/docs/content/i18n.mdx +1 -1
  26. package/docs/content/meta.mdx +2 -1
  27. package/docs/content/meta.ts +1 -0
  28. package/docs/content/navigation.mdx +35 -1
  29. package/docs/content/versioning.mdx +106 -0
  30. package/docs/reference/cli.mdx +2 -1
  31. package/docs/reference/frontmatter.mdx +3 -0
  32. package/package.json +3 -1
  33. package/skills/blume-migrate/SKILL.md +2 -2
  34. package/skills/blume-migrate/references/docusaurus.md +1 -1
  35. package/skills/blume-migrate/references/fumadocs.md +1 -1
  36. package/skills/blume-migrate/references/mintlify.md +1 -1
  37. package/src/ai/agent-readability.ts +37 -10
  38. package/src/ai/ask-context.ts +5 -1
  39. package/src/ai/ask.ts +10 -1
  40. package/src/ai/component-markdown.ts +80 -43
  41. package/src/ai/llms.ts +40 -16
  42. package/src/ai/mcp/data.ts +48 -12
  43. package/src/ai/mcp/discovery.ts +28 -11
  44. package/src/ai/mcp/server.ts +183 -38
  45. package/src/ai/mcp/tools.ts +3 -3
  46. package/src/ai/skills.ts +32 -9
  47. package/src/ai/visibility.ts +2 -2
  48. package/src/astro/component-slots.ts +2 -0
  49. package/src/astro/examples.ts +6 -2
  50. package/src/astro/generate.ts +64 -34
  51. package/src/astro/integration.ts +13 -2
  52. package/src/astro/islands.ts +16 -9
  53. package/src/astro/templates.ts +181 -40
  54. package/src/audit/agent.ts +2 -2
  55. package/src/audit/checks/content.ts +26 -11
  56. package/src/audit/checks/dns-aid.ts +3 -0
  57. package/src/audit/checks/indexability.ts +24 -6
  58. package/src/audit/checks/llms.ts +9 -4
  59. package/src/audit/checks/network.ts +2 -0
  60. package/src/audit/checks/social.ts +18 -10
  61. package/src/audit/crawl.ts +37 -9
  62. package/src/audit/report.ts +20 -19
  63. package/src/audit/run.ts +5 -2
  64. package/src/audit/snapshot.ts +2 -4
  65. package/src/audit/types.ts +25 -3
  66. package/src/blume-modules.d.ts +5 -1
  67. package/src/cli/commands/audit.ts +9 -4
  68. package/src/cli/commands/build.ts +15 -9
  69. package/src/cli/commands/dev.ts +2 -0
  70. package/src/cli/commands/doctor.ts +2 -0
  71. package/src/cli/commands/eval.ts +7 -3
  72. package/src/cli/commands/init.ts +9 -9
  73. package/src/cli/commands/mcp-stdio.ts +3 -0
  74. package/src/cli/commands/translate.ts +14 -3
  75. package/src/cli/commands/version.ts +85 -0
  76. package/src/cli/dev-lock.ts +31 -10
  77. package/src/cli/eject-scripts.ts +17 -2
  78. package/src/cli/index.ts +2 -0
  79. package/src/cli/init/questions.ts +1 -1
  80. package/src/cli/init/scaffold.ts +22 -15
  81. package/src/cli/internal-error.ts +1 -0
  82. package/src/components/content/auto-type-table.ts +3 -0
  83. package/src/components/content/diff.ts +9 -5
  84. package/src/components/content/github-info.ts +2 -0
  85. package/src/components/islands/ask-ai.tsx +33 -25
  86. package/src/components/islands/hooks.ts +5 -1
  87. package/src/components/islands/webmcp.ts +49 -12
  88. package/src/components/layout/Fonts.astro +23 -3
  89. package/src/components/layout/Header.astro +25 -1
  90. package/src/components/layout/NavSelector.astro +11 -2
  91. package/src/components/layout/NavTree.astro +4 -2
  92. package/src/components/layout/PageLayout.astro +72 -3
  93. package/src/components/layout/ReferenceLayout.astro +2 -1
  94. package/src/components/layout/RootLayout.astro +20 -1
  95. package/src/components/layout/Search.astro +77 -13
  96. package/src/components/layout/VersionBanner.astro +39 -0
  97. package/src/components/layout/analytics-client.ts +8 -5
  98. package/src/components/layout/hydration-hint.ts +1 -1
  99. package/src/components/layout/nav-utils.ts +1 -4
  100. package/src/components/layout/overrides.ts +25 -12
  101. package/src/components/layout/search/algolia.ts +18 -5
  102. package/src/components/layout/search/endpoint.ts +3 -0
  103. package/src/components/layout/search/flexsearch.ts +23 -7
  104. package/src/components/layout/search/orama-cloud.ts +1 -1
  105. package/src/components/layout/search/orama.ts +4 -1
  106. package/src/components/layout/search/pagefind.ts +2 -0
  107. package/src/components/layout/search/types.ts +13 -1
  108. package/src/components/layout/search/typesense.ts +19 -3
  109. package/src/components/openapi/ApiOverview.astro +32 -6
  110. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  111. package/src/components/openapi/Bindings.astro +89 -0
  112. package/src/components/openapi/MethodBadge.astro +3 -0
  113. package/src/components/openapi/Operation.astro +7 -2
  114. package/src/components/openapi/PanelTabs.astro +131 -0
  115. package/src/components/openapi/ParametersTable.astro +2 -0
  116. package/src/components/openapi/RequestPanel.astro +12 -119
  117. package/src/components/openapi/async-snippets.ts +174 -0
  118. package/src/components/openapi/async.ts +348 -0
  119. package/src/components/openapi/helpers.ts +52 -20
  120. package/src/components/openapi/security.ts +102 -29
  121. package/src/components/openapi/snippets.ts +11 -11
  122. package/src/core/component-overrides.ts +28 -23
  123. package/src/core/config-input.ts +89 -28
  124. package/src/core/config.ts +20 -7
  125. package/src/core/content.ts +3 -1
  126. package/src/core/data.ts +19 -3
  127. package/src/core/define-components.ts +5 -0
  128. package/src/core/diagnostics.ts +46 -38
  129. package/src/core/frontmatter.ts +33 -7
  130. package/src/core/graph.ts +137 -53
  131. package/src/core/i18n-ui.ts +15 -0
  132. package/src/core/i18n.ts +16 -8
  133. package/src/core/last-modified.ts +49 -0
  134. package/src/core/load-module.ts +1 -0
  135. package/src/core/manifest.ts +92 -3
  136. package/src/core/meta.ts +44 -14
  137. package/src/core/nav-diagnostics.ts +3 -3
  138. package/src/core/navigation.ts +247 -67
  139. package/src/core/project-graph.ts +26 -3
  140. package/src/core/schema.ts +214 -68
  141. package/src/core/sources/assets.ts +2 -0
  142. package/src/core/sources/cache.ts +6 -0
  143. package/src/core/sources/github-releases.ts +39 -31
  144. package/src/core/sources/mdx-remote.ts +4 -0
  145. package/src/core/sources/normalize.ts +67 -20
  146. package/src/core/sources/notion.ts +49 -17
  147. package/src/core/sources/portable-text.ts +32 -11
  148. package/src/core/sources/sanity.ts +68 -14
  149. package/src/core/sources/types.ts +4 -0
  150. package/src/core/sources/watch.ts +1 -1
  151. package/src/core/standard-schema.ts +9 -3
  152. package/src/core/text-width.ts +26 -0
  153. package/src/core/tsconfig-aliases.ts +9 -5
  154. package/src/core/types.ts +45 -2
  155. package/src/core/ui-packs/index.ts +9 -1
  156. package/src/core/version-cut.ts +301 -0
  157. package/src/core/version.ts +2 -0
  158. package/src/core/versions.ts +170 -0
  159. package/src/deploy/adapter-output.ts +5 -2
  160. package/src/deploy/cloudflare-negotiation.ts +25 -10
  161. package/src/deploy/sitemap.ts +33 -1
  162. package/src/deploy/vercel-negotiation.ts +45 -18
  163. package/src/eval/report.ts +4 -4
  164. package/src/eval/run.ts +2 -2
  165. package/src/eval/schema.ts +1 -1
  166. package/src/markdown/base-links.ts +6 -6
  167. package/src/markdown/directives.ts +7 -1
  168. package/src/markdown/heading-anchors.ts +17 -6
  169. package/src/markdown/index.ts +73 -24
  170. package/src/markdown/inline-code.ts +14 -2
  171. package/src/markdown/language-icon.ts +6 -2
  172. package/src/markdown/mdast.ts +18 -4
  173. package/src/markdown/package-commands.ts +6 -8
  174. package/src/markdown/table-wrap.ts +4 -1
  175. package/src/markdown/twoslash.ts +2 -0
  176. package/src/og/card.ts +33 -12
  177. package/src/og/derive.ts +43 -27
  178. package/src/openapi/asyncapi.ts +366 -0
  179. package/src/openapi/model.ts +126 -57
  180. package/src/openapi/parse.ts +97 -5
  181. package/src/openapi/references.ts +12 -10
  182. package/src/openapi/render-mdx.ts +73 -34
  183. package/src/openapi/scalar.ts +6 -8
  184. package/src/openapi/source.ts +98 -28
  185. package/src/registry/eject.ts +7 -2
  186. package/src/search/documents.ts +25 -5
  187. package/src/search/facets.ts +7 -5
  188. package/src/search/orama-index.ts +66 -20
  189. package/src/search/popular.ts +10 -5
  190. package/src/search/providers.ts +2 -2
  191. package/src/search/sync/index.ts +2 -0
  192. package/src/search/sync/typesense.ts +4 -2
  193. package/src/seo/jsonld.ts +24 -6
  194. package/src/seo/x-handle.ts +8 -3
  195. package/src/theme/chrome-icons.ts +7 -2
  196. package/src/theme/entry.ts +24 -2
  197. package/src/theme/fonts.ts +83 -7
  198. package/src/theme/icons.ts +4 -2
  199. package/src/theme/palette.ts +22 -14
  200. package/src/translate/meta.ts +15 -6
  201. package/src/translate/report.ts +9 -5
  202. package/src/translate/run.ts +10 -4
  203. package/src/translate/validate.ts +52 -17
  204. package/src/translate/work-list.ts +0 -0
@@ -61,7 +61,7 @@ import {
61
61
  examplesEntryTemplate,
62
62
  tailwindEntryTemplate,
63
63
  } from "../theme/entry.ts";
64
- import { buildFontsCss, configuredCssVars } from "../theme/fonts.ts";
64
+ import { buildFontsCss, configuredFonts } from "../theme/fonts.ts";
65
65
  import { buildThemeCss } from "../theme/palette.ts";
66
66
  import { twoslashCss } from "../theme/twoslash.ts";
67
67
  import { planComponentSlots } from "./component-slots.ts";
@@ -489,12 +489,14 @@ export const ensureDepsLink = async (
489
489
  * (`<build.server>/.prerender/`) output — so it fires for exactly that build.
490
490
  * Inert in dev, where there is no build and `writeBundle` never runs.
491
491
  */
492
- export const prerenderDepsPlugin = (
493
- pkgDir: string = packageRoot()
494
- ): {
492
+ export interface PrerenderDepsPlugin {
495
493
  name: string;
496
494
  writeBundle: (options: { dir?: string }) => Promise<void>;
497
- } => ({
495
+ }
496
+
497
+ export const prerenderDepsPlugin = (
498
+ pkgDir: string = packageRoot()
499
+ ): PrerenderDepsPlugin => ({
498
500
  name: "blume:prerender-deps",
499
501
  writeBundle: async (options) => {
500
502
  if (!options.dir || basename(options.dir) !== ".prerender") {
@@ -528,14 +530,16 @@ interface ServerAppResolveContext {
528
530
  * Stripping the spurious `.js` and delegating back to Astro's resolver lets the
529
531
  * reload complete cleanly, so the renamed route resolves without a restart.
530
532
  */
531
- export const serverAppResolvePlugin = (): {
533
+ export interface ServerAppResolvePlugin {
532
534
  enforce: "pre";
533
535
  name: string;
534
536
  resolveId: (
535
537
  this: ServerAppResolveContext,
536
538
  id: string
537
539
  ) => Promise<string | null>;
538
- } => ({
540
+ }
541
+
542
+ export const serverAppResolvePlugin = (): ServerAppResolvePlugin => ({
539
543
  enforce: "pre",
540
544
  name: "blume:server-app-resolve",
541
545
  async resolveId(id) {
@@ -548,20 +552,20 @@ export const serverAppResolvePlugin = (): {
548
552
  });
549
553
 
550
554
  /** Astro integration package each non-React island framework needs installed. */
551
- const ISLAND_FRAMEWORK_DEPS: Record<string, string> = {
552
- svelte: "@astrojs/svelte",
553
- vue: "@astrojs/vue",
554
- };
555
+ const ISLAND_FRAMEWORK_DEPS = new Map([
556
+ ["svelte", "@astrojs/svelte"],
557
+ ["vue", "@astrojs/vue"],
558
+ ]);
555
559
 
556
560
  /**
557
561
  * Adapter package the project must install itself for each deployment
558
562
  * platform whose adapter Blume doesn't ship. Node and Vercel ship with Blume,
559
563
  * so they never need this.
560
564
  */
561
- const DEPLOYMENT_ADAPTER_DEPS: Record<string, string> = {
562
- cloudflare: "@astrojs/cloudflare",
563
- netlify: "@astrojs/netlify",
564
- };
565
+ const DEPLOYMENT_ADAPTER_DEPS = new Map([
566
+ ["cloudflare", "@astrojs/cloudflare"],
567
+ ["netlify", "@astrojs/netlify"],
568
+ ]);
565
569
 
566
570
  /**
567
571
  * Warn when a Vue/Svelte island is present but its Astro integration isn't
@@ -574,7 +578,7 @@ const islandFrameworkWarnings = (
574
578
  ): string[] => {
575
579
  const warnings: string[] = [];
576
580
  for (const framework of frameworks) {
577
- const dep = ISLAND_FRAMEWORK_DEPS[framework];
581
+ const dep = ISLAND_FRAMEWORK_DEPS.get(framework);
578
582
  if (dep && !canResolveFrom(root, dep)) {
579
583
  warnings.push(
580
584
  `Islands use ${framework}, which needs "${dep}". Install it (e.g. \`npm install ${dep} ${framework}\`).`
@@ -599,7 +603,7 @@ const deploymentAdapterWarnings = (
599
603
  ): string[] => {
600
604
  const dep =
601
605
  deployment.output === "server" && deployment.adapter
602
- ? DEPLOYMENT_ADAPTER_DEPS[deployment.adapter]
606
+ ? DEPLOYMENT_ADAPTER_DEPS.get(deployment.adapter)
603
607
  : undefined;
604
608
  if (
605
609
  dep &&
@@ -906,6 +910,10 @@ const readLogoSvg = (
906
910
  return file ? readFileSync(file, "utf-8") : undefined;
907
911
  };
908
912
 
913
+ /** Narrows a config union's string shorthand from its object form. */
914
+ const isStringShorthand = <T>(value: T | string): value is string =>
915
+ typeof value === "string";
916
+
909
917
  /**
910
918
  * Resolve the configured logo. A single SVG is read and inlined so a
911
919
  * `currentColor` logo follows the theme; other images keep their URL for an
@@ -916,12 +924,12 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
916
924
  if (!logo) {
917
925
  return null;
918
926
  }
919
- const config = typeof logo === "string" ? { image: logo } : logo;
927
+ const config = isStringShorthand(logo) ? { image: logo } : logo;
920
928
  // `text` is passed through verbatim: `undefined` lets the brand fall back to
921
929
  // the site title, `""` renders the mark alone (a logo with the wordmark baked
922
930
  // in).
923
931
  const { href, image: source, text } = config;
924
- const image = typeof source === "string" ? { light: source } : source;
932
+ const image = isStringShorthand(source) ? { light: source } : source;
925
933
  const light = image?.light ?? image?.dark;
926
934
  const dark = image?.dark ?? image?.light;
927
935
  const alt = image?.alt ?? "";
@@ -957,18 +965,18 @@ const FAVICON_CANDIDATES = [
957
965
  ];
958
966
 
959
967
  /** `<link type>` MIME for the favicon extensions we recognize. */
960
- const FAVICON_TYPES: Record<string, string> = {
961
- ico: "image/x-icon",
962
- jpeg: "image/jpeg",
963
- jpg: "image/jpeg",
964
- png: "image/png",
965
- svg: "image/svg+xml",
966
- };
968
+ const FAVICON_TYPES = new Map([
969
+ ["ico", "image/x-icon"],
970
+ ["jpeg", "image/jpeg"],
971
+ ["jpg", "image/jpeg"],
972
+ ["png", "image/png"],
973
+ ["svg", "image/svg+xml"],
974
+ ]);
967
975
 
968
976
  /** Infer the `<link type>` MIME from a filename, when we recognize the extension. */
969
977
  const faviconType = (name: string): string | undefined => {
970
978
  const ext = name.split(".").pop()?.toLowerCase();
971
- return ext ? FAVICON_TYPES[ext] : undefined;
979
+ return ext ? FAVICON_TYPES.get(ext) : undefined;
972
980
  };
973
981
 
974
982
  /** Read a file and encode it as a `data:` URI of the given MIME type. */
@@ -1041,7 +1049,7 @@ const resolveBanner = (config: ResolvedConfig): BlumeBanner | null => {
1041
1049
  if (!banner) {
1042
1050
  return null;
1043
1051
  }
1044
- if (typeof banner === "string") {
1052
+ if (isStringShorthand(banner)) {
1045
1053
  return { content: banner, dismissible: false, key: banner };
1046
1054
  }
1047
1055
  return {
@@ -1234,6 +1242,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
1234
1242
  theme: config.theme,
1235
1243
  title: config.title,
1236
1244
  toc: config.toc,
1245
+ versions: config.versions ?? null,
1237
1246
  webmcp: {
1238
1247
  enabled: config.ai.webmcp,
1239
1248
  llms: config.ai.llmsTxt.enabled,
@@ -1246,10 +1255,23 @@ export const buildRuntimeData = (project: BlumeProject): string => {
1246
1255
  })),
1247
1256
  // CSS variables for Astro's <Font> component; matches the astro.config
1248
1257
  // `fonts:` entries derived from the same theme.fonts config.
1249
- fontCssVars: configuredCssVars(config.theme.fonts),
1258
+ fontCssVars: configuredFonts(config.theme.fonts),
1250
1259
  navigation: withRepoUrl(graph.navigation),
1251
1260
  // Per-locale navigation; the catch-all selects the active locale's tree.
1252
1261
  navigationByLocale,
1262
+ // Per-archived-version navigation; the catch-all selects by the route's
1263
+ // version, then locale.
1264
+ navigationByVersion: Object.fromEntries(
1265
+ Object.entries(graph.navigationByVersion).map(([id, byLocale]) => [
1266
+ id,
1267
+ Object.fromEntries(
1268
+ Object.entries(byLocale).map(([code, nav]) => [
1269
+ code,
1270
+ withRepoUrl(nav),
1271
+ ])
1272
+ ),
1273
+ ])
1274
+ ),
1253
1275
  routes: manifest.routes.map((route) => ({
1254
1276
  alternates: route.alternates,
1255
1277
  collection: route.collection,
@@ -1264,6 +1286,8 @@ export const buildRuntimeData = (project: BlumeProject): string => {
1264
1286
  locale: route.locale,
1265
1287
  path: route.path,
1266
1288
  title: route.title,
1289
+ version: route.version,
1290
+ versionAlternates: route.versionAlternates,
1267
1291
  })),
1268
1292
  // Default-locale chrome strings (English baseline when not under i18n).
1269
1293
  ui: defaultUi,
@@ -1600,6 +1624,11 @@ export const generateRuntime = async (
1600
1624
  // private and filtered out anyway, but the intent is the user's pages.
1601
1625
  const ogRoutes = customOgRoutes(pages, config.title, config.seo.og.titles);
1602
1626
 
1627
+ // Whether the generated `/changelog` index exists — shared by the OG endpoint
1628
+ // (which adds the index's own card) and the page write below. Computed here,
1629
+ // before the MCP discovery pages are appended, on the user's own pages.
1630
+ const changelogIndex = hasGeneratedChangelog(project, pages);
1631
+
1603
1632
  // The hosted MCP server. The `.well-known` discovery docs are injected as
1604
1633
  // prerendered routes alongside user pages; the server endpoint itself is a
1605
1634
  // normal (server-rendered) page written by `writeMcpFiles`.
@@ -1756,12 +1785,12 @@ export const generateRuntime = async (
1756
1785
  if (config.seo.og.enabled) {
1757
1786
  await write(
1758
1787
  join(srcDir, "pages", "og", "[...slug].png.ts"),
1759
- ogEndpointTemplate(ogRoutes, projectOgFonts(project))
1788
+ ogEndpointTemplate(ogRoutes, projectOgFonts(project), changelogIndex)
1760
1789
  );
1761
1790
  }
1762
1791
 
1763
1792
  // Changelog index (`/changelog`), rendered through the Update timeline layout.
1764
- if (hasGeneratedChangelog(project, pages)) {
1793
+ if (changelogIndex) {
1765
1794
  await write(
1766
1795
  join(srcDir, "pages", "changelog.astro"),
1767
1796
  changelogIndexTemplate({
@@ -1861,8 +1890,9 @@ export const generateRuntime = async (
1861
1890
  ]);
1862
1891
  }
1863
1892
 
1864
- // API/AsyncAPI reference pages (Scalar). One self-contained page per source,
1865
- // mounted on its configured route and regenerated each run.
1893
+ // Scalar-rendered API/AsyncAPI reference pages (`renderer: "scalar"`). One
1894
+ // self-contained page per source, mounted on its configured route and
1895
+ // regenerated each run.
1866
1896
  const warnings: string[] = [
1867
1897
  ...(depsLinkWarning ? [depsLinkWarning] : []),
1868
1898
  ...reactCompilerWarnings(config, needsReact, reactCompilerPath),
@@ -1881,7 +1911,7 @@ export const generateRuntime = async (
1881
1911
  ...pages.map((page) => page.pattern),
1882
1912
  ...referenceRoutes(config),
1883
1913
  ]);
1884
- if (hasGeneratedChangelog(project, pages)) {
1914
+ if (changelogIndex) {
1885
1915
  navTargetRoutes.add("/changelog");
1886
1916
  }
1887
1917
  // Curated `search.popular` icons live outside the navigation model, so they
@@ -6,9 +6,20 @@ import { enrichDiagnostic } from "../core/diagnostics.ts";
6
6
  import type { Diagnostic } from "../core/types.ts";
7
7
  import { markdownVariantUrl, prefersMarkdown } from "./markdown-negotiation.ts";
8
8
 
9
+ /** The `{ type: "error" }` payload Vite's browser overlay renders. */
10
+ interface OverlayErrorPayload {
11
+ err: {
12
+ id?: string;
13
+ message: string;
14
+ plugin: string;
15
+ stack: string;
16
+ };
17
+ type: "error";
18
+ }
19
+
9
20
  /** The dev server's HMR channel — either `.ws` (Vite ≤5) or `.hot` (Vite 6+). */
10
21
  interface OverlayChannel {
11
- send: (payload: unknown) => void;
22
+ send: (payload: OverlayErrorPayload) => void;
12
23
  }
13
24
  interface OverlayServer {
14
25
  hot?: OverlayChannel;
@@ -164,7 +175,7 @@ export const blumeIntegration = (
164
175
  "astro:server:setup": ({ server }) => {
165
176
  // Keep a handle on the dev server so Blume diagnostics can be pushed to
166
177
  // its browser error overlay (see `showBlumeErrorOverlay`).
167
- overlayServer = server as unknown as OverlayServer;
178
+ overlayServer = server;
168
179
  // Prepend so the rewrite happens before Astro's own request handler,
169
180
  // letting the rewritten URL resolve to the `.md` endpoint.
170
181
  server.middlewares.stack.unshift({
@@ -40,13 +40,20 @@ const VALID_MODES = new Set<IslandClientMode>([
40
40
  "visible",
41
41
  ]);
42
42
 
43
+ /** Whether a declared client mode is one Astro's directives support. */
44
+ const isClientMode = (mode: string): mode is IslandClientMode =>
45
+ // SAFETY: `Set.has` only compares identity at runtime; the assertion widens
46
+ // the lookup key so the narrower-typed set accepts it, and the `has` result
47
+ // is exactly the predicate being claimed.
48
+ VALID_MODES.has(mode as IslandClientMode);
49
+
43
50
  /** Island extensions mapped to the Astro renderer that handles them. */
44
- const FRAMEWORK_BY_EXT: Record<string, IslandFramework> = {
45
- jsx: "react",
46
- svelte: "svelte",
47
- tsx: "react",
48
- vue: "vue",
49
- };
51
+ const FRAMEWORK_BY_EXT = new Map<string, IslandFramework>([
52
+ ["jsx", "react"],
53
+ ["svelte", "svelte"],
54
+ ["tsx", "react"],
55
+ ["vue", "vue"],
56
+ ]);
50
57
 
51
58
  // Captures the extension so we can both strip it from the name and pick the
52
59
  // framework. Kept in sync with the glob below.
@@ -66,13 +73,13 @@ export const readClientMode = (
66
73
  if (!mode) {
67
74
  return DEFAULT_CLIENT;
68
75
  }
69
- if (!VALID_MODES.has(mode as IslandClientMode)) {
76
+ if (!isClientMode(mode)) {
70
77
  warnings.push(
71
78
  `Island "${file}" declares an unknown client mode "${mode}"; defaulting to "${DEFAULT_CLIENT}". Use "load", "idle", "visible", or "only".`
72
79
  );
73
80
  return DEFAULT_CLIENT;
74
81
  }
75
- return mode as IslandClientMode;
82
+ return mode;
76
83
  };
77
84
 
78
85
  /**
@@ -109,7 +116,7 @@ export const discoverIslands = async (
109
116
  const collectIsland = (file: string, source: string): void => {
110
117
  const base = basename(file);
111
118
  const ext = base.match(ISLAND_FILE)?.groups?.ext;
112
- const framework = ext ? FRAMEWORK_BY_EXT[ext] : undefined;
119
+ const framework = ext ? FRAMEWORK_BY_EXT.get(ext) : undefined;
113
120
  if (!framework) {
114
121
  return;
115
122
  }