blume 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1631 -940
- package/dist/cli/index.js.map +62 -50
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/project.d.ts +12 -2
- package/dist/types/core/schema.d.ts +442 -292
- package/dist/types/core/types.d.ts +7 -0
- package/dist/types/migrate/mintlify/assets.d.ts +8 -0
- package/docs/01-quickstart.mdx +5 -16
- package/docs/02-deployment.mdx +21 -54
- package/docs/advanced/api-reference.mdx +34 -51
- package/docs/advanced/blog.mdx +9 -25
- package/docs/advanced/bridge.mdx +74 -0
- package/docs/advanced/changelog.mdx +10 -33
- package/docs/advanced/custom-pages.mdx +21 -78
- package/docs/advanced/meta.ts +8 -1
- package/docs/advanced/migrate.mdx +119 -0
- package/docs/configuration/ai.mdx +42 -103
- package/docs/configuration/analytics.mdx +20 -38
- package/docs/configuration/customization.mdx +40 -73
- package/docs/configuration/export.mdx +9 -34
- package/docs/configuration/index.mdx +67 -87
- package/docs/configuration/search.mdx +17 -54
- package/docs/configuration/seo.mdx +17 -48
- package/docs/configuration/theming.mdx +20 -42
- package/docs/content/components.mdx +95 -101
- package/docs/content/i18n.mdx +21 -72
- package/docs/content/index.mdx +18 -48
- package/docs/content/islands.mdx +25 -52
- package/docs/content/meta.mdx +23 -50
- package/docs/content/navigation.mdx +23 -62
- package/docs/content/sources.mdx +20 -83
- package/docs/content/syntax.mdx +37 -105
- package/docs/index.mdx +12 -41
- package/docs/reference/cli.mdx +47 -30
- package/docs/reference/frontmatter.mdx +7 -5
- package/package.json +11 -1
- package/src/astro/generate.ts +18 -8
- package/src/astro/integration.ts +26 -3
- package/src/astro/islands.ts +6 -2
- package/src/astro/markdown-negotiation.ts +17 -3
- package/src/astro/pages.ts +6 -1
- package/src/astro/static-assets.ts +117 -0
- package/src/astro/templates.ts +76 -30
- package/src/cli/args.ts +23 -0
- package/src/cli/commands/build.ts +129 -62
- package/src/cli/commands/check.ts +20 -0
- package/src/cli/commands/dev.ts +11 -2
- package/src/cli/commands/doctor.ts +10 -1
- package/src/cli/commands/eject.ts +3 -1
- package/src/cli/commands/init.ts +21 -1
- package/src/cli/commands/preview.ts +2 -1
- package/src/cli/commands/validate.ts +12 -1
- package/src/cli/dev-lock.ts +92 -0
- package/src/cli/log.ts +11 -0
- package/src/cli/prepare.ts +3 -0
- package/src/components/BlumePage.astro +8 -0
- package/src/components/Icon.astro +13 -10
- package/src/components/content/ApiField.astro +75 -0
- package/src/components/content/ParamField.astro +39 -0
- package/src/components/content/RequestField.astro +23 -0
- package/src/components/content/ResponseField.astro +23 -0
- package/src/components/content/Step.astro +1 -1
- package/src/components/content/YouTube.astro +35 -0
- package/src/components/content/youtube.ts +46 -0
- package/src/components/islands/ask-ai.tsx +14 -14
- package/src/components/layout/Breadcrumbs.astro +7 -2
- package/src/components/layout/NavTree.astro +24 -8
- package/src/components/layout/RootLayout.astro +56 -34
- package/src/components/layout/Search.astro +1 -1
- package/src/components/openapi/ApiOverview.astro +84 -0
- package/src/components/openapi/MethodBadge.astro +28 -0
- package/src/components/openapi/Operation.astro +140 -0
- package/src/components/openapi/ParametersTable.astro +97 -0
- package/src/components/openapi/RequestBody.astro +58 -0
- package/src/components/openapi/RequestPanel.astro +169 -0
- package/src/components/openapi/Responses.astro +91 -0
- package/src/components/openapi/SchemaProperty.astro +118 -0
- package/src/components/openapi/SchemaTable.astro +86 -0
- package/src/components/openapi/helpers.ts +238 -0
- package/src/components/openapi/panel.ts +59 -0
- package/src/components/openapi/snippets.ts +201 -0
- package/src/components/props.ts +3 -0
- package/src/core/assets.ts +31 -0
- package/src/core/bridge.ts +10 -0
- package/src/core/builtin-tags.ts +6 -0
- package/src/core/data.ts +2 -0
- package/src/core/diagnostics.ts +6 -1
- package/src/core/gitignore.ts +30 -0
- package/src/core/links.ts +60 -19
- package/src/core/project-graph.ts +5 -1
- package/src/core/project.ts +25 -3
- package/src/core/schema.ts +54 -6
- package/src/core/sources/mdx-remote.ts +54 -8
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/normalize.ts +6 -1
- package/src/core/sources/notion.ts +49 -5
- package/src/core/sources/resolve.ts +28 -6
- package/src/core/sources/sanity.ts +5 -1
- package/src/core/types.ts +7 -0
- package/src/deploy/rss.ts +1 -8
- package/src/deploy/sitemap.ts +20 -1
- package/src/deploy/xml.ts +8 -0
- package/src/markdown/directives.ts +15 -7
- package/src/markdown/package-commands.ts +26 -4
- package/src/migrate/fumadocs/content.ts +14 -1
- package/src/migrate/fumadocs/groups.ts +7 -0
- package/src/migrate/fumadocs/index.ts +5 -2
- package/src/migrate/mintlify/assets.ts +46 -0
- package/src/migrate/mintlify/config.ts +153 -1
- package/src/migrate/mintlify/content.ts +8 -2
- package/src/migrate/mintlify/index.ts +111 -46
- package/src/migrate/shared.ts +12 -27
- package/src/og/card.ts +14 -2
- package/src/openapi/model.ts +174 -0
- package/src/openapi/parse.ts +48 -0
- package/src/openapi/references.ts +164 -0
- package/src/openapi/render-mdx.ts +76 -0
- package/src/openapi/scalar.ts +15 -103
- package/src/openapi/source.ts +140 -0
- package/src/registry/eject.ts +28 -5
- package/src/registry/registry.ts +6 -0
- package/src/registry/rewrite-imports.ts +31 -19
- package/src/search/documents.ts +23 -5
- package/src/search/sync/algolia.ts +5 -1
- package/src/search/sync/typesense.ts +24 -16
- package/src/theme/chrome-icons.ts +22 -0
- package/src/theme/icons.ts +151 -161
- package/src/theme/palette.ts +26 -7
package/docs/reference/cli.mdx
CHANGED
|
@@ -9,19 +9,19 @@ blume <command> [options]
|
|
|
9
9
|
|
|
10
10
|
## Commands
|
|
11
11
|
|
|
12
|
-
| Command
|
|
13
|
-
|
|
|
14
|
-
| `blume init`
|
|
15
|
-
| `blume dev`
|
|
16
|
-
| `blume build`
|
|
17
|
-
| `blume preview`
|
|
18
|
-
| `blume add <item>`
|
|
19
|
-
| `blume migrate <tool>` | Migrate from Mintlify, Starlight, Nextra, or Fumadocs. |
|
|
20
|
-
| `blume sync`
|
|
21
|
-
| `blume eject`
|
|
22
|
-
| `blume check`
|
|
23
|
-
| `blume doctor`
|
|
24
|
-
| `blume validate`
|
|
12
|
+
| Command | Description |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `blume init` | Scaffold a minimal project. |
|
|
15
|
+
| `blume dev` | Start the dev server with hot reload. |
|
|
16
|
+
| `blume build` | Build the static (or server) site. |
|
|
17
|
+
| `blume preview` | Preview the last build. |
|
|
18
|
+
| `blume add <item>` | Install a source component from the registry. |
|
|
19
|
+
| [`blume migrate <tool>`](/docs/advanced/migrate) | Migrate from Mintlify, Starlight, Nextra, or Fumadocs. |
|
|
20
|
+
| `blume sync` | Re-fetch remote content sources and regenerate. |
|
|
21
|
+
| `blume eject` | Promote the runtime into a standalone Astro app. |
|
|
22
|
+
| `blume check` | Type-check the site with `astro check`. |
|
|
23
|
+
| `blume doctor` | Diagnose config and content problems. |
|
|
24
|
+
| `blume validate` | Validate links across your content. |
|
|
25
25
|
|
|
26
26
|
## Common flags
|
|
27
27
|
|
|
@@ -37,25 +37,47 @@ blume <command> [options]
|
|
|
37
37
|
- `blume build --output static|server --adapter vercel|node|netlify|cloudflare --base /docs` — override the deployment output, adapter, and base path from `blume.config.ts`.
|
|
38
38
|
- `blume build --analyze` — print the client JavaScript bundle sizes (largest first) after the build.
|
|
39
39
|
- `blume build --budget-js <kb> --budget-css <kb>` — fail the build when total client JavaScript/CSS exceeds the budget, turning a performance target into a CI gate.
|
|
40
|
+
- `blume build --isolated` — build into a throwaway `.blume-verify/` runtime (and its own `dist/`) instead of `.blume/`, so a running `blume dev` server and your real `dist/` are left untouched. See [Verifying while the dev server runs](#verifying-while-the-dev-server-runs).
|
|
40
41
|
- `blume preview --host --port <n>` — bind the preview server.
|
|
41
42
|
- `blume sync --force` — re-fetch remote sources, dropping the cached snapshot first.
|
|
42
43
|
- `blume add <item> --force` — overwrite files that already exist.
|
|
43
44
|
- `blume check --preview` — include drafts and unpublished CMS content when checking.
|
|
44
45
|
- `blume check --strict` — fail on content diagnostics as well as type errors.
|
|
46
|
+
- `blume check --isolated` — type-check in a throwaway `.blume-verify/` runtime so a running `blume dev` server is left untouched. See [Verifying while the dev server runs](#verifying-while-the-dev-server-runs).
|
|
45
47
|
- `blume eject --yes` — skip the confirmation prompt.
|
|
46
48
|
- `blume validate --external` — also check external links over the network.
|
|
47
49
|
- `blume validate --strict` — exit non-zero on warnings too.
|
|
48
|
-
- `blume validate --json` / `blume doctor --json` — emit diagnostics as JSON on
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
- `blume validate --json` / `blume doctor --json` — emit diagnostics as JSON on stdout (with `code`, `severity`, `file`, `line`/`column`, and `docsUrl`) for CI and editor integrations.
|
|
51
|
+
|
|
52
|
+
## Verifying while the dev server runs
|
|
53
|
+
|
|
54
|
+
`blume dev` serves a live Astro server rooted at the generated `.blume/` runtime and regenerates it on every change. `blume build` and `blume check` regenerate the _same_ `.blume/`, so running either while the dev server is live would corrupt it — both refuse with an error and exit non-zero:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
A `blume dev` server is running against .blume; building would corrupt it.
|
|
58
|
+
Stop the dev server, or re-run with --isolated to build/verify against
|
|
59
|
+
.blume-verify without touching it.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The `--isolated` flag is the escape hatch. It relocates the entire generated runtime (and, for `build`, its output `dist/`) to a sibling `.blume-verify/` directory, so the verification never writes anything the dev server — or your real `dist/` — depends on:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# In a second terminal, while `blume dev` is running:
|
|
66
|
+
blume check --isolated # fast: type-check the .astro/config changes
|
|
67
|
+
blume build --isolated # thorough: full production render into .blume-verify/dist
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`check --isolated` is the quick path (Astro type + template diagnostics, no `dist/`); `build --isolated` is the heavier one that also catches runtime render errors. Isolated builds skip the deploy post-steps (search index, hosted-provider sync, `llms.txt`, sitemap/robots, redirects) — a verify only needs to confirm the site compiles and renders, not publish it. Blume adds `.blume-verify/` to your `.gitignore` automatically.
|
|
71
|
+
|
|
72
|
+
This is especially useful when a coding agent needs to verify changes while you keep the dev server open. To make plain `blume build`/`blume check` isolate without the flag — for example in an agent's shell — set `BLUME_RUNTIME_DIR` to the runtime directory to use:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export BLUME_RUNTIME_DIR=.blume-verify
|
|
76
|
+
```
|
|
51
77
|
|
|
52
78
|
## Type-checking
|
|
53
79
|
|
|
54
|
-
`blume check` runs [`astro check`](https://docs.astro.build/en/reference/cli-reference/#astro-check)
|
|
55
|
-
over your project. It regenerates the `.blume` runtime, syncs Astro's content
|
|
56
|
-
types, then reports any TypeScript errors — in your `blume.config.ts`, in custom
|
|
57
|
-
`.astro` pages, and in the components they import. It exits non-zero when there
|
|
58
|
-
are errors, so it works as a `typecheck` step in CI:
|
|
80
|
+
`blume check` runs [`astro check`](https://docs.astro.build/en/reference/cli-reference/#astro-check) over your project. It regenerates the `.blume` runtime, syncs Astro's content types, then reports any TypeScript errors — in your `blume.config.ts`, in custom `.astro` pages, and in the components they import. It exits non-zero when there are errors, so it works as a `typecheck` step in CI:
|
|
59
81
|
|
|
60
82
|
```json title="package.json"
|
|
61
83
|
{
|
|
@@ -65,8 +87,7 @@ are errors, so it works as a `typecheck` step in CI:
|
|
|
65
87
|
}
|
|
66
88
|
```
|
|
67
89
|
|
|
68
|
-
Add a `tsconfig.json` extending Astro's config to your project root so authored
|
|
69
|
-
pages resolve `blume/*` imports and virtual modules like `blume:data`:
|
|
90
|
+
Add a `tsconfig.json` extending Astro's config to your project root so authored pages resolve `blume/*` imports and virtual modules like `blume:data`:
|
|
70
91
|
|
|
71
92
|
```json title="tsconfig.json"
|
|
72
93
|
{
|
|
@@ -81,11 +102,7 @@ Without a project `tsconfig.json`, only the generated runtime is checked.
|
|
|
81
102
|
|
|
82
103
|
`blume validate` checks every link discovered in your content:
|
|
83
104
|
|
|
84
|
-
- **Internal page links** (`/guides/intro`, `./sibling`) must resolve to a real
|
|
85
|
-
|
|
86
|
-
- **Anchor links** (`#section`, `/guides/intro#setup`) must match a heading on
|
|
87
|
-
the target page — misses are warnings.
|
|
105
|
+
- **Internal page links** (`/guides/intro`, `./sibling`) must resolve to a real page — broken ones are reported as errors.
|
|
106
|
+
- **Anchor links** (`#section`, `/guides/intro#setup`) must match a heading on the target page — misses are warnings.
|
|
88
107
|
- **Asset links** (`/logo.png`) are checked against the `public/` directory.
|
|
89
|
-
- **External links** are only checked with `--external` (off by default since it
|
|
90
|
-
requires the network); dead links (404/410/unreachable) are errors, while
|
|
91
|
-
rate-limited or transient responses (403/429/5xx/timeout) are warnings.
|
|
108
|
+
- **External links** are only checked with `--external` (off by default since it requires the network); dead links (404/410/unreachable) are errors, while rate-limited or transient responses (403/429/5xx/timeout) are warnings.
|
|
@@ -18,6 +18,11 @@ Every page accepts the following frontmatter. All fields are optional.
|
|
|
18
18
|
type: "string",
|
|
19
19
|
description: "Publish date for blog/changelog feeds (ISO or YAML date).",
|
|
20
20
|
},
|
|
21
|
+
authors: {
|
|
22
|
+
type: "string | string[] | object[]",
|
|
23
|
+
description:
|
|
24
|
+
"Post author(s) for blog/changelog content — a name, or objects with a name plus optional avatar/url and any extra fields. Preserved as-is.",
|
|
25
|
+
},
|
|
21
26
|
slug: { type: "string", description: "Override the generated slug." },
|
|
22
27
|
draft: {
|
|
23
28
|
type: "boolean",
|
|
@@ -64,8 +69,7 @@ search:
|
|
|
64
69
|
|
|
65
70
|
## Changelog
|
|
66
71
|
|
|
67
|
-
Changelog entries (`type: changelog`) accept an optional `changelog` object for
|
|
68
|
-
richer feed and display metadata:
|
|
72
|
+
Changelog entries (`type: changelog`) accept an optional `changelog` object for richer feed and display metadata:
|
|
69
73
|
|
|
70
74
|
```yaml lineNumbers
|
|
71
75
|
type: changelog
|
|
@@ -75,8 +79,6 @@ changelog:
|
|
|
75
79
|
category: Features
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
`date` may live here or at the top level — both feed the
|
|
79
|
-
[changelog RSS feed](/docs/content#feeds). See [Changelog](/docs/advanced/changelog) for the
|
|
80
|
-
generated timeline page and feed.
|
|
82
|
+
`date` may live here or at the top level — both feed the [changelog RSS feed](/docs/content#feeds). See [Changelog](/docs/advanced/changelog) for the generated timeline page and feed.
|
|
81
83
|
|
|
82
84
|
Schemas are exported from `blume/schema` for editor and migration tooling.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blume",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Documentation that's fast, AI-ready, and zero-config.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"./components": "./src/components/index.ts",
|
|
51
51
|
"./components/*": "./src/components/*",
|
|
52
52
|
"./core/*": "./src/core/*",
|
|
53
|
+
"./openapi/*": "./src/openapi/*",
|
|
53
54
|
"./sources/*": "./src/core/sources/*",
|
|
54
55
|
"./theme/*": "./src/theme/*",
|
|
55
56
|
"./package.json": "./package.json"
|
|
@@ -69,10 +70,19 @@
|
|
|
69
70
|
"@astrojs/node": "^11.0.0",
|
|
70
71
|
"@astrojs/react": "^6.0.0",
|
|
71
72
|
"@astrojs/vercel": "^11.0.0",
|
|
73
|
+
"@iconify-json/fa6-brands": "^1.2.6",
|
|
74
|
+
"@iconify-json/fa6-regular": "^1.2.4",
|
|
75
|
+
"@iconify-json/fa6-solid": "^1.2.4",
|
|
76
|
+
"@iconify-json/lucide": "^1.2.115",
|
|
77
|
+
"@iconify-json/tabler": "^1.2.35",
|
|
78
|
+
"@iconify/types": "^2.0.0",
|
|
79
|
+
"@iconify/utils": "^3.1.3",
|
|
72
80
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
73
81
|
"@orama/orama": "^3.1.18",
|
|
74
82
|
"@pierre/diffs": "^1.2.11",
|
|
75
83
|
"@scalar/astro": "^0.4.5",
|
|
84
|
+
"@scalar/openapi-parser": "^0.28.8",
|
|
85
|
+
"@scalar/openapi-types": "^0.9.1",
|
|
76
86
|
"@shikijs/transformers": "^4.2.0",
|
|
77
87
|
"@shikijs/twoslash": "^4.2.0",
|
|
78
88
|
"@tailwindcss/typography": "^0.5.20",
|
package/src/astro/generate.ts
CHANGED
|
@@ -36,11 +36,9 @@ import type { ResolvedConfig } from "../core/schema.ts";
|
|
|
36
36
|
import { resolveTsconfigAliases } from "../core/tsconfig-aliases.ts";
|
|
37
37
|
import type { Navigation } from "../core/types.ts";
|
|
38
38
|
import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
referenceTabs,
|
|
43
|
-
} from "../openapi/scalar.ts";
|
|
39
|
+
import { hasScalarReferences, referenceTabs } from "../openapi/references.ts";
|
|
40
|
+
import { buildReferenceFiles } from "../openapi/scalar.ts";
|
|
41
|
+
import { isOpenApiSource } from "../openapi/source.ts";
|
|
44
42
|
import { registry } from "../registry/registry.ts";
|
|
45
43
|
import { buildSearchDocuments } from "../search/documents.ts";
|
|
46
44
|
import { searchProviderMeta, servesStaticIndex } from "../search/providers.ts";
|
|
@@ -582,8 +580,9 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
582
580
|
|
|
583
581
|
const { i18n } = config;
|
|
584
582
|
|
|
585
|
-
// API reference routes
|
|
586
|
-
//
|
|
583
|
+
// API reference routes surface as header tabs alongside the content-derived
|
|
584
|
+
// ones (Blume-rendered references also own a tab-scoped sidebar of operations),
|
|
585
|
+
// so the reference stays discoverable in every locale.
|
|
587
586
|
const withReferenceTabs = (nav: Navigation): Navigation => ({
|
|
588
587
|
...nav,
|
|
589
588
|
repoUrl: config.navigation.repo && repoUrl ? repoUrl : null,
|
|
@@ -650,6 +649,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
650
649
|
})),
|
|
651
650
|
}
|
|
652
651
|
: null,
|
|
652
|
+
icons: config.icons,
|
|
653
653
|
imageZoom: config.markdown.imageZoom,
|
|
654
654
|
logo: resolveLogo(project),
|
|
655
655
|
mcp: config.mcp.enabled
|
|
@@ -912,6 +912,7 @@ export const generateRuntime = async (
|
|
|
912
912
|
const themePath = join(srcDir, "generated", "app.css");
|
|
913
913
|
const searchClientPath = join(srcDir, "generated", "search-client.ts");
|
|
914
914
|
const examplesPath = join(srcDir, "generated", "examples.ts");
|
|
915
|
+
const openapiPath = join(srcDir, "generated", "openapi.json");
|
|
915
916
|
|
|
916
917
|
// Record every file this pass writes so orphans (from a now-disabled feature)
|
|
917
918
|
// can be pruned afterwards. `write` wraps the atomic writer and tracks paths.
|
|
@@ -987,6 +988,7 @@ export const generateRuntime = async (
|
|
|
987
988
|
needsReact,
|
|
988
989
|
needsSvelte,
|
|
989
990
|
needsVue,
|
|
991
|
+
openapiPath,
|
|
990
992
|
pages,
|
|
991
993
|
searchClientPath,
|
|
992
994
|
themePath,
|
|
@@ -1235,7 +1237,7 @@ export const generateRuntime = async (
|
|
|
1235
1237
|
// React ships with Blume; Vue/Svelte islands need their Astro integration
|
|
1236
1238
|
// installed by the project. Warn early rather than let Vite fail to resolve it.
|
|
1237
1239
|
warnings.push(...islandFrameworkWarnings(frameworks, context.root));
|
|
1238
|
-
if (
|
|
1240
|
+
if (hasScalarReferences(config)) {
|
|
1239
1241
|
const references = await buildReferenceFiles({
|
|
1240
1242
|
config,
|
|
1241
1243
|
contentRoutes: new Set(project.graph.pages.map((page) => page.route)),
|
|
@@ -1254,6 +1256,14 @@ export const generateRuntime = async (
|
|
|
1254
1256
|
join(srcDir, "generated", "data.json"),
|
|
1255
1257
|
buildRuntimeData(project)
|
|
1256
1258
|
);
|
|
1259
|
+
// The parsed OpenAPI specs behind the `blume:openapi` alias. Always written
|
|
1260
|
+
// (even as `{}`) so the alias resolves whether or not a reference is enabled;
|
|
1261
|
+
// the source parsed the specs during the scan, so this is just serialization.
|
|
1262
|
+
const openApiSource = project.sources.find(isOpenApiSource);
|
|
1263
|
+
await write(
|
|
1264
|
+
openapiPath,
|
|
1265
|
+
`${JSON.stringify(openApiSource ? openApiSource.openApiData() : {})}\n`
|
|
1266
|
+
);
|
|
1257
1267
|
await write(
|
|
1258
1268
|
join(out, "blume.manifest.json"),
|
|
1259
1269
|
`${JSON.stringify(project.manifest, null, 2)}\n`
|
package/src/astro/integration.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
2
3
|
|
|
3
4
|
import type { AstroIntegration } from "astro";
|
|
4
5
|
|
|
6
|
+
import type { AssetMount } from "../core/assets.ts";
|
|
5
7
|
import { enrichDiagnostic } from "../core/diagnostics.ts";
|
|
6
8
|
import type { Diagnostic } from "../core/types.ts";
|
|
7
9
|
import { markdownVariantUrl, prefersMarkdown } from "./markdown-negotiation.ts";
|
|
10
|
+
import { copyAssetMounts, serveAssetMounts } from "./static-assets.ts";
|
|
8
11
|
|
|
9
12
|
/** The dev server's HMR channel — either `.ws` (Vite ≤5) or `.hot` (Vite 6+). */
|
|
10
13
|
interface OverlayChannel {
|
|
@@ -72,6 +75,13 @@ export interface BlumeIntegrationOptions {
|
|
|
72
75
|
pages: BlumePageRoute[];
|
|
73
76
|
/** Page routes that have a raw-Markdown variant (the content manifest). */
|
|
74
77
|
contentRoutes: string[];
|
|
78
|
+
/** Configured `deployment.base`, stripped from dev URLs before matching. */
|
|
79
|
+
base?: string;
|
|
80
|
+
/**
|
|
81
|
+
* `content.assets` mounts: top-level dirs served at the site root in dev and
|
|
82
|
+
* copied into the build output, so root-served assets need no relocation.
|
|
83
|
+
*/
|
|
84
|
+
assets?: AssetMount[];
|
|
75
85
|
}
|
|
76
86
|
|
|
77
87
|
/**
|
|
@@ -83,13 +93,13 @@ export interface BlumeIntegrationOptions {
|
|
|
83
93
|
* keep serving HTML.
|
|
84
94
|
*/
|
|
85
95
|
const negotiateMarkdown =
|
|
86
|
-
(routes: ReadonlySet<string
|
|
96
|
+
(routes: ReadonlySet<string>, base?: string) =>
|
|
87
97
|
(req: IncomingMessage, res: ServerResponse, next: () => void): void => {
|
|
88
98
|
if (
|
|
89
99
|
(req.method === "GET" || req.method === "HEAD") &&
|
|
90
100
|
prefersMarkdown(req.headers.accept)
|
|
91
101
|
) {
|
|
92
|
-
const variant = markdownVariantUrl(req.url, routes);
|
|
102
|
+
const variant = markdownVariantUrl(req.url, routes, base);
|
|
93
103
|
if (variant) {
|
|
94
104
|
res.setHeader("Vary", "Accept");
|
|
95
105
|
req.url = variant;
|
|
@@ -108,6 +118,12 @@ export const blumeIntegration = (
|
|
|
108
118
|
options: BlumeIntegrationOptions
|
|
109
119
|
): AstroIntegration => ({
|
|
110
120
|
hooks: {
|
|
121
|
+
"astro:build:done": async ({ dir }) => {
|
|
122
|
+
// Copy in-place asset mounts into the build output, mirroring publicDir.
|
|
123
|
+
if (options.assets?.length) {
|
|
124
|
+
await copyAssetMounts(options.assets, fileURLToPath(dir));
|
|
125
|
+
}
|
|
126
|
+
},
|
|
111
127
|
"astro:config:setup": ({ injectRoute }) => {
|
|
112
128
|
for (const page of options.pages) {
|
|
113
129
|
injectRoute({
|
|
@@ -124,9 +140,16 @@ export const blumeIntegration = (
|
|
|
124
140
|
// Prepend so the rewrite happens before Astro's own request handler,
|
|
125
141
|
// letting the rewritten URL resolve to the `.md` endpoint.
|
|
126
142
|
server.middlewares.stack.unshift({
|
|
127
|
-
handle: negotiateMarkdown(new Set(options.contentRoutes)),
|
|
143
|
+
handle: negotiateMarkdown(new Set(options.contentRoutes), options.base),
|
|
128
144
|
route: "",
|
|
129
145
|
});
|
|
146
|
+
// Serve `content.assets` mounts (Astro only serves publicDir in dev).
|
|
147
|
+
if (options.assets?.length) {
|
|
148
|
+
server.middlewares.stack.unshift({
|
|
149
|
+
handle: serveAssetMounts(options.assets),
|
|
150
|
+
route: "",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
130
153
|
},
|
|
131
154
|
},
|
|
132
155
|
name: "blume",
|
package/src/astro/islands.ts
CHANGED
|
@@ -105,9 +105,13 @@ export const discoverIslands = async (
|
|
|
105
105
|
continue;
|
|
106
106
|
}
|
|
107
107
|
const name = base.replace(ISLAND_FILE, "");
|
|
108
|
-
|
|
108
|
+
// The name is used verbatim as both an MDX tag and an unquoted object key
|
|
109
|
+
// in the generated island map, so it must be a plain PascalCase identifier
|
|
110
|
+
// — a `-`, `.`, or space (e.g. `Time-Picker.tsx`) would otherwise emit a
|
|
111
|
+
// syntax-error module and fail the whole build with no pointer to the file.
|
|
112
|
+
if (!/^[A-Z][A-Za-z0-9_]*$/u.test(name)) {
|
|
109
113
|
warnings.push(
|
|
110
|
-
`Island "${file}" must have a PascalCase filename to be used in MDX (e.g. Counter.tsx → <Counter />); skipping it.`
|
|
114
|
+
`Island "${file}" must have a PascalCase identifier filename to be used in MDX (letters, digits, and underscores only, e.g. Counter.tsx → <Counter />); skipping it.`
|
|
111
115
|
);
|
|
112
116
|
continue;
|
|
113
117
|
}
|
|
@@ -50,7 +50,8 @@ export const prefersMarkdown = (accept: string | null | undefined): boolean => {
|
|
|
50
50
|
*/
|
|
51
51
|
export const markdownVariantUrl = (
|
|
52
52
|
rawUrl: string | null | undefined,
|
|
53
|
-
routes: ReadonlySet<string
|
|
53
|
+
routes: ReadonlySet<string>,
|
|
54
|
+
base?: string
|
|
54
55
|
): string | null => {
|
|
55
56
|
if (!rawUrl) {
|
|
56
57
|
return null;
|
|
@@ -58,11 +59,24 @@ export const markdownVariantUrl = (
|
|
|
58
59
|
const queryIndex = rawUrl.indexOf("?");
|
|
59
60
|
const query = queryIndex === -1 ? "" : rawUrl.slice(queryIndex);
|
|
60
61
|
const rawPath = queryIndex === -1 ? rawUrl : rawUrl.slice(0, queryIndex);
|
|
62
|
+
|
|
63
|
+
// A non-root `deployment.base` prefixes the dev-server URL but not the logical
|
|
64
|
+
// content routes, so strip it before matching and re-add it to the variant.
|
|
65
|
+
const prefix = base && base !== "/" ? base.replace(/\/$/u, "") : "";
|
|
66
|
+
let path = rawPath;
|
|
67
|
+
if (prefix) {
|
|
68
|
+
if (path === prefix || path.startsWith(`${prefix}/`)) {
|
|
69
|
+
path = path.slice(prefix.length) || "/";
|
|
70
|
+
} else {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
const pathname =
|
|
62
|
-
|
|
76
|
+
path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
63
77
|
if (!routes.has(pathname)) {
|
|
64
78
|
return null;
|
|
65
79
|
}
|
|
66
80
|
const target = pathname === "/" ? "/index" : pathname;
|
|
67
|
-
return `${target}.md${query}`;
|
|
81
|
+
return `${prefix}${target}.md${query}`;
|
|
68
82
|
};
|
package/src/astro/pages.ts
CHANGED
|
@@ -21,7 +21,12 @@ export const discoverPages = async (
|
|
|
21
21
|
return files.map((file) => {
|
|
22
22
|
const rel = relative(pagesRoot, file);
|
|
23
23
|
const withoutExt = rel.slice(0, rel.length - extname(rel).length);
|
|
24
|
-
const parts = withoutExt.split("/")
|
|
24
|
+
const parts = withoutExt.split("/");
|
|
25
|
+
// Only a trailing `index` maps to its parent dir; a folder literally named
|
|
26
|
+
// `index` (e.g. `index/foo.astro`) must keep its segment.
|
|
27
|
+
if (parts.at(-1) === "index") {
|
|
28
|
+
parts.pop();
|
|
29
|
+
}
|
|
25
30
|
const pattern = parts.length === 0 ? "/" : `/${parts.join("/")}`;
|
|
26
31
|
return { entrypoint: file, pattern };
|
|
27
32
|
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
2
|
+
import type { Stats } from "node:fs";
|
|
3
|
+
import { cp } from "node:fs/promises";
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
5
|
+
|
|
6
|
+
import { extname, join, relative, resolve, sep } from "pathe";
|
|
7
|
+
|
|
8
|
+
import type { AssetMount } from "../core/assets.ts";
|
|
9
|
+
|
|
10
|
+
/** Content types for the asset extensions a docs project commonly serves. */
|
|
11
|
+
const MIME_TYPES: Record<string, string> = {
|
|
12
|
+
".avif": "image/avif",
|
|
13
|
+
".css": "text/css; charset=utf-8",
|
|
14
|
+
".gif": "image/gif",
|
|
15
|
+
".ico": "image/x-icon",
|
|
16
|
+
".jpeg": "image/jpeg",
|
|
17
|
+
".jpg": "image/jpeg",
|
|
18
|
+
".js": "text/javascript; charset=utf-8",
|
|
19
|
+
".json": "application/json; charset=utf-8",
|
|
20
|
+
".mp4": "video/mp4",
|
|
21
|
+
".otf": "font/otf",
|
|
22
|
+
".pdf": "application/pdf",
|
|
23
|
+
".png": "image/png",
|
|
24
|
+
".svg": "image/svg+xml",
|
|
25
|
+
".ttf": "font/ttf",
|
|
26
|
+
".txt": "text/plain; charset=utf-8",
|
|
27
|
+
".webm": "video/webm",
|
|
28
|
+
".webp": "image/webp",
|
|
29
|
+
".woff": "font/woff",
|
|
30
|
+
".woff2": "font/woff2",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const mimeType = (file: string): string =>
|
|
34
|
+
MIME_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
35
|
+
|
|
36
|
+
/** Whether `child` is `parent` itself or a path nested under it. */
|
|
37
|
+
const isContained = (parent: string, child: string): boolean => {
|
|
38
|
+
const rel = relative(parent, child);
|
|
39
|
+
return rel === "" || (!rel.startsWith("..") && !rel.startsWith(sep));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Resolve a request URL to an on-disk file within one of the mounts, if any. */
|
|
43
|
+
const resolveRequest = (url: string, mounts: AssetMount[]): string | null => {
|
|
44
|
+
const pathname = decodeURIComponent(
|
|
45
|
+
(url.split("?")[0] ?? "").split("#")[0] ?? ""
|
|
46
|
+
);
|
|
47
|
+
for (const mount of mounts) {
|
|
48
|
+
if (pathname !== mount.url && !pathname.startsWith(`${mount.url}/`)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// `.` + rel keeps the join relative so an absolute-looking suffix can't
|
|
52
|
+
// escape the mount; the containment check rejects `..` traversal outright.
|
|
53
|
+
const file = resolve(mount.dir, `.${pathname.slice(mount.url.length)}`);
|
|
54
|
+
if (isContained(mount.dir, file)) {
|
|
55
|
+
return file;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** `statSync` that returns null instead of throwing on a missing file. */
|
|
62
|
+
const statFile = (file: string): Stats | null => {
|
|
63
|
+
try {
|
|
64
|
+
return statSync(file);
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A dev-server middleware that serves `content.assets` mounts (top-level dirs
|
|
72
|
+
* kept in place, e.g. a migrated `images/`) at their site URL. Astro only serves
|
|
73
|
+
* `publicDir` in dev, so without this those references would 404. Non-matching
|
|
74
|
+
* requests, and any path that isn't a real file, fall through to Astro. Stat is
|
|
75
|
+
* synchronous — this is dev-only middleware and mirrors how sirv serves statics.
|
|
76
|
+
*/
|
|
77
|
+
export const serveAssetMounts =
|
|
78
|
+
(mounts: AssetMount[]) =>
|
|
79
|
+
(req: IncomingMessage, res: ServerResponse, next: () => void): void => {
|
|
80
|
+
if ((req.method !== "GET" && req.method !== "HEAD") || !req.url) {
|
|
81
|
+
next();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const file = resolveRequest(req.url, mounts);
|
|
85
|
+
const stats = file ? statFile(file) : null;
|
|
86
|
+
if (!(file && stats?.isFile())) {
|
|
87
|
+
next();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
res.setHeader("Content-Type", mimeType(file));
|
|
91
|
+
res.setHeader("Content-Length", String(stats.size));
|
|
92
|
+
if (req.method === "HEAD") {
|
|
93
|
+
res.end();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
createReadStream(file).pipe(res);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Copy every asset mount into the build output, mirroring what Astro does with
|
|
101
|
+
* `publicDir`. Runs on `astro:build:done` so in-place asset dirs ship in the
|
|
102
|
+
* final `dist/`. A missing source dir is skipped (it may be referenced but
|
|
103
|
+
* absent); `cp` handles both directories and single files.
|
|
104
|
+
*/
|
|
105
|
+
export const copyAssetMounts = async (
|
|
106
|
+
mounts: AssetMount[],
|
|
107
|
+
outDir: string
|
|
108
|
+
): Promise<void> => {
|
|
109
|
+
for (const mount of mounts) {
|
|
110
|
+
if (!existsSync(mount.dir)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const dest = join(outDir, mount.url.replace(/^\/+/u, ""));
|
|
114
|
+
// oxlint-disable-next-line no-await-in-loop -- sequential fs copies
|
|
115
|
+
await cp(mount.dir, dest, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
};
|