blume 1.1.0 → 1.1.2
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 +24 -0
- package/dist/cli/index.js +319 -91
- package/dist/cli/index.js.map +18 -18
- package/dist/types/ai/component-markdown.d.ts +12 -1
- package/dist/types/core/config-input.d.ts +18 -8
- package/dist/types/core/schema.d.ts +38 -24
- package/dist/types/core/types.d.ts +14 -0
- package/dist/types/markdown/themes.d.ts +21 -0
- package/docs/advanced/changelog.mdx +1 -1
- package/docs/configuration/ai.mdx +2 -2
- package/docs/configuration/index.mdx +1 -1
- package/docs/configuration/seo.mdx +16 -0
- package/docs/content/components.mdx +1 -1
- package/docs/content/navigation.mdx +4 -0
- package/docs/content/syntax.mdx +14 -0
- package/docs/reference/cli.mdx +1 -1
- package/docs/reference/frontmatter.mdx +2 -0
- package/package.json +2 -1
- package/src/ai/component-markdown.ts +39 -11
- package/src/ai/llms.ts +4 -2
- package/src/ai/markdown.ts +5 -1
- package/src/astro/generate.ts +121 -32
- package/src/astro/pages.ts +21 -5
- package/src/astro/templates.ts +70 -7
- package/src/audit/checks/llms.ts +4 -1
- package/src/cli/commands/build.ts +13 -1
- package/src/cli/prepare.ts +10 -2
- package/src/components/content/Component.astro +99 -6
- package/src/components/content/diff.ts +53 -4
- package/src/components/layout/RootLayout.astro +9 -1
- package/src/components/layout/nav-utils.ts +18 -7
- package/src/core/config-input.ts +18 -8
- package/src/core/diagnostics.ts +2 -0
- package/src/core/graph.ts +23 -4
- package/src/core/navigation.ts +180 -21
- package/src/core/project-graph.ts +54 -28
- package/src/core/schema.ts +39 -2
- package/src/core/sources/github-releases.ts +65 -2
- package/src/core/types.ts +14 -0
- package/src/markdown/index.ts +3 -0
- package/src/markdown/inline-code.ts +1 -1
- package/src/markdown/themes.ts +7 -2
- package/src/markdown/twoslash.ts +60 -0
- package/src/registry/eject.ts +3 -1
- /package/docs/{03-faq.mdx → 07-faq.mdx} +0 -0
|
@@ -15,7 +15,7 @@ import { resolveProjectContext } from "./project.ts";
|
|
|
15
15
|
import type { ResolvedConfig } from "./schema.ts";
|
|
16
16
|
import { normalizeEntry } from "./sources/normalize.ts";
|
|
17
17
|
import { resolveDocsCollection, resolveSources } from "./sources/resolve.ts";
|
|
18
|
-
import type { ContentSource } from "./sources/types.ts";
|
|
18
|
+
import type { ContentSource, SourceLoadResult } from "./sources/types.ts";
|
|
19
19
|
import type {
|
|
20
20
|
BlumeManifest,
|
|
21
21
|
ContentGraph,
|
|
@@ -70,6 +70,8 @@ export interface BlumeProject {
|
|
|
70
70
|
graph: ContentGraph;
|
|
71
71
|
manifest: BlumeManifest;
|
|
72
72
|
diagnostics: Diagnostic[];
|
|
73
|
+
/** Entries excluded from the graph because their frontmatter failed validation. */
|
|
74
|
+
droppedPages: number;
|
|
73
75
|
/** The instantiated content sources, for lazy entry reads (search/AI/raw). */
|
|
74
76
|
sources: ContentSource[];
|
|
75
77
|
}
|
|
@@ -116,6 +118,51 @@ const entryIdDiagnostics = (
|
|
|
116
118
|
return diagnostics;
|
|
117
119
|
};
|
|
118
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Funnel every loaded source's entries through the shared `normalizeEntry`,
|
|
123
|
+
* collecting pages, diagnostics, and the count of entries dropped outright —
|
|
124
|
+
* an entry that yields no pages but did yield diagnostics was rejected for
|
|
125
|
+
* invalid frontmatter, and callers surface that count so a build with missing
|
|
126
|
+
* pages can't read as clean.
|
|
127
|
+
*/
|
|
128
|
+
const normalizeLoadedEntries = (
|
|
129
|
+
loaded: ({ source: ContentSource } & SourceLoadResult)[],
|
|
130
|
+
config: ResolvedConfig
|
|
131
|
+
): { pages: PageRecord[]; diagnostics: Diagnostic[]; droppedPages: number } => {
|
|
132
|
+
// Only thread `frontmatter.extend` through when a project opts in, so the
|
|
133
|
+
// known-key split in `normalizeEntry` stays off the default path.
|
|
134
|
+
const frontmatterExtend =
|
|
135
|
+
Object.keys(config.frontmatter.extend).length > 0
|
|
136
|
+
? config.frontmatter.extend
|
|
137
|
+
: undefined;
|
|
138
|
+
|
|
139
|
+
const pages: PageRecord[] = [];
|
|
140
|
+
const allDiagnostics: Diagnostic[] = [];
|
|
141
|
+
let droppedPages = 0;
|
|
142
|
+
for (const { source, entries, diagnostics } of loaded) {
|
|
143
|
+
allDiagnostics.push(...diagnostics);
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
const normalized = normalizeEntry(entry, {
|
|
146
|
+
basePath: config.basePath,
|
|
147
|
+
defaultType: config.content.defaultType,
|
|
148
|
+
frontmatterExtend,
|
|
149
|
+
i18n: config.i18n,
|
|
150
|
+
source: {
|
|
151
|
+
name: source.name,
|
|
152
|
+
prefix: source.prefix,
|
|
153
|
+
staged: source.staged,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
if (normalized.pages.length === 0 && normalized.diagnostics.length > 0) {
|
|
157
|
+
droppedPages += 1;
|
|
158
|
+
}
|
|
159
|
+
pages.push(...normalized.pages);
|
|
160
|
+
allDiagnostics.push(...normalized.diagnostics);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { diagnostics: allDiagnostics, droppedPages, pages };
|
|
164
|
+
};
|
|
165
|
+
|
|
119
166
|
/**
|
|
120
167
|
* Run the full core pipeline for a project root: load config, resolve paths,
|
|
121
168
|
* discover content and folder meta, build the graph, and assemble the manifest.
|
|
@@ -190,33 +237,11 @@ export const scanProject = async (
|
|
|
190
237
|
discoverFolderMeta(metaSources, { localeDirs }),
|
|
191
238
|
]);
|
|
192
239
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
: undefined;
|
|
199
|
-
|
|
200
|
-
const allPages: PageRecord[] = [];
|
|
201
|
-
const contentDiagnostics: Diagnostic[] = [];
|
|
202
|
-
for (const { source, entries, diagnostics } of loaded) {
|
|
203
|
-
contentDiagnostics.push(...diagnostics);
|
|
204
|
-
for (const entry of entries) {
|
|
205
|
-
const normalized = normalizeEntry(entry, {
|
|
206
|
-
basePath: config.basePath,
|
|
207
|
-
defaultType: config.content.defaultType,
|
|
208
|
-
frontmatterExtend,
|
|
209
|
-
i18n: config.i18n,
|
|
210
|
-
source: {
|
|
211
|
-
name: source.name,
|
|
212
|
-
prefix: source.prefix,
|
|
213
|
-
staged: source.staged,
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
allPages.push(...normalized.pages);
|
|
217
|
-
contentDiagnostics.push(...normalized.diagnostics);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
240
|
+
const {
|
|
241
|
+
diagnostics: contentDiagnostics,
|
|
242
|
+
droppedPages,
|
|
243
|
+
pages: allPages,
|
|
244
|
+
} = normalizeLoadedEntries(loaded, config);
|
|
220
245
|
|
|
221
246
|
// Drafts render in dev and in preview, but are excluded from production builds.
|
|
222
247
|
const pages =
|
|
@@ -267,6 +292,7 @@ export const scanProject = async (
|
|
|
267
292
|
...graph.diagnostics,
|
|
268
293
|
...i18nWarnings,
|
|
269
294
|
],
|
|
295
|
+
droppedPages,
|
|
270
296
|
graph,
|
|
271
297
|
manifest,
|
|
272
298
|
mode,
|
package/src/core/schema.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
import type { ComponentMarkdown } from "../ai/component-markdown.ts";
|
|
4
|
+
import type { CodeTheme } from "../markdown/themes.ts";
|
|
4
5
|
import { normalizeRoute } from "../openapi/references.ts";
|
|
5
6
|
import { normalizeXHandle } from "../seo/x-handle.ts";
|
|
6
7
|
import { FONT_SLUGS, isFontSlug } from "../theme/fonts.ts";
|
|
@@ -870,6 +871,13 @@ const ogConfigSchema = z.strictObject({
|
|
|
870
871
|
logo: z.string().optional(),
|
|
871
872
|
/** Optional generated-card colors. */
|
|
872
873
|
palette: ogPaletteSchema.optional(),
|
|
874
|
+
/**
|
|
875
|
+
* Card headlines for custom `.astro` pages, keyed by route (`"/"`, `"/cli"`).
|
|
876
|
+
* A custom page has no frontmatter to read, so its card is otherwise titled
|
|
877
|
+
* by humanizing its last URL segment (`/cli` → "Cli"); an entry here wins.
|
|
878
|
+
* Content pages always take their card headline from the page title.
|
|
879
|
+
*/
|
|
880
|
+
titles: z.record(z.string(), z.string()).optional(),
|
|
873
881
|
});
|
|
874
882
|
|
|
875
883
|
const rssConfigSchema = z.strictObject({
|
|
@@ -943,9 +951,38 @@ const githubConfigSchema = z.strictObject({
|
|
|
943
951
|
repo: z.string(),
|
|
944
952
|
});
|
|
945
953
|
|
|
954
|
+
const codeThemeSchema = z.custom<CodeTheme>((value) => {
|
|
955
|
+
if (typeof value === "string") {
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
// Token rules live in `settings` (Shiki's canonical field, also the TextMate
|
|
962
|
+
// form themes like createCssVariablesTheme() produce) or `tokenColors` (the
|
|
963
|
+
// VS Code spelling Shiki falls back to). A colors-only theme (editor fg/bg,
|
|
964
|
+
// no token rules) is also valid — Shiki renders it from `colors` alone. Each
|
|
965
|
+
// field present must have the right shape, and at least one must be present.
|
|
966
|
+
const theme = value as Record<string, unknown>;
|
|
967
|
+
const settingsValid =
|
|
968
|
+
theme.settings === undefined || Array.isArray(theme.settings);
|
|
969
|
+
const tokenColorsValid =
|
|
970
|
+
theme.tokenColors === undefined || Array.isArray(theme.tokenColors);
|
|
971
|
+
const colorsValid =
|
|
972
|
+
theme.colors === undefined ||
|
|
973
|
+
(typeof theme.colors === "object" &&
|
|
974
|
+
theme.colors !== null &&
|
|
975
|
+
!Array.isArray(theme.colors));
|
|
976
|
+
const hasContent =
|
|
977
|
+
theme.settings !== undefined ||
|
|
978
|
+
theme.tokenColors !== undefined ||
|
|
979
|
+
theme.colors !== undefined;
|
|
980
|
+
return settingsValid && tokenColorsValid && colorsValid && hasContent;
|
|
981
|
+
}, "Expected a Shiki theme name or custom theme object");
|
|
982
|
+
|
|
946
983
|
const codeBlockThemeSchema = z.strictObject({
|
|
947
|
-
dark:
|
|
948
|
-
light:
|
|
984
|
+
dark: codeThemeSchema.default("github-dark"),
|
|
985
|
+
light: codeThemeSchema.default("github-light"),
|
|
949
986
|
});
|
|
950
987
|
|
|
951
988
|
const codeBlocksConfigSchema = z.strictObject({
|
|
@@ -56,6 +56,63 @@ const LEADING_V = /^v/iu;
|
|
|
56
56
|
const NON_SLUG = /[^a-z0-9]+/gu;
|
|
57
57
|
const EDGE_DASHES = /^-+|-+$/gu;
|
|
58
58
|
|
|
59
|
+
// `blume audit` grades meta descriptions against the 110–160 character search
|
|
60
|
+
// snippet range (audit/types.ts thresholds), so the derived summary aims for
|
|
61
|
+
// the longest word-boundary cut under the cap.
|
|
62
|
+
const DESCRIPTION_MAX = 160;
|
|
63
|
+
const DESCRIPTION_MIN = 110;
|
|
64
|
+
|
|
65
|
+
const CODE_FENCE = /```[\s\S]*?```/gu;
|
|
66
|
+
const HEADING_LINE = /^#{1,6}\s.*$/gmu;
|
|
67
|
+
const LIST_MARK = /^\s*(?:[-*+]|\d+[.)])\s+/u;
|
|
68
|
+
// Changesets-generated release bullets open with the changeset's short commit
|
|
69
|
+
// hash (`- cf8fa22: Fix …`) — noise in a search snippet.
|
|
70
|
+
const CHANGESET_HASH = /^[0-9a-f]{7,40}:\s+/u;
|
|
71
|
+
const IMAGE = /!\[[^\]]*\]\([^)]*\)/gu;
|
|
72
|
+
const LINK = /\[(?<text>[^\]]*)\]\([^)]*\)/gu;
|
|
73
|
+
const INLINE_CODE = /`(?<code>[^`]+)`/gu;
|
|
74
|
+
// Tag-shaped only: a bare `<` in prose must not swallow text up to a later `>`.
|
|
75
|
+
const HTML_OR_JSX = /<\/?[a-zA-Z][^\n<>]*>|<\/?>/gu;
|
|
76
|
+
const MARKDOWN_PUNCT = /[*_~>]+/gu;
|
|
77
|
+
const WHITESPACE = /\s+/gu;
|
|
78
|
+
const TRAILING_FRAGMENT = /[\s,;:.—–-]+$/u;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Derive a meta description from release notes: markdown reduced to plain
|
|
82
|
+
* text — section headings ("### Patch Changes") and changesets' commit-hash
|
|
83
|
+
* bullet prefixes dropped — then cut at a word boundary to fit the search
|
|
84
|
+
* snippet cap. Undefined when the notes have no prose at all.
|
|
85
|
+
*/
|
|
86
|
+
const releaseDescription = (body: string): string | undefined => {
|
|
87
|
+
const text = body
|
|
88
|
+
.replaceAll(CODE_FENCE, " ")
|
|
89
|
+
.replaceAll(HEADING_LINE, "")
|
|
90
|
+
.split("\n")
|
|
91
|
+
.map((line) => line.replace(LIST_MARK, "").replace(CHANGESET_HASH, ""))
|
|
92
|
+
.join("\n")
|
|
93
|
+
.replaceAll(IMAGE, " ")
|
|
94
|
+
.replaceAll(LINK, "$<text>")
|
|
95
|
+
.replaceAll(INLINE_CODE, "$<code>")
|
|
96
|
+
.replaceAll(HTML_OR_JSX, " ")
|
|
97
|
+
.replaceAll(MARKDOWN_PUNCT, " ")
|
|
98
|
+
.replaceAll(WHITESPACE, " ")
|
|
99
|
+
.trim();
|
|
100
|
+
if (!text) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
if (text.length <= DESCRIPTION_MAX) {
|
|
104
|
+
return text;
|
|
105
|
+
}
|
|
106
|
+
// Cut before the cap at a word boundary (kept only when it doesn't drop the
|
|
107
|
+
// summary under the minimum), shed any dangling punctuation, and mark the cut.
|
|
108
|
+
const slice = text.slice(0, DESCRIPTION_MAX - 1);
|
|
109
|
+
const boundary = slice.lastIndexOf(" ");
|
|
110
|
+
const head = (
|
|
111
|
+
boundary >= DESCRIPTION_MIN ? slice.slice(0, boundary) : slice
|
|
112
|
+
).replace(TRAILING_FRAGMENT, "");
|
|
113
|
+
return `${head}…`;
|
|
114
|
+
};
|
|
115
|
+
|
|
59
116
|
/** Slugify a tag into a stable, URL-safe source ref (`v1.2.0` -> `v1-2-0`). */
|
|
60
117
|
const slugifyTag = (tag: string): string =>
|
|
61
118
|
tag.toLowerCase().replaceAll(NON_SLUG, "-").replaceAll(EDGE_DASHES, "");
|
|
@@ -71,9 +128,10 @@ const githubHeaders = (): Headers => {
|
|
|
71
128
|
};
|
|
72
129
|
|
|
73
130
|
/**
|
|
74
|
-
* Lower one release to a staged Markdown entry: the notes become the body
|
|
131
|
+
* Lower one release to a staged Markdown entry: the notes become the body,
|
|
75
132
|
* `type: changelog` frontmatter (title/date/version/category) drives the
|
|
76
|
-
* generated `/changelog` timeline and RSS feed
|
|
133
|
+
* generated `/changelog` timeline and RSS feed, and a summary derived from the
|
|
134
|
+
* notes becomes the release page's meta description.
|
|
77
135
|
*/
|
|
78
136
|
const releaseToEntry = (release: GithubRelease): SourceEntry => {
|
|
79
137
|
const version = release.tag_name.replace(LEADING_V, "");
|
|
@@ -81,9 +139,14 @@ const releaseToEntry = (release: GithubRelease): SourceEntry => {
|
|
|
81
139
|
const date = release.published_at ?? release.created_at;
|
|
82
140
|
const category = release.prerelease ? "Prerelease" : "Release";
|
|
83
141
|
const body = (release.body ?? "").replaceAll("\r\n", "\n").trim();
|
|
142
|
+
// A summary in `seo.description` gives each release page a unique meta
|
|
143
|
+
// description (instead of the site-wide fallback) without also rendering the
|
|
144
|
+
// visible lede paragraph a top-level `description` would add.
|
|
145
|
+
const description = releaseDescription(body);
|
|
84
146
|
const data = {
|
|
85
147
|
changelog: { category, version },
|
|
86
148
|
date,
|
|
149
|
+
...(description ? { seo: { description } } : {}),
|
|
87
150
|
title,
|
|
88
151
|
type: "changelog",
|
|
89
152
|
};
|
package/src/core/types.ts
CHANGED
|
@@ -108,6 +108,13 @@ export interface PageRecord {
|
|
|
108
108
|
* `/guides/x`). Pages with the same key are translations of each other.
|
|
109
109
|
*/
|
|
110
110
|
translationKey: string;
|
|
111
|
+
/**
|
|
112
|
+
* True for entries filled in from the fallback locale to pad a locale's
|
|
113
|
+
* navigation for pages it hasn't translated yet. The record's content —
|
|
114
|
+
* title included — belongs to the fallback locale, so per-locale content
|
|
115
|
+
* checks skip these.
|
|
116
|
+
*/
|
|
117
|
+
fallback?: boolean;
|
|
111
118
|
/**
|
|
112
119
|
* Content-relative path with the leading locale directory stripped, used for
|
|
113
120
|
* sidebar grouping so the locale dir is not surfaced as a nav group. Equals
|
|
@@ -220,6 +227,13 @@ export interface Navigation {
|
|
|
220
227
|
tabs: NavTab[];
|
|
221
228
|
selectors: NavSelector[];
|
|
222
229
|
sidebar: NavNode[];
|
|
230
|
+
/**
|
|
231
|
+
* The tree root in final path space — localized and based (`/`, `/en`,
|
|
232
|
+
* `/docs`). Tab paths arrive in the same space, so the tab sitting at this
|
|
233
|
+
* path spans the whole tree and must be scoped as the root tab, not as a
|
|
234
|
+
* section tab. Absent on older serialized graphs; treat as `/`.
|
|
235
|
+
*/
|
|
236
|
+
root?: string;
|
|
223
237
|
/** Pinned links shown above the sidebar sections, unscoped by tab. */
|
|
224
238
|
featured: FeaturedLink[];
|
|
225
239
|
/** Repo URL for the header link, or null when hidden (`navigation.repo`). */
|
package/src/markdown/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ import { tableWrapPlugin } from "./table-wrap.ts";
|
|
|
21
21
|
import { DEFAULT_CODE_THEMES } from "./themes.ts";
|
|
22
22
|
import type { CodeThemes } from "./themes.ts";
|
|
23
23
|
|
|
24
|
+
export type { CodeTheme, CodeThemes } from "./themes.ts";
|
|
25
|
+
|
|
24
26
|
/** A Shiki transformer, derived from the upstream factories' return type. */
|
|
25
27
|
type ShikiTransformer = ReturnType<typeof transformerNotationDiff>;
|
|
26
28
|
|
|
@@ -37,6 +39,7 @@ export { calloutTypeFor } from "./directives.ts";
|
|
|
37
39
|
export { headingAnchorPlugin } from "./heading-anchors.ts";
|
|
38
40
|
export { mermaidPlugin } from "./mermaid.ts";
|
|
39
41
|
export { packageInstallPlugin } from "./package-install.ts";
|
|
42
|
+
export { blumeTwoslashTransformer } from "./twoslash.ts";
|
|
40
43
|
|
|
41
44
|
/** Element type of Satteri's `mdastPlugins`, sourced from the (alpha) core. */
|
|
42
45
|
type MdastPlugin = NonNullable<
|
package/src/markdown/themes.ts
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* single home for the github fallback used when nothing is configured.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { ThemeRegistrationAny } from "shiki";
|
|
10
|
+
|
|
11
|
+
/** A bundled Shiki theme name or an inline custom Shiki theme definition. */
|
|
12
|
+
export type CodeTheme = string | ThemeRegistrationAny;
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* A light/dark Shiki theme pair (`markdown.codeBlocks.theme`). A `type` (not an
|
|
11
16
|
* `interface`) so it keeps the implicit index signature Shiki's `themes`
|
|
@@ -13,8 +18,8 @@
|
|
|
13
18
|
*/
|
|
14
19
|
// oxlint-disable-next-line typescript/consistent-type-definitions -- interface loses the implicit index signature Shiki's `themes` param needs
|
|
15
20
|
export type CodeThemes = {
|
|
16
|
-
dark:
|
|
17
|
-
light:
|
|
21
|
+
dark: CodeTheme;
|
|
22
|
+
light: CodeTheme;
|
|
18
23
|
};
|
|
19
24
|
|
|
20
25
|
/** The default pair, used when `markdown.codeBlocks.theme` is unset. */
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Twoslash transformer for fenced code blocks, compiled with Blume's own
|
|
3
|
+
* pinned TypeScript instead of whatever copy the user's project hoists.
|
|
4
|
+
*
|
|
5
|
+
* The stock `transformerTwoslash` from `@shikijs/twoslash` resolves the
|
|
6
|
+
* ambient `typescript` package, which is whatever version the surrounding
|
|
7
|
+
* project installed. Under TypeScript 7 (the native tsgo compiler) the
|
|
8
|
+
* package's main export is a version stub with no classic compiler API — no
|
|
9
|
+
* `ts.sys`, no language service — and its `lib/` directory ships no
|
|
10
|
+
* `lib.*.d.ts` files, so Twoslash breaks the moment a fence uses it. Blume
|
|
11
|
+
* ships a classic `typescript` as its own runtime dependency, so this factory
|
|
12
|
+
* resolves that copy from inside the package and hands it to Twoslash
|
|
13
|
+
* explicitly (`tsModule` for the compiler, `tsLibDirectory` for the default
|
|
14
|
+
* lib files), leaving the user's project free to use any TypeScript version.
|
|
15
|
+
*
|
|
16
|
+
* Composed from the `core` entrypoints of both packages: the main `twoslash`
|
|
17
|
+
* entry eagerly imports the ambient `typescript` (harmless under TS7 — the
|
|
18
|
+
* stub loads fine — but pointless), and its `transformerTwoslash` wrapper
|
|
19
|
+
* forwards `tsModule` to `createTwoslasher` while dropping `tsLibDirectory`.
|
|
20
|
+
* The core entries import no `typescript` at all and take both options.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createRequire } from "node:module";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
|
|
26
|
+
import { createTransformerFactory, rendererRich } from "@shikijs/twoslash/core";
|
|
27
|
+
import type { ShikiTransformer } from "shiki";
|
|
28
|
+
import { createTwoslasher } from "twoslash/core";
|
|
29
|
+
import type TS from "typescript";
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Twoslash transformer preconfigured for Blume: opt-in per fence via the
|
|
35
|
+
* `twoslash` meta keyword (explicitTrigger), compiling with Blume's own
|
|
36
|
+
* pinned classic TypeScript. The compiler is resolved lazily at call time —
|
|
37
|
+
* this runs once, at Astro config load — so merely importing this module
|
|
38
|
+
* never pays the TypeScript parse cost.
|
|
39
|
+
*/
|
|
40
|
+
export const blumeTwoslashTransformer = (): ShikiTransformer => {
|
|
41
|
+
const tsModule = require("typescript") as typeof TS;
|
|
42
|
+
const twoslasher = createTwoslasher({
|
|
43
|
+
// Match the stock transformer's default: fence snippets are authored
|
|
44
|
+
// bundler-style (extensionless relative imports, package imports).
|
|
45
|
+
compilerOptions: {
|
|
46
|
+
moduleResolution: tsModule.ModuleResolutionKind.Bundler,
|
|
47
|
+
},
|
|
48
|
+
// `require.resolve("typescript")` is the package's main entry,
|
|
49
|
+
// `lib/typescript.js`; its directory holds the `lib.*.d.ts` default libs.
|
|
50
|
+
tsLibDirectory: path.dirname(require.resolve("typescript")),
|
|
51
|
+
tsModule,
|
|
52
|
+
vfsRoot: process.cwd(),
|
|
53
|
+
});
|
|
54
|
+
return createTransformerFactory(
|
|
55
|
+
twoslasher,
|
|
56
|
+
rendererRich()
|
|
57
|
+
)({
|
|
58
|
+
explicitTrigger: true,
|
|
59
|
+
});
|
|
60
|
+
};
|
package/src/registry/eject.ts
CHANGED
|
@@ -472,7 +472,9 @@ export const eject = async (
|
|
|
472
472
|
|
|
473
473
|
if (config.seo.og.enabled) {
|
|
474
474
|
files.push({
|
|
475
|
-
content: ogEndpointTemplate(
|
|
475
|
+
content: ogEndpointTemplate(
|
|
476
|
+
customOgRoutes(pages, config.title, config.seo.og.titles)
|
|
477
|
+
),
|
|
476
478
|
path: join(srcDir, "pages", "og", "[...slug].png.ts"),
|
|
477
479
|
});
|
|
478
480
|
}
|
|
File without changes
|