blume 1.5.2 → 1.6.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/CHANGELOG.md +85 -0
- package/dist/cli/index.js +3639 -1377
- package/dist/cli/index.js.map +103 -91
- 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 +23 -1
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +8 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +117 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +23 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +21 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +3 -3
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +21 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +27 -0
- package/docs/configuration/theming.mdx +17 -0
- package/docs/content/components.mdx +7 -0
- 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 +23 -14
- 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/generate.ts +183 -116
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +3 -5
- package/src/astro/templates.ts +125 -76
- package/src/cli/commands/build.ts +84 -15
- 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/dropdown-dismiss.ts +122 -0
- 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 +29 -8
- package/src/components/layout/PageLayout.astro +10 -1
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +46 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +53 -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 +18 -2
- 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 +11 -0
- package/src/core/includes.ts +632 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- 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 +226 -35
- 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 +25 -2
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +2 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +2 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +2 -0
- package/src/core/ui-packs/el.ts +2 -0
- package/src/core/ui-packs/es.ts +2 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +2 -0
- package/src/core/ui-packs/hu.ts +2 -0
- package/src/core/ui-packs/id.ts +2 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +2 -0
- package/src/core/ui-packs/ko.ts +2 -0
- package/src/core/ui-packs/nl.ts +2 -0
- package/src/core/ui-packs/no.ts +2 -0
- package/src/core/ui-packs/pl.ts +2 -0
- package/src/core/ui-packs/pt-br.ts +2 -0
- package/src/core/ui-packs/pt.ts +2 -0
- package/src/core/ui-packs/ro.ts +2 -0
- package/src/core/ui-packs/ru.ts +2 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +2 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +2 -0
- package/src/core/ui-packs/uk.ts +2 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +21 -3
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- 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/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 +89 -13
- package/src/openapi/render-mdx.ts +48 -8
- package/src/openapi/scalar.ts +5 -12
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +11 -0
- 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 +65 -11
- package/src/theme/fonts.ts +189 -16
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
package/src/core/ui-packs/pt.ts
CHANGED
|
@@ -20,6 +20,8 @@ export const pt: UIStringsOverride = {
|
|
|
20
20
|
generating: "A gerar…",
|
|
21
21
|
openIn: "Abrir em {name}",
|
|
22
22
|
openInChat: "Abrir no chat",
|
|
23
|
+
openInChatPrompt:
|
|
24
|
+
"Leia {url} para que eu possa fazer perguntas sobre esta página.",
|
|
23
25
|
scrollToTop: "Voltar ao topo",
|
|
24
26
|
},
|
|
25
27
|
ask: {
|
package/src/core/ui-packs/ro.ts
CHANGED
|
@@ -20,6 +20,8 @@ export const ro: UIStringsOverride = {
|
|
|
20
20
|
generating: "Se generează…",
|
|
21
21
|
openIn: "Deschide în {name}",
|
|
22
22
|
openInChat: "Deschide în chat",
|
|
23
|
+
openInChatPrompt:
|
|
24
|
+
"Citește {url} ca să îți pot pune întrebări despre această pagină.",
|
|
23
25
|
scrollToTop: "Sus de tot",
|
|
24
26
|
},
|
|
25
27
|
ask: {
|
package/src/core/ui-packs/ru.ts
CHANGED
|
@@ -20,6 +20,8 @@ export const ru: UIStringsOverride = {
|
|
|
20
20
|
generating: "Генерация…",
|
|
21
21
|
openIn: "Открыть в {name}",
|
|
22
22
|
openInChat: "Открыть в чате",
|
|
23
|
+
openInChatPrompt:
|
|
24
|
+
"Прочитайте {url} — я буду задавать вопросы об этой странице.",
|
|
23
25
|
scrollToTop: "Наверх",
|
|
24
26
|
},
|
|
25
27
|
ask: {
|
package/src/core/ui-packs/sk.ts
CHANGED
package/src/core/ui-packs/sr.ts
CHANGED
package/src/core/ui-packs/sv.ts
CHANGED
package/src/core/ui-packs/th.ts
CHANGED
package/src/core/ui-packs/tr.ts
CHANGED
|
@@ -20,6 +20,8 @@ export const tr: UIStringsOverride = {
|
|
|
20
20
|
generating: "Oluşturuluyor…",
|
|
21
21
|
openIn: "{name} içinde aç",
|
|
22
22
|
openInChat: "Sohbette aç",
|
|
23
|
+
openInChatPrompt:
|
|
24
|
+
"Bu sayfa hakkında sorular sorabilmem için {url} adresini okuyun.",
|
|
23
25
|
scrollToTop: "Başa dön",
|
|
24
26
|
},
|
|
25
27
|
ask: {
|
package/src/core/ui-packs/uk.ts
CHANGED
|
@@ -20,6 +20,8 @@ export const uk: UIStringsOverride = {
|
|
|
20
20
|
generating: "Генерація…",
|
|
21
21
|
openIn: "Відкрити в {name}",
|
|
22
22
|
openInChat: "Відкрити в чаті",
|
|
23
|
+
openInChatPrompt:
|
|
24
|
+
"Прочитайте {url} — я ставитиму запитання щодо цієї сторінки.",
|
|
23
25
|
scrollToTop: "Нагору",
|
|
24
26
|
},
|
|
25
27
|
ask: {
|
package/src/core/ui-packs/vi.ts
CHANGED
package/src/core/ui-packs/zh.ts
CHANGED
package/src/core/version-cut.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { stripBasePath } from "./base-path.ts";
|
|
|
7
7
|
import { writeTextAtomic } from "./fs-atomic.ts";
|
|
8
8
|
import { localizeRoute } from "./i18n.ts";
|
|
9
9
|
import type { BlumeProject } from "./project-graph.ts";
|
|
10
|
-
import { scanProject } from "./project-graph.ts";
|
|
10
|
+
import { isWithin, scanProject } from "./project-graph.ts";
|
|
11
11
|
import { VERSION_ID } from "./schema.ts";
|
|
12
12
|
import { nextFenceState } from "./sources/normalize.ts";
|
|
13
13
|
import type { FenceState } from "./sources/normalize.ts";
|
|
@@ -60,11 +60,16 @@ const buildRouteRewrites = (
|
|
|
60
60
|
const { contentRoot } = project.context;
|
|
61
61
|
const rewrites = new Map<string, string>();
|
|
62
62
|
for (const page of project.graph.pages) {
|
|
63
|
-
// `sourcePath`
|
|
64
|
-
//
|
|
63
|
+
// `sourcePath` marks a page backed by a local file, which excludes
|
|
64
|
+
// generated and remote pages; the content-root check below excludes local
|
|
65
|
+
// files that live outside the tree a version snapshot copies. A staged
|
|
66
|
+
// page (an Obsidian note) is served from its source, not from the copied
|
|
67
|
+
// tree, so it keeps publishing as current even when its file sits under
|
|
68
|
+
// the content root.
|
|
65
69
|
const { sourcePath } = page;
|
|
66
70
|
if (
|
|
67
71
|
page.version !== "" ||
|
|
72
|
+
page.collection === "staged" ||
|
|
68
73
|
!sourcePath ||
|
|
69
74
|
relative(contentRoot, sourcePath).startsWith("..")
|
|
70
75
|
) {
|
|
@@ -240,6 +245,18 @@ export const cutVersion = async (
|
|
|
240
245
|
"node_modules",
|
|
241
246
|
...(project.config.versions?.archived.map((version) => version.id) ?? []),
|
|
242
247
|
]);
|
|
248
|
+
// Another local source's tree under the content root — an Obsidian vault the
|
|
249
|
+
// filesystem source excludes — is that source's to publish. Copying it would
|
|
250
|
+
// republish its raw notes (wikilinks and `%%comments%%` intact) as
|
|
251
|
+
// filesystem pages of the snapshot, while the source itself keeps serving
|
|
252
|
+
// them as current.
|
|
253
|
+
const foreignRoots = project.sources
|
|
254
|
+
.flatMap((source) =>
|
|
255
|
+
source.staged && source.contentRoot ? [source.contentRoot] : []
|
|
256
|
+
)
|
|
257
|
+
.filter((tree) => isWithin(contentRoot, tree));
|
|
258
|
+
const isForeign = (path: string): boolean =>
|
|
259
|
+
foreignRoots.some((tree) => isWithin(tree, path));
|
|
243
260
|
const entries = await readdir(contentRoot, { withFileTypes: true });
|
|
244
261
|
let copied = 0;
|
|
245
262
|
await Promise.all(
|
|
@@ -252,6 +269,7 @@ export const cutVersion = async (
|
|
|
252
269
|
return;
|
|
253
270
|
}
|
|
254
271
|
await cp(join(contentRoot, entry.name), join(dir, entry.name), {
|
|
272
|
+
filter: (source) => !isForeign(source),
|
|
255
273
|
recursive: true,
|
|
256
274
|
});
|
|
257
275
|
})
|
package/src/core/yaml.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
binaryTag,
|
|
3
|
+
CORE_SCHEMA,
|
|
4
|
+
mergeTag,
|
|
5
|
+
omapTag,
|
|
6
|
+
pairsTag,
|
|
7
|
+
setTag,
|
|
8
|
+
timestampTag,
|
|
9
|
+
} from "js-yaml";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The schema js-yaml 4 loaded with by default: YAML 1.2 core scalars plus the
|
|
13
|
+
* `!!timestamp`, `!!merge`, `!!binary`, `!!omap`, `!!pairs`, and `!!set` tags.
|
|
14
|
+
* js-yaml 5 loads with the bare core schema instead, which turns
|
|
15
|
+
* `date: 2024-01-01` into a string and leaves `<<:` merge keys unresolved — a
|
|
16
|
+
* silent change to every page's front matter. Every Blume `load` call passes
|
|
17
|
+
* this schema so parsing stays identical across the upgrade.
|
|
18
|
+
*/
|
|
19
|
+
export const YAML_SCHEMA = CORE_SCHEMA.withTags(
|
|
20
|
+
timestampTag,
|
|
21
|
+
mergeTag,
|
|
22
|
+
binaryTag,
|
|
23
|
+
omapTag,
|
|
24
|
+
pairsTag,
|
|
25
|
+
setTag
|
|
26
|
+
);
|
|
@@ -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
|
+
};
|
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("{");
|