blume 0.1.0 → 0.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/dist/cli/index.js +1213 -473
- package/dist/cli/index.js.map +33 -29
- package/dist/types/core/bridge.d.ts +24 -0
- package/dist/types/core/config.d.ts +35 -0
- package/dist/types/core/data.d.ts +129 -0
- package/dist/types/core/define-components.d.ts +27 -0
- package/dist/types/core/define-meta.d.ts +16 -0
- package/dist/types/core/deployment-env.d.ts +9 -0
- package/dist/types/core/diagnostics.d.ts +17 -0
- package/dist/types/core/i18n-ui.d.ts +500 -0
- package/dist/types/core/load-module.d.ts +7 -0
- package/dist/types/core/package-root.d.ts +17 -0
- package/dist/types/core/project.d.ts +9 -0
- package/dist/types/core/schema.d.ts +3461 -0
- package/dist/types/core/sources/types.d.ts +107 -0
- package/dist/types/core/types.d.ts +245 -0
- package/dist/types/core/ui-packs/ar.d.ts +3 -0
- package/dist/types/core/ui-packs/bg.d.ts +3 -0
- package/dist/types/core/ui-packs/bn.d.ts +3 -0
- package/dist/types/core/ui-packs/ca.d.ts +3 -0
- package/dist/types/core/ui-packs/cs.d.ts +3 -0
- package/dist/types/core/ui-packs/da.d.ts +3 -0
- package/dist/types/core/ui-packs/de.d.ts +3 -0
- package/dist/types/core/ui-packs/el.d.ts +3 -0
- package/dist/types/core/ui-packs/es.d.ts +3 -0
- package/dist/types/core/ui-packs/fa.d.ts +3 -0
- package/dist/types/core/ui-packs/fi.d.ts +3 -0
- package/dist/types/core/ui-packs/fr.d.ts +3 -0
- package/dist/types/core/ui-packs/he.d.ts +3 -0
- package/dist/types/core/ui-packs/hi.d.ts +3 -0
- package/dist/types/core/ui-packs/hr.d.ts +3 -0
- package/dist/types/core/ui-packs/hu.d.ts +3 -0
- package/dist/types/core/ui-packs/id.d.ts +3 -0
- package/dist/types/core/ui-packs/index.d.ts +13 -0
- package/dist/types/core/ui-packs/it.d.ts +3 -0
- package/dist/types/core/ui-packs/ja.d.ts +3 -0
- package/dist/types/core/ui-packs/ko.d.ts +3 -0
- package/dist/types/core/ui-packs/nl.d.ts +3 -0
- package/dist/types/core/ui-packs/no.d.ts +3 -0
- package/dist/types/core/ui-packs/pl.d.ts +3 -0
- package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
- package/dist/types/core/ui-packs/pt.d.ts +3 -0
- package/dist/types/core/ui-packs/ro.d.ts +3 -0
- package/dist/types/core/ui-packs/ru.d.ts +3 -0
- package/dist/types/core/ui-packs/sk.d.ts +3 -0
- package/dist/types/core/ui-packs/sr.d.ts +3 -0
- package/dist/types/core/ui-packs/sv.d.ts +3 -0
- package/dist/types/core/ui-packs/th.d.ts +3 -0
- package/dist/types/core/ui-packs/tr.d.ts +3 -0
- package/dist/types/core/ui-packs/uk.d.ts +3 -0
- package/dist/types/core/ui-packs/vi.d.ts +3 -0
- package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
- package/dist/types/core/ui-packs/zh.d.ts +3 -0
- package/dist/types/core/version.d.ts +8 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/migrate/mintlify/config.d.ts +2 -0
- package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
- package/dist/types/theme/fonts.d.ts +163 -0
- package/docs/advanced/custom-pages.mdx +91 -10
- package/docs/configuration/index.mdx +19 -0
- package/docs/content/components.mdx +103 -1
- package/package.json +13 -3
- package/src/ai/llms.ts +1 -2
- package/src/astro/examples.ts +106 -0
- package/src/astro/generate.ts +300 -75
- package/src/astro/index.ts +1 -1
- package/src/astro/islands.ts +1 -1
- package/src/astro/pages.ts +59 -0
- package/src/astro/templates.ts +180 -22
- package/src/cli/commands/validate.ts +1 -0
- package/src/components/content/CodeBlock.astro +28 -0
- package/src/components/content/Component.astro +65 -0
- package/src/components/content/Diff.astro +44 -0
- package/src/components/content/Tab.astro +8 -2
- package/src/components/content/diff.ts +95 -0
- package/src/components/layout/Favicon.astro +11 -4
- package/src/components/layout/PageLayout.astro +173 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +9 -5
- package/src/components/layout/head-scripts.ts +19 -0
- package/src/core/data.ts +128 -0
- package/src/core/frontmatter.ts +43 -0
- package/src/core/links.ts +26 -10
- package/src/core/schema.ts +8 -0
- package/src/core/sources/filesystem.ts +1 -1
- package/src/core/sources/mdx-remote.ts +1 -2
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/normalize.ts +19 -3
- package/src/core/sources/notion.ts +1 -1
- package/src/core/sources/sanity.ts +1 -1
- package/src/core/tsconfig-aliases.ts +201 -0
- package/src/index.ts +12 -0
- package/src/markdown/index.ts +69 -0
- package/src/migrate/fumadocs/config.ts +53 -4
- package/src/migrate/fumadocs/content.ts +1 -1
- package/src/migrate/fumadocs/groups.ts +230 -0
- package/src/migrate/fumadocs/index.ts +104 -19
- package/src/migrate/fumadocs/meta.ts +140 -10
- package/src/migrate/mintlify/snippets.ts +2 -1
- package/src/migrate/mintlify/transform.ts +1 -2
- package/src/migrate/nextra/index.ts +1 -1
- package/src/migrate/shared.ts +98 -1
- package/src/migrate/starlight/index.ts +1 -1
- package/src/registry/eject.ts +57 -10
- package/src/search/documents.ts +1 -2
- package/src/theme/entry.ts +29 -0
package/src/astro/templates.ts
CHANGED
|
@@ -8,8 +8,10 @@ import type { ResolvedConfig } from "../core/schema.ts";
|
|
|
8
8
|
import type { ProjectContext } from "../core/types.ts";
|
|
9
9
|
import { searchProviderMeta } from "../search/providers.ts";
|
|
10
10
|
import { buildFontEntries } from "../theme/fonts.ts";
|
|
11
|
+
import type { ExampleSpec } from "./examples.ts";
|
|
11
12
|
import type { BlumePageRoute } from "./integration.ts";
|
|
12
13
|
import type { IslandSpec } from "./islands.ts";
|
|
14
|
+
import type { OgCustomRoute } from "./pages.ts";
|
|
13
15
|
|
|
14
16
|
const WORKSPACE_MARKERS = [
|
|
15
17
|
".git",
|
|
@@ -122,6 +124,59 @@ export const runtimeDependencies = (options: {
|
|
|
122
124
|
};
|
|
123
125
|
|
|
124
126
|
/** Generate `.blume/astro.config.mjs`. */
|
|
127
|
+
/**
|
|
128
|
+
* Render project tsconfig path aliases as `vite.resolve.alias` object entries.
|
|
129
|
+
* Longest find first, so a more specific prefix (`@components`) is matched
|
|
130
|
+
* before a broader one (`@`); these follow Blume's `blume:*` aliases, which
|
|
131
|
+
* never overlap with a project's.
|
|
132
|
+
*/
|
|
133
|
+
/**
|
|
134
|
+
* Blume's render-time dependencies, forced external on the build's SSR and
|
|
135
|
+
* static-prerender Vite environments.
|
|
136
|
+
*
|
|
137
|
+
* Two reasons a dep lands here:
|
|
138
|
+
* - `@takumi-rs/core` (OG image rendering) is a native NAPI addon that loads a
|
|
139
|
+
* platform-specific `.node` binding via `createRequire(import.meta.url)`.
|
|
140
|
+
* Bundling it relocates `import.meta.url` and breaks the binding lookup
|
|
141
|
+
* ("Cannot find native binding") on other platforms (e.g. the Linux CI
|
|
142
|
+
* runner), so it must resolve from `node_modules` at runtime instead.
|
|
143
|
+
* - The rest are pure-JS packages kept external so an isolated linker (Bun's
|
|
144
|
+
* `isolated` mode, pnpm) doesn't bundle their symlinked store copies. When
|
|
145
|
+
* Vite bundles such a package but leaves its own `node_modules` child
|
|
146
|
+
* external, that child surfaces as an unresolvable bare import in the
|
|
147
|
+
* prerender chunk (e.g. `batchwork` via `@astrojs/markdown-satteri`). Kept
|
|
148
|
+
* external, each package's transitive imports resolve relative to its real
|
|
149
|
+
* store location — reachable through the `node_modules` junction {@link
|
|
150
|
+
* prerenderDepsPlugin} drops beside the prerender bundle.
|
|
151
|
+
*
|
|
152
|
+
* Astro 7 configures externalization per Vite environment, so this must be
|
|
153
|
+
* applied to both `prerender` (static) and `ssr` (server) — a top-level
|
|
154
|
+
* `ssr.external` only reaches the latter.
|
|
155
|
+
*/
|
|
156
|
+
const RENDER_EXTERNAL_DEPS = [
|
|
157
|
+
"@astrojs/markdown-satteri",
|
|
158
|
+
"@pierre/diffs",
|
|
159
|
+
"@shikijs/transformers",
|
|
160
|
+
"@takumi-rs/core",
|
|
161
|
+
"@takumi-rs/helpers",
|
|
162
|
+
"github-slugger",
|
|
163
|
+
"katex",
|
|
164
|
+
"shiki",
|
|
165
|
+
"simple-icons",
|
|
166
|
+
"zod",
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const renderUserAliases = (
|
|
170
|
+
aliases: Record<string, string> | undefined
|
|
171
|
+
): string =>
|
|
172
|
+
Object.entries(aliases ?? {})
|
|
173
|
+
.toSorted(([a], [b]) => b.length - a.length)
|
|
174
|
+
.map(
|
|
175
|
+
([find, replacement]) =>
|
|
176
|
+
`\n ${JSON.stringify(find)}: ${JSON.stringify(replacement)},`
|
|
177
|
+
)
|
|
178
|
+
.join("");
|
|
179
|
+
|
|
125
180
|
export const astroConfigTemplate = (options: {
|
|
126
181
|
context: ProjectContext;
|
|
127
182
|
config: ResolvedConfig;
|
|
@@ -131,12 +186,22 @@ export const astroConfigTemplate = (options: {
|
|
|
131
186
|
pages: BlumePageRoute[];
|
|
132
187
|
contentRoutes: string[];
|
|
133
188
|
dataPath: string;
|
|
189
|
+
examplesPath: string;
|
|
134
190
|
themePath: string;
|
|
135
191
|
searchClientPath: string;
|
|
192
|
+
/** Project tsconfig path aliases (`find` -> absolute dir), e.g. `@` -> src. */
|
|
193
|
+
aliases?: Record<string, string>;
|
|
136
194
|
}): string => {
|
|
137
195
|
const { context, config, needsReact, pages, dataPath, themePath } = options;
|
|
138
|
-
const {
|
|
196
|
+
const {
|
|
197
|
+
contentRoutes,
|
|
198
|
+
examplesPath,
|
|
199
|
+
needsSvelte,
|
|
200
|
+
needsVue,
|
|
201
|
+
searchClientPath,
|
|
202
|
+
} = options;
|
|
139
203
|
const { deployment } = config;
|
|
204
|
+
const userAliasLines = renderUserAliases(options.aliases);
|
|
140
205
|
const server = deployment.output === "server";
|
|
141
206
|
|
|
142
207
|
// The project root plus the workspace root, so hoisted dependencies (e.g.
|
|
@@ -214,7 +279,7 @@ export const astroConfigTemplate = (options: {
|
|
|
214
279
|
const svelteImport = needsSvelte
|
|
215
280
|
? `import svelte from "@astrojs/svelte";\n`
|
|
216
281
|
: "";
|
|
217
|
-
const blumeImport = `import { blumeIntegration } from "blume/astro";\n`;
|
|
282
|
+
const blumeImport = `import { blumeIntegration, prerenderDepsPlugin } from "blume/astro";\n`;
|
|
218
283
|
|
|
219
284
|
// Twoslash runs first, before the always-on transformers, but only on fences
|
|
220
285
|
// with the `twoslash` meta (explicitTrigger) — so it's opt-in per block with
|
|
@@ -276,25 +341,21 @@ export default defineConfig({
|
|
|
276
341
|
},
|
|
277
342
|
devToolbar: { enabled: false },
|
|
278
343
|
vite: {
|
|
279
|
-
plugins: [tailwindcss()],
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
// native binding") on other platforms (e.g. the Linux CI runner). Astro 7
|
|
285
|
-
// configures externalization per Vite environment, so it must be forced
|
|
286
|
-
// external on the prerender (static) and ssr (server) environments -- a
|
|
287
|
-
// top-level ssr.external only reaches the latter -- so the binding resolves
|
|
288
|
-
// from node_modules at runtime instead.
|
|
344
|
+
plugins: [tailwindcss(), prerenderDepsPlugin()],
|
|
345
|
+
// Blume's render-time deps are forced external on both build environments so
|
|
346
|
+
// native bindings resolve at runtime and isolated linkers don't bundle
|
|
347
|
+
// symlinked store copies (which would surface their children as unresolvable
|
|
348
|
+
// imports). See RENDER_EXTERNAL_DEPS / prerenderDepsPlugin.
|
|
289
349
|
environments: {
|
|
290
|
-
prerender: { resolve: { external:
|
|
291
|
-
ssr: { resolve: { external:
|
|
350
|
+
prerender: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
|
|
351
|
+
ssr: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
|
|
292
352
|
},
|
|
293
353
|
resolve: {
|
|
294
354
|
alias: {
|
|
295
355
|
"blume:data": ${JSON.stringify(dataPath)},
|
|
356
|
+
"blume:examples": ${JSON.stringify(examplesPath)},
|
|
296
357
|
"blume:search-client": ${JSON.stringify(searchClientPath)},
|
|
297
|
-
"blume:theme": ${JSON.stringify(themePath)}
|
|
358
|
+
"blume:theme": ${JSON.stringify(themePath)},${userAliasLines}
|
|
298
359
|
},
|
|
299
360
|
},
|
|
300
361
|
server: {
|
|
@@ -645,25 +706,47 @@ export function GET({ props }) {
|
|
|
645
706
|
`;
|
|
646
707
|
|
|
647
708
|
/** Generate the OG image endpoint (`.blume/src/pages/_og/[...slug].png.ts`). */
|
|
648
|
-
export const ogEndpointTemplate = (
|
|
709
|
+
export const ogEndpointTemplate = (
|
|
710
|
+
customRoutes: OgCustomRoute[] = []
|
|
711
|
+
): string =>
|
|
649
712
|
`// Generated by Blume. Do not edit.
|
|
650
713
|
import { renderOgImage } from "blume/og";
|
|
651
714
|
import data from "../../generated/data.json";
|
|
652
715
|
|
|
653
716
|
export const prerender = true;
|
|
654
717
|
|
|
718
|
+
// Custom (non-content) pages opted into a generated card, baked in at build.
|
|
719
|
+
const customRoutes = ${JSON.stringify(customRoutes)};
|
|
720
|
+
|
|
655
721
|
export function getStaticPaths() {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
722
|
+
const seen = new Set();
|
|
723
|
+
const paths = [];
|
|
724
|
+
const add = (slug, title, eyebrow) => {
|
|
725
|
+
if (seen.has(slug)) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
seen.add(slug);
|
|
729
|
+
paths.push({ params: { slug }, props: { eyebrow, title } });
|
|
730
|
+
};
|
|
731
|
+
// A custom page wins over a content route sharing its path, so add it first.
|
|
732
|
+
for (const route of customRoutes) {
|
|
733
|
+
add(route.slug, route.title, route.eyebrow);
|
|
734
|
+
}
|
|
735
|
+
for (const route of data.routes) {
|
|
736
|
+
add(
|
|
737
|
+
route.path === "/" ? "index" : route.path.slice(1),
|
|
738
|
+
route.title,
|
|
739
|
+
data.config.title
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
return paths;
|
|
660
743
|
}
|
|
661
744
|
|
|
662
745
|
export async function GET({ props }) {
|
|
663
746
|
const png = await renderOgImage({
|
|
664
|
-
title: props.title,
|
|
665
|
-
eyebrow: data.config.title,
|
|
666
747
|
accent: data.config.theme.accent,
|
|
748
|
+
eyebrow: props.eyebrow,
|
|
749
|
+
title: props.title,
|
|
667
750
|
});
|
|
668
751
|
return new Response(png, {
|
|
669
752
|
headers: {
|
|
@@ -706,6 +789,7 @@ const configuration = ${JSON.stringify(options.configuration, null, 2)};
|
|
|
706
789
|
fontCssVars={data.fontCssVars}
|
|
707
790
|
logo={data.config.logo}
|
|
708
791
|
favicon={data.config.favicon}
|
|
792
|
+
appleIcon={data.config.appleIcon}
|
|
709
793
|
navigation={data.navigation}
|
|
710
794
|
pageTitle={${JSON.stringify(options.title)}}
|
|
711
795
|
route={${JSON.stringify(options.route)}}
|
|
@@ -746,12 +830,15 @@ import Badge from "blume/components/content/Badge.astro";
|
|
|
746
830
|
import Callout from "blume/components/content/Callout.astro";
|
|
747
831
|
import Card from "blume/components/content/Card.astro";
|
|
748
832
|
import CardGroup from "blume/components/content/CardGroup.astro";
|
|
833
|
+
import CodeBlock from "blume/components/content/CodeBlock.astro";
|
|
749
834
|
import CodeGroup from "blume/components/content/CodeGroup.astro";
|
|
750
835
|
import ColorRoot from "blume/components/content/Color.astro";
|
|
751
836
|
import ColorItem from "blume/components/content/ColorItem.astro";
|
|
752
837
|
import ColorRow from "blume/components/content/ColorRow.astro";
|
|
753
838
|
import Column from "blume/components/content/Column.astro";
|
|
754
839
|
import Columns from "blume/components/content/Columns.astro";
|
|
840
|
+
import Component from "blume/components/content/Component.astro";
|
|
841
|
+
import Diff from "blume/components/content/Diff.astro";
|
|
755
842
|
import Expandable from "blume/components/content/Expandable.astro";
|
|
756
843
|
import FileTree from "blume/components/content/FileTree.astro";
|
|
757
844
|
import Frame from "blume/components/content/Frame.astro";
|
|
@@ -791,10 +878,13 @@ const components = {
|
|
|
791
878
|
Callout,
|
|
792
879
|
Card,
|
|
793
880
|
CardGroup,
|
|
881
|
+
CodeBlock,
|
|
794
882
|
CodeGroup,
|
|
795
883
|
Color,
|
|
796
884
|
Column,
|
|
797
885
|
Columns,
|
|
886
|
+
Component,
|
|
887
|
+
Diff,
|
|
798
888
|
Expandable,
|
|
799
889
|
FileTree,
|
|
800
890
|
Frame,
|
|
@@ -915,6 +1005,7 @@ const localeSwitch = i18n
|
|
|
915
1005
|
logo={data.config.logo}
|
|
916
1006
|
mcp={data.config.mcp}
|
|
917
1007
|
favicon={data.config.favicon}
|
|
1008
|
+
appleIcon={data.config.appleIcon}
|
|
918
1009
|
banner={data.config.banner}
|
|
919
1010
|
analytics={data.config.analytics}
|
|
920
1011
|
imageZoom={data.config.imageZoom}
|
|
@@ -1053,6 +1144,7 @@ const canonical = base ? base + "/changelog" : null;
|
|
|
1053
1144
|
logo={data.config.logo}
|
|
1054
1145
|
mcp={data.config.mcp}
|
|
1055
1146
|
favicon={data.config.favicon}
|
|
1147
|
+
appleIcon={data.config.appleIcon}
|
|
1056
1148
|
banner={data.config.banner}
|
|
1057
1149
|
analytics={data.config.analytics}
|
|
1058
1150
|
imageZoom={data.config.imageZoom}
|
|
@@ -1164,10 +1256,76 @@ ${entries}
|
|
|
1164
1256
|
`;
|
|
1165
1257
|
};
|
|
1166
1258
|
|
|
1259
|
+
/** The literal Astro hydration directive for an example's framework/client. */
|
|
1260
|
+
const exampleDirective = (spec: ExampleSpec): string => {
|
|
1261
|
+
if (spec.framework === "astro" || !spec.client) {
|
|
1262
|
+
return "";
|
|
1263
|
+
}
|
|
1264
|
+
return spec.client === "only"
|
|
1265
|
+
? `client:only="${spec.framework}" `
|
|
1266
|
+
: `client:${spec.client} `;
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
/** Filesystem-safe slug for an example's generated wrapper file. */
|
|
1270
|
+
export const exampleSlug = (path: string): string =>
|
|
1271
|
+
path.replaceAll("/", "__").replaceAll(/[^a-zA-Z0-9_]+/gu, "-");
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Generate `.blume/src/generated/examples/<slug>.astro` — a wrapper that renders
|
|
1275
|
+
* one example live, with its hydration directive applied (none for `.astro`).
|
|
1276
|
+
* Mirrors {@link islandWrapperTemplate}; `<Component>` resolves these by path.
|
|
1277
|
+
*/
|
|
1278
|
+
export const exampleWrapperTemplate = (spec: ExampleSpec): string =>
|
|
1279
|
+
`---
|
|
1280
|
+
// Generated by Blume. Do not edit.
|
|
1281
|
+
import Example from ${JSON.stringify(spec.file)};
|
|
1282
|
+
---
|
|
1283
|
+
<Example ${exampleDirective(spec)}{...Astro.props}><slot /></Example>
|
|
1284
|
+
`;
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Generate `.blume/src/generated/examples.ts` — a map of example path to its live
|
|
1288
|
+
* wrapper component plus raw source and language for the code tab. Reached by the
|
|
1289
|
+
* shipped `Component.astro` via the `blume:examples` alias. Always written (an
|
|
1290
|
+
* empty object when there are no examples) so the alias resolves.
|
|
1291
|
+
*/
|
|
1292
|
+
export const exampleMapTemplate = (specs: ExampleSpec[]): string => {
|
|
1293
|
+
if (specs.length === 0) {
|
|
1294
|
+
return `// Generated by Blume. Do not edit.
|
|
1295
|
+
export const examples = {};
|
|
1296
|
+
`;
|
|
1297
|
+
}
|
|
1298
|
+
const imports = specs
|
|
1299
|
+
.map(
|
|
1300
|
+
(spec, index) =>
|
|
1301
|
+
`import E${index} from "./examples/${exampleSlug(spec.path)}.astro";`
|
|
1302
|
+
)
|
|
1303
|
+
.join("\n");
|
|
1304
|
+
const entries = specs
|
|
1305
|
+
.map(
|
|
1306
|
+
(spec, index) =>
|
|
1307
|
+
` ${JSON.stringify(spec.path)}: { Component: E${index}, code: ${JSON.stringify(
|
|
1308
|
+
spec.source
|
|
1309
|
+
)}, lang: ${JSON.stringify(spec.lang)} },`
|
|
1310
|
+
)
|
|
1311
|
+
.join("\n");
|
|
1312
|
+
return `// Generated by Blume. Do not edit.
|
|
1313
|
+
${imports}
|
|
1314
|
+
export const examples = {
|
|
1315
|
+
${entries}
|
|
1316
|
+
};
|
|
1317
|
+
`;
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1167
1320
|
/** Generate `.blume/src/env.d.ts`. */
|
|
1168
1321
|
export const envTemplate =
|
|
1169
1322
|
(): string => `/// <reference path="../.astro/types.d.ts" />
|
|
1170
1323
|
/// <reference types="astro/client" />
|
|
1324
|
+
|
|
1325
|
+
declare module "blume:data" {
|
|
1326
|
+
const data: import("blume").BlumeData;
|
|
1327
|
+
export default data;
|
|
1328
|
+
}
|
|
1171
1329
|
`;
|
|
1172
1330
|
|
|
1173
1331
|
/** Generate `.blume/package.json`. */
|
|
@@ -39,6 +39,7 @@ export const validateCommand = defineCommand({
|
|
|
39
39
|
...(await validateLinks(project.graph, {
|
|
40
40
|
checkExternal: Boolean(args.external),
|
|
41
41
|
publicDir: existsSync(publicDir) ? publicDir : null,
|
|
42
|
+
redirects: project.config.redirects,
|
|
42
43
|
}))
|
|
43
44
|
);
|
|
44
45
|
} catch (error) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Render a code string highlighted with Blume's Markdown code-block theme — the
|
|
3
|
+
// same Shiki themes, transformers, and dual light/dark swap as fenced code.
|
|
4
|
+
// Use it to show themed code outside the Markdown pipeline (a landing page, a
|
|
5
|
+
// custom component) where there's no fence to write.
|
|
6
|
+
//
|
|
7
|
+
// <CodeBlock lang="ts" code={`export const x = 1;`} />
|
|
8
|
+
//
|
|
9
|
+
// The theme's code styles are scoped to `.prose`, so the output is wrapped in a
|
|
10
|
+
// `.prose` container (`max-w-none` so it fills its own column rather than the
|
|
11
|
+
// prose reading measure); that's what makes the light/dark swap work on a bare
|
|
12
|
+
// page without the manual `[data-theme]` CSS it would otherwise need.
|
|
13
|
+
import { highlightCode } from "../../markdown/index.ts";
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** The source to highlight. */
|
|
17
|
+
code: string;
|
|
18
|
+
/** Shiki language id (default `txt`); an unknown one renders plain. */
|
|
19
|
+
lang?: string;
|
|
20
|
+
/** Show the brand language icon in the header (default on, like fences). */
|
|
21
|
+
icons?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { code, lang = "txt", icons } = Astro.props;
|
|
25
|
+
const html = await highlightCode(code.replace(/\n+$/u, ""), lang, { icons });
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<div class="prose max-w-none"><Fragment set:html={html} /></div>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Render an example file (under the project's `examples/` dir) as a live,
|
|
3
|
+
// hydrated preview alongside its highlighted source, in tabs. `path` is the
|
|
4
|
+
// file's location under `examples/` without its extension, e.g.
|
|
5
|
+
// `<Component path="forms/login" />` for `examples/forms/login.tsx`.
|
|
6
|
+
//
|
|
7
|
+
// Blume discovers `examples/` at build time and generates `blume:examples`: a
|
|
8
|
+
// map of path → { Component (a static hydration wrapper), code, lang }. React,
|
|
9
|
+
// Vue, Svelte, and Astro examples are all supported. The source is highlighted
|
|
10
|
+
// with the same Shiki setup as ordinary code fences.
|
|
11
|
+
import { highlightCode } from "../../markdown/index.ts";
|
|
12
|
+
import Tab from "./Tab.astro";
|
|
13
|
+
import Tabs from "./Tabs.astro";
|
|
14
|
+
// Generated per project; resolves to an empty map when there are no examples.
|
|
15
|
+
import { examples } from "blume:examples";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
path: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { path } = Astro.props;
|
|
22
|
+
const entry = examples[path];
|
|
23
|
+
const Preview = entry?.Component;
|
|
24
|
+
|
|
25
|
+
// Highlight the source with the shared helper so it matches Markdown fences.
|
|
26
|
+
// `blume-source` makes the inner `<code>` (not the `<pre>`) the scroll/height-
|
|
27
|
+
// capped box — see the theme entry — so the copy button, pinned to the `<pre>`,
|
|
28
|
+
// stays put. This pane already lives inside `.prose`, so the theme styles it.
|
|
29
|
+
const codeHtml = entry
|
|
30
|
+
? await highlightCode(entry.code, entry.lang, { className: "blume-source" })
|
|
31
|
+
: undefined;
|
|
32
|
+
|
|
33
|
+
// Both tabs share one height so toggling them never shifts the layout. Size it
|
|
34
|
+
// to the source (≈21px/line + padding), clamped to a comfortable 18rem floor and
|
|
35
|
+
// a 400px ceiling — taller sources scroll inside the pane.
|
|
36
|
+
const LINE_PX = 21;
|
|
37
|
+
const PADDING_PX = 36;
|
|
38
|
+
const lineCount = entry ? entry.code.replace(/\n+$/u, "").split("\n").length : 0;
|
|
39
|
+
const paneHeight = Math.min(400, Math.max(288, lineCount * LINE_PX + PADDING_PX));
|
|
40
|
+
const paneStyle = `height:${paneHeight}px`;
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
entry ? (
|
|
45
|
+
// `sync={false}`: each preview's Preview/Code tabs are independent — unlike
|
|
46
|
+
// CodeGroup, switching one Component must not switch the others.
|
|
47
|
+
<Tabs hash={false} sync={false}>
|
|
48
|
+
<Tab
|
|
49
|
+
class="flex items-center justify-center overflow-auto"
|
|
50
|
+
style={paneStyle}
|
|
51
|
+
title="Preview"
|
|
52
|
+
>
|
|
53
|
+
<Preview />
|
|
54
|
+
</Tab>
|
|
55
|
+
<Tab class="overflow-hidden" style={paneStyle} title="Code">
|
|
56
|
+
<Fragment set:html={codeHtml} />
|
|
57
|
+
</Tab>
|
|
58
|
+
</Tabs>
|
|
59
|
+
) : (
|
|
60
|
+
<div class="not-prose my-6 rounded-blume border border-border px-3 py-2 text-muted-foreground text-sm">
|
|
61
|
+
No example found at <code class="font-mono">{path}</code>. Add it under{" "}
|
|
62
|
+
<code class="font-mono">examples/</code> in your project.
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Render a git-style diff with `@pierre/diffs`, fully at build time. The library
|
|
3
|
+
// emits a self-contained blob whose styles are scoped to `:host`, so we mount it
|
|
4
|
+
// in a declarative shadow root — no client JS. `--blume-diff-color-scheme`
|
|
5
|
+
// (set in the theme entry from `data-theme`) drives the diff's `light-dark()`.
|
|
6
|
+
//
|
|
7
|
+
// Inputs (one of): `patch`/`src` (unified patch), `before`+`after` (file paths),
|
|
8
|
+
// or `old`+`new` (inline strings, with optional `lang`).
|
|
9
|
+
import { renderDiff } from "./diff.ts";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
after?: string;
|
|
13
|
+
before?: string;
|
|
14
|
+
lang?: string;
|
|
15
|
+
new?: string;
|
|
16
|
+
old?: string;
|
|
17
|
+
patch?: string;
|
|
18
|
+
src?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let html: string | undefined;
|
|
22
|
+
let error: string | undefined;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
html = await renderDiff(Astro.props);
|
|
26
|
+
} catch (cause) {
|
|
27
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const COLOR_SCHEME =
|
|
31
|
+
"<style>:host{color-scheme:var(--blume-diff-color-scheme, light dark)}</style>";
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
error ? (
|
|
36
|
+
<div class="not-prose my-6 rounded-blume border border-border px-3 py-2 text-muted-foreground text-sm">
|
|
37
|
+
Could not render diff: {error}
|
|
38
|
+
</div>
|
|
39
|
+
) : (
|
|
40
|
+
<blume-diff class="not-prose my-6 block overflow-hidden rounded-blume border border-border">
|
|
41
|
+
<template shadowrootmode="open" set:html={html + COLOR_SCHEME} />
|
|
42
|
+
</blume-diff>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
import Icon from "../Icon.astro";
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
|
+
class?: string;
|
|
5
6
|
icon?: string;
|
|
6
7
|
iconType?: string;
|
|
7
8
|
id?: string;
|
|
9
|
+
style?: string;
|
|
8
10
|
title: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
const { icon, iconType, id, title } = Astro.props;
|
|
13
|
+
const { class: className, icon, iconType, id, style, title } = Astro.props;
|
|
12
14
|
const slugify = (value: string): string =>
|
|
13
15
|
value
|
|
14
16
|
.toLowerCase()
|
|
@@ -21,8 +23,12 @@ const tabId = id ?? slugify(title);
|
|
|
21
23
|
---
|
|
22
24
|
|
|
23
25
|
<div
|
|
24
|
-
class=
|
|
26
|
+
class:list={[
|
|
27
|
+
"px-4 py-3 [&:has(>pre)]:p-0 [&>:first-child]:mt-0! [&>:last-child]:mb-0!",
|
|
28
|
+
className,
|
|
29
|
+
]}
|
|
25
30
|
data-blume-tab-panel
|
|
31
|
+
style={style}
|
|
26
32
|
data-icon={icon}
|
|
27
33
|
data-tab-id={tabId}
|
|
28
34
|
data-title={title}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time diff rendering for `<Diff>`.
|
|
3
|
+
*
|
|
4
|
+
* Produces a self-contained HTML string with [`@pierre/diffs`](https://diffs.com)
|
|
5
|
+
* via its `./ssr` entry — no DOM, no React, no client JS. The returned markup
|
|
6
|
+
* carries its own `:host` stylesheet and is meant to be placed inside a
|
|
7
|
+
* declarative shadow root (see `Diff.astro`). Three input shapes are supported:
|
|
8
|
+
* a unified patch (string or `.patch`/`.diff` file), a pair of file paths, or a
|
|
9
|
+
* pair of inline strings.
|
|
10
|
+
*/
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
|
|
13
|
+
import { preloadDiffHTML, preloadPatchDiff } from "@pierre/diffs/ssr";
|
|
14
|
+
import { isAbsolute, join } from "pathe";
|
|
15
|
+
|
|
16
|
+
export interface DiffOptions {
|
|
17
|
+
/** Path to the "after" file, resolved relative to {@link DiffOptions.root}. */
|
|
18
|
+
after?: string;
|
|
19
|
+
/** Path to the "before" file, resolved relative to {@link DiffOptions.root}. */
|
|
20
|
+
before?: string;
|
|
21
|
+
/** Language for inline {@link DiffOptions.old}/{@link DiffOptions.new} input. */
|
|
22
|
+
lang?: string;
|
|
23
|
+
/** The "after" contents, as an inline string. Pairs with {@link DiffOptions.old}. */
|
|
24
|
+
new?: string;
|
|
25
|
+
/** The "before" contents, as an inline string. Pairs with {@link DiffOptions.new}. */
|
|
26
|
+
old?: string;
|
|
27
|
+
/** A unified diff/patch as an inline string. */
|
|
28
|
+
patch?: string;
|
|
29
|
+
/** Base directory for resolving relative paths. Defaults to `process.cwd()`. */
|
|
30
|
+
root?: string;
|
|
31
|
+
/** Path to a `.patch`/`.diff` file, resolved relative to {@link DiffOptions.root}. */
|
|
32
|
+
src?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Dual-theme config mirroring Blume's Shiki convention (see `templates.ts`). */
|
|
36
|
+
const THEME = { dark: "github-dark", light: "github-light" } as const;
|
|
37
|
+
|
|
38
|
+
const resolvePath = (path: string, root: string): string =>
|
|
39
|
+
isAbsolute(path) ? path : join(root, path);
|
|
40
|
+
|
|
41
|
+
const readText = (path: string, root: string): Promise<string> =>
|
|
42
|
+
readFile(resolvePath(path, root), "utf-8");
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve `<Diff>` inputs to a prerendered HTML string. Throws when no input
|
|
46
|
+
* group is supplied or a pair is half-specified, so the component can degrade
|
|
47
|
+
* to an inline notice.
|
|
48
|
+
*/
|
|
49
|
+
export const renderDiff = async (options: DiffOptions): Promise<string> => {
|
|
50
|
+
const {
|
|
51
|
+
after,
|
|
52
|
+
before,
|
|
53
|
+
lang,
|
|
54
|
+
new: newText,
|
|
55
|
+
old,
|
|
56
|
+
patch,
|
|
57
|
+
root = process.cwd(),
|
|
58
|
+
src,
|
|
59
|
+
} = options;
|
|
60
|
+
|
|
61
|
+
if (patch !== undefined || src !== undefined) {
|
|
62
|
+
const text = patch ?? (await readText(src as string, root));
|
|
63
|
+
const result = await preloadPatchDiff({
|
|
64
|
+
options: { theme: THEME },
|
|
65
|
+
patch: text,
|
|
66
|
+
});
|
|
67
|
+
return result.prerenderedHTML;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (before !== undefined || after !== undefined) {
|
|
71
|
+
if (before === undefined || after === undefined) {
|
|
72
|
+
throw new Error("<Diff> needs both `before` and `after` file paths.");
|
|
73
|
+
}
|
|
74
|
+
return await preloadDiffHTML({
|
|
75
|
+
newFile: { contents: await readText(after, root), name: after },
|
|
76
|
+
oldFile: { contents: await readText(before, root), name: before },
|
|
77
|
+
options: { theme: THEME },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (old !== undefined || newText !== undefined) {
|
|
82
|
+
if (old === undefined || newText === undefined) {
|
|
83
|
+
throw new Error("<Diff> needs both `old` and `new` strings.");
|
|
84
|
+
}
|
|
85
|
+
return await preloadDiffHTML({
|
|
86
|
+
newFile: { contents: newText, lang, name: "snippet" },
|
|
87
|
+
oldFile: { contents: old, lang, name: "snippet" },
|
|
88
|
+
options: { disableFileHeader: true, theme: THEME },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error(
|
|
93
|
+
"<Diff> requires one input: `patch`/`src`, `before`+`after`, or `old`+`new`."
|
|
94
|
+
);
|
|
95
|
+
};
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
---
|
|
2
|
-
// Emits the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Emits the icon <link>s for the document <head>: the favicon (rel="icon") and,
|
|
3
|
+
// when the project ships one, the Apple touch icon (rel="apple-touch-icon").
|
|
4
|
+
// Each href is either a URL to a file the project ships in public/, or a data
|
|
5
|
+
// URI (a root-level icon, or the bundled Blume favicon default) — both resolved
|
|
6
|
+
// by the generator's resolveFavicon / resolveAppleIcon.
|
|
5
7
|
interface Props {
|
|
6
8
|
favicon?: {
|
|
7
9
|
href: string;
|
|
8
10
|
type?: string;
|
|
9
11
|
} | null;
|
|
12
|
+
appleIcon?: {
|
|
13
|
+
href: string;
|
|
14
|
+
type?: string;
|
|
15
|
+
} | null;
|
|
10
16
|
}
|
|
11
17
|
|
|
12
|
-
const { favicon } = Astro.props;
|
|
18
|
+
const { appleIcon, favicon } = Astro.props;
|
|
13
19
|
---
|
|
14
20
|
|
|
15
21
|
{favicon && <link href={favicon.href} rel="icon" type={favicon.type} />}
|
|
22
|
+
{appleIcon && <link href={appleIcon.href} rel="apple-touch-icon" />}
|