blume 1.4.2 → 1.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/CHANGELOG.md +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +116 -15
- package/dist/types/core/sources/types.d.ts +11 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +13 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +8 -5
- package/src/components/layout/search/types.ts +45 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +74 -4
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
package/src/markdown/mdast.ts
CHANGED
|
@@ -5,15 +5,29 @@
|
|
|
5
5
|
* Satteri's real `MdastPlugin` type at a single boundary in `index.ts`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* A property value on an MDAST node: primitives, nested nodes, and lists of
|
|
10
|
+
* either. Covers everything Blume's plugins read or build (positions, data
|
|
11
|
+
* flags, attribute lists) without admitting functions or class instances.
|
|
12
|
+
*/
|
|
13
|
+
export type MdastValue =
|
|
14
|
+
| string
|
|
15
|
+
| number
|
|
16
|
+
| boolean
|
|
17
|
+
| null
|
|
18
|
+
| undefined
|
|
19
|
+
| MdastValue[]
|
|
20
|
+
| { [key: string]: MdastValue };
|
|
21
|
+
|
|
8
22
|
/** The visitor context Blume's plugins use to mutate the tree. */
|
|
9
23
|
export interface MdastVisitorContext {
|
|
10
|
-
replaceNode: (node:
|
|
24
|
+
replaceNode: (node: MdastNode, replacement: MdastNode) => void;
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
/** Any MDAST node, keyed loosely since we build a small subset by hand. */
|
|
14
28
|
export interface MdastNode {
|
|
15
29
|
type: string;
|
|
16
|
-
[key: string]:
|
|
30
|
+
[key: string]: MdastValue;
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
/** Build an MDX JSX attribute. A `null` value renders as a boolean attribute. */
|
|
@@ -29,14 +43,14 @@ type JsxAttribute = ReturnType<typeof jsxAttribute>;
|
|
|
29
43
|
export const jsxFlowElement = (
|
|
30
44
|
name: string,
|
|
31
45
|
attributes: JsxAttribute[],
|
|
32
|
-
children:
|
|
46
|
+
children: MdastValue[]
|
|
33
47
|
) => ({ attributes, children, name, type: "mdxJsxFlowElement" });
|
|
34
48
|
|
|
35
49
|
/** Build an inline MDX JSX element (phrasing context). */
|
|
36
50
|
export const jsxTextElement = (
|
|
37
51
|
name: string,
|
|
38
52
|
attributes: JsxAttribute[],
|
|
39
|
-
children:
|
|
53
|
+
children: MdastValue[] = []
|
|
40
54
|
) => ({ attributes, children, name, type: "mdxJsxTextElement" });
|
|
41
55
|
|
|
42
56
|
/** Build a fenced code block node. */
|
|
@@ -1,8 +1,26 @@
|
|
|
1
|
+
import type { Agent, Command } from "package-manager-detector";
|
|
2
|
+
import { resolveCommand } from "package-manager-detector/commands";
|
|
3
|
+
|
|
1
4
|
/** Supported package managers, in the order tabs are displayed. */
|
|
2
5
|
export const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"] as const;
|
|
3
6
|
|
|
4
7
|
export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
5
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The agent each tab renders. The yarn tab is pinned to Berry (yarn 2+ — the
|
|
11
|
+
* only supported line): the pre-existing mix emitted Berry-only `yarn dlx` and
|
|
12
|
+
* `--immutable` next to Classic-only `yarn global add`, so no single yarn
|
|
13
|
+
* version could run every rendered command. Berry removed `global`
|
|
14
|
+
* (yarnpkg/berry#821), so global installs on the yarn tab honestly render
|
|
15
|
+
* npm's form, matching `ni`'s table.
|
|
16
|
+
*/
|
|
17
|
+
const AGENT_FOR = {
|
|
18
|
+
bun: "bun",
|
|
19
|
+
npm: "npm",
|
|
20
|
+
pnpm: "pnpm",
|
|
21
|
+
yarn: "yarn@berry",
|
|
22
|
+
} satisfies Record<PackageManager, Agent>;
|
|
23
|
+
|
|
6
24
|
/** Words that mark the input as an explicit command rather than a bare list. */
|
|
7
25
|
const MANAGER_PREFIXES = new Set(["bun", "bunx", "npm", "npx", "pnpm", "yarn"]);
|
|
8
26
|
|
|
@@ -118,62 +136,51 @@ const parseIntent = (input: string): Intent => {
|
|
|
118
136
|
return { args: normalizeFlags(verbArgs), operation };
|
|
119
137
|
};
|
|
120
138
|
|
|
121
|
-
/**
|
|
139
|
+
/** The package-manager-detector command for each non-global operation. */
|
|
140
|
+
const COMMAND_FOR = {
|
|
141
|
+
add: "add",
|
|
142
|
+
ci: "frozen",
|
|
143
|
+
exec: "execute",
|
|
144
|
+
install: "install",
|
|
145
|
+
remove: "uninstall",
|
|
146
|
+
run: "run",
|
|
147
|
+
} satisfies Record<Exclude<Operation, "create">, Command>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Render one manager's command for the given intent, via
|
|
151
|
+
* package-manager-detector's maintained agent tables (the engine behind `ni`).
|
|
152
|
+
*/
|
|
122
153
|
const buildCommand = (manager: PackageManager, intent: Intent): string => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (manager === "npm") {
|
|
152
|
-
return "npm ci";
|
|
153
|
-
}
|
|
154
|
-
if (manager === "yarn") {
|
|
155
|
-
return "yarn install --immutable";
|
|
156
|
-
}
|
|
157
|
-
return `${manager} install --frozen-lockfile`;
|
|
158
|
-
}
|
|
159
|
-
case "remove": {
|
|
160
|
-
if (manager === "npm") {
|
|
161
|
-
return `npm uninstall ${args}`;
|
|
162
|
-
}
|
|
163
|
-
// Yarn Classic has no `remove -g`; the global form is `yarn global remove`.
|
|
164
|
-
if (manager === "yarn" && intent.args.some((a) => GLOBAL_FLAGS.has(a))) {
|
|
165
|
-
const pkgs = intent.args.filter((a) => !GLOBAL_FLAGS.has(a)).join(" ");
|
|
166
|
-
return `yarn global remove ${pkgs}`;
|
|
167
|
-
}
|
|
168
|
-
return `${manager} remove ${args}`;
|
|
169
|
-
}
|
|
170
|
-
case "run": {
|
|
171
|
-
return `${manager} run ${args}`;
|
|
172
|
-
}
|
|
173
|
-
default: {
|
|
174
|
-
return `${manager} install`;
|
|
175
|
-
}
|
|
154
|
+
// The tables carry no `create`; every manager spells it the same way.
|
|
155
|
+
if (intent.operation === "create") {
|
|
156
|
+
return `${manager} create ${intent.args.join(" ")}`;
|
|
157
|
+
}
|
|
158
|
+
// A `-g`/`--global` flag selects the dedicated global command (the tables
|
|
159
|
+
// place the flag themselves, per manager).
|
|
160
|
+
const global =
|
|
161
|
+
(intent.operation === "add" || intent.operation === "remove") &&
|
|
162
|
+
intent.args.some((arg) => GLOBAL_FLAGS.has(arg));
|
|
163
|
+
const args = global
|
|
164
|
+
? intent.args.filter((arg) => !GLOBAL_FLAGS.has(arg))
|
|
165
|
+
: intent.args;
|
|
166
|
+
const command: Command = global
|
|
167
|
+
? (intent.operation === "add" && "global") || "global_uninstall"
|
|
168
|
+
: COMMAND_FOR[intent.operation];
|
|
169
|
+
// Non-null: every operation above maps to a command each supported agent's
|
|
170
|
+
// table defines (null is only possible for gaps like npm's
|
|
171
|
+
// upgrade-interactive, which no Operation reaches).
|
|
172
|
+
const resolved = resolveCommand(AGENT_FOR[manager], command, args);
|
|
173
|
+
const words = resolved
|
|
174
|
+
? [resolved.command, ...resolved.args]
|
|
175
|
+
: [manager, ...args];
|
|
176
|
+
// Docs favor the explicit spellings over the tables' terse aliases.
|
|
177
|
+
if (words[1] === "i") {
|
|
178
|
+
words[1] = "install";
|
|
179
|
+
}
|
|
180
|
+
if (words[0] === "bun" && words[1] === "x") {
|
|
181
|
+
words.splice(0, 2, "bunx");
|
|
176
182
|
}
|
|
183
|
+
return words.join(" ");
|
|
177
184
|
};
|
|
178
185
|
|
|
179
186
|
/**
|
|
@@ -181,9 +188,7 @@ const buildCommand = (manager: PackageManager, intent: Intent): string => {
|
|
|
181
188
|
* manager. Accepts a bare package list (`react`) or a full command
|
|
182
189
|
* (`npm i -D typescript`, `npx astro add react`).
|
|
183
190
|
*/
|
|
184
|
-
export const toPackageCommands = (
|
|
185
|
-
input: string
|
|
186
|
-
): Record<PackageManager, string> => {
|
|
191
|
+
export const toPackageCommands = (input: string) => {
|
|
187
192
|
const intent = parseIntent(input);
|
|
188
193
|
const normalize = (command: string): string =>
|
|
189
194
|
command.replaceAll(WHITESPACE_RUN, " ").trim();
|
|
@@ -192,5 +197,5 @@ export const toPackageCommands = (
|
|
|
192
197
|
npm: normalize(buildCommand("npm", intent)),
|
|
193
198
|
pnpm: normalize(buildCommand("pnpm", intent)),
|
|
194
199
|
yarn: normalize(buildCommand("yarn", intent)),
|
|
195
|
-
}
|
|
200
|
+
} satisfies Record<PackageManager, string>;
|
|
196
201
|
};
|
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
* containing any non-text content (an image, an icon) counts as non-empty.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
/** The value shapes hast allows on an element's `properties`. */
|
|
19
|
+
type HastPropertyValue = string | number | boolean | (string | number)[];
|
|
20
|
+
|
|
18
21
|
/** A minimal hast node (avoids a hast type dependency). */
|
|
19
22
|
interface HastNode {
|
|
20
23
|
children?: HastNode[];
|
|
21
|
-
properties?: Record<string,
|
|
24
|
+
properties?: Record<string, HastPropertyValue>;
|
|
22
25
|
tagName?: string;
|
|
23
26
|
type: string;
|
|
24
27
|
value?: string;
|
package/src/markdown/twoslash.ts
CHANGED
|
@@ -38,6 +38,8 @@ const require = createRequire(import.meta.url);
|
|
|
38
38
|
* never pays the TypeScript parse cost.
|
|
39
39
|
*/
|
|
40
40
|
export const blumeTwoslashTransformer = (): ShikiTransformer => {
|
|
41
|
+
// SAFETY: this resolves Blume's own pinned `typescript` dependency, whose
|
|
42
|
+
// CJS entry exports exactly the API namespace `typeof TS` describes.
|
|
41
43
|
const tsModule = require("typescript") as typeof TS;
|
|
42
44
|
const twoslasher = createTwoslasher({
|
|
43
45
|
// Match the stock transformer's default: fence snippets are authored
|
package/src/og/card.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
+
import { imageSize } from "image-size";
|
|
3
4
|
import { render } from "takumi-js";
|
|
4
5
|
import type { RenderOptions } from "takumi-js";
|
|
5
6
|
import { container, googleFonts, image, text } from "takumi-js/helpers";
|
|
6
7
|
import type { FontSubset, GoogleFontFamily, Node } from "takumi-js/helpers";
|
|
7
8
|
|
|
9
|
+
import { ACCENTS, isAccentPreset } from "../theme/palette.ts";
|
|
8
10
|
import { OG_IMAGE_HEIGHT, OG_IMAGE_WIDTH } from "./dimensions.ts";
|
|
9
11
|
|
|
10
12
|
/** A local font file registered with the OG card renderer, read at build. */
|
|
@@ -54,23 +56,15 @@ export interface OgFontFamilies {
|
|
|
54
56
|
title?: string;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
teal: "#14b8a6",
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// Named presets map to Blume's palette hex (the preset "blue" is not CSS
|
|
68
|
-
// blue); anything else is handed to Takumi as-is — it parses the full CSS
|
|
69
|
-
// color grammar, and a genuinely malformed value fails the build with a
|
|
70
|
-
// parse error naming it. `hasOwn` keeps a preset name like "constructor"
|
|
71
|
-
// from resolving up the prototype chain.
|
|
59
|
+
// Named presets resolve from the theme's own OKLCH table — Takumi parses the
|
|
60
|
+
// full CSS color grammar, so the card renders exactly the accent the site
|
|
61
|
+
// shows (a separate hand-synced hex palette used to drift: the card's "blue"
|
|
62
|
+
// was Tailwind's, not Blume's). Anything else is handed to Takumi as-is, and
|
|
63
|
+
// a genuinely malformed value fails the build with a parse error naming it.
|
|
64
|
+
// `isAccentPreset` keeps a preset name like "constructor" from resolving up
|
|
65
|
+
// the prototype chain.
|
|
72
66
|
const resolveAccent = (accent: string): string =>
|
|
73
|
-
|
|
67
|
+
isAccentPreset(accent) ? ACCENTS[accent] : accent;
|
|
74
68
|
|
|
75
69
|
export interface OgCardPalette {
|
|
76
70
|
accent?: string;
|
|
@@ -159,6 +153,9 @@ const loadFonts = (
|
|
|
159
153
|
const key = JSON.stringify(fonts);
|
|
160
154
|
let pending = fontSubsetCache.get(key);
|
|
161
155
|
if (!pending) {
|
|
156
|
+
// SAFETY: OgFont's weight strings are documented as variable ranges like
|
|
157
|
+
// "100..900" (GoogleFontFamily's WeightRange); Takumi validates the value
|
|
158
|
+
// at fetch time and fails the build naming a malformed one.
|
|
162
159
|
pending = googleFonts(fonts as GoogleFontFamily[]);
|
|
163
160
|
fontSubsetCache.set(key, pending);
|
|
164
161
|
}
|
|
@@ -171,13 +168,29 @@ const loadFonts = (
|
|
|
171
168
|
* registers each file once across a build's per-page renders; a missing file
|
|
172
169
|
* rejects at first use, failing the build with the path in the cause.
|
|
173
170
|
*/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
171
|
+
/** A lazily-read local font file, in the shape `render` accepts for `fonts`. */
|
|
172
|
+
interface LocalFontSource {
|
|
173
|
+
data: () => Promise<Buffer>;
|
|
174
|
+
key: string;
|
|
175
|
+
name: string;
|
|
176
|
+
weight?: number;
|
|
177
|
+
style?: "normal" | "italic";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const localFontLoader = (font: OgLocalFont): LocalFontSource => {
|
|
181
|
+
const loader: LocalFontSource = {
|
|
182
|
+
data: () => readFile(font.src),
|
|
183
|
+
key: font.src,
|
|
184
|
+
name: font.name,
|
|
185
|
+
};
|
|
186
|
+
if (font.weight !== undefined) {
|
|
187
|
+
loader.weight = font.weight;
|
|
188
|
+
}
|
|
189
|
+
if (font.style !== undefined) {
|
|
190
|
+
loader.style = font.style;
|
|
191
|
+
}
|
|
192
|
+
return loader;
|
|
193
|
+
};
|
|
181
194
|
|
|
182
195
|
// Light neutral scale mirrored from the docs homepage theme tokens:
|
|
183
196
|
// FOREGROUND = --foreground, MUTED = --muted-foreground, FAINT = that lighter,
|
|
@@ -219,17 +232,21 @@ export const truncate = (value: string, max: number): string => {
|
|
|
219
232
|
// at full height — it stands alone as the brand (no text label beside it).
|
|
220
233
|
const MARK_HEIGHT = 32;
|
|
221
234
|
const MARK_MAX_WIDTH = 240;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
235
|
+
/**
|
|
236
|
+
* The SVG's aspect ratio (w/h), or null when no usable dimensions exist (the
|
|
237
|
+
* caller falls back to a square mark). image-size (already a dependency)
|
|
238
|
+
* reads explicit width/height and falls back to the viewBox, tolerating the
|
|
239
|
+
* quote/whitespace/attribute spellings the old regex silently missed —
|
|
240
|
+
* `viewBox = "…"`, newline-separated values — which shipped visibly-squashed
|
|
241
|
+
* marks instead of failing loudly.
|
|
242
|
+
*/
|
|
228
243
|
const logoAspect = (svg: string): number | null => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
244
|
+
try {
|
|
245
|
+
const { height, width } = imageSize(Buffer.from(svg));
|
|
246
|
+
return width && height ? width / height : null;
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
233
250
|
};
|
|
234
251
|
|
|
235
252
|
// Render the configured logo as the brand mark. A `currentColor` logo carries
|
package/src/og/derive.ts
CHANGED
|
@@ -13,7 +13,6 @@ import type {
|
|
|
13
13
|
FontsConfig,
|
|
14
14
|
FontValue,
|
|
15
15
|
LocalFontConfig,
|
|
16
|
-
RemoteFontConfig,
|
|
17
16
|
} from "../theme/fonts.ts";
|
|
18
17
|
import { GOOGLE_FONTS, isFontSlug } from "../theme/fonts.ts";
|
|
19
18
|
import type { OgFont, OgFontFamilies, OgLocalFont } from "./card.ts";
|
|
@@ -31,6 +30,23 @@ const CARD_WEIGHTS = [400, 600];
|
|
|
31
30
|
const absoluteSrc = (root: string, src: string): string =>
|
|
32
31
|
isAbsolute(src) ? src : join(root, src);
|
|
33
32
|
|
|
33
|
+
/** A concrete numeric face weight (as opposed to a variable-range string). */
|
|
34
|
+
const isNumericWeight = (
|
|
35
|
+
weight: number | string | undefined
|
|
36
|
+
): weight is number => typeof weight === "number";
|
|
37
|
+
|
|
38
|
+
/** A variable-range weight spec string, e.g. `"100..900"`. */
|
|
39
|
+
const isRangeWeight = (weight: number | string | undefined): weight is string =>
|
|
40
|
+
typeof weight === "string";
|
|
41
|
+
|
|
42
|
+
/** A theme role configured as a font slug / family-name string. */
|
|
43
|
+
const isFontName = (value: FontValue): value is string =>
|
|
44
|
+
typeof value === "string";
|
|
45
|
+
|
|
46
|
+
/** An OG font entry that reads a local file (as opposed to a Google family). */
|
|
47
|
+
const isLocalOgFont = (font: OgFont): font is OgLocalFont =>
|
|
48
|
+
typeof font !== "string" && "src" in font;
|
|
49
|
+
|
|
34
50
|
/**
|
|
35
51
|
* The weight spec to fetch for a derived Google family: the declared weights
|
|
36
52
|
* the card uses, the declared numeric weights otherwise, a lone variable
|
|
@@ -39,9 +55,7 @@ const absoluteSrc = (root: string, src: string): string =>
|
|
|
39
55
|
const googleWeights = (
|
|
40
56
|
weights: (number | string)[]
|
|
41
57
|
): number[] | string | undefined => {
|
|
42
|
-
const numbers = weights.filter(
|
|
43
|
-
(weight): weight is number => typeof weight === "number"
|
|
44
|
-
);
|
|
58
|
+
const numbers = weights.filter(isNumericWeight);
|
|
45
59
|
const used = numbers.filter((weight) => CARD_WEIGHTS.includes(weight));
|
|
46
60
|
if (used.length > 0) {
|
|
47
61
|
return used;
|
|
@@ -50,7 +64,7 @@ const googleWeights = (
|
|
|
50
64
|
return numbers;
|
|
51
65
|
}
|
|
52
66
|
const [first] = weights;
|
|
53
|
-
return weights.length === 1 &&
|
|
67
|
+
return weights.length === 1 && isRangeWeight(first) ? first : undefined;
|
|
54
68
|
};
|
|
55
69
|
|
|
56
70
|
const googleOgFont = (name: string, weights: (number | string)[]): OgFont => {
|
|
@@ -60,15 +74,19 @@ const googleOgFont = (name: string, weights: (number | string)[]): OgFont => {
|
|
|
60
74
|
|
|
61
75
|
/** Per-variant local entries for the renderer (paths made absolute). */
|
|
62
76
|
const localOgFonts = (font: LocalFontConfig, root: string): OgLocalFont[] =>
|
|
63
|
-
font.variants.map((variant) =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
font.variants.map((variant) => {
|
|
78
|
+
const entry: OgLocalFont = {
|
|
79
|
+
name: font.name,
|
|
80
|
+
src: absoluteSrc(root, variant.src),
|
|
81
|
+
};
|
|
82
|
+
const withWeight: OgLocalFont = isNumericWeight(variant.weight)
|
|
83
|
+
? { ...entry, weight: variant.weight }
|
|
84
|
+
: entry;
|
|
67
85
|
// Takumi's per-face style is normal/italic; oblique falls back to the file.
|
|
68
|
-
|
|
69
|
-
? { style: variant.style }
|
|
70
|
-
:
|
|
71
|
-
})
|
|
86
|
+
return variant.style === "normal" || variant.style === "italic"
|
|
87
|
+
? { ...withWeight, style: variant.style }
|
|
88
|
+
: withWeight;
|
|
89
|
+
});
|
|
72
90
|
|
|
73
91
|
/**
|
|
74
92
|
* The card fonts for one theme role, or null when the role can't flow into
|
|
@@ -76,7 +94,7 @@ const localOgFonts = (font: LocalFontConfig, root: string): OgLocalFont[] =>
|
|
|
76
94
|
* `googleFonts` only speaks Google's css2 endpoint).
|
|
77
95
|
*/
|
|
78
96
|
const roleFonts = (value: FontValue, root: string): OgFont[] | null => {
|
|
79
|
-
if (
|
|
97
|
+
if (isFontName(value)) {
|
|
80
98
|
if (!isFontSlug(value)) {
|
|
81
99
|
return null;
|
|
82
100
|
}
|
|
@@ -86,16 +104,15 @@ const roleFonts = (value: FontValue, root: string): OgFont[] | null => {
|
|
|
86
104
|
if ("variants" in value) {
|
|
87
105
|
return localOgFonts(value, root);
|
|
88
106
|
}
|
|
89
|
-
|
|
90
|
-
if ((remote.provider ?? "google") !== "google") {
|
|
107
|
+
if ((value.provider ?? "google") !== "google") {
|
|
91
108
|
return null;
|
|
92
109
|
}
|
|
93
|
-
return [googleOgFont(
|
|
110
|
+
return [googleOgFont(value.name, value.weights ?? CARD_WEIGHTS)];
|
|
94
111
|
};
|
|
95
112
|
|
|
96
113
|
/** The family name a theme role registers under. */
|
|
97
114
|
const roleFamily = (value: FontValue): string | null => {
|
|
98
|
-
if (
|
|
115
|
+
if (isFontName(value)) {
|
|
99
116
|
return isFontSlug(value) ? GOOGLE_FONTS[value].family : null;
|
|
100
117
|
}
|
|
101
118
|
return value.name;
|
|
@@ -136,18 +153,17 @@ export const deriveOgFonts = (
|
|
|
136
153
|
}
|
|
137
154
|
}
|
|
138
155
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
156
|
+
const result: DerivedOgFonts = { fonts: derived };
|
|
157
|
+
if (families.title || families.body) {
|
|
158
|
+
result.families = families;
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
143
161
|
};
|
|
144
162
|
|
|
145
163
|
/** Explicit `seo.og.fonts` with local `src` paths resolved to absolute. */
|
|
146
164
|
export const resolveOgFontSources = (fonts: OgFont[], root: string): OgFont[] =>
|
|
147
165
|
fonts.map((font) =>
|
|
148
|
-
|
|
149
|
-
? { ...font, src: absoluteSrc(root, font.src) }
|
|
150
|
-
: font
|
|
166
|
+
isLocalOgFont(font) ? { ...font, src: absoluteSrc(root, font.src) } : font
|
|
151
167
|
);
|
|
152
168
|
|
|
153
169
|
/**
|
|
@@ -185,14 +201,14 @@ export const missingFontFiles = (
|
|
|
185
201
|
): string[] => {
|
|
186
202
|
const sources: string[] = [];
|
|
187
203
|
for (const value of Object.values(options.themeFonts ?? {})) {
|
|
188
|
-
if (
|
|
204
|
+
if (!isFontName(value) && "variants" in value) {
|
|
189
205
|
sources.push(
|
|
190
206
|
...value.variants.map((variant) => absoluteSrc(root, variant.src))
|
|
191
207
|
);
|
|
192
208
|
}
|
|
193
209
|
}
|
|
194
210
|
for (const font of options.ogFonts) {
|
|
195
|
-
if (
|
|
211
|
+
if (isLocalOgFont(font)) {
|
|
196
212
|
sources.push(absoluteSrc(root, font.src));
|
|
197
213
|
}
|
|
198
214
|
}
|