blume 0.1.1 → 0.1.3
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 +91 -44
- package/dist/cli/index.js.map +7 -7
- package/dist/types/core/schema.d.ts +16 -0
- package/docs/content/components.mdx +36 -0
- package/package.json +1 -1
- package/src/astro/examples.ts +50 -11
- package/src/astro/generate.ts +134 -28
- package/src/astro/index.ts +1 -1
- package/src/astro/templates.ts +44 -13
- package/src/core/schema.ts +14 -0
- package/src/registry/eject.ts +1 -1
|
@@ -1457,6 +1457,20 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
1457
1457
|
site?: string | undefined;
|
|
1458
1458
|
}>>;
|
|
1459
1459
|
description: z.ZodOptional<z.ZodString>;
|
|
1460
|
+
/**
|
|
1461
|
+
* Where `<Component path>` resolves live previews and their source from,
|
|
1462
|
+
* relative to the project root. Defaults to the `examples` directory; point
|
|
1463
|
+
* it elsewhere when examples live outside a top-level `examples/`.
|
|
1464
|
+
*
|
|
1465
|
+
* May be a glob (anything with `*`/`?`/`[]`/`{}`/`!`), in which case only
|
|
1466
|
+
* matching files are discovered and a `<Component path>` key is relative to
|
|
1467
|
+
* the glob's static prefix. Use this for a registry layout that colocates
|
|
1468
|
+
* component sources with their examples — `registry/<pkg>/**\/examples/*`
|
|
1469
|
+
* targets just the examples, leaving the sources (which have no default
|
|
1470
|
+
* export to wrap) out, so the registry needn't be forked into its own
|
|
1471
|
+
* examples directory.
|
|
1472
|
+
*/
|
|
1473
|
+
examples: z.ZodDefault<z.ZodString>;
|
|
1460
1474
|
export: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodBoolean, z.ZodObject<{
|
|
1461
1475
|
epub: z.ZodDefault<z.ZodBoolean>;
|
|
1462
1476
|
pdf: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -2898,6 +2912,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2898
2912
|
base?: string | undefined;
|
|
2899
2913
|
site?: string | undefined;
|
|
2900
2914
|
};
|
|
2915
|
+
examples: string;
|
|
2901
2916
|
export: {
|
|
2902
2917
|
epub: boolean;
|
|
2903
2918
|
pdf: boolean;
|
|
@@ -3299,6 +3314,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
3299
3314
|
output?: "static" | "server" | undefined;
|
|
3300
3315
|
site?: string | undefined;
|
|
3301
3316
|
} | undefined;
|
|
3317
|
+
examples?: string | undefined;
|
|
3302
3318
|
export?: boolean | {
|
|
3303
3319
|
epub?: boolean | undefined;
|
|
3304
3320
|
pdf?: boolean | undefined;
|
|
@@ -573,6 +573,42 @@ a live preview alongside its highlighted source, in tabs. Point it at a file wit
|
|
|
573
573
|
examples are all supported; framework examples hydrate, Astro ones render
|
|
574
574
|
statically. It keeps the preview and the code in sync from a single file.
|
|
575
575
|
|
|
576
|
+
The directory is configurable — set `examples` in `blume.config.ts` when your
|
|
577
|
+
examples live elsewhere (e.g. a registry layout). `path` is always relative to
|
|
578
|
+
it:
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
// blume.config.ts
|
|
582
|
+
export default defineConfig({
|
|
583
|
+
examples: "registry/files-sdk",
|
|
584
|
+
});
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
```astro
|
|
588
|
+
<!-- registry/files-sdk/file-list/basic.tsx -->
|
|
589
|
+
<Component path="file-list/basic" />
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
`examples` can also be a glob (anything with `*`, `?`, `[]`, `{}`, or `!`). Only
|
|
593
|
+
matching files are discovered, and `path` is relative to the glob's static prefix
|
|
594
|
+
(the part before the first wildcard). This is for a registry that colocates each
|
|
595
|
+
component's source with its example — point at just the examples so the sources,
|
|
596
|
+
which have no default export to preview, aren't swept in:
|
|
597
|
+
|
|
598
|
+
```ts
|
|
599
|
+
// blume.config.ts
|
|
600
|
+
export default defineConfig({
|
|
601
|
+
// registry/files-sdk/file-list/file-list.tsx — source, left out
|
|
602
|
+
// registry/files-sdk/file-list/examples/basic.tsx — discovered
|
|
603
|
+
examples: "registry/files-sdk/**/examples/*",
|
|
604
|
+
});
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
```astro
|
|
608
|
+
<!-- keyed relative to registry/files-sdk -->
|
|
609
|
+
<Component path="file-list/examples/basic" />
|
|
610
|
+
```
|
|
611
|
+
|
|
576
612
|
<Component path="counter" />
|
|
577
613
|
|
|
578
614
|
```astro
|
package/package.json
CHANGED
package/src/astro/examples.ts
CHANGED
|
@@ -40,23 +40,62 @@ const FRAMEWORK_BY_EXT: Record<string, ExampleFramework> = {
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
// Captures the extension so we can strip it from the path key and pick the
|
|
43
|
-
// framework. Kept in sync with the glob below
|
|
43
|
+
// framework. Kept in sync with the glob below — non-matching files a user glob
|
|
44
|
+
// happens to sweep in (e.g. a registry's `.ts` sources) are dropped here.
|
|
44
45
|
const EXAMPLE_FILE = /\.(?<ext>astro|jsx|svelte|tsx|vue)$/u;
|
|
45
46
|
|
|
47
|
+
// Renderable example files when `examples` names a plain directory.
|
|
48
|
+
const DEFAULT_EXAMPLE_GLOB = "**/*.{astro,jsx,svelte,tsx,vue}";
|
|
49
|
+
|
|
50
|
+
// Glob magic that turns `examples` from a plain directory into a pattern. `()`,
|
|
51
|
+
// `@`, and `+` are excluded so literal path segments (npm scopes, parens) keep
|
|
52
|
+
// resolving as directories; the extglob leads `*?!` still trigger here.
|
|
53
|
+
const GLOB_MAGIC = /[!*?[\]{}]/u;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Split a glob into its static directory prefix and the remaining pattern, so
|
|
57
|
+
* discovered files can be keyed relative to that prefix (e.g.
|
|
58
|
+
* `registry/x/**\/examples/*` → `{ base: "registry/x", rest: "**\/examples/*" }`).
|
|
59
|
+
*/
|
|
60
|
+
const splitGlobBase = (pattern: string): { base: string; rest: string } => {
|
|
61
|
+
const segments = pattern.split("/");
|
|
62
|
+
const firstMagic = segments.findIndex((segment) => GLOB_MAGIC.test(segment));
|
|
63
|
+
if (firstMagic === -1) {
|
|
64
|
+
return { base: pattern, rest: "" };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
base: segments.slice(0, firstMagic).join("/"),
|
|
68
|
+
rest: segments.slice(firstMagic).join("/"),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
46
72
|
/**
|
|
47
|
-
* Discover preview examples
|
|
48
|
-
* `.jsx`/`.vue`/`.svelte` file becomes addressable by
|
|
49
|
-
* where the path is the file's location
|
|
50
|
-
* (e.g. `forms/login.tsx` → `forms/login`).
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
73
|
+
* Discover preview examples for the `examples` config (default `examples`).
|
|
74
|
+
* Every `.astro`/`.tsx`/`.jsx`/`.vue`/`.svelte` file becomes addressable by
|
|
75
|
+
* `<Component path="...">`, where the path is the file's location without its
|
|
76
|
+
* extension (e.g. `forms/login.tsx` → `forms/login`).
|
|
77
|
+
*
|
|
78
|
+
* `pattern` is a directory by default, but may be a glob (anything with
|
|
79
|
+
* `*`/`?`/`[]`/`{}`/`!`) — then only matching files are discovered and each
|
|
80
|
+
* `<Component path>` key is relative to the glob's static prefix. This lets a
|
|
81
|
+
* registry layout that colocates component sources with their examples be
|
|
82
|
+
* targeted directly (e.g. `registry/<pkg>/**\/examples/*`) without the sources —
|
|
83
|
+
* which have no default export and so can't be wrapped — being swept in.
|
|
84
|
+
*
|
|
85
|
+
* Discovery is path-based (a glob), so no example code is executed. Framework
|
|
86
|
+
* examples carry a hydration mode (default `client:visible`, overridable via
|
|
87
|
+
* `export const client`); `.astro` examples render statically with no client
|
|
88
|
+
* directive.
|
|
54
89
|
*/
|
|
55
90
|
export const discoverExamples = async (
|
|
56
|
-
root: string
|
|
91
|
+
root: string,
|
|
92
|
+
pattern = "examples"
|
|
57
93
|
): Promise<ExampleDiscovery> => {
|
|
58
|
-
const
|
|
59
|
-
|
|
94
|
+
const { base, rest } = GLOB_MAGIC.test(pattern)
|
|
95
|
+
? splitGlobBase(pattern)
|
|
96
|
+
: { base: pattern, rest: DEFAULT_EXAMPLE_GLOB };
|
|
97
|
+
const dir = join(root, base);
|
|
98
|
+
const matches = await glob([rest], {
|
|
60
99
|
absolute: true,
|
|
61
100
|
cwd: dir,
|
|
62
101
|
onlyFiles: true,
|
package/src/astro/generate.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
import { pathToFileURL } from "node:url";
|
|
13
13
|
|
|
14
|
-
import { dirname, join, normalize, relative } from "pathe";
|
|
14
|
+
import { basename, dirname, join, normalize, relative } from "pathe";
|
|
15
15
|
import { glob } from "tinyglobby";
|
|
16
16
|
|
|
17
17
|
import { resolveAskBackend } from "../ai/ask.ts";
|
|
@@ -128,6 +128,61 @@ export const blumeDepsDir = (pkgDir: string = packageRoot()): string | null => {
|
|
|
128
128
|
return candidates.find((dir) => existsSync(join(dir, "astro"))) ?? null;
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Point `link` at Blume's dependency directory via a `node_modules` junction,
|
|
133
|
+
* replacing a stale junction we own and leaving a real directory untouched.
|
|
134
|
+
*
|
|
135
|
+
* `lstat`, not `existsSync`, so a broken junction (target since moved) is still
|
|
136
|
+
* detected — `existsSync` follows the link and reports a dangling one as absent.
|
|
137
|
+
*/
|
|
138
|
+
const linkDepsJunction = async (
|
|
139
|
+
link: string,
|
|
140
|
+
depsDir: string
|
|
141
|
+
): Promise<void> => {
|
|
142
|
+
const existing = await lstat(link).catch(() => null);
|
|
143
|
+
if (existing) {
|
|
144
|
+
if (!existing.isSymbolicLink()) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
await rm(link, { force: true });
|
|
148
|
+
}
|
|
149
|
+
await mkdir(dirname(link), { recursive: true });
|
|
150
|
+
await symlink(depsDir, link, "junction");
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Read the `version` field of a `package.json`, or null when unreadable. */
|
|
154
|
+
const readPkgVersion = (pkgJsonPath: string | null): string | null => {
|
|
155
|
+
if (!pkgJsonPath) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(readFileSync(pkgJsonPath, "utf-8")).version ?? null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Build the diagnostic for a split-layout Astro conflict that a symlink can't
|
|
167
|
+
* repair: a different Astro is hoisted to the project root, shadowing Blume's,
|
|
168
|
+
* and `@astrojs/mdx` binds to the wrong copy. `blumeAstroPkg`/`shadowAstroPkg`
|
|
169
|
+
* are the resolved `astro/package.json` paths for Blume's set and the one the
|
|
170
|
+
* runtime actually resolves.
|
|
171
|
+
*/
|
|
172
|
+
const astroConflictWarning = (
|
|
173
|
+
blumeAstroPkg: string | null,
|
|
174
|
+
shadowAstroPkg: string | null
|
|
175
|
+
): string => {
|
|
176
|
+
const blume = readPkgVersion(blumeAstroPkg);
|
|
177
|
+
const shadow = readPkgVersion(shadowAstroPkg);
|
|
178
|
+
const versions =
|
|
179
|
+
blume && shadow
|
|
180
|
+
? `astro@${shadow} shadowing Blume's astro@${blume}`
|
|
181
|
+
: "a second copy of Astro shadowing Blume's";
|
|
182
|
+
const pin = blume ?? "<Blume's astro version>";
|
|
183
|
+
return `Astro version conflict: another dependency hoisted ${versions} to the project root, so @astrojs/mdx binds to the wrong copy and the build fails on a missing export (e.g. "chunkToString"). A single symlink can't reconcile a split install — pin Blume's Astro by adding a package.json "overrides" (npm/bun/pnpm) or "resolutions" (yarn) entry { "astro": "${pin}" }, then reinstall. Run \`npm ls astro\` to find the dependency pulling the older copy.`;
|
|
184
|
+
};
|
|
185
|
+
|
|
131
186
|
/**
|
|
132
187
|
* Make the generated runtime resolve Astro and its integrations against Blume's
|
|
133
188
|
* own dependency set. Two failure modes this repairs:
|
|
@@ -146,40 +201,81 @@ export const blumeDepsDir = (pkgDir: string = packageRoot()): string | null => {
|
|
|
146
201
|
* are a *co-located, consistent* set (astro beside the `@astrojs/mdx` that binds
|
|
147
202
|
* to it). A split layout — an integration hoisted away from a conflicting astro
|
|
148
203
|
* — can't be made consistent by a single symlink and needs a root `overrides`/
|
|
149
|
-
* `resolutions` pin instead
|
|
204
|
+
* `resolutions` pin instead. We can't fix that from `.blume/`, so we return a
|
|
205
|
+
* diagnostic naming the conflict rather than silently shipping a runtime that
|
|
206
|
+
* crashes downstream. Returns the warning, or null when nothing needs saying.
|
|
150
207
|
*/
|
|
151
208
|
export const ensureDepsLink = async (
|
|
152
209
|
outDir: string,
|
|
153
210
|
pkgDir: string = packageRoot()
|
|
154
|
-
): Promise<
|
|
211
|
+
): Promise<string | null> => {
|
|
155
212
|
const depsDir = blumeDepsDir(pkgDir);
|
|
156
|
-
if (!depsDir
|
|
157
|
-
return;
|
|
213
|
+
if (!depsDir) {
|
|
214
|
+
return null;
|
|
158
215
|
}
|
|
159
216
|
// Already correct when `.blume/` resolves the very same astro Blume's deps
|
|
160
|
-
// provide — the clean hoisted case
|
|
161
|
-
// astro shadowing Blume's) link Blume's deps in.
|
|
217
|
+
// provide — the clean hoisted case, nothing to do.
|
|
162
218
|
const blumeAstro = resolvedAstroPath(depsDir);
|
|
163
|
-
|
|
164
|
-
|
|
219
|
+
const outDirAstro = resolvedAstroPath(outDir);
|
|
220
|
+
if (blumeAstro && outDirAstro === blumeAstro) {
|
|
221
|
+
return null;
|
|
165
222
|
}
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (existing) {
|
|
174
|
-
if (!existing.isSymbolicLink()) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
await rm(link, { force: true });
|
|
223
|
+
// A co-located, consistent set (astro beside the @astrojs/mdx that binds to
|
|
224
|
+
// it) can be linked in wholesale; this repairs the unreachable and the
|
|
225
|
+
// repairable-conflict cases. Any existing link here is stale and gets
|
|
226
|
+
// replaced.
|
|
227
|
+
if (existsSync(join(depsDir, "@astrojs", "mdx"))) {
|
|
228
|
+
await linkDepsJunction(join(outDir, "node_modules"), depsDir);
|
|
229
|
+
return null;
|
|
178
230
|
}
|
|
179
|
-
|
|
180
|
-
|
|
231
|
+
// Split layout: Blume's astro is nested (a conflicting astro took the root
|
|
232
|
+
// spot) but @astrojs/mdx hoisted away from it, binding to the shadow. Only a
|
|
233
|
+
// root pin fixes this — surface it.
|
|
234
|
+
return astroConflictWarning(blumeAstro, outDirAstro);
|
|
181
235
|
};
|
|
182
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Vite plugin that makes Blume's externalized runtime deps (zod, shiki, sharp,
|
|
239
|
+
* `@takumi-rs/core`, …) resolvable when Astro executes the static prerender
|
|
240
|
+
* bundle under an isolated linker (Bun's `isolated` mode, pnpm).
|
|
241
|
+
*
|
|
242
|
+
* Astro's static build emits a self-contained SSR bundle to
|
|
243
|
+
* `<outDir>/.prerender/` and `import()`s it in-process to generate the HTML.
|
|
244
|
+
* That bundle externalizes Blume's render-time deps, so Node resolves them at
|
|
245
|
+
* prerender time by walking up from `.prerender/chunks/*.mjs`. {@link
|
|
246
|
+
* ensureDepsLink} only repairs resolution rooted at `.blume/`; `.prerender/`
|
|
247
|
+
* lives under `dist/`, a separate tree an isolated linker never hoists Blume's
|
|
248
|
+
* deps into — so the import dies with `Cannot find package 'zod'`. We drop the
|
|
249
|
+
* same `node_modules` junction into the prerender root, mirroring
|
|
250
|
+
* `.blume/node_modules`, so every externalized specifier — native bindings
|
|
251
|
+
* included, which can't be bundled — resolves. Astro deletes `.prerender/` once
|
|
252
|
+
* generation finishes (and the junction with it: `fs.rm` unlinks symlinks, it
|
|
253
|
+
* never follows them), so nothing leaks into the published `dist/`.
|
|
254
|
+
*
|
|
255
|
+
* Keyed off the output dir's basename (`.prerender`) — the name Astro 7 gives
|
|
256
|
+
* the prerender build for both static (`<outDir>/.prerender/`) and server
|
|
257
|
+
* (`<build.server>/.prerender/`) output — so it fires for exactly that build.
|
|
258
|
+
* Inert in dev, where there is no build and `writeBundle` never runs.
|
|
259
|
+
*/
|
|
260
|
+
export const prerenderDepsPlugin = (
|
|
261
|
+
pkgDir: string = packageRoot()
|
|
262
|
+
): {
|
|
263
|
+
name: string;
|
|
264
|
+
writeBundle: (options: { dir?: string }) => Promise<void>;
|
|
265
|
+
} => ({
|
|
266
|
+
name: "blume:prerender-deps",
|
|
267
|
+
writeBundle: async (options) => {
|
|
268
|
+
if (!options.dir || basename(options.dir) !== ".prerender") {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const depsDir = blumeDepsDir(pkgDir);
|
|
272
|
+
if (!depsDir) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
await linkDepsJunction(join(options.dir, "node_modules"), depsDir);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
183
279
|
/** Astro integration package each non-React island framework needs installed. */
|
|
184
280
|
const ISLAND_FRAMEWORK_DEPS: Record<string, string> = {
|
|
185
281
|
svelte: "@astrojs/svelte",
|
|
@@ -720,7 +816,7 @@ export const generateRuntime = async (
|
|
|
720
816
|
return writeIfChanged(path, content);
|
|
721
817
|
};
|
|
722
818
|
|
|
723
|
-
await ensureDepsLink(out);
|
|
819
|
+
const depsLinkWarning = await ensureDepsLink(out);
|
|
724
820
|
|
|
725
821
|
const askEnabled = config.ai.ask?.enabled ?? false;
|
|
726
822
|
const exportPdf = config.export.pdf;
|
|
@@ -733,7 +829,7 @@ export const generateRuntime = async (
|
|
|
733
829
|
detectNeedsReact(context.root),
|
|
734
830
|
readOptional(context.themeFile),
|
|
735
831
|
discoverIslands(context.root),
|
|
736
|
-
discoverExamples(context.root),
|
|
832
|
+
discoverExamples(context.root, config.examples),
|
|
737
833
|
]);
|
|
738
834
|
// Each island/example framework enables its Astro renderer. React also
|
|
739
835
|
// switches on for any project `.tsx`/`.jsx` and for Ask AI; Vue/Svelte are
|
|
@@ -952,15 +1048,25 @@ export const generateRuntime = async (
|
|
|
952
1048
|
// API/AsyncAPI reference pages (Scalar). One self-contained page per source,
|
|
953
1049
|
// mounted on its configured route and regenerated each run.
|
|
954
1050
|
const warnings: string[] = [
|
|
1051
|
+
...(depsLinkWarning ? [depsLinkWarning] : []),
|
|
955
1052
|
...mcp.warnings,
|
|
956
1053
|
...islandDiscovery.warnings,
|
|
957
1054
|
...exampleDiscovery.warnings,
|
|
958
1055
|
];
|
|
959
1056
|
|
|
960
|
-
//
|
|
961
|
-
//
|
|
1057
|
+
// Provider SDKs are optional peers; warn (rather than fail opaquely in Vite)
|
|
1058
|
+
// when the configured provider's package isn't installed. A dep is available
|
|
1059
|
+
// if the project installed it (resolves from the root) OR Blume ships it
|
|
1060
|
+
// (resolves from the Blume package — the same set the `.blume` deps link
|
|
1061
|
+
// exposes to the build). Resolving from the project root alone falsely flagged
|
|
1062
|
+
// a shipped SDK like Orama (the default provider) as missing whenever it
|
|
1063
|
+
// wasn't hoisted into the project, e.g. under isolated linkers. We resolve
|
|
1064
|
+
// from each package's real location rather than through the `.blume` junction,
|
|
1065
|
+
// which can't be traversed reliably for store-symlinked deps.
|
|
962
1066
|
for (const dep of searchProviderMeta(config.search.provider).runtimeDeps) {
|
|
963
|
-
if (
|
|
1067
|
+
if (
|
|
1068
|
+
!(canResolveFrom(context.root, dep) || canResolveFrom(packageRoot(), dep))
|
|
1069
|
+
) {
|
|
964
1070
|
warnings.push(
|
|
965
1071
|
`Search provider "${config.search.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`
|
|
966
1072
|
);
|
package/src/astro/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { generateRuntime } from "./generate.ts";
|
|
1
|
+
export { generateRuntime, prerenderDepsPlugin } from "./generate.ts";
|
|
2
2
|
export type { GenerateResult } from "./generate.ts";
|
|
3
3
|
export { blumeIntegration } from "./integration.ts";
|
|
4
4
|
export type { BlumeIntegrationOptions, BlumePageRoute } from "./integration.ts";
|
package/src/astro/templates.ts
CHANGED
|
@@ -130,6 +130,42 @@ export const runtimeDependencies = (options: {
|
|
|
130
130
|
* before a broader one (`@`); these follow Blume's `blume:*` aliases, which
|
|
131
131
|
* never overlap with a project's.
|
|
132
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
|
+
|
|
133
169
|
const renderUserAliases = (
|
|
134
170
|
aliases: Record<string, string> | undefined
|
|
135
171
|
): string =>
|
|
@@ -243,7 +279,7 @@ export const astroConfigTemplate = (options: {
|
|
|
243
279
|
const svelteImport = needsSvelte
|
|
244
280
|
? `import svelte from "@astrojs/svelte";\n`
|
|
245
281
|
: "";
|
|
246
|
-
const blumeImport = `import { blumeIntegration } from "blume/astro";\n`;
|
|
282
|
+
const blumeImport = `import { blumeIntegration, prerenderDepsPlugin } from "blume/astro";\n`;
|
|
247
283
|
|
|
248
284
|
// Twoslash runs first, before the always-on transformers, but only on fences
|
|
249
285
|
// with the `twoslash` meta (explicitTrigger) — so it's opt-in per block with
|
|
@@ -305,19 +341,14 @@ export default defineConfig({
|
|
|
305
341
|
},
|
|
306
342
|
devToolbar: { enabled: false },
|
|
307
343
|
vite: {
|
|
308
|
-
plugins: [tailwindcss()],
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
// native binding") on other platforms (e.g. the Linux CI runner). Astro 7
|
|
314
|
-
// configures externalization per Vite environment, so it must be forced
|
|
315
|
-
// external on the prerender (static) and ssr (server) environments -- a
|
|
316
|
-
// top-level ssr.external only reaches the latter -- so the binding resolves
|
|
317
|
-
// 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.
|
|
318
349
|
environments: {
|
|
319
|
-
prerender: { resolve: { external:
|
|
320
|
-
ssr: { resolve: { external:
|
|
350
|
+
prerender: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
|
|
351
|
+
ssr: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
|
|
321
352
|
},
|
|
322
353
|
resolve: {
|
|
323
354
|
alias: {
|
package/src/core/schema.ts
CHANGED
|
@@ -1021,6 +1021,20 @@ export const blumeConfigSchema = z
|
|
|
1021
1021
|
contextual: contextualConfigSchema.default({}),
|
|
1022
1022
|
deployment: deploymentConfigSchema.default({}),
|
|
1023
1023
|
description: z.string().optional(),
|
|
1024
|
+
/**
|
|
1025
|
+
* Where `<Component path>` resolves live previews and their source from,
|
|
1026
|
+
* relative to the project root. Defaults to the `examples` directory; point
|
|
1027
|
+
* it elsewhere when examples live outside a top-level `examples/`.
|
|
1028
|
+
*
|
|
1029
|
+
* May be a glob (anything with `*`/`?`/`[]`/`{}`/`!`), in which case only
|
|
1030
|
+
* matching files are discovered and a `<Component path>` key is relative to
|
|
1031
|
+
* the glob's static prefix. Use this for a registry layout that colocates
|
|
1032
|
+
* component sources with their examples — `registry/<pkg>/**\/examples/*`
|
|
1033
|
+
* targets just the examples, leaving the sources (which have no default
|
|
1034
|
+
* export to wrap) out, so the registry needn't be forked into its own
|
|
1035
|
+
* examples directory.
|
|
1036
|
+
*/
|
|
1037
|
+
examples: z.string().default("examples"),
|
|
1024
1038
|
export: exportConfigSchema.default(false),
|
|
1025
1039
|
favicon: faviconConfigSchema.optional(),
|
|
1026
1040
|
feedback: z.boolean().default(true),
|
package/src/registry/eject.ts
CHANGED
|
@@ -73,7 +73,7 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
73
73
|
: Promise.resolve(""),
|
|
74
74
|
buildRawMarkdown(project),
|
|
75
75
|
discoverIslands(root),
|
|
76
|
-
discoverExamples(root),
|
|
76
|
+
discoverExamples(root, config.examples),
|
|
77
77
|
]);
|
|
78
78
|
// Island/example frameworks drive which Astro renderers the ejected config
|
|
79
79
|
// wires in; React also switches on for project `.tsx`/`.jsx` and Ask AI.
|