blume 0.2.0 → 0.4.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 +2429 -792
- package/dist/cli/index.js.map +63 -44
- package/dist/types/core/data.d.ts +16 -0
- package/dist/types/core/define-components.d.ts +9 -2
- package/dist/types/core/diagnostics.d.ts +5 -0
- package/dist/types/core/schema.d.ts +313 -778
- package/dist/types/core/types.d.ts +2 -2
- package/dist/types/migrate/mintlify/assets.d.ts +8 -0
- package/docs/01-quickstart.mdx +5 -16
- package/docs/02-deployment.mdx +26 -40
- package/docs/advanced/api-reference.mdx +10 -37
- package/docs/advanced/blog.mdx +9 -25
- package/docs/advanced/changelog.mdx +10 -33
- package/docs/advanced/custom-pages.mdx +66 -61
- package/docs/configuration/ai.mdx +47 -91
- package/docs/configuration/analytics.mdx +20 -38
- package/docs/configuration/customization.mdx +92 -27
- package/docs/configuration/export.mdx +9 -34
- package/docs/configuration/index.mdx +78 -85
- package/docs/configuration/search.mdx +17 -54
- package/docs/configuration/seo.mdx +18 -44
- package/docs/configuration/theming.mdx +20 -42
- package/docs/content/components.mdx +42 -101
- package/docs/content/i18n.mdx +21 -72
- package/docs/content/index.mdx +18 -48
- package/docs/content/islands.mdx +79 -33
- package/docs/content/meta.mdx +23 -50
- package/docs/content/navigation.mdx +42 -56
- package/docs/content/sources.mdx +20 -83
- package/docs/content/syntax.mdx +37 -105
- package/docs/index.mdx +13 -51
- package/docs/reference/cli.mdx +49 -18
- package/docs/reference/frontmatter.mdx +2 -5
- package/package.json +3 -1
- package/src/ai/ask-context.ts +131 -0
- package/src/ai/ask-data.ts +25 -0
- package/src/astro/component-slots.ts +165 -0
- package/src/astro/generate.ts +132 -13
- package/src/astro/integration.ts +85 -3
- package/src/astro/islands.ts +6 -2
- package/src/astro/markdown-negotiation.ts +17 -3
- package/src/astro/pages.ts +11 -13
- package/src/astro/static-assets.ts +117 -0
- package/src/astro/templates.ts +120 -50
- package/src/blume-modules.d.ts +25 -0
- package/src/cli/args.ts +23 -0
- package/src/cli/commands/build.ts +209 -1
- package/src/cli/commands/check.ts +62 -0
- package/src/cli/commands/dev.ts +32 -3
- package/src/cli/commands/doctor.ts +32 -6
- package/src/cli/commands/eject.ts +3 -1
- package/src/cli/commands/init.ts +184 -16
- package/src/cli/commands/preview.ts +2 -1
- package/src/cli/commands/validate.ts +27 -2
- package/src/cli/dev-lock.ts +84 -0
- package/src/cli/index.ts +15 -0
- package/src/cli/internal-error.ts +63 -0
- package/src/cli/log.ts +41 -1
- package/src/cli/prepare.ts +17 -3
- package/src/cli/required-secrets.ts +44 -0
- package/src/components/BlumePage.astro +109 -0
- package/src/components/content/YouTube.astro +35 -0
- package/src/components/content/youtube.ts +46 -0
- package/src/components/index.ts +3 -3
- package/src/components/islands/ask-ai.tsx +29 -15
- package/src/components/islands/hooks.ts +188 -0
- package/src/components/layout/Empty.astro +6 -0
- package/src/components/layout/Header.astro +24 -39
- package/src/components/layout/Logo.astro +50 -0
- package/src/components/layout/NavSelector.astro +75 -0
- package/src/components/layout/PageLayout.astro +38 -2
- package/src/components/layout/RootLayout.astro +70 -4
- package/src/components/layout/hydration-hint.ts +30 -0
- package/src/components/layout/overrides.ts +6 -4
- package/src/components/props.ts +71 -0
- package/src/core/assets.ts +31 -0
- package/src/core/bridge.ts +10 -0
- package/src/core/builtin-tags.ts +40 -0
- package/src/core/component-diagnostics.ts +44 -0
- package/src/core/component-overrides.ts +478 -0
- package/src/core/config.ts +8 -0
- package/src/core/data.ts +14 -0
- package/src/core/define-components.ts +9 -2
- package/src/core/diagnostics.ts +95 -1
- package/src/core/gitignore.ts +30 -0
- package/src/core/graph.ts +7 -0
- package/src/core/links.ts +60 -19
- package/src/core/nav-diagnostics.ts +205 -0
- package/src/core/project-graph.ts +40 -1
- package/src/core/schema.ts +35 -96
- package/src/core/sources/mdx-remote.ts +54 -8
- package/src/core/sources/normalize.ts +57 -1
- package/src/core/sources/notion.ts +49 -5
- package/src/core/sources/sanity.ts +5 -1
- package/src/core/types.ts +2 -2
- package/src/deploy/redirects.ts +43 -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 +1 -176
- package/src/migrate/mintlify/index.ts +53 -45
- package/src/migrate/shared.ts +12 -27
- package/src/migrate/starlight/config.ts +0 -4
- package/src/og/card.ts +175 -38
- package/src/registry/eject.ts +52 -12
- package/src/registry/registry.ts +172 -0
- package/src/registry/rewrite-imports.ts +31 -19
- package/src/runtime/index.ts +61 -0
- 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/palette.ts +26 -7
- package/src/vite-env.d.ts +14 -0
package/src/astro/integration.ts
CHANGED
|
@@ -1,8 +1,67 @@
|
|
|
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";
|
|
7
|
+
import { enrichDiagnostic } from "../core/diagnostics.ts";
|
|
8
|
+
import type { Diagnostic } from "../core/types.ts";
|
|
5
9
|
import { markdownVariantUrl, prefersMarkdown } from "./markdown-negotiation.ts";
|
|
10
|
+
import { copyAssetMounts, serveAssetMounts } from "./static-assets.ts";
|
|
11
|
+
|
|
12
|
+
/** The dev server's HMR channel — either `.ws` (Vite ≤5) or `.hot` (Vite 6+). */
|
|
13
|
+
interface OverlayChannel {
|
|
14
|
+
send: (payload: unknown) => void;
|
|
15
|
+
}
|
|
16
|
+
interface OverlayServer {
|
|
17
|
+
hot?: OverlayChannel;
|
|
18
|
+
ws?: OverlayChannel;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Set on `astro:server:setup`; read by `showBlumeErrorOverlay` so the CLI's
|
|
22
|
+
// regeneration can push Blume diagnostics into Vite's browser error overlay.
|
|
23
|
+
// Same-process module singleton (dev and the integration share the instance).
|
|
24
|
+
let overlayServer: OverlayServer | null = null;
|
|
25
|
+
|
|
26
|
+
const overlayChannel = (): OverlayChannel | undefined =>
|
|
27
|
+
overlayServer?.ws ?? overlayServer?.hot;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Surface Blume's own diagnostics (config/frontmatter/content errors) in the
|
|
31
|
+
* Vite/Astro browser error overlay during `blume dev`, so they don't hide in the
|
|
32
|
+
* terminal. A no-op when there are no errors or the dev server isn't up. The
|
|
33
|
+
* overlay clears itself on the next successful HMR update.
|
|
34
|
+
*/
|
|
35
|
+
export const showBlumeErrorOverlay = (diagnostics: Diagnostic[]): void => {
|
|
36
|
+
const errors = diagnostics
|
|
37
|
+
.filter((diagnostic) => diagnostic.severity === "error")
|
|
38
|
+
.map(enrichDiagnostic);
|
|
39
|
+
const channel = overlayChannel();
|
|
40
|
+
if (errors.length === 0 || !channel) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const body = errors
|
|
44
|
+
.map((diagnostic) => {
|
|
45
|
+
const where = diagnostic.file
|
|
46
|
+
? `\n at ${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}` : ""}`
|
|
47
|
+
: "";
|
|
48
|
+
const fix = diagnostic.suggestion
|
|
49
|
+
? `\n fix: ${diagnostic.suggestion}`
|
|
50
|
+
: "";
|
|
51
|
+
const docs = diagnostic.docsUrl ? `\n docs: ${diagnostic.docsUrl}` : "";
|
|
52
|
+
return `[${diagnostic.code}] ${diagnostic.message}${where}${fix}${docs}`;
|
|
53
|
+
})
|
|
54
|
+
.join("\n\n");
|
|
55
|
+
channel.send({
|
|
56
|
+
err: {
|
|
57
|
+
id: errors[0]?.file,
|
|
58
|
+
message: `Blume found ${errors.length} error(s):\n\n${body}`,
|
|
59
|
+
plugin: "blume",
|
|
60
|
+
stack: "",
|
|
61
|
+
},
|
|
62
|
+
type: "error",
|
|
63
|
+
});
|
|
64
|
+
};
|
|
6
65
|
|
|
7
66
|
/** A user page mounted into the generated runtime. */
|
|
8
67
|
export interface BlumePageRoute {
|
|
@@ -16,6 +75,13 @@ export interface BlumeIntegrationOptions {
|
|
|
16
75
|
pages: BlumePageRoute[];
|
|
17
76
|
/** Page routes that have a raw-Markdown variant (the content manifest). */
|
|
18
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[];
|
|
19
85
|
}
|
|
20
86
|
|
|
21
87
|
/**
|
|
@@ -27,13 +93,13 @@ export interface BlumeIntegrationOptions {
|
|
|
27
93
|
* keep serving HTML.
|
|
28
94
|
*/
|
|
29
95
|
const negotiateMarkdown =
|
|
30
|
-
(routes: ReadonlySet<string
|
|
96
|
+
(routes: ReadonlySet<string>, base?: string) =>
|
|
31
97
|
(req: IncomingMessage, res: ServerResponse, next: () => void): void => {
|
|
32
98
|
if (
|
|
33
99
|
(req.method === "GET" || req.method === "HEAD") &&
|
|
34
100
|
prefersMarkdown(req.headers.accept)
|
|
35
101
|
) {
|
|
36
|
-
const variant = markdownVariantUrl(req.url, routes);
|
|
102
|
+
const variant = markdownVariantUrl(req.url, routes, base);
|
|
37
103
|
if (variant) {
|
|
38
104
|
res.setHeader("Vary", "Accept");
|
|
39
105
|
req.url = variant;
|
|
@@ -52,6 +118,12 @@ export const blumeIntegration = (
|
|
|
52
118
|
options: BlumeIntegrationOptions
|
|
53
119
|
): AstroIntegration => ({
|
|
54
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
|
+
},
|
|
55
127
|
"astro:config:setup": ({ injectRoute }) => {
|
|
56
128
|
for (const page of options.pages) {
|
|
57
129
|
injectRoute({
|
|
@@ -62,12 +134,22 @@ export const blumeIntegration = (
|
|
|
62
134
|
}
|
|
63
135
|
},
|
|
64
136
|
"astro:server:setup": ({ server }) => {
|
|
137
|
+
// Keep a handle on the dev server so Blume diagnostics can be pushed to
|
|
138
|
+
// its browser error overlay (see `showBlumeErrorOverlay`).
|
|
139
|
+
overlayServer = server as unknown as OverlayServer;
|
|
65
140
|
// Prepend so the rewrite happens before Astro's own request handler,
|
|
66
141
|
// letting the rewritten URL resolve to the `.md` endpoint.
|
|
67
142
|
server.middlewares.stack.unshift({
|
|
68
|
-
handle: negotiateMarkdown(new Set(options.contentRoutes)),
|
|
143
|
+
handle: negotiateMarkdown(new Set(options.contentRoutes), options.base),
|
|
69
144
|
route: "",
|
|
70
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
|
+
}
|
|
71
153
|
},
|
|
72
154
|
},
|
|
73
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
|
});
|
|
@@ -45,10 +50,8 @@ export const routeIsTaken = (
|
|
|
45
50
|
export interface OgCustomRoute {
|
|
46
51
|
/** `og/<slug>.png` path segment; `index` for the site root. */
|
|
47
52
|
slug: string;
|
|
48
|
-
/** Card
|
|
53
|
+
/** Card headline. */
|
|
49
54
|
title: string;
|
|
50
|
-
/** Small eyebrow line above the title. */
|
|
51
|
-
eyebrow?: string;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
/** Skip private (`_partial`, `.well-known`) and Astro dynamic (`[param]`) parts. */
|
|
@@ -68,13 +71,12 @@ const humanizeSegment = (segment: string): string =>
|
|
|
68
71
|
*
|
|
69
72
|
* Dynamic (`[param]`) routes and private segments (`_partials`, `.well-known`)
|
|
70
73
|
* are skipped: they aren't shareable pages. The home is titled with the site
|
|
71
|
-
* title
|
|
72
|
-
*
|
|
74
|
+
* title; a deeper page is titled from its last path segment. The card's brand
|
|
75
|
+
* lockup, description, and footer come from the resolved config at render time.
|
|
73
76
|
*/
|
|
74
77
|
export const customOgRoutes = (
|
|
75
78
|
pages: BlumePageRoute[],
|
|
76
|
-
siteTitle: string
|
|
77
|
-
siteDescription?: string
|
|
79
|
+
siteTitle: string
|
|
78
80
|
): OgCustomRoute[] => {
|
|
79
81
|
const seen = new Set<string>();
|
|
80
82
|
const routes: OgCustomRoute[] = [];
|
|
@@ -91,11 +93,7 @@ export const customOgRoutes = (
|
|
|
91
93
|
}
|
|
92
94
|
seen.add(slug);
|
|
93
95
|
const last = segments.at(-1);
|
|
94
|
-
routes.push(
|
|
95
|
-
last
|
|
96
|
-
? { eyebrow: siteTitle, slug, title: humanizeSegment(last) }
|
|
97
|
-
: { eyebrow: siteDescription, slug, title: siteTitle }
|
|
98
|
-
);
|
|
96
|
+
routes.push({ slug, title: last ? humanizeSegment(last) : siteTitle });
|
|
99
97
|
}
|
|
100
98
|
return routes;
|
|
101
99
|
};
|
|
@@ -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
|
+
};
|