blume 1.7.0 → 1.7.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 +18 -0
- package/dist/cli/{chunk-9qs6acpw.js → chunk-12dxjqk7.js} +32 -43
- package/dist/cli/{chunk-9qs6acpw.js.map → chunk-12dxjqk7.js.map} +2 -2
- package/dist/cli/{chunk-s5dsk8bj.js → chunk-196vjxp9.js} +65 -64
- package/dist/cli/chunk-196vjxp9.js.map +13 -0
- package/dist/cli/{chunk-27gtm2ym.js → chunk-2mzebbbz.js} +1 -1
- package/dist/cli/{chunk-s5e5jt53.js → chunk-2z47ypj8.js} +1 -1
- package/dist/cli/{chunk-tnskyrej.js → chunk-30e87n55.js} +15 -24
- package/dist/cli/{chunk-tnskyrej.js.map → chunk-30e87n55.js.map} +2 -2
- package/dist/cli/{chunk-4trphnvy.js → chunk-3w7b2vcx.js} +10 -13
- package/dist/cli/{chunk-4trphnvy.js.map → chunk-3w7b2vcx.js.map} +2 -2
- package/dist/cli/{chunk-v5mm027v.js → chunk-450a7rcr.js} +8 -8
- package/dist/cli/{chunk-v5mm027v.js.map → chunk-450a7rcr.js.map} +1 -1
- package/dist/cli/{chunk-5d4q7121.js → chunk-5n7t497w.js} +142 -134
- package/dist/cli/{chunk-5d4q7121.js.map → chunk-5n7t497w.js.map} +5 -5
- package/dist/cli/{chunk-xv91q4nm.js → chunk-61j18dwk.js} +37 -9
- package/dist/cli/{chunk-xv91q4nm.js.map → chunk-61j18dwk.js.map} +3 -3
- package/dist/cli/{chunk-3r94j3tc.js → chunk-688e0dde.js} +2 -2
- package/dist/cli/{chunk-vxv4x1n8.js → chunk-88by27n5.js} +2 -2
- package/dist/cli/{chunk-cfw6x4rm.js → chunk-8cd8tj54.js} +28 -35
- package/dist/cli/{chunk-cfw6x4rm.js.map → chunk-8cd8tj54.js.map} +2 -2
- package/dist/cli/{chunk-8gnpdsn1.js → chunk-9bkjd11x.js} +2 -2
- package/dist/cli/{chunk-qq9nm3qd.js → chunk-9he6crym.js} +21 -28
- package/dist/cli/{chunk-qq9nm3qd.js.map → chunk-9he6crym.js.map} +2 -2
- package/dist/cli/{chunk-jk1zwka1.js → chunk-aztttvb3.js} +27 -33
- package/dist/cli/{chunk-jk1zwka1.js.map → chunk-aztttvb3.js.map} +2 -2
- package/dist/cli/{chunk-ckh3a410.js → chunk-cvky9gb2.js} +18 -24
- package/dist/cli/{chunk-ckh3a410.js.map → chunk-cvky9gb2.js.map} +2 -2
- package/dist/cli/{chunk-kwx90v78.js → chunk-eevwt1sc.js} +23 -32
- package/dist/cli/{chunk-kwx90v78.js.map → chunk-eevwt1sc.js.map} +2 -2
- package/dist/cli/{chunk-agy5rzxy.js → chunk-ejjx8znq.js} +126 -77
- package/dist/cli/chunk-ejjx8znq.js.map +15 -0
- package/dist/cli/{chunk-ye9zdkgv.js → chunk-exeeb35e.js} +3 -3
- package/dist/cli/{chunk-v2ymm99c.js → chunk-fmceyezb.js} +41 -50
- package/dist/cli/{chunk-v2ymm99c.js.map → chunk-fmceyezb.js.map} +2 -2
- package/dist/cli/{chunk-s102bysw.js → chunk-hdm2dkd2.js} +209 -97
- package/dist/cli/{chunk-s102bysw.js.map → chunk-hdm2dkd2.js.map} +6 -5
- package/dist/cli/{chunk-zr3ygrq3.js → chunk-hs3gbh8p.js} +10 -15
- package/dist/cli/{chunk-zr3ygrq3.js.map → chunk-hs3gbh8p.js.map} +2 -2
- package/dist/cli/{chunk-n0nyat6g.js → chunk-jbj4qhfw.js} +3 -3
- package/dist/cli/{chunk-ev67ycx0.js → chunk-jq5n4avg.js} +1 -1
- package/dist/cli/{chunk-jxkxjsc1.js → chunk-mqb2ka8m.js} +22 -30
- package/dist/cli/{chunk-jxkxjsc1.js.map → chunk-mqb2ka8m.js.map} +2 -2
- package/dist/cli/{chunk-drke6t0h.js → chunk-mt76t7dj.js} +26 -34
- package/dist/cli/{chunk-drke6t0h.js.map → chunk-mt76t7dj.js.map} +2 -2
- package/dist/cli/{chunk-jtb45atp.js → chunk-n9sra6sy.js} +13 -13
- package/dist/cli/{chunk-jtb45atp.js.map → chunk-n9sra6sy.js.map} +1 -1
- package/dist/cli/{chunk-x66c5yjn.js → chunk-ppfvdcd4.js} +2 -2
- package/dist/cli/{chunk-wd27zjcz.js → chunk-q4rae3bg.js} +1 -1
- package/dist/cli/{chunk-pxj10x8y.js → chunk-ra1v2nc2.js} +1 -1
- package/dist/cli/{chunk-y3g15rvv.js → chunk-t3tj0dgr.js} +26 -33
- package/dist/cli/{chunk-y3g15rvv.js.map → chunk-t3tj0dgr.js.map} +2 -2
- package/dist/cli/{chunk-j6pxe0dt.js → chunk-tqa1s0k8.js} +4 -4
- package/dist/cli/{chunk-cbjnx4s8.js → chunk-vh9w1sgp.js} +1 -1
- package/dist/cli/{chunk-0qhq7b8q.js → chunk-vkrsvbr5.js} +13 -17
- package/dist/cli/{chunk-0qhq7b8q.js.map → chunk-vkrsvbr5.js.map} +2 -2
- package/dist/cli/{chunk-sbdqrjbb.js → chunk-vrfp10qk.js} +1 -1
- package/dist/cli/{chunk-ynacq3ev.js → chunk-wjt80jps.js} +27 -40
- package/dist/cli/{chunk-ynacq3ev.js.map → chunk-wjt80jps.js.map} +3 -4
- package/dist/cli/{chunk-18tjv4f7.js → chunk-xhtpx3ff.js} +21 -30
- package/dist/cli/{chunk-18tjv4f7.js.map → chunk-xhtpx3ff.js.map} +2 -2
- package/dist/cli/{chunk-5hs6gb7n.js → chunk-yzhm0j9q.js} +1 -1
- package/dist/cli/index.js +397 -34
- package/dist/cli/index.js.map +12 -4
- package/dist/types/components/layout/nav-utils.d.ts +15 -2
- package/dist/types/core/config-input.d.ts +33 -0
- package/dist/types/core/schema.d.ts +29 -3
- package/docs/08-faq.mdx +21 -0
- package/docs/configuration/ask-ai.mdx +63 -0
- package/docs/content/sources.mdx +2 -2
- package/package.json +1 -1
- package/src/ai/ask.ts +31 -4
- package/src/ai/cors.ts +87 -0
- package/src/astro/generate.ts +3 -0
- package/src/astro/templates.ts +192 -83
- package/src/components/content/YouTube.astro +1 -1
- package/src/components/layout/Header.astro +1 -1
- package/src/components/layout/NavTree.astro +75 -66
- package/src/components/layout/RootLayout.astro +4 -1
- package/src/components/layout/Search.astro +11 -0
- package/src/components/layout/nav-utils.ts +47 -15
- package/src/core/adapter.ts +61 -0
- package/src/core/config-input.ts +33 -0
- package/src/core/schema.ts +61 -0
- package/src/core/sources/assets.ts +162 -26
- package/src/core/sources/notion.ts +60 -1
- package/src/registry/eject.ts +3 -0
- package/src/theme/entry.ts +9 -0
- package/dist/cli/chunk-2aj8ddew.js +0 -72
- package/dist/cli/chunk-2aj8ddew.js.map +0 -10
- package/dist/cli/chunk-4xyggvgf.js +0 -21
- package/dist/cli/chunk-4xyggvgf.js.map +0 -10
- package/dist/cli/chunk-6kzzpsx8.js +0 -26
- package/dist/cli/chunk-6kzzpsx8.js.map +0 -10
- package/dist/cli/chunk-agy5rzxy.js.map +0 -15
- package/dist/cli/chunk-bcy492zc.js +0 -16
- package/dist/cli/chunk-bcy492zc.js.map +0 -10
- package/dist/cli/chunk-btfr9yvw.js +0 -41
- package/dist/cli/chunk-btfr9yvw.js.map +0 -10
- package/dist/cli/chunk-ey89bjj1.js +0 -209
- package/dist/cli/chunk-ey89bjj1.js.map +0 -11
- package/dist/cli/chunk-s5dsk8bj.js.map +0 -13
- package/dist/cli/chunk-vt8fgygt.js +0 -23
- package/dist/cli/chunk-vt8fgygt.js.map +0 -10
- /package/dist/cli/{chunk-27gtm2ym.js.map → chunk-2mzebbbz.js.map} +0 -0
- /package/dist/cli/{chunk-s5e5jt53.js.map → chunk-2z47ypj8.js.map} +0 -0
- /package/dist/cli/{chunk-3r94j3tc.js.map → chunk-688e0dde.js.map} +0 -0
- /package/dist/cli/{chunk-vxv4x1n8.js.map → chunk-88by27n5.js.map} +0 -0
- /package/dist/cli/{chunk-8gnpdsn1.js.map → chunk-9bkjd11x.js.map} +0 -0
- /package/dist/cli/{chunk-ye9zdkgv.js.map → chunk-exeeb35e.js.map} +0 -0
- /package/dist/cli/{chunk-n0nyat6g.js.map → chunk-jbj4qhfw.js.map} +0 -0
- /package/dist/cli/{chunk-ev67ycx0.js.map → chunk-jq5n4avg.js.map} +0 -0
- /package/dist/cli/{chunk-x66c5yjn.js.map → chunk-ppfvdcd4.js.map} +0 -0
- /package/dist/cli/{chunk-wd27zjcz.js.map → chunk-q4rae3bg.js.map} +0 -0
- /package/dist/cli/{chunk-pxj10x8y.js.map → chunk-ra1v2nc2.js.map} +0 -0
- /package/dist/cli/{chunk-j6pxe0dt.js.map → chunk-tqa1s0k8.js.map} +0 -0
- /package/dist/cli/{chunk-cbjnx4s8.js.map → chunk-vh9w1sgp.js.map} +0 -0
- /package/dist/cli/{chunk-sbdqrjbb.js.map → chunk-vrfp10qk.js.map} +0 -0
- /package/dist/cli/{chunk-5hs6gb7n.js.map → chunk-yzhm0j9q.js.map} +0 -0
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
access,
|
|
4
|
+
mkdir,
|
|
5
|
+
readdir,
|
|
6
|
+
rename,
|
|
7
|
+
rm,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from "node:fs/promises";
|
|
2
10
|
|
|
3
11
|
import { extname, join } from "pathe";
|
|
4
12
|
|
|
@@ -6,6 +14,12 @@ import type { Diagnostic } from "../types.ts";
|
|
|
6
14
|
import { hashText } from "./cache.ts";
|
|
7
15
|
|
|
8
16
|
const MD_IMAGE = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
|
|
17
|
+
// A `<video>` tag's `src`, split so the rewrite can swap the URL and keep the
|
|
18
|
+
// surrounding attributes untouched. Notion's uploaded videos arrive as signed,
|
|
19
|
+
// expiring URLs exactly like its images, so they rot the same way. `[^>]`
|
|
20
|
+
// bounds the attribute run to a single tag.
|
|
21
|
+
const HTML_VIDEO_SRC =
|
|
22
|
+
/(?<open><video\b[^>]*?\ssrc=")(?<url>[^"]+)(?<close>")/gu;
|
|
9
23
|
const REMOTE = /^https?:\/\//u;
|
|
10
24
|
const SAFE_EXT = /^\.[a-z0-9]+$/iu;
|
|
11
25
|
const CODE_FENCE_BLOCK =
|
|
@@ -14,32 +28,107 @@ const CODE_FENCE_BLOCK =
|
|
|
14
28
|
// oxlint-disable-next-line no-control-regex -- the NUL is the collision guard.
|
|
15
29
|
const FENCE_TOKEN = /\u0000blume-fence-(?<index>\d+)\u0000/gu;
|
|
16
30
|
|
|
31
|
+
// The extension for a URL whose path carries none, keyed by the media type the
|
|
32
|
+
// server reports. A static host serves by extension, so a `.png` holding JPEG
|
|
33
|
+
// bytes is mislabeled and a `.png` holding a video is refused by strict
|
|
34
|
+
// players — the response is the source of truth, not the reference kind.
|
|
35
|
+
const EXT_BY_MIME = new Map([
|
|
36
|
+
["image/avif", ".avif"],
|
|
37
|
+
["image/gif", ".gif"],
|
|
38
|
+
["image/jpeg", ".jpg"],
|
|
39
|
+
["image/png", ".png"],
|
|
40
|
+
["image/svg+xml", ".svg"],
|
|
41
|
+
["image/webp", ".webp"],
|
|
42
|
+
["video/mp4", ".mp4"],
|
|
43
|
+
["video/ogg", ".ogv"],
|
|
44
|
+
["video/quicktime", ".mov"],
|
|
45
|
+
["video/webm", ".webm"],
|
|
46
|
+
]);
|
|
47
|
+
const UNKNOWN_EXT = ".bin";
|
|
48
|
+
// Response types that are never a media file. A pasted Vimeo/Loom/Wistia link
|
|
49
|
+
// is a video block in Notion whose URL is a watch page (200 text/html), and an
|
|
50
|
+
// API endpoint answers JSON or XML; `image/svg+xml` is media and stays.
|
|
51
|
+
const NON_MEDIA_TYPE =
|
|
52
|
+
/^(?:text\/|application\/(?:[\w.-]+\+)?(?:json|xml|javascript))/u;
|
|
53
|
+
const PART_SUFFIX = ".part";
|
|
54
|
+
// Generous enough for a multi-hundred-megabyte recording on an ordinary
|
|
55
|
+
// connection; its job is to fail a stalled download rather than hang the build.
|
|
56
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
57
|
+
|
|
17
58
|
/** Where to write downloaded assets and how to reference them publicly. */
|
|
18
59
|
export interface AssetContext {
|
|
19
60
|
assetsDir: string;
|
|
20
61
|
assetsBaseUrl: string;
|
|
21
62
|
/** Injected for tests; defaults to the global `fetch`. */
|
|
22
63
|
fetchImpl?: typeof fetch;
|
|
64
|
+
/**
|
|
65
|
+
* Gate for concurrent downloads. A source shares one gate across every page
|
|
66
|
+
* it materializes, so a database of video-heavy pages doesn't open every
|
|
67
|
+
* download at once. Defaults to no gate.
|
|
68
|
+
*/
|
|
69
|
+
limit?: <T>(task: () => Promise<T>) => Promise<T>;
|
|
70
|
+
/** Abort a download that hasn't completed within this many milliseconds. */
|
|
71
|
+
timeoutMs?: number;
|
|
23
72
|
}
|
|
24
73
|
|
|
25
|
-
/**
|
|
26
|
-
const
|
|
74
|
+
/** The extension in a URL's path, or null when it carries none we'd trust. */
|
|
75
|
+
const extFromUrl = (url: string): string | null => {
|
|
27
76
|
const clean = url.split("?")[0] ?? url;
|
|
28
77
|
const ext = extname(clean);
|
|
29
|
-
return SAFE_EXT.test(ext) ? ext.toLowerCase() :
|
|
78
|
+
return SAFE_EXT.test(ext) ? ext.toLowerCase() : null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** The response's media type, lowercased and stripped of parameters. */
|
|
82
|
+
const mediaType = (res: Response): string =>
|
|
83
|
+
res.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
84
|
+
|
|
85
|
+
const exists = async (path: string): Promise<boolean> => {
|
|
86
|
+
try {
|
|
87
|
+
await access(path);
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The completed file for a stem whose extension came from an earlier response
|
|
96
|
+
* rather than the URL, so an extension-less asset is not fetched on every
|
|
97
|
+
* poll. Only a lone candidate counts: query-less URLs can collide, and picking
|
|
98
|
+
* between two files would be a guess.
|
|
99
|
+
*/
|
|
100
|
+
const completedFor = async (
|
|
101
|
+
dir: string,
|
|
102
|
+
stem: string
|
|
103
|
+
): Promise<string | null> => {
|
|
104
|
+
let names: string[];
|
|
105
|
+
try {
|
|
106
|
+
names = await readdir(dir);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const candidates = names.filter(
|
|
111
|
+
(name) => name.startsWith(`${stem}.`) && !name.endsWith(PART_SUFFIX)
|
|
112
|
+
);
|
|
113
|
+
return candidates.length === 1 ? (candidates[0] ?? null) : null;
|
|
30
114
|
};
|
|
31
115
|
|
|
32
116
|
/**
|
|
33
|
-
* Download remote
|
|
34
|
-
* rewrite
|
|
35
|
-
*
|
|
36
|
-
*
|
|
117
|
+
* Download remote media referenced in a Markdown body into the asset dir and
|
|
118
|
+
* rewrite the reference to the local public path. Markdown images and the
|
|
119
|
+
* `src` of a `<video>` tag are both covered. Remote CMS URLs (notably Notion's
|
|
120
|
+
* signed, expiring links) would otherwise rot a static build. Assets are
|
|
121
|
+
* content-addressed by URL hash, so repeated builds are stable and deduped —
|
|
122
|
+
* and a file already on disk is not fetched again, which keeps a dev poll
|
|
123
|
+
* from re-downloading every video on every tick.
|
|
37
124
|
*/
|
|
38
125
|
export const materializeAssets = async (
|
|
39
126
|
markdown: string,
|
|
40
127
|
ctx: AssetContext
|
|
41
128
|
): Promise<{ markdown: string; diagnostics: Diagnostic[] }> => {
|
|
42
129
|
const doFetch = ctx.fetchImpl ?? globalThis.fetch;
|
|
130
|
+
const limit = ctx.limit ?? ((task) => task());
|
|
131
|
+
const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
43
132
|
const diagnostics: Diagnostic[] = [];
|
|
44
133
|
|
|
45
134
|
// Mask fenced code blocks so an image URL inside a code sample is neither
|
|
@@ -52,34 +141,77 @@ export const materializeAssets = async (
|
|
|
52
141
|
});
|
|
53
142
|
|
|
54
143
|
const urls = new Set<string>();
|
|
55
|
-
for (const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
144
|
+
for (const pattern of [MD_IMAGE, HTML_VIDEO_SRC]) {
|
|
145
|
+
for (const match of masked.matchAll(pattern)) {
|
|
146
|
+
const url = match.groups?.url;
|
|
147
|
+
if (url && REMOTE.test(url)) {
|
|
148
|
+
urls.add(url);
|
|
149
|
+
}
|
|
59
150
|
}
|
|
60
151
|
}
|
|
61
152
|
|
|
153
|
+
/** Fetch one asset into the asset dir and return its file name. */
|
|
154
|
+
const download = async (url: string): Promise<string> => {
|
|
155
|
+
// Hash the query-less URL: CMS asset URLs are pre-signed, so the query
|
|
156
|
+
// changes on every fetch of the same file — hashing it would mint a new
|
|
157
|
+
// file each refresh and re-dirty the content digest. Two real assets
|
|
158
|
+
// sharing scheme+host+path and differing only in query are rare enough to
|
|
159
|
+
// accept colliding.
|
|
160
|
+
const stem = hashText(url.split("?")[0] ?? url);
|
|
161
|
+
const urlExt = extFromUrl(url);
|
|
162
|
+
// The name is known up front whenever the path has an extension (every
|
|
163
|
+
// Notion upload does), so a file from an earlier run is reused as is;
|
|
164
|
+
// otherwise the extension came from the last response, so look for it.
|
|
165
|
+
if (urlExt && (await exists(join(ctx.assetsDir, `${stem}${urlExt}`)))) {
|
|
166
|
+
return `${stem}${urlExt}`;
|
|
167
|
+
}
|
|
168
|
+
const completed = urlExt ? null : await completedFor(ctx.assetsDir, stem);
|
|
169
|
+
if (completed) {
|
|
170
|
+
return completed;
|
|
171
|
+
}
|
|
172
|
+
const res = await doFetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
throw new Error(`${res.status}`);
|
|
175
|
+
}
|
|
176
|
+
// Writing a watch page or an API response out as `.mp4` gives a player
|
|
177
|
+
// that can't play and a green build, so refuse what is never media.
|
|
178
|
+
const type = mediaType(res);
|
|
179
|
+
if (NON_MEDIA_TYPE.test(type)) {
|
|
180
|
+
throw new Error(`responded with ${type}, not a media file`);
|
|
181
|
+
}
|
|
182
|
+
if (!res.body) {
|
|
183
|
+
throw new Error("empty response body");
|
|
184
|
+
}
|
|
185
|
+
const file = `${stem}${urlExt ?? EXT_BY_MIME.get(type) ?? UNKNOWN_EXT}`;
|
|
186
|
+
const target = join(ctx.assetsDir, file);
|
|
187
|
+
await mkdir(ctx.assetsDir, { recursive: true });
|
|
188
|
+
// Stream the body to disk rather than buffering it: a video is hundreds of
|
|
189
|
+
// megabytes where an image was a hundred kilobytes. Write to a temporary
|
|
190
|
+
// name unique to this attempt and rename on completion, so a download that
|
|
191
|
+
// dies midway never leaves a truncated file the next run would trust as
|
|
192
|
+
// complete, and two pages fetching the same asset at once (the gate bounds
|
|
193
|
+
// concurrency, it doesn't dedupe) never write into each other's file —
|
|
194
|
+
// both publish identical bytes and the last rename wins.
|
|
195
|
+
const part = `${target}.${randomUUID()}${PART_SUFFIX}`;
|
|
196
|
+
try {
|
|
197
|
+
await writeFile(part, res.body);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
await rm(part, { force: true });
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
await rename(part, target);
|
|
203
|
+
return file;
|
|
204
|
+
};
|
|
205
|
+
|
|
62
206
|
const rewrites = new Map<string, string>();
|
|
63
207
|
await Promise.all(
|
|
64
208
|
[...urls].map(async (url) => {
|
|
65
209
|
try {
|
|
66
|
-
const
|
|
67
|
-
if (!res.ok) {
|
|
68
|
-
throw new Error(`${res.status}`);
|
|
69
|
-
}
|
|
70
|
-
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
71
|
-
// Hash the query-less URL (as `extFor` does): CMS asset URLs are
|
|
72
|
-
// pre-signed, so the query changes on every fetch of the same image —
|
|
73
|
-
// hashing it would mint a new file each refresh and re-dirty the
|
|
74
|
-
// content digest. Two real assets sharing scheme+host+path and
|
|
75
|
-
// differing only in query are rare enough to accept colliding.
|
|
76
|
-
const file = `${hashText(url.split("?")[0] ?? url)}${extFor(url)}`;
|
|
77
|
-
await mkdir(ctx.assetsDir, { recursive: true });
|
|
78
|
-
await writeFile(join(ctx.assetsDir, file), bytes);
|
|
210
|
+
const file = await limit(() => download(url));
|
|
79
211
|
rewrites.set(url, `${ctx.assetsBaseUrl}/${file}`);
|
|
80
212
|
} catch (error) {
|
|
81
213
|
// SAFETY: everything thrown in this block is an Error — the manual
|
|
82
|
-
//
|
|
214
|
+
// throws in `download`, and fetch/fs failures.
|
|
83
215
|
diagnostics.push({
|
|
84
216
|
code: "BLUME_ASSET_FETCH_FAILED",
|
|
85
217
|
message: `Failed to download asset ${url}: ${(error as Error).message}`,
|
|
@@ -94,6 +226,10 @@ export const materializeAssets = async (
|
|
|
94
226
|
const local = rewrites.get(url);
|
|
95
227
|
return local ? `` : match;
|
|
96
228
|
})
|
|
229
|
+
.replaceAll(HTML_VIDEO_SRC, (match, open, url, close) => {
|
|
230
|
+
const local = rewrites.get(url);
|
|
231
|
+
return local ? `${open}${local}${close}` : match;
|
|
232
|
+
})
|
|
97
233
|
.replaceAll(FENCE_TOKEN, (token, index) => fences[Number(index)] ?? token);
|
|
98
234
|
|
|
99
235
|
return { diagnostics, markdown: rewritten };
|
|
@@ -3,6 +3,7 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
3
3
|
import pLimit from "p-limit";
|
|
4
4
|
import { join } from "pathe";
|
|
5
5
|
|
|
6
|
+
import { parseYouTubeId } from "../../components/content/youtube.ts";
|
|
6
7
|
import { BlumeError } from "../diagnostics.ts";
|
|
7
8
|
import matter from "../frontmatter.ts";
|
|
8
9
|
import type { Diagnostic } from "../types.ts";
|
|
@@ -189,6 +190,7 @@ const MAX_RETRIES = 4;
|
|
|
189
190
|
const BASE_DELAY_MS = 500;
|
|
190
191
|
const SECOND_MS = 1000;
|
|
191
192
|
const DEFAULT_CONCURRENCY = 3;
|
|
193
|
+
const ASSET_DOWNLOAD_CONCURRENCY = 4;
|
|
192
194
|
|
|
193
195
|
/**
|
|
194
196
|
* Retry a Notion API call on a `429 rate_limited`, honoring the `Retry-After`
|
|
@@ -250,6 +252,55 @@ const LIST_BLOCKS = new Set([
|
|
|
250
252
|
const isListItem = (block: NotionBlock | undefined): boolean =>
|
|
251
253
|
block !== undefined && LIST_BLOCKS.has(block.type);
|
|
252
254
|
|
|
255
|
+
const richToPlain = (rich: NotionRichText[] = []): string =>
|
|
256
|
+
rich.map((node) => node.plain_text).join("");
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* A string prop in JSX expression form. The quoted form (`title="…"`) keeps
|
|
260
|
+
* `\"` and `\n` as literal characters — MDX decodes no escapes there, so a
|
|
261
|
+
* caption holding a double quote fails to compile the whole page — where the
|
|
262
|
+
* expression form (`title={"…"}`) is a JSON string literal and decodes them.
|
|
263
|
+
*/
|
|
264
|
+
const jsxString = (value: string): string => `{${JSON.stringify(value)}}`;
|
|
265
|
+
|
|
266
|
+
const YOUTUBE_HOST = /(?:^|\.)(?:youtube(?:-nocookie)?\.com|youtu\.be)$/u;
|
|
267
|
+
|
|
268
|
+
// `parseYouTubeId` is host-agnostic on purpose (the component accepts bare
|
|
269
|
+
// ids), so gate on the hostname first: `https://cdn.example.com/live/promo.mp4`
|
|
270
|
+
// matches its `/live/<11 chars>` shape but is a media file, not an embed.
|
|
271
|
+
const isYouTubeUrl = (url: string): boolean => {
|
|
272
|
+
const host = URL.parse(url)?.hostname ?? "";
|
|
273
|
+
return YOUTUBE_HOST.test(host) && parseYouTubeId(url) !== null;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Render a Notion `video` block. Kept out of `renderLeaf`'s switch so that
|
|
278
|
+
* function stays under the complexity limit.
|
|
279
|
+
*/
|
|
280
|
+
const renderVideo = (data: NotionBlockPayload): string => {
|
|
281
|
+
const url = data.external?.url ?? data.file?.url;
|
|
282
|
+
if (!url) {
|
|
283
|
+
return "";
|
|
284
|
+
}
|
|
285
|
+
const caption = richToMarkdown(data.caption);
|
|
286
|
+
// A YouTube link pasted into Notion becomes a `video` block holding an
|
|
287
|
+
// external URL. That URL is a watch page, not a media file, so it has to
|
|
288
|
+
// become the embed component — a `<video src>` pointing at it plays nothing,
|
|
289
|
+
// and `materializeAssets` would download the HTML page. The iframe's title
|
|
290
|
+
// is its accessible name, so it gets the caption's plain text; the Markdown
|
|
291
|
+
// rendering goes to the Frame's caption, as it does for an upload.
|
|
292
|
+
const title = caption ? ` title=${jsxString(richToPlain(data.caption))}` : "";
|
|
293
|
+
// Everything else (a Notion upload, or a direct link to a media file) is a
|
|
294
|
+
// real video file: `materializeAssets` rewrites the `src`, which matters most
|
|
295
|
+
// for uploads, whose Notion URLs are signed and expire.
|
|
296
|
+
const media = isYouTubeUrl(url)
|
|
297
|
+
? `<YouTube${title} url=${JSON.stringify(url)} />`
|
|
298
|
+
: `<video controls src=${JSON.stringify(url)} />`;
|
|
299
|
+
return caption
|
|
300
|
+
? `<Frame caption=${jsxString(caption)}>\n${media}\n</Frame>`
|
|
301
|
+
: media;
|
|
302
|
+
};
|
|
303
|
+
|
|
253
304
|
/** Render a leaf (non-container) block to Markdown, or null for containers. */
|
|
254
305
|
const renderLeaf = (block: NotionBlock): string | null => {
|
|
255
306
|
const data = payloadOf(block) ?? {};
|
|
@@ -289,6 +340,9 @@ const renderLeaf = (block: NotionBlock): string | null => {
|
|
|
289
340
|
const url = data.external?.url ?? data.file?.url;
|
|
290
341
|
return url ? `` : "";
|
|
291
342
|
}
|
|
343
|
+
case "video": {
|
|
344
|
+
return renderVideo(data);
|
|
345
|
+
}
|
|
292
346
|
default: {
|
|
293
347
|
return null;
|
|
294
348
|
}
|
|
@@ -311,6 +365,10 @@ export const notionSource = (
|
|
|
311
365
|
// container — an unbounded burst guarantees 429s that even the retry loop
|
|
312
366
|
// can't recover from, so every API call funnels through this limiter.
|
|
313
367
|
const limit = pLimit(Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY));
|
|
368
|
+
// Asset downloads get their own gate: they go to Notion's S3, not its rate-
|
|
369
|
+
// limited API, and a slow video must not hold API slots. One gate per source
|
|
370
|
+
// bounds the fan-out across every page, not just within one.
|
|
371
|
+
const downloads = pLimit(ASSET_DOWNLOAD_CONCURRENCY);
|
|
314
372
|
// Every Notion API call goes through the limiter, inside the retry — so a
|
|
315
373
|
// call sleeping through a backoff doesn't hold a slot while it waits.
|
|
316
374
|
const notionCall = <T>(call: () => Promise<T>): Promise<T> =>
|
|
@@ -380,7 +438,7 @@ export const notionSource = (
|
|
|
380
438
|
return `<Callout>\n${body}\n</Callout>`;
|
|
381
439
|
}
|
|
382
440
|
if (block.type === "toggle") {
|
|
383
|
-
const title =
|
|
441
|
+
const title = jsxString(richToMarkdown(blockField(block)));
|
|
384
442
|
return `<Accordion>\n<AccordionItem title=${title}>\n${await children(block)}\n</AccordionItem>\n</Accordion>`;
|
|
385
443
|
}
|
|
386
444
|
if (block.type === "column_list") {
|
|
@@ -510,6 +568,7 @@ export const notionSource = (
|
|
|
510
568
|
assetsBaseUrl,
|
|
511
569
|
assetsDir,
|
|
512
570
|
fetchImpl: options.fetchImpl,
|
|
571
|
+
limit: downloads,
|
|
513
572
|
});
|
|
514
573
|
const raw = matter.stringify(assets.markdown, data);
|
|
515
574
|
return {
|
package/src/registry/eject.ts
CHANGED
|
@@ -137,7 +137,9 @@ const askFiles = async (
|
|
|
137
137
|
const files = [
|
|
138
138
|
{
|
|
139
139
|
content: askEndpointTemplate(resolveAskBackend(ask), grounded, {
|
|
140
|
+
cors: ask.cors,
|
|
140
141
|
instructions: ask.instructions,
|
|
142
|
+
reasoning: ask.reasoning,
|
|
141
143
|
retrieval: ask.retrieval,
|
|
142
144
|
}),
|
|
143
145
|
path: join(srcDir, "pages", "api", "ask.ts"),
|
|
@@ -551,6 +553,7 @@ export const eject = async (
|
|
|
551
553
|
...changelogFiles(project, pages, srcDir, {
|
|
552
554
|
exportEpub,
|
|
553
555
|
exportPdf,
|
|
556
|
+
mathEnabled: usesMath,
|
|
554
557
|
needsReact,
|
|
555
558
|
staged: hasStaged,
|
|
556
559
|
})
|
package/src/theme/entry.ts
CHANGED
|
@@ -194,6 +194,15 @@ ${THEME_MAPPING}
|
|
|
194
194
|
scroll-padding-top: 4.5rem;
|
|
195
195
|
text-rendering: optimizeLegibility;
|
|
196
196
|
}
|
|
197
|
+
/* Modal surfaces coordinate through independent root attributes. The lock
|
|
198
|
+
remains until every owner releases its attribute, while !important lets it
|
|
199
|
+
temporarily override (and therefore preserve) an authored inline value.
|
|
200
|
+
Classic (non-overlay) scrollbars would otherwise vanish while locked and
|
|
201
|
+
shift the page and the centered dialog sideways, so keep their gutter. */
|
|
202
|
+
html:where([data-blume-nav-open], [data-blume-search-dialog-open]) {
|
|
203
|
+
overflow: hidden !important;
|
|
204
|
+
scrollbar-gutter: stable;
|
|
205
|
+
}
|
|
197
206
|
/* Headings use the display font (defaults to the body font when unset).
|
|
198
207
|
The tightened tracking is part of the theme, not the font: display-tuned
|
|
199
208
|
families bake it into their metrics, but a text family promoted to
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
-
|
|
5
|
-
// src/cli/command-meta.ts
|
|
6
|
-
var commandMeta = {
|
|
7
|
-
add: {
|
|
8
|
-
description: "Install a source component or template from the registry.",
|
|
9
|
-
name: "add"
|
|
10
|
-
},
|
|
11
|
-
audit: {
|
|
12
|
-
description: "Audit the built site for SEO and site-health issues.",
|
|
13
|
-
name: "audit"
|
|
14
|
-
},
|
|
15
|
-
build: {
|
|
16
|
-
description: "Build the docs site for production.",
|
|
17
|
-
name: "build"
|
|
18
|
-
},
|
|
19
|
-
check: {
|
|
20
|
-
description: "Type-check the docs site with astro check.",
|
|
21
|
-
name: "check"
|
|
22
|
-
},
|
|
23
|
-
dev: {
|
|
24
|
-
description: "Start the Blume development server.",
|
|
25
|
-
name: "dev"
|
|
26
|
-
},
|
|
27
|
-
doctor: {
|
|
28
|
-
description: "Diagnose common configuration and content problems.",
|
|
29
|
-
name: "doctor"
|
|
30
|
-
},
|
|
31
|
-
eject: {
|
|
32
|
-
description: "Promote the generated runtime into an owned Astro project.",
|
|
33
|
-
name: "eject"
|
|
34
|
-
},
|
|
35
|
-
eval: {
|
|
36
|
-
description: "Test the docs: an agent answers your questions using only the documentation.",
|
|
37
|
-
name: "eval"
|
|
38
|
-
},
|
|
39
|
-
init: {
|
|
40
|
-
description: "Scaffold a minimal Blume project.",
|
|
41
|
-
name: "init"
|
|
42
|
-
},
|
|
43
|
-
"mcp-stdio": {
|
|
44
|
-
description: "Serve an MCP data snapshot over stdio (internal, used by `blume eval`).",
|
|
45
|
-
name: "mcp-stdio"
|
|
46
|
-
},
|
|
47
|
-
preview: {
|
|
48
|
-
description: "Preview the last production build.",
|
|
49
|
-
name: "preview"
|
|
50
|
-
},
|
|
51
|
-
sync: {
|
|
52
|
-
description: "Re-fetch remote content sources and regenerate the runtime.",
|
|
53
|
-
name: "sync"
|
|
54
|
-
},
|
|
55
|
-
translate: {
|
|
56
|
-
description: "Translate docs into the configured locales with a local agent CLI.",
|
|
57
|
-
name: "translate"
|
|
58
|
-
},
|
|
59
|
-
validate: {
|
|
60
|
-
description: "Validate internal, anchor, asset, and external links.",
|
|
61
|
-
name: "validate"
|
|
62
|
-
},
|
|
63
|
-
version: {
|
|
64
|
-
description: "Freeze the current docs as an archived version.",
|
|
65
|
-
name: "version"
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export { __require, commandMeta };
|
|
70
|
-
|
|
71
|
-
//# debugId=3DF60EB79641093164756E2164756E21
|
|
72
|
-
//# sourceMappingURL=chunk-2aj8ddew.js.map
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/cli/command-meta.ts"],
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"import type { CommandMeta } from \"citty\";\n\n/**\n * Every command's `meta`, held apart from the command modules themselves.\n *\n * The CLI entry loads each command lazily (see `lazy-command.ts`), but citty\n * still reads every subcommand's `meta` to render `blume --help` and to match\n * an unknown name against aliases. Keeping that table here lets those paths\n * run without importing a single command module — `dev` alone drags in Astro,\n * `mcp-stdio` the MCP SDK. The command modules read their `meta` from this\n * table too, so the entry and the command can't drift.\n */\nexport const commandMeta = {\n add: {\n description: \"Install a source component or template from the registry.\",\n name: \"add\",\n },\n audit: {\n description: \"Audit the built site for SEO and site-health issues.\",\n name: \"audit\",\n },\n build: {\n description: \"Build the docs site for production.\",\n name: \"build\",\n },\n check: {\n description: \"Type-check the docs site with astro check.\",\n name: \"check\",\n },\n dev: {\n description: \"Start the Blume development server.\",\n name: \"dev\",\n },\n doctor: {\n description: \"Diagnose common configuration and content problems.\",\n name: \"doctor\",\n },\n eject: {\n description: \"Promote the generated runtime into an owned Astro project.\",\n name: \"eject\",\n },\n eval: {\n description:\n \"Test the docs: an agent answers your questions using only the documentation.\",\n name: \"eval\",\n },\n init: {\n description: \"Scaffold a minimal Blume project.\",\n name: \"init\",\n },\n \"mcp-stdio\": {\n description:\n \"Serve an MCP data snapshot over stdio (internal, used by `blume eval`).\",\n name: \"mcp-stdio\",\n },\n preview: {\n description: \"Preview the last production build.\",\n name: \"preview\",\n },\n sync: {\n description: \"Re-fetch remote content sources and regenerate the runtime.\",\n name: \"sync\",\n },\n translate: {\n description:\n \"Translate docs into the configured locales with a local agent CLI.\",\n name: \"translate\",\n },\n validate: {\n description: \"Validate internal, anchor, asset, and external links.\",\n name: \"validate\",\n },\n version: {\n description: \"Freeze the current docs as an archived version.\",\n name: \"version\",\n },\n} satisfies Record<string, CommandMeta>;\n"
|
|
6
|
-
],
|
|
7
|
-
"mappings": ";;;;;AAYO,IAAM,cAAc;AAAA,EACzB,KAAK;AAAA,IACH,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,OAAO;AAAA,IACL,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,OAAO;AAAA,IACL,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,OAAO;AAAA,IACL,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,KAAK;AAAA,IACH,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,QAAQ;AAAA,IACN,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,OAAO;AAAA,IACL,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,MAAM;AAAA,IACJ,aACE;AAAA,IACF,MAAM;AAAA,EACR;AAAA,EACA,MAAM;AAAA,IACJ,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,aAAa;AAAA,IACX,aACE;AAAA,IACF,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,MAAM;AAAA,IACJ,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,WAAW;AAAA,IACT,aACE;AAAA,IACF,MAAM;AAAA,EACR;AAAA,EACA,UAAU;AAAA,IACR,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AACF;",
|
|
8
|
-
"debugId": "3DF60EB79641093164756E2164756E21",
|
|
9
|
-
"names": []
|
|
10
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
packageRoot
|
|
4
|
-
} from "./chunk-6kzzpsx8.js";
|
|
5
|
-
|
|
6
|
-
// src/core/version.ts
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
|
-
import { join } from "pathe";
|
|
9
|
-
var cached;
|
|
10
|
-
var getBlumeVersion = () => {
|
|
11
|
-
if (cached === undefined) {
|
|
12
|
-
const pkgPath = join(packageRoot(), "package.json");
|
|
13
|
-
cached = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
14
|
-
}
|
|
15
|
-
return cached;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export { getBlumeVersion };
|
|
19
|
-
|
|
20
|
-
//# debugId=C3D76F992F5A14AE64756E2164756E21
|
|
21
|
-
//# sourceMappingURL=chunk-4xyggvgf.js.map
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/core/version.ts"],
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"import { readFileSync } from \"node:fs\";\n\nimport { join } from \"pathe\";\n\nimport { packageRoot } from \"./package-root.ts\";\n\nlet cached: string | undefined;\n\n/**\n * The installed Blume package version, read lazily from its `package.json`.\n *\n * Computed on demand (not at module load) so importing the `blume` barrel has no\n * filesystem side effect, and anchored at the package root so it resolves the\n * same whether running from source or the bundled CLI.\n */\nexport const getBlumeVersion = (): string => {\n if (cached === undefined) {\n const pkgPath = join(packageRoot(), \"package.json\");\n // SAFETY: this reads blume's own package.json, which always declares a\n // `version` (publishing requires it).\n cached = (JSON.parse(readFileSync(pkgPath, \"utf-8\")) as { version: string })\n .version;\n }\n return cached;\n};\n"
|
|
6
|
-
],
|
|
7
|
-
"mappings": ";;;;;;AAAA;AAEA;AAIA,IAAI;AASG,IAAM,kBAAkB,MAAc;AAAA,EAC3C,IAAI,WAAW,WAAW;AAAA,IACxB,MAAM,UAAU,KAAK,YAAY,GAAG,cAAc;AAAA,IAGlD,SAAU,KAAK,MAAM,aAAa,SAAS,OAAO,CAAC,EAChD;AAAA,EACL;AAAA,EACA,OAAO;AAAA;",
|
|
8
|
-
"debugId": "C3D76F992F5A14AE64756E2164756E21",
|
|
9
|
-
"names": []
|
|
10
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/core/package-root.ts
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { dirname, join } from "pathe";
|
|
6
|
-
var findPackageRoot = (start) => {
|
|
7
|
-
let dir = start;
|
|
8
|
-
while (!existsSync(join(dir, "package.json"))) {
|
|
9
|
-
const parent = dirname(dir);
|
|
10
|
-
if (parent === dir) {
|
|
11
|
-
throw new Error("blume: unable to locate the package root");
|
|
12
|
-
}
|
|
13
|
-
dir = parent;
|
|
14
|
-
}
|
|
15
|
-
return dir;
|
|
16
|
-
};
|
|
17
|
-
var cached;
|
|
18
|
-
var packageRoot = () => {
|
|
19
|
-
cached ??= findPackageRoot(import.meta.dirname);
|
|
20
|
-
return cached;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export { packageRoot };
|
|
24
|
-
|
|
25
|
-
//# debugId=CE30053977BCD83D64756E2164756E21
|
|
26
|
-
//# sourceMappingURL=chunk-6kzzpsx8.js.map
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/core/package-root.ts"],
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"import { existsSync } from \"node:fs\";\n\nimport { dirname, join } from \"pathe\";\n\n/**\n * Walk up from `start` until a directory containing a `package.json` is found.\n *\n * Exported for testing; call sites should use {@link packageRoot}.\n */\nexport const findPackageRoot = (start: string): string => {\n let dir = start;\n while (!existsSync(join(dir, \"package.json\"))) {\n const parent = dirname(dir);\n if (parent === dir) {\n throw new Error(\"blume: unable to locate the package root\");\n }\n dir = parent;\n }\n return dir;\n};\n\nlet cached: string | undefined;\n\n/**\n * Absolute path to the installed Blume package root (the directory holding its\n * `package.json`), found by walking up from this module.\n *\n * Anchoring here — rather than at a fixed offset from `import.meta` — keeps the\n * package's own `src/`, assets, and `node_modules` locatable whether the code\n * runs from source under Bun (`src/...`) or from the published, bundled CLI\n * (`dist/cli/*.js`). The two layouts sit at different depths, so a relative\n * `../..` resolves to different places; locating `package.json` does not.\n */\nexport const packageRoot = (): string => {\n cached ??= findPackageRoot(import.meta.dirname);\n return cached;\n};\n"
|
|
6
|
-
],
|
|
7
|
-
"mappings": ";;;AAAA;AAEA;AAOO,IAAM,kBAAkB,CAAC,UAA0B;AAAA,EACxD,IAAI,MAAM;AAAA,EACV,OAAO,CAAC,WAAW,KAAK,KAAK,cAAc,CAAC,GAAG;AAAA,IAC7C,MAAM,SAAS,QAAQ,GAAG;AAAA,IAC1B,IAAI,WAAW,KAAK;AAAA,MAClB,MAAM,IAAI,MAAM,0CAA0C;AAAA,IAC5D;AAAA,IACA,MAAM;AAAA,EACR;AAAA,EACA,OAAO;AAAA;AAGT,IAAI;AAYG,IAAM,cAAc,MAAc;AAAA,EACvC,WAAW,gBAAgB,YAAY,OAAO;AAAA,EAC9C,OAAO;AAAA;",
|
|
8
|
-
"debugId": "CE30053977BCD83D64756E2164756E21",
|
|
9
|
-
"names": []
|
|
10
|
-
}
|