blume 0.6.6 → 0.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 (109) hide show
  1. package/dist/cli/index.js +1180 -739
  2. package/dist/cli/index.js.map +52 -51
  3. package/dist/types/core/base-path.d.ts +38 -0
  4. package/dist/types/core/config-input.d.ts +74 -10
  5. package/dist/types/core/config.d.ts +3 -2
  6. package/dist/types/core/data.d.ts +2 -0
  7. package/dist/types/core/i18n-ui.d.ts +1 -3
  8. package/dist/types/core/schema.d.ts +95 -52
  9. package/dist/types/core/sources/types.d.ts +2 -0
  10. package/dist/types/core/types.d.ts +6 -1
  11. package/docs/02-deployment.mdx +16 -1
  12. package/docs/03-faq.mdx +8 -8
  13. package/docs/configuration/index.mdx +6 -0
  14. package/docs/content/components.mdx +29 -2
  15. package/docs/content/islands.mdx +8 -0
  16. package/docs/content/syntax.mdx +13 -0
  17. package/package.json +2 -1
  18. package/src/ai/agent-readability.ts +7 -2
  19. package/src/ai/ask.ts +12 -7
  20. package/src/ai/llms.ts +15 -4
  21. package/src/ai/mcp/data.ts +8 -4
  22. package/src/ai/mcp/server.ts +3 -0
  23. package/src/astro/component-slots.ts +5 -3
  24. package/src/astro/examples.ts +12 -7
  25. package/src/astro/generate.ts +317 -144
  26. package/src/astro/index.ts +5 -1
  27. package/src/astro/integration.ts +8 -4
  28. package/src/astro/islands.ts +11 -5
  29. package/src/astro/markdown-negotiation.ts +1 -1
  30. package/src/astro/pages.ts +8 -3
  31. package/src/astro/templates.ts +166 -19
  32. package/src/cli/commands/build.ts +32 -19
  33. package/src/cli/commands/dev.ts +48 -15
  34. package/src/cli/commands/doctor.ts +2 -2
  35. package/src/cli/commands/validate.ts +1 -0
  36. package/src/cli/dev-lock.ts +26 -15
  37. package/src/cli/required-secrets.ts +2 -1
  38. package/src/components/content/CodeBlock.astro +3 -0
  39. package/src/components/content/Component.astro +30 -16
  40. package/src/components/content/Diff.astro +3 -1
  41. package/src/components/content/Update.astro +1 -1
  42. package/src/components/content/auto-type-table.ts +18 -8
  43. package/src/components/content/diff.ts +12 -6
  44. package/src/components/content/mermaid-element.ts +3 -0
  45. package/src/components/index.ts +23 -1
  46. package/src/components/islands/ask-ai.tsx +12 -6
  47. package/src/components/islands/base-path.ts +28 -0
  48. package/src/components/islands/hooks.ts +16 -1
  49. package/src/components/layout/Banner.astro +2 -1
  50. package/src/components/layout/Breadcrumbs.astro +2 -1
  51. package/src/components/layout/Favicon.astro +3 -2
  52. package/src/components/layout/Header.astro +2 -1
  53. package/src/components/layout/LanguageSwitcher.astro +2 -1
  54. package/src/components/layout/Logo.astro +2 -1
  55. package/src/components/layout/NavSelector.astro +2 -1
  56. package/src/components/layout/NavTree.astro +5 -4
  57. package/src/components/layout/PageFeedback.astro +4 -1
  58. package/src/components/layout/PageLayout.astro +9 -4
  59. package/src/components/layout/Pagination.astro +3 -2
  60. package/src/components/layout/RootLayout.astro +7 -4
  61. package/src/components/layout/Search.astro +13 -5
  62. package/src/components/layout/nav-utils.ts +18 -10
  63. package/src/components/layout/search/pagefind.ts +3 -0
  64. package/src/components/layout/toc-element.ts +7 -1
  65. package/src/components/openapi/RequestPanel.astro +7 -1
  66. package/src/components/openapi/snippets.ts +25 -11
  67. package/src/core/base-path.ts +70 -0
  68. package/src/core/component-overrides.ts +103 -74
  69. package/src/core/config-input.ts +81 -15
  70. package/src/core/config.ts +5 -3
  71. package/src/core/content.ts +2 -0
  72. package/src/core/data.ts +2 -0
  73. package/src/core/diagnostics.ts +54 -34
  74. package/src/core/gitignore.ts +4 -1
  75. package/src/core/graph.ts +156 -88
  76. package/src/core/i18n-ui.ts +18 -3
  77. package/src/core/last-modified.ts +2 -0
  78. package/src/core/links.ts +38 -18
  79. package/src/core/manifest.ts +62 -45
  80. package/src/core/nav-diagnostics.ts +1 -1
  81. package/src/core/navigation.ts +116 -55
  82. package/src/core/project-graph.ts +10 -9
  83. package/src/core/schema.ts +572 -621
  84. package/src/core/sources/github-releases.ts +2 -1
  85. package/src/core/sources/mdx-remote.ts +58 -54
  86. package/src/core/sources/normalize.ts +116 -73
  87. package/src/core/sources/notion.ts +19 -10
  88. package/src/core/sources/types.ts +2 -0
  89. package/src/core/tsconfig-aliases.ts +59 -30
  90. package/src/core/types.ts +6 -1
  91. package/src/deploy/redirects.ts +18 -0
  92. package/src/deploy/robots.ts +6 -1
  93. package/src/deploy/rss.ts +10 -3
  94. package/src/deploy/sitemap.ts +14 -10
  95. package/src/markdown/base-links.ts +58 -0
  96. package/src/markdown/code-title.ts +11 -14
  97. package/src/markdown/index.ts +34 -9
  98. package/src/markdown/inline-code.ts +7 -2
  99. package/src/markdown/themes.ts +24 -0
  100. package/src/openapi/model.ts +3 -1
  101. package/src/openapi/references.ts +41 -17
  102. package/src/openapi/render-mdx.ts +11 -6
  103. package/src/openapi/scalar.ts +32 -16
  104. package/src/registry/eject.ts +64 -8
  105. package/src/search/build.ts +3 -0
  106. package/src/search/documents.ts +2 -2
  107. package/src/search/sync/typesense.ts +6 -4
  108. package/src/seo/jsonld.ts +16 -6
  109. package/src/theme/entry.ts +86 -21
@@ -162,10 +162,14 @@ export interface NavSelectorItem {
162
162
  icon?: string;
163
163
  tag?: string;
164
164
  }
165
+ /** Context-partition selector kinds (a versioned/localized/multi-product site). */
166
+ type NavSelectorContextKind = "product" | "version";
167
+ /** What a top-level partition selector switches between. */
168
+ type NavSelectorKind = "dropdown" | "language" | NavSelectorContextKind;
165
169
  /** Top-level partition selectors (products, versions, languages). */
166
170
  export interface NavSelector {
167
171
  label: string;
168
- kind: "dropdown" | "language" | "product" | "version";
172
+ kind: NavSelectorKind;
169
173
  items: NavSelectorItem[];
170
174
  }
171
175
  /** A pinned link rendered above the sidebar sections (external or internal). */
@@ -252,3 +256,4 @@ export interface BlumeManifest {
252
256
  output: ResolvedConfig["deployment"]["output"];
253
257
  routes: RouteManifestEntry[];
254
258
  }
259
+ export {};
@@ -54,7 +54,7 @@ blume preview
54
54
 
55
55
  ## Subpath deploys
56
56
 
57
- Serving docs under a path like `example.com/docs`? Set `deployment.base` — common for GitHub Pages project sites. Internal links and assets are rewritten to include the base path.
57
+ Serving docs under a path like `example.com/docs`? Set `deployment.base` — common for GitHub Pages project sites. The whole site, root included, moves under the base, and internal links and assets are rewritten to include it.
58
58
 
59
59
  ```ts blume.config.ts lineNumbers
60
60
  deployment: {
@@ -62,6 +62,21 @@ deployment: {
62
62
  }
63
63
  ```
64
64
 
65
+ ## Mount the docs under a path
66
+
67
+ `basePath` mounts every generated route under a segment (`/docs/getting-started`) while leaving the sidebar untouched — the top level is your sections, not a wrapper group. Use it when the docs live at `/docs/*` but the site root stays yours (like Docusaurus `routeBasePath` or Fumadocs `baseUrl`).
68
+
69
+ ```ts blume.config.ts lineNumbers
70
+ basePath: "/docs",
71
+ ```
72
+
73
+ Write links as if mounted at root (`/getting-started`); Blume rewrites them, along with redirects, the sitemap, canonical URLs, Open Graph images, `llms.txt`, and the search index. Public assets (images, files under `public/`) stay at the site root.
74
+
75
+ This is a distinct concept from the two paths above:
76
+
77
+ - A per-source [`prefix`](/docs/content/sources#multiple-sources) namespaces **one** source and **does** add a sidebar group.
78
+ - `deployment.base` is the host subdirectory the **whole** app is served from. The two compose — with both set, a page lands at `{deployment.base}/{basePath}/page`.
79
+
65
80
  ## Server rendering
66
81
 
67
82
  Static output covers most docs. Switch to server output when you need request-time features — most notably the [Ask AI](/docs/configuration/ai) endpoint:
package/docs/03-faq.mdx CHANGED
@@ -87,13 +87,13 @@ We reported it upstream in [oxc-project/oxc#24096](https://github.com/oxc-projec
87
87
 
88
88
  Patch oxfmt so it preserves the line break that sits directly against a `:::` fence. Blume ships exactly this patch in its own repo, and you can apply the same one in any project.
89
89
 
90
- 1. Save the patch as `patches/oxfmt@0.55.0.patch`:
90
+ 1. Save the patch as `patches/oxfmt@0.57.0.patch`:
91
91
 
92
- ```diff patches/oxfmt@0.55.0.patch
93
- diff --git a/dist/markdown-Bowe09rB.js b/dist/markdown-Bowe09rB.js
94
- index 0fe63ff712e07c9c2188711fa0940f7a7d8f7d74..44435d91a2f5a6711512981c9bcaddedc1760be4 100644
95
- --- a/dist/markdown-Bowe09rB.js
96
- +++ b/dist/markdown-Bowe09rB.js
92
+ ```diff patches/oxfmt@0.57.0.patch
93
+ diff --git a/dist/markdown-B5hFVJKQ.js b/dist/markdown-B5hFVJKQ.js
94
+ index 58322b247b263f87975c2a97eecfe0b97d7143c4..0f39f1901603026a391c6d343cb838dfaccbfd03 100644
95
+ --- a/dist/markdown-B5hFVJKQ.js
96
+ +++ b/dist/markdown-B5hFVJKQ.js
97
97
  @@ -1141,7 +1141,14 @@ function Yf(e, r, t) {
98
98
  case "sentence": return Yi(e, t);
99
99
  case "word": return Li(e);
@@ -117,7 +117,7 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
117
117
  ```json package.json
118
118
  {
119
119
  "patchedDependencies": {
120
- "oxfmt@0.55.0": "patches/oxfmt@0.55.0.patch"
120
+ "oxfmt@0.57.0": "patches/oxfmt@0.57.0.patch"
121
121
  }
122
122
  }
123
123
  ```
@@ -128,5 +128,5 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
128
128
  bun install
129
129
  ```
130
130
 
131
- :::warning[Version-pinned] The patch targets oxfmt `0.55.0` — its diff references a file hashed to that build (`dist/markdown-Bowe09rB.js`). When you bump oxfmt, regenerate the patch (e.g. `bun patch oxfmt`) or check whether the upstream fix has landed and the patch is no longer needed.
131
+ :::warning[Version-pinned] The patch targets oxfmt `0.57.0` — its diff references a file hashed to that build (`dist/markdown-B5hFVJKQ.js`). When you bump oxfmt, regenerate the patch (e.g. `bun patch oxfmt`) or check whether the upstream fix has landed and the patch is no longer needed.
132
132
  :::
@@ -53,6 +53,12 @@ export default defineConfig({
53
53
  icons: true, // language icon in the code-block header
54
54
  wrap: false, // wrap long lines instead of scrolling
55
55
  },
56
+ codeBlocks: {
57
+ theme: {
58
+ light: "github-light", // any bundled Shiki theme
59
+ dark: "github-dark",
60
+ },
61
+ },
56
62
  },
57
63
 
58
64
  // AI — see the AI guide
@@ -546,7 +546,33 @@ A card linking to a GitHub repository with its live star and fork counts. Counts
546
546
 
547
547
  `Component` renders an example file from your project's `examples/` directory as a live preview alongside its highlighted source, in tabs. Point it at a file with `path` — its location under `examples/`, without the extension (so `examples/counter.tsx` is `path="counter"`). React, Vue, Svelte, and Astro examples are all supported; framework examples hydrate, Astro ones render statically. It keeps the preview and the code in sync from a single file.
548
548
 
549
- The directory is configurableset `examples` in `blume.config.ts` when your examples live elsewhere (e.g. a registry layout). `path` is always relative to it:
549
+ The preview renders in an isolated frame that the docs styles never reach no prose margins, typography, or theme chrome bleed into your component. The frame gets Tailwind (preflight + utilities scanned from your example files and anything they import), Blume's design tokens so classes like `bg-background` follow the site palette by default, and it follows the site's light/dark toggle live.
550
+
551
+ To style previews with your own design system — say, shadcn variables — point `examples.css` at a stylesheet. It's injected into every preview frame after Blume's defaults, so your tokens win. Don't `@import "tailwindcss"` in it; the frame already provides Tailwind. Both `.dark` and `[data-theme="dark"]` work for dark-mode overrides:
552
+
553
+ ```ts
554
+ // blume.config.ts
555
+ export default defineConfig({
556
+ examples: { css: "examples/theme.css" },
557
+ });
558
+ ```
559
+
560
+ ```css
561
+ /* examples/theme.css */
562
+ :root {
563
+ --primary: oklch(0.6 0.2 260);
564
+ }
565
+
566
+ .dark {
567
+ --primary: oklch(0.75 0.15 260);
568
+ }
569
+
570
+ @theme inline {
571
+ --color-primary: var(--primary);
572
+ }
573
+ ```
574
+
575
+ The directory is configurable too — set `source` (or use the string shorthand, `examples: "..."`) when your examples live elsewhere (e.g. a registry layout). `path` is always relative to it:
550
576
 
551
577
  ```ts
552
578
  // blume.config.ts
@@ -593,6 +619,7 @@ An Astro example renders live with no client JavaScript:
593
619
 
594
620
  <CodeBlock
595
621
  lang="ts"
622
+ icons={false}
596
623
  code={`export const greet = (name: string): string =>
597
624
  \`Hello, \${name}!\`;`}
598
625
  />
@@ -602,7 +629,7 @@ An Astro example renders live with no client JavaScript:
602
629
  import CodeBlock from "blume/components/content/CodeBlock.astro";
603
630
  ---
604
631
 
605
- <CodeBlock lang="ts" code={source} />
632
+ <CodeBlock lang="ts" code={source} icons={false} />
606
633
  ```
607
634
 
608
635
  To highlight to an HTML string yourself (e.g. inside your own component), import the underlying helper from `blume/markdown`:
@@ -77,6 +77,14 @@ export default function Chart() {
77
77
 
78
78
  **React works out of the box** — Blume turns it on automatically the moment your project contains a `.tsx`/`.jsx` island.
79
79
 
80
+ The [React Compiler](https://react.dev/learn/react-compiler) is on by default whenever React is enabled, so your islands are auto-memoized — no hand-written `useMemo`/`useCallback` needed. It ships with Blume; there's nothing to install. Opt out in `blume.config.ts`:
81
+
82
+ ```ts blume.config.ts
83
+ export default defineConfig({
84
+ react: { compiler: false },
85
+ });
86
+ ```
87
+
80
88
  **Vue and Svelte** are supported too; install the matching Astro integration and Blume wires up the renderer when it sees a `.vue` or `.svelte` island:
81
89
 
82
90
  ```bash
@@ -33,6 +33,7 @@ For footnote markers, ordinals, and scientific or chemical notation inline.
33
33
 
34
34
  E = mc^2^ and H~2~O.
35
35
 
36
+ {/* prettier-ignore */}
36
37
  ```md
37
38
  E = mc^2^ and H~2~O.
38
39
  ```
@@ -141,6 +142,18 @@ export default defineConfig({
141
142
 
142
143
  Inline code can be highlighted too: add a `{:lang}` marker inside a backtick span and it's colored like a tiny code block — `useState(){:js}` or `T extends object{:ts}`. It only kicks in when you add the marker, so plain inline code stays untouched — nothing to switch on.
143
144
 
145
+ Highlighting uses the `github-light`/`github-dark` themes by default. Swap in any [bundled Shiki theme](https://shiki.style/themes) per color mode with `markdown.codeBlocks.theme` — it colors every code surface at once (fences, inline snippets, `<CodeBlock>`, and `<Diff>`):
146
+
147
+ ```ts blume.config.ts
148
+ export default defineConfig({
149
+ markdown: {
150
+ codeBlocks: {
151
+ theme: { light: "github-light", dark: "vesper" },
152
+ },
153
+ },
154
+ });
155
+ ```
156
+
144
157
  ### Line numbers
145
158
 
146
159
  Append `lineNumbers` to render a line-number gutter — on its own or alongside a title:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -89,6 +89,7 @@
89
89
  "@vercel/analytics": "^2.0.1",
90
90
  "ai": "^5.0.0",
91
91
  "astro": "^7.0.2",
92
+ "babel-plugin-react-compiler": "^1.0.0",
92
93
  "citty": "^0.1.6",
93
94
  "consola": "^3.4.0",
94
95
  "deepmerge": "^4.3.1",
@@ -1,3 +1,4 @@
1
+ import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
1
2
  import type { BlumeProject } from "../core/project-graph.ts";
2
3
  import type { ContentSignalPolicy, ContentSignals } from "../core/schema.ts";
3
4
  import { buildRssFeeds } from "../deploy/rss.ts";
@@ -42,9 +43,13 @@ export const buildAgentReadability = (
42
43
  }
43
44
 
44
45
  const site = config.deployment.site ?? null;
45
- // Concatenate rather than `new URL()` so a subpath deployment's base is kept.
46
+ // Every artifact is served under `deployment.base`; concatenate rather than
47
+ // `new URL()` so the subpath is preserved.
48
+ const deployBase = normalizeBasePath(config.deployment.base);
46
49
  const abs = (path: string): string =>
47
- site ? `${site.replace(/\/+$/u, "")}${path}` : path;
50
+ site
51
+ ? `${site.replace(/\/+$/u, "")}${withBasePath(deployBase, path)}`
52
+ : path;
48
53
 
49
54
  const artifacts: Record<string, unknown> = {
50
55
  markdown: {
package/src/ai/ask.ts CHANGED
@@ -30,20 +30,25 @@ interface AskPreset {
30
30
  * dedicated AI SDK provider; LLMGateway and Inkeep are OpenAI-compatible
31
31
  * endpoints with no dedicated provider, so they reuse `@ai-sdk/openai-compatible`.
32
32
  */
33
+ /** The discriminant/name shared by the OpenAI-compatible providers. */
34
+ const OPENAI_COMPATIBLE = "openai-compatible";
35
+ /** The AI SDK provider package the OpenAI-compatible providers install. */
36
+ const OPENAI_COMPATIBLE_DEP = "@ai-sdk/openai-compatible";
37
+
33
38
  const ASK_PRESETS: Record<string, AskPreset> = {
34
39
  inkeep: {
35
40
  apiKeyEnv: "INKEEP_API_KEY",
36
41
  baseUrl: "https://api.inkeep.com/v1",
37
- kind: "openai-compatible",
42
+ kind: OPENAI_COMPATIBLE,
38
43
  name: "inkeep",
39
- runtimeDep: "@ai-sdk/openai-compatible",
44
+ runtimeDep: OPENAI_COMPATIBLE_DEP,
40
45
  },
41
46
  llmgateway: {
42
47
  apiKeyEnv: "LLMGATEWAY_API_KEY",
43
48
  baseUrl: "https://api.llmgateway.io/v1",
44
- kind: "openai-compatible",
49
+ kind: OPENAI_COMPATIBLE,
45
50
  name: "llmgateway",
46
- runtimeDep: "@ai-sdk/openai-compatible",
51
+ runtimeDep: OPENAI_COMPATIBLE_DEP,
47
52
  },
48
53
  openrouter: {
49
54
  apiKeyEnv: "OPENROUTER_API_KEY",
@@ -73,9 +78,9 @@ export const resolveAskBackend = (ask?: AskAiConfig): AskBackend => {
73
78
  return {
74
79
  apiKeyEnv,
75
80
  baseUrl: ask?.baseUrl ?? preset?.baseUrl ?? "",
76
- kind: "openai-compatible",
81
+ kind: OPENAI_COMPATIBLE,
77
82
  model,
78
- name: preset?.name ?? "openai-compatible",
83
+ name: preset?.name ?? OPENAI_COMPATIBLE,
79
84
  };
80
85
  };
81
86
 
@@ -89,5 +94,5 @@ export const askBackendRuntimeDep = (ask?: AskAiConfig): string | undefined => {
89
94
  if (provider === "gateway") {
90
95
  return undefined;
91
96
  }
92
- return ASK_PRESETS[provider]?.runtimeDep ?? "@ai-sdk/openai-compatible";
97
+ return ASK_PRESETS[provider]?.runtimeDep ?? OPENAI_COMPATIBLE_DEP;
93
98
  };
package/src/ai/llms.ts CHANGED
@@ -1,13 +1,16 @@
1
+ import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
1
2
  import matter from "../core/frontmatter.ts";
2
3
  import type { BlumeProject } from "../core/project-graph.ts";
3
4
  import { readEntryText } from "../core/sources/read.ts";
4
5
  import type { PageRecord } from "../core/types.ts";
5
6
 
6
- const pageUrl = (route: string, site?: string): string => {
7
+ // Routes carry `basePath`; a `deployment.base` subdirectory is layered on top so
8
+ // the emitted URL matches where the page is served.
9
+ const pageUrl = (route: string, site?: string, base = ""): string => {
7
10
  if (!site) {
8
11
  return route;
9
12
  }
10
- return `${site.replace(/\/$/u, "")}${route}`;
13
+ return `${site.replace(/\/$/u, "")}${withBasePath(base, route)}`;
11
14
  };
12
15
 
13
16
  const orderedPages = (project: BlumeProject): PageRecord[] =>
@@ -26,7 +29,11 @@ const buildIndex = (project: BlumeProject): string => {
26
29
  lines.push("", "## Docs", "");
27
30
 
28
31
  for (const page of orderedPages(project)) {
29
- const url = pageUrl(page.route, site);
32
+ const url = pageUrl(
33
+ page.route,
34
+ site,
35
+ normalizeBasePath(config.deployment.base)
36
+ );
30
37
  const summary = page.description ? `: ${page.description}` : "";
31
38
  lines.push(`- [${page.title}](${url})${summary}`);
32
39
  }
@@ -43,7 +50,11 @@ const buildFull = async (project: BlumeProject): Promise<string> => {
43
50
  pages.map(async (page) => {
44
51
  const raw = await readEntryText(project, page);
45
52
  const body = matter(raw).content.trim();
46
- const url = pageUrl(page.route, config.deployment.site);
53
+ const url = pageUrl(
54
+ page.route,
55
+ config.deployment.site,
56
+ normalizeBasePath(config.deployment.base)
57
+ );
47
58
  return [`# ${page.title}`, `Source: ${url}`, "", body].join("\n");
48
59
  })
49
60
  );
@@ -45,16 +45,20 @@ export const buildMcpData = async (project: BlumeProject): Promise<McpData> => {
45
45
  graph.pages.map((page) => [page.id, page.description])
46
46
  );
47
47
 
48
- const routes: McpRoute[] = manifest.routes
49
- .filter((route) => !route.hidden)
50
- .map((route) => ({
48
+ const routes: McpRoute[] = [];
49
+ for (const route of manifest.routes) {
50
+ if (route.hidden) {
51
+ continue;
52
+ }
53
+ routes.push({
51
54
  contentType: route.contentType,
52
55
  description: descriptionById.get(route.id),
53
56
  indexable: route.indexable,
54
57
  lastModified: route.lastModified ?? null,
55
58
  route: route.path,
56
59
  title: route.title,
57
- }));
60
+ });
61
+ }
58
62
 
59
63
  return {
60
64
  documents: documents.map((doc) => ({
@@ -209,6 +209,9 @@ export const createMcpFetchHandler = (
209
209
  const server = buildServer(data, index);
210
210
  const transport = new WebStandardStreamableHTTPServerTransport({
211
211
  enableJsonResponse: true,
212
+ // The SDK enables stateless mode only when this is `undefined`; `null` is
213
+ // not an accepted value for the `(() => string) | undefined` option.
214
+ // oxlint-disable-next-line sonarjs/no-undefined-assignment
212
215
  sessionIdGenerator: undefined,
213
216
  });
214
217
  await server.connect(transport);
@@ -41,6 +41,8 @@ export const layoutOverrides = {};
41
41
  const attributeValue = (value: string): string =>
42
42
  value.replaceAll(/["\n\r]/gu, " ").trim();
43
43
 
44
+ const CLIENT_LOAD = "client:load";
45
+
44
46
  /** Astro client directive for a hydrated override. */
45
47
  const directiveFor = (override: NormalizedOverride): string => {
46
48
  const framework = override.source?.framework;
@@ -54,15 +56,15 @@ const directiveFor = (override: NormalizedOverride): string => {
54
56
  case "media": {
55
57
  return override.media
56
58
  ? `client:media="${attributeValue(override.media)}"`
57
- : "client:load";
59
+ : CLIENT_LOAD;
58
60
  }
59
61
  case "only": {
60
62
  return framework
61
63
  ? `client:only="${attributeValue(framework)}"`
62
- : "client:load";
64
+ : CLIENT_LOAD;
63
65
  }
64
66
  default: {
65
- return "client:load";
67
+ return CLIENT_LOAD;
66
68
  }
67
69
  }
68
70
  };
@@ -59,10 +59,10 @@ const GLOB_MAGIC = /[!*?[\]{}]/u;
59
59
  */
60
60
  const splitGlobBase = (pattern: string): { base: string; rest: string } => {
61
61
  const segments = pattern.split("/");
62
+ // Only called when the pattern contains glob magic (see the caller), and `/`
63
+ // is never magic, so the magic char always lands in a segment — `findIndex`
64
+ // is never -1 here.
62
65
  const firstMagic = segments.findIndex((segment) => GLOB_MAGIC.test(segment));
63
- if (firstMagic === -1) {
64
- return { base: pattern, rest: "" };
65
- }
66
66
  return {
67
67
  base: segments.slice(0, firstMagic).join("/"),
68
68
  rest: segments.slice(firstMagic).join("/"),
@@ -109,11 +109,13 @@ export const discoverExamples = async (
109
109
  const warnings: string[] = [];
110
110
  const seen = new Map<string, string>();
111
111
 
112
- for (const [index, file] of files.entries()) {
112
+ // Extracted so the two skip paths become early `return`s (one `continue`
113
+ // budget per loop under the lint rule) instead of `continue` statements.
114
+ const collectExample = (file: string, source: string): void => {
113
115
  const ext = file.match(EXAMPLE_FILE)?.groups?.ext;
114
116
  const framework = ext ? FRAMEWORK_BY_EXT[ext] : undefined;
115
117
  if (!(ext && framework)) {
116
- continue;
118
+ return;
117
119
  }
118
120
  // Strip the trailing `.<ext>` to form the `<Component path>` key.
119
121
  const path = relative(dir, file).slice(0, -(ext.length + 1));
@@ -122,10 +124,9 @@ export const discoverExamples = async (
122
124
  warnings.push(
123
125
  `Two examples both resolve to "${path}" ("${existing}" and "${file}"); ignoring the second. Give them distinct paths.`
124
126
  );
125
- continue;
127
+ return;
126
128
  }
127
129
  seen.set(path, file);
128
- const source = sources[index] ?? "";
129
130
  examples.push({
130
131
  client:
131
132
  framework === "astro"
@@ -137,6 +138,10 @@ export const discoverExamples = async (
137
138
  path,
138
139
  source,
139
140
  });
141
+ };
142
+
143
+ for (const [index, file] of files.entries()) {
144
+ collectExample(file, sources[index] ?? "");
140
145
  }
141
146
 
142
147
  return { examples, warnings };