blume 1.5.3 → 1.6.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.
- package/CHANGELOG.md +94 -0
- package/dist/cli/index.js +3949 -1403
- package/dist/cli/index.js.map +111 -96
- package/dist/types/ai/component-markdown.d.ts +79 -0
- package/dist/types/components/layout/nav-utils.d.ts +60 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +206 -4
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +33 -2
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +10 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +122 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +29 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +26 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/07-faq.mdx +9 -9
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +13 -4
- package/docs/advanced/custom-pages.mdx +4 -2
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +25 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +30 -3
- package/docs/configuration/theming.mdx +23 -0
- package/docs/content/components.mdx +15 -1
- package/docs/content/includes.mdx +68 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +42 -1
- package/docs/content/syntax.mdx +69 -1
- package/docs/content/versioning.mdx +15 -9
- package/docs/reference/cli.mdx +2 -1
- package/package.json +66 -57
- package/skills/blume-migrate/SKILL.md +16 -7
- package/skills/blume-migrate/references/docusaurus.md +5 -3
- package/skills/blume-migrate/references/fumadocs.md +10 -2
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +1 -1
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/ask-data.ts +2 -1
- package/src/ai/component-markdown.ts +199 -36
- package/src/ai/llms.ts +93 -6
- package/src/ai/markdown.ts +2 -2
- package/src/ai/mcp/discovery.ts +10 -2
- package/src/ai/mcp/server.ts +74 -2
- package/src/astro/examples.ts +29 -2
- package/src/astro/generate.ts +282 -177
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +10 -5
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/runtime-modules.ts +196 -0
- package/src/astro/templates.ts +365 -113
- package/src/cli/commands/build.ts +91 -16
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/host-args.ts +18 -0
- package/src/cli/index.ts +2 -1
- package/src/cli/init/questions.ts +1 -0
- package/src/cli/init/scaffold.ts +27 -4
- package/src/components/colors.ts +142 -0
- package/src/components/content/Badge.astro +5 -12
- package/src/components/content/Callout.astro +19 -36
- package/src/components/content/Card.astro +15 -21
- package/src/components/content/Component.astro +10 -1
- package/src/components/content/GithubInfo.astro +28 -9
- package/src/components/content/Tabs.astro +27 -5
- package/src/components/content/github-info.ts +20 -5
- package/src/components/copy-feedback.ts +93 -9
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/hooks.ts +3 -1
- package/src/components/layout/Fonts.astro +15 -8
- package/src/components/layout/Header.astro +44 -0
- package/src/components/layout/LanguageSwitcher.astro +9 -1
- package/src/components/layout/NavSelector.astro +12 -3
- package/src/components/layout/NavTree.astro +6 -18
- package/src/components/layout/PageActions.astro +54 -22
- package/src/components/layout/PageLayout.astro +2 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +42 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +30 -1
- package/src/components/openapi/ApiOverview.astro +13 -3
- package/src/components/openapi/AsyncApiOperation.astro +7 -14
- package/src/components/openapi/GraphqlChip.astro +33 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
- package/src/components/openapi/GraphqlOperation.astro +186 -0
- package/src/components/openapi/GraphqlType.astro +154 -0
- package/src/components/openapi/MethodBadge.astro +3 -14
- package/src/components/openapi/Operation.astro +12 -5
- package/src/components/openapi/OperationPanel.astro +43 -0
- package/src/components/openapi/RequestPanel.astro +5 -10
- package/src/components/openapi/Responses.astro +1 -16
- package/src/components/openapi/graphql-helpers.ts +466 -0
- package/src/components/openapi/playground-client.ts +15 -0
- package/src/components/openapi/sample-panels.ts +45 -0
- package/src/components/openapi/snippets.ts +13 -35
- package/src/core/base-path.ts +11 -0
- package/src/core/config-input.ts +209 -2
- package/src/core/config.ts +6 -4
- package/src/core/content-assets.ts +15 -4
- package/src/core/data.ts +28 -3
- package/src/core/define-components.ts +2 -0
- package/src/core/diagnostics.ts +8 -0
- package/src/core/frontmatter.ts +20 -8
- package/src/core/github.ts +71 -0
- package/src/core/graph.ts +22 -8
- package/src/core/heading-markers.ts +96 -0
- package/src/core/i18n-ui.ts +12 -0
- package/src/core/includes.ts +633 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/manifest.ts +10 -0
- package/src/core/meta.ts +2 -1
- package/src/core/nav-diagnostics.ts +11 -2
- package/src/core/navigation.ts +27 -6
- package/src/core/project-graph.ts +61 -9
- package/src/core/schema.ts +235 -36
- package/src/core/server-features.ts +5 -9
- package/src/core/sources/github-releases.ts +2 -2
- package/src/core/sources/normalize.ts +502 -115
- package/src/core/sources/notion.ts +43 -8
- package/src/core/sources/obsidian.ts +1038 -0
- package/src/core/sources/read.ts +36 -1
- package/src/core/sources/resolve.ts +34 -1
- package/src/core/sources/types.ts +28 -6
- package/src/core/sources/watch.ts +12 -8
- package/src/core/tsconfig-aliases.ts +48 -35
- package/src/core/types.ts +31 -2
- package/src/core/ui-packs/ar.ts +2 -0
- package/src/core/ui-packs/bg.ts +3 -0
- package/src/core/ui-packs/bn.ts +2 -0
- package/src/core/ui-packs/ca.ts +3 -0
- package/src/core/ui-packs/cs.ts +2 -0
- package/src/core/ui-packs/da.ts +2 -0
- package/src/core/ui-packs/de.ts +3 -0
- package/src/core/ui-packs/el.ts +3 -0
- package/src/core/ui-packs/es.ts +3 -0
- package/src/core/ui-packs/fa.ts +2 -0
- package/src/core/ui-packs/fi.ts +2 -0
- package/src/core/ui-packs/fr.ts +3 -0
- package/src/core/ui-packs/he.ts +2 -0
- package/src/core/ui-packs/hi.ts +2 -0
- package/src/core/ui-packs/hr.ts +3 -0
- package/src/core/ui-packs/hu.ts +3 -0
- package/src/core/ui-packs/id.ts +3 -0
- package/src/core/ui-packs/it.ts +2 -0
- package/src/core/ui-packs/ja.ts +3 -0
- package/src/core/ui-packs/ko.ts +3 -0
- package/src/core/ui-packs/nl.ts +3 -0
- package/src/core/ui-packs/no.ts +3 -0
- package/src/core/ui-packs/pl.ts +3 -0
- package/src/core/ui-packs/pt-br.ts +3 -0
- package/src/core/ui-packs/pt.ts +3 -0
- package/src/core/ui-packs/ro.ts +3 -0
- package/src/core/ui-packs/ru.ts +3 -0
- package/src/core/ui-packs/sk.ts +2 -0
- package/src/core/ui-packs/sr.ts +2 -0
- package/src/core/ui-packs/sv.ts +3 -0
- package/src/core/ui-packs/th.ts +2 -0
- package/src/core/ui-packs/tr.ts +3 -0
- package/src/core/ui-packs/uk.ts +3 -0
- package/src/core/ui-packs/vi.ts +2 -0
- package/src/core/ui-packs/zh-tw.ts +2 -0
- package/src/core/ui-packs/zh.ts +2 -0
- package/src/core/version-cut.ts +26 -6
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/deploy/vercel-negotiation.ts +49 -6
- package/src/eval/schema.ts +3 -1
- package/src/markdown/code-title.ts +22 -16
- package/src/markdown/features.ts +21 -0
- package/src/markdown/fence-meta.ts +50 -0
- package/src/markdown/heading-anchors.ts +198 -37
- package/src/markdown/include.ts +247 -0
- package/src/markdown/index.ts +43 -34
- package/src/markdown/language-icon.ts +2 -2
- package/src/markdown/mdast.ts +7 -3
- package/src/markdown/ts2js.ts +264 -0
- package/src/og/card.ts +1 -1
- package/src/openapi/asyncapi.ts +4 -1
- package/src/openapi/graphql-build.ts +293 -0
- package/src/openapi/graphql.ts +212 -0
- package/src/openapi/model.ts +38 -5
- package/src/openapi/parse.ts +34 -0
- package/src/openapi/proxy.ts +30 -5
- package/src/openapi/references.ts +97 -13
- package/src/openapi/render-mdx.ts +66 -12
- package/src/openapi/scalar.ts +5 -16
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +47 -17
- package/src/search/documents.ts +229 -37
- package/src/search/orama-index.ts +9 -5
- package/src/seo/jsonld.ts +293 -51
- package/src/theme/code-block-padding.ts +16 -0
- package/src/theme/entry.ts +67 -13
- package/src/theme/fonts.ts +189 -16
- package/src/theme/sources.ts +49 -0
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { builtinModules } from "node:module";
|
|
4
|
+
|
|
5
|
+
import { dirname, join, relative } from "pathe";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
import { packageRoot } from "../core/package-root.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Post-build audit of a Vercel serverless function bundle.
|
|
12
|
+
*
|
|
13
|
+
* The Vercel adapter traces the server entry with `@vercel/nft` and copies
|
|
14
|
+
* every file it reaches into `.vercel/output/functions/_render.func`. A bare
|
|
15
|
+
* import the trace cannot resolve — a package that isn't reachable by walking
|
|
16
|
+
* `node_modules` up from the chunk that imports it — is dropped *silently*,
|
|
17
|
+
* and the deployed function dies on its first request with
|
|
18
|
+
* `ERR_MODULE_NOT_FOUND` (Vercel reports it as `FUNCTION_INVOCATION_FAILED`).
|
|
19
|
+
* Under an isolated linker (pnpm, Bun's `isolated` mode) that is exactly the
|
|
20
|
+
* shape of Blume's own runtime dependencies: the SSR build leaves them
|
|
21
|
+
* external, and they live under `blume`'s store directory, not the project
|
|
22
|
+
* root. Nothing before deploy surfaces it — the build is green, a warm-cache
|
|
23
|
+
* preview is green — so the bundle is re-checked here with Node's own
|
|
24
|
+
* resolution rule: a package is present when some `node_modules/<name>` on
|
|
25
|
+
* the walk from the importing chunk up to the function root exists.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** A bare package import the function bundle cannot satisfy. */
|
|
29
|
+
export interface MissingPackage {
|
|
30
|
+
/** Function-relative paths of the chunks that import the package. */
|
|
31
|
+
importers: string[];
|
|
32
|
+
name: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One function directory's audit result. */
|
|
36
|
+
export interface FunctionBundleAudit {
|
|
37
|
+
/** Absolute path of the `*.func` directory. */
|
|
38
|
+
dir: string;
|
|
39
|
+
missing: MissingPackage[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const BUILTINS = new Set(builtinModules);
|
|
43
|
+
|
|
44
|
+
/** Specifier prefixes that never name an installed package. */
|
|
45
|
+
const NON_PACKAGE_PREFIXES = ["node:", "astro:", "virtual:", "data:", "\0"];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The package a bare specifier resolves through (`@scope/name` or `name`), or
|
|
49
|
+
* null for relative/absolute paths, Node builtins, and virtual ids.
|
|
50
|
+
*/
|
|
51
|
+
export const packageName = (specifier: string): string | null => {
|
|
52
|
+
if (
|
|
53
|
+
specifier.startsWith(".") ||
|
|
54
|
+
specifier.startsWith("/") ||
|
|
55
|
+
NON_PACKAGE_PREFIXES.some((prefix) => specifier.startsWith(prefix)) ||
|
|
56
|
+
BUILTINS.has(specifier)
|
|
57
|
+
) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const segments = specifier.split("/");
|
|
61
|
+
if (specifier.startsWith("@")) {
|
|
62
|
+
return segments.length >= 2 ? `${segments[0]}/${segments[1]}` : null;
|
|
63
|
+
}
|
|
64
|
+
return segments[0] || null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Static specifiers: a side-effect `import "x"` or an `import`/`export … from
|
|
69
|
+
* "x"` clause. Only `import` takes the bare-string form — `export "x"` is not
|
|
70
|
+
* syntax, so a runtime message quoting `export 'ALL'` must not match. The
|
|
71
|
+
* specifier quote must not be escaped either: a backslash-quoted `from \"zod\"`
|
|
72
|
+
* is a code sample serialized into a string (the MCP snapshot carries page
|
|
73
|
+
* Markdown), not module syntax.
|
|
74
|
+
*/
|
|
75
|
+
const STATIC_IMPORT =
|
|
76
|
+
/(?:^|[;\s}])(?:import\s*(?<!\\)["'](?<bare>[^"'\n]+)["']|(?:import|export)\s*[\w$*{},\s]*?\s*from\s*(?<!\\)["'](?<from>[^"'\n]+)["'])/gu;
|
|
77
|
+
|
|
78
|
+
/** Dynamic `import("…")` specifiers, same escaped-quote rule. */
|
|
79
|
+
const DYNAMIC_IMPORT =
|
|
80
|
+
/\bimport\(\s*(?<!\\)["'](?<dynamic>[^"'\n]+)["']\s*\)/gu;
|
|
81
|
+
|
|
82
|
+
/** Every bare package name a module's source imports. */
|
|
83
|
+
export const importedPackages = (source: string): string[] => {
|
|
84
|
+
const names = new Set<string>();
|
|
85
|
+
for (const pattern of [STATIC_IMPORT, DYNAMIC_IMPORT]) {
|
|
86
|
+
for (const match of source.matchAll(pattern)) {
|
|
87
|
+
const groups = match.groups ?? {};
|
|
88
|
+
const name = packageName(
|
|
89
|
+
groups.bare ?? groups.from ?? groups.dynamic ?? ""
|
|
90
|
+
);
|
|
91
|
+
if (name) {
|
|
92
|
+
names.add(name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return [...names];
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Whether `node_modules/<name>` exists on the walk from `from` up to `root`. */
|
|
100
|
+
const resolvable = (name: string, from: string, root: string): boolean => {
|
|
101
|
+
let dir = from;
|
|
102
|
+
for (;;) {
|
|
103
|
+
if (existsSync(join(dir, "node_modules", name, "package.json"))) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (dir === root) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const parent = dirname(dir);
|
|
110
|
+
if (parent === dir) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
dir = parent;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const MODULE_FILE = /\.(?:m?js|cjs)$/u;
|
|
118
|
+
|
|
119
|
+
/** The one `.vc-config.json` field the audit reads; the rest passes through. */
|
|
120
|
+
const vcConfigSchema = z.looseObject({ handler: z.string().optional() });
|
|
121
|
+
|
|
122
|
+
/** Every JavaScript module under `dir`, skipping any `node_modules`. */
|
|
123
|
+
const listModules = async (dir: string): Promise<string[]> => {
|
|
124
|
+
const files: string[] = [];
|
|
125
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
const path = join(dir, entry.name);
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
if (entry.name !== "node_modules") {
|
|
130
|
+
// oxlint-disable-next-line no-await-in-loop -- sequential walk keeps ordering deterministic
|
|
131
|
+
files.push(...(await listModules(path)));
|
|
132
|
+
}
|
|
133
|
+
} else if (entry.isFile() && MODULE_FILE.test(entry.name)) {
|
|
134
|
+
files.push(path);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return files;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Audit one function directory: read its `.vc-config.json` handler, scan
|
|
142
|
+
* every module beside the handler (the adapter's `dist/server` tree) for bare
|
|
143
|
+
* imports, and report the packages no `node_modules` on the walk up to the
|
|
144
|
+
* function root provides. A function without a handler has nothing to check.
|
|
145
|
+
*/
|
|
146
|
+
export const auditFunctionBundle = async (
|
|
147
|
+
funcDir: string
|
|
148
|
+
): Promise<MissingPackage[]> => {
|
|
149
|
+
const configPath = join(funcDir, ".vc-config.json");
|
|
150
|
+
if (!existsSync(configPath)) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
const config = vcConfigSchema.safeParse(
|
|
154
|
+
JSON.parse(await readFile(configPath, "utf-8"))
|
|
155
|
+
);
|
|
156
|
+
if (!config.success || !config.data.handler) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const serverDir = dirname(join(funcDir, config.data.handler));
|
|
160
|
+
if (!existsSync(serverDir)) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
const missing = new Map<string, string[]>();
|
|
164
|
+
for (const file of await listModules(serverDir)) {
|
|
165
|
+
// oxlint-disable-next-line no-await-in-loop -- sequential read keeps the importer lists ordered
|
|
166
|
+
const source = await readFile(file, "utf-8");
|
|
167
|
+
for (const name of importedPackages(source)) {
|
|
168
|
+
if (resolvable(name, dirname(file), funcDir)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const importers = missing.get(name) ?? [];
|
|
172
|
+
importers.push(relative(funcDir, file));
|
|
173
|
+
missing.set(name, importers);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return [...missing]
|
|
177
|
+
.map(([name, importers]) => ({ importers, name }))
|
|
178
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Audit every function in a Vercel Build Output tree (`.vercel/output`).
|
|
183
|
+
* Functions with nothing missing are omitted; an output tree without a
|
|
184
|
+
* `functions/` directory (a static build) yields nothing.
|
|
185
|
+
*/
|
|
186
|
+
export const auditVercelFunctions = async (
|
|
187
|
+
outputDir: string
|
|
188
|
+
): Promise<FunctionBundleAudit[]> => {
|
|
189
|
+
const functionsDir = join(outputDir, "functions");
|
|
190
|
+
if (!existsSync(functionsDir)) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
const audits: FunctionBundleAudit[] = [];
|
|
194
|
+
const entries = await readdir(functionsDir, { withFileTypes: true });
|
|
195
|
+
for (const entry of entries.toSorted((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
196
|
+
if (!(entry.isDirectory() && entry.name.endsWith(".func"))) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const dir = join(functionsDir, entry.name);
|
|
200
|
+
// oxlint-disable-next-line no-await-in-loop -- sequential audit keeps the report ordered
|
|
201
|
+
const missing = await auditFunctionBundle(dir);
|
|
202
|
+
if (missing.length > 0) {
|
|
203
|
+
audits.push({ dir, missing });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return audits;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** Blume's own runtime dependency names, from its published `package.json`. */
|
|
210
|
+
export const blumeDependencyNames = (): Set<string> => {
|
|
211
|
+
const manifest: { dependencies?: Record<string, string> } = JSON.parse(
|
|
212
|
+
readFileSync(join(packageRoot(), "package.json"), "utf-8")
|
|
213
|
+
);
|
|
214
|
+
return new Set(Object.keys(manifest.dependencies ?? {}));
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** What a build should do about an audit: nothing, warn, or fail. */
|
|
218
|
+
export interface FunctionBundleVerdict {
|
|
219
|
+
/**
|
|
220
|
+
* A missing package is one of Blume's own dependencies — the runtime Blume
|
|
221
|
+
* generated imports it, so the function is certain to crash. Anything else
|
|
222
|
+
* (a project's own external import) is reported but left to the author.
|
|
223
|
+
*/
|
|
224
|
+
fatal: boolean;
|
|
225
|
+
message: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Describe a function's missing packages and how to fix them. Vercel's trace
|
|
230
|
+
* only reaches packages resolvable from the project root, so the remedy is a
|
|
231
|
+
* root-level dependency entry for each — the same mirror rule Blume's native
|
|
232
|
+
* dependencies (`sharp`, `takumi-js`) already follow.
|
|
233
|
+
*/
|
|
234
|
+
export const functionBundleVerdict = (
|
|
235
|
+
audit: FunctionBundleAudit,
|
|
236
|
+
root: string,
|
|
237
|
+
ownDependencies: ReadonlySet<string>
|
|
238
|
+
): FunctionBundleVerdict => {
|
|
239
|
+
const names = audit.missing.map((entry) => entry.name);
|
|
240
|
+
const lines = audit.missing.map(
|
|
241
|
+
(entry) => ` - ${entry.name} (imported by ${entry.importers.join(", ")})`
|
|
242
|
+
);
|
|
243
|
+
const fatal = names.some((name) => ownDependencies.has(name));
|
|
244
|
+
const message = [
|
|
245
|
+
`The Vercel function bundle at ${relative(root, audit.dir) || audit.dir} is missing packages its server code imports, so the deployed function would fail on every request with ERR_MODULE_NOT_FOUND:`,
|
|
246
|
+
...lines,
|
|
247
|
+
"Vercel's dependency trace only includes packages resolvable from the project root; under an isolated linker (pnpm, Bun's isolated mode) Blume's own dependencies are not. Add them to the project's package.json so the trace can find them:",
|
|
248
|
+
` npm install -D ${names.join(" ")}`,
|
|
249
|
+
].join("\n");
|
|
250
|
+
return { fatal, message };
|
|
251
|
+
};
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* into it so a content-page request that prefers `text/markdown` is rewritten
|
|
10
10
|
* (not redirected) to the page's prerendered `.md` mirror — the deployed
|
|
11
11
|
* counterpart of the dev-server rewrite in `astro/markdown-negotiation.ts`.
|
|
12
|
+
* The same routing config also answers a *missing* page: a request that
|
|
13
|
+
* prefers Markdown (or asks for a `.md` URL no page backs) gets the
|
|
14
|
+
* prerendered Markdown 404 body with the 404 status, instead of the HTML shell.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -49,6 +52,32 @@ const ACCEPT_MARKDOWN_CONDITION: VercelRoute["has"] = [
|
|
|
49
52
|
|
|
50
53
|
const VARY_ACCEPT = { vary: "Accept" };
|
|
51
54
|
|
|
55
|
+
/** Where the prerendered Markdown 404 (`pages/404.md.ts`) lands. */
|
|
56
|
+
const NOT_FOUND_MARKDOWN_DEST = "/404.md";
|
|
57
|
+
|
|
58
|
+
/** The adapter's own not-found fallback — the anchor the Markdown 404 precedes. */
|
|
59
|
+
const NOT_FOUND_HTML_DEST = "/404.html";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Miss-phase routes that answer a missing page with the Markdown 404 body: any
|
|
63
|
+
* path when the client prefers Markdown, and any `.md`/`.mdx` URL (a request
|
|
64
|
+
* for a raw-Markdown mirror that has no page wants Markdown back, not the HTML
|
|
65
|
+
* shell). Both keep the 404 status. Spliced immediately before the adapter's
|
|
66
|
+
* `/404.html` fallback, so they run after every server route (the MCP
|
|
67
|
+
* endpoint, server islands, images) has had its turn and never hijack a
|
|
68
|
+
* request one of those would have answered.
|
|
69
|
+
*/
|
|
70
|
+
const NOT_FOUND_MARKDOWN_ROUTES: readonly VercelRoute[] = [
|
|
71
|
+
{
|
|
72
|
+
dest: NOT_FOUND_MARKDOWN_DEST,
|
|
73
|
+
has: ACCEPT_MARKDOWN_CONDITION,
|
|
74
|
+
headers: VARY_ACCEPT,
|
|
75
|
+
src: "^/.*$",
|
|
76
|
+
status: 404,
|
|
77
|
+
},
|
|
78
|
+
{ dest: NOT_FOUND_MARKDOWN_DEST, src: "^/.*\\.mdx?$", status: 404 },
|
|
79
|
+
];
|
|
80
|
+
|
|
52
81
|
/**
|
|
53
82
|
* Vercel rejects route `src` patterns longer than 4096 characters, so route
|
|
54
83
|
* alternations are split across as many route entries as needed. The budget
|
|
@@ -186,12 +215,14 @@ export const TRAILING_SLASH_REDIRECT: VercelRoute = {
|
|
|
186
215
|
* user-authored route of that identical shape would be semantically equal to
|
|
187
216
|
* the one re-added); the homepage `Link` route by its three-field
|
|
188
217
|
* continue-with-link shape (the Build Output config is adapter-generated, so
|
|
189
|
-
* no user-authored route competes in this file)
|
|
218
|
+
* no user-authored route competes in this file); the Markdown 404 routes by
|
|
219
|
+
* their `/404.md` destination.
|
|
190
220
|
*/
|
|
191
221
|
const isNegotiationRoute = (route: VercelRoute): boolean =>
|
|
192
222
|
route.has?.some(
|
|
193
223
|
(condition) => condition.value === ACCEPT_MARKDOWN_HEADER_VALUE
|
|
194
224
|
) === true ||
|
|
225
|
+
(route.dest === NOT_FOUND_MARKDOWN_DEST && route.status === 404) ||
|
|
195
226
|
(route.continue === true &&
|
|
196
227
|
route.headers?.vary === "Accept" &&
|
|
197
228
|
isString(route.src) &&
|
|
@@ -213,17 +244,21 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
|
|
|
213
244
|
* platform's mechanism for extensionless static files (e.g. the Web Bot Auth
|
|
214
245
|
* signature directory). The trailing-slash 308 redirect is always spliced in
|
|
215
246
|
* alongside, so slashed duplicates of every page collapse onto the canonical
|
|
216
|
-
* slashless URL.
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
247
|
+
* slashless URL. With `notFoundMarkdown` (the build emitted `404.md`), the
|
|
248
|
+
* Markdown 404 routes go into the miss phase right before the adapter's
|
|
249
|
+
* `/404.html` fallback — and nowhere when that fallback is absent, since a
|
|
250
|
+
* `dest` with no file behind it would serve nothing. Returns the updated JSON
|
|
251
|
+
* text (tab-indented, like the adapter's own output), or `null` when there is
|
|
252
|
+
* nowhere safe to splice: an unparsable config, no `routes` array, or no
|
|
253
|
+
* `handle: "filesystem"` marker to anchor the splice.
|
|
220
254
|
*/
|
|
221
255
|
export const injectNegotiationRoutes = (
|
|
222
256
|
configText: string,
|
|
223
257
|
routePaths: readonly string[],
|
|
224
258
|
homeLinkHeader?: string | null,
|
|
225
259
|
contentTypeOverrides?: Record<string, string>,
|
|
226
|
-
homeTokens?: number
|
|
260
|
+
homeTokens?: number,
|
|
261
|
+
notFoundMarkdown = false
|
|
227
262
|
): string | null => {
|
|
228
263
|
const overrideEntries = Object.entries(contentTypeOverrides ?? {});
|
|
229
264
|
let config: {
|
|
@@ -272,6 +307,14 @@ export const injectNegotiationRoutes = (
|
|
|
272
307
|
...rewriteRoutes,
|
|
273
308
|
TRAILING_SLASH_REDIRECT
|
|
274
309
|
);
|
|
310
|
+
if (notFoundMarkdown) {
|
|
311
|
+
const fallbackIndex = routes.findIndex(
|
|
312
|
+
(route) => route.status === 404 && route.dest === NOT_FOUND_HTML_DEST
|
|
313
|
+
);
|
|
314
|
+
if (fallbackIndex !== -1) {
|
|
315
|
+
routes.splice(fallbackIndex, 0, ...NOT_FOUND_MARKDOWN_ROUTES);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
275
318
|
config.routes = routes;
|
|
276
319
|
return `${JSON.stringify(config, null, "\t")}\n`;
|
|
277
320
|
};
|
package/src/eval/schema.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { load } from "js-yaml";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
|
+
import { YAML_SCHEMA } from "../core/yaml.ts";
|
|
7
|
+
|
|
6
8
|
/** Question ids are kebab-case slugs so they read well in reports and CI logs. */
|
|
7
9
|
const ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
|
|
8
10
|
|
|
@@ -90,7 +92,7 @@ export const loadEvalsFile = async (
|
|
|
90
92
|
|
|
91
93
|
let parsed: unknown;
|
|
92
94
|
try {
|
|
93
|
-
parsed = load(raw);
|
|
95
|
+
parsed = load(raw, { schema: YAML_SCHEMA });
|
|
94
96
|
} catch (error) {
|
|
95
97
|
const detail = error instanceof Error ? error.message : String(error);
|
|
96
98
|
throw new EvalsFileError(path, `Invalid YAML in ${path}: ${detail}`);
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
* `data-line-numbers`; the theme renders a counter-driven line-number gutter.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
isLineRange,
|
|
14
|
+
metaTokens,
|
|
15
|
+
QUOTED_ATTR,
|
|
16
|
+
RESERVED_META_KEYWORDS,
|
|
17
|
+
} from "./fence-meta.ts";
|
|
18
|
+
|
|
12
19
|
/** The slice of Shiki's transformer `this` context Blume reads. */
|
|
13
20
|
interface CodeMetaContext {
|
|
14
21
|
options: { meta?: { __raw?: string } };
|
|
@@ -25,29 +32,25 @@ export interface CodeTitleTransformer {
|
|
|
25
32
|
pre: (this: CodeMetaContext, node: PreNode) => void;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
// The body excludes only the delimiting quote, so `title="foo's file.ts"`
|
|
29
|
-
// (an apostrophe inside double quotes) still matches. The left boundary stops
|
|
30
|
-
// `subtitle="..."` (or any `*title=` attr) from reading as a title.
|
|
31
35
|
const TITLE_ATTR = /(?:^|\s)title=(?:"(?<dq>[^"]*)"|'(?<sq>[^']*)')/u;
|
|
32
36
|
const LINE_NUMBERS = /(?:^|\s)lineNumbers(?=\s|$)/u;
|
|
33
|
-
// Any quoted `key="..."` attr — blanked before keyword/bare-token scans so a
|
|
34
|
-
// quoted value can't leak tokens (`title="enable lineNumbers later"`).
|
|
35
|
-
const QUOTED_ATTR = /[\w-]+=(?:"[^"]*"|'[^']*')/gu;
|
|
36
37
|
|
|
38
|
+
// Quoted attrs are blanked before keyword/bare-token scans so a quoted value
|
|
39
|
+
// can't leak tokens (`title="enable lineNumbers later"`).
|
|
37
40
|
const withoutQuotedAttrs = (raw: string): string =>
|
|
38
41
|
raw.replace(QUOTED_ATTR, " ");
|
|
39
42
|
|
|
40
43
|
// The first bare token is the title (```ts blume.config.ts): a non-empty token
|
|
41
44
|
// that isn't a Shiki line range (`{1,3-5}`), a `key=value` attr, or a reserved
|
|
42
|
-
//
|
|
43
|
-
const isTitleToken = (token: string): boolean =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
};
|
|
45
|
+
// keyword.
|
|
46
|
+
const isTitleToken = (token: string): boolean =>
|
|
47
|
+
token.length > 0 &&
|
|
48
|
+
!isLineRange(token) &&
|
|
49
|
+
!token.includes("=") &&
|
|
50
|
+
!RESERVED_META_KEYWORDS.has(token);
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
/** The title a fence's meta string promotes to `data-title`, if any. */
|
|
53
|
+
export const parseCodeTitle = (raw: string | undefined): string | undefined => {
|
|
51
54
|
if (!raw) {
|
|
52
55
|
return undefined;
|
|
53
56
|
}
|
|
@@ -62,7 +65,10 @@ const parseTitle = (raw: string | undefined): string | undefined => {
|
|
|
62
65
|
if (attrTitle) {
|
|
63
66
|
return attrTitle;
|
|
64
67
|
}
|
|
65
|
-
|
|
68
|
+
// The shared tokenizer keeps a quoted attr (rejected below by its `=`) and
|
|
69
|
+
// a spaced line range (`{1, 3-5}`) whole, so neither can shed a fragment
|
|
70
|
+
// that reads as a bare title.
|
|
71
|
+
return metaTokens(raw).find(isTitleToken);
|
|
66
72
|
};
|
|
67
73
|
|
|
68
74
|
const hasLineNumbers = (raw: string | undefined): boolean =>
|
|
@@ -73,7 +79,7 @@ export const codeTitleTransformer = (): CodeTitleTransformer => ({
|
|
|
73
79
|
name: "blume:code-meta",
|
|
74
80
|
pre(node) {
|
|
75
81
|
const raw = this.options.meta?.__raw;
|
|
76
|
-
const title =
|
|
82
|
+
const title = parseCodeTitle(raw);
|
|
77
83
|
if (title) {
|
|
78
84
|
node.properties.dataTitle = title;
|
|
79
85
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Features } from "satteri";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sätteri's feature set for plain `.md` pages. Shared by the renderer and the
|
|
5
|
+
* search extractor so the index reads the same grammar the page renders.
|
|
6
|
+
*/
|
|
7
|
+
export const MARKDOWN_FEATURES = {
|
|
8
|
+
subscript: true,
|
|
9
|
+
superscript: true,
|
|
10
|
+
} satisfies Features;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The `.mdx` feature set: Markdown's plus `:::` directives (→ `<Callout>`) and
|
|
14
|
+
* block-only math — `singleDollarTextMath: false` keeps a bare `$` (currency,
|
|
15
|
+
* shell, code) as literal text and only parses `$$…$$`.
|
|
16
|
+
*/
|
|
17
|
+
export const MDX_FEATURES = {
|
|
18
|
+
...MARKDOWN_FEATURES,
|
|
19
|
+
directive: true,
|
|
20
|
+
math: { singleDollarTextMath: false },
|
|
21
|
+
} satisfies Features;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared fence-meta grammar: how the tokens after a code fence's language
|
|
3
|
+
* (```ts title="..." {1,3-5} lineNumbers) split, and which of them carry
|
|
4
|
+
* reserved meaning. Both the Shiki-side meta reader (`code-title.ts`) and the
|
|
5
|
+
* MDAST plugins that rewrite marked fences (`ts2js.ts`) consume this, so the
|
|
6
|
+
* two layers can't drift on token boundaries or keyword lists.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Any quoted `key="..."` attr (spaces allowed inside the quotes). The body
|
|
11
|
+
* excludes only the delimiting quote, so `title="foo's file.ts"` (an
|
|
12
|
+
* apostrophe inside double quotes) still matches. The left boundary stops
|
|
13
|
+
* `subtitle="..."` (or any `*title=` attr) from reading as a title.
|
|
14
|
+
*/
|
|
15
|
+
export const QUOTED_ATTR = /[\w-]+=(?:"[^"]*"|'[^']*')/gu;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A Shiki `{1,3-5}` line-range token, braces included. Shiki tolerates
|
|
19
|
+
* whitespace inside the braces (`{1, 3-5}`), so the range is one token even
|
|
20
|
+
* when it contains spaces — split on whitespace alone, `3-5}` would surface
|
|
21
|
+
* as a bare word and be promoted to the block title.
|
|
22
|
+
*/
|
|
23
|
+
const LINE_RANGE = /\{[^}]*\}/u;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One fence-meta token: a quoted attribute, a line range, or a bare word, so
|
|
27
|
+
* a keyword inside a quoted value (`title="enable ts2js later"`) never reads
|
|
28
|
+
* as a bare token and a spaced line range never splits.
|
|
29
|
+
*/
|
|
30
|
+
const META_TOKEN = new RegExp(
|
|
31
|
+
`${QUOTED_ATTR.source}|${LINE_RANGE.source}|\\S+`,
|
|
32
|
+
"gu"
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/** Split a raw fence meta string into its tokens. */
|
|
36
|
+
export const metaTokens = (meta: string | null | undefined): string[] =>
|
|
37
|
+
meta?.match(META_TOKEN) ?? [];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Bare keywords with reserved meaning after the language; never promoted to
|
|
41
|
+
* a block title.
|
|
42
|
+
*/
|
|
43
|
+
export const RESERVED_META_KEYWORDS: ReadonlySet<string> = new Set([
|
|
44
|
+
"lineNumbers",
|
|
45
|
+
"ts2js",
|
|
46
|
+
"twoslash",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/** A Shiki `{1,3-5}` line-range token. */
|
|
50
|
+
export const isLineRange = (token: string): boolean => token.startsWith("{");
|