blume 2.1.1 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/cli/{chunk-ncj7w596.js → chunk-0pmb4tr4.js} +22 -18
- package/dist/cli/{chunk-ncj7w596.js.map → chunk-0pmb4tr4.js.map} +3 -3
- package/dist/cli/{chunk-nq1d20w2.js → chunk-0w6hs479.js} +15 -15
- package/dist/cli/{chunk-ehweg4fr.js → chunk-124fvmzq.js} +1031 -903
- package/dist/cli/{chunk-ehweg4fr.js.map → chunk-124fvmzq.js.map} +20 -18
- package/dist/cli/{chunk-kma9f6sf.js → chunk-1f882900.js} +2 -2
- package/dist/cli/{chunk-e8xw9qkh.js → chunk-297ajdcv.js} +4 -4
- package/dist/cli/{chunk-e6atmn4h.js → chunk-32khwcb6.js} +7 -7
- package/dist/cli/{chunk-073rdvgb.js → chunk-36bxs9c7.js} +16 -16
- package/dist/cli/{chunk-mrcccgnh.js → chunk-53jqw8z0.js} +3 -3
- package/dist/cli/{chunk-ft9vfys6.js → chunk-58bt3hag.js} +4 -4
- package/dist/cli/{chunk-sds923fa.js → chunk-5cmts6dv.js} +3 -3
- package/dist/cli/{chunk-cfyg47zg.js → chunk-60hz265r.js} +4 -4
- package/dist/cli/{chunk-j134vrp2.js → chunk-74wfhv79.js} +1 -1
- package/dist/cli/{chunk-nrns10mj.js → chunk-9qzgg6mj.js} +3 -3
- package/dist/cli/{chunk-tphac74f.js → chunk-a57yh0h9.js} +5 -5
- package/dist/cli/{chunk-hebn0tpx.js → chunk-ave1wcbr.js} +1 -1
- package/dist/cli/{chunk-htrwgabr.js → chunk-bqxaqn5p.js} +6 -6
- package/dist/cli/{chunk-5vjtgs6e.js → chunk-bxwhtvw8.js} +5 -5
- package/dist/cli/{chunk-66w9j7p6.js → chunk-ckrcm08h.js} +5 -3
- package/dist/cli/{chunk-66w9j7p6.js.map → chunk-ckrcm08h.js.map} +3 -3
- package/dist/cli/{chunk-n0v7m8ry.js → chunk-desvm72z.js} +3 -3
- package/dist/cli/{chunk-3v3ffbrj.js → chunk-dhwtqnnd.js} +2 -2
- package/dist/cli/{chunk-gb420vvz.js → chunk-dpy8qdjf.js} +7 -4
- package/dist/cli/chunk-dpy8qdjf.js.map +17 -0
- package/dist/cli/{chunk-c1y538kd.js → chunk-dqhg54mr.js} +1 -1
- package/dist/cli/{chunk-42t4jxdx.js → chunk-dv2z0rzf.js} +1 -1
- package/dist/cli/{chunk-cm89scdv.js → chunk-es8h0v3j.js} +9 -11
- package/dist/cli/{chunk-cm89scdv.js.map → chunk-es8h0v3j.js.map} +3 -3
- package/dist/cli/{chunk-hx3xqnk9.js → chunk-gfc5fqvf.js} +14 -14
- package/dist/cli/{chunk-7ppf1rva.js → chunk-gxtg6ed2.js} +16 -16
- package/dist/cli/{chunk-8y2kmjv8.js → chunk-hvcxng5f.js} +6 -6
- package/dist/cli/{chunk-54ztw1nj.js → chunk-j1x92mx9.js} +1 -1
- package/dist/cli/{chunk-313vnn40.js → chunk-k3gr1nm9.js} +11 -9
- package/dist/cli/chunk-k3gr1nm9.js.map +10 -0
- package/dist/cli/{chunk-c887fj0s.js → chunk-kdpzw38a.js} +2 -2
- package/dist/cli/{chunk-t2m5j4pr.js → chunk-ms9z66nz.js} +2 -2
- package/dist/cli/{chunk-t2m5j4pr.js.map → chunk-ms9z66nz.js.map} +2 -2
- package/dist/cli/{chunk-ep6nd0rn.js → chunk-nbyvgnnm.js} +26 -14
- package/dist/cli/{chunk-ep6nd0rn.js.map → chunk-nbyvgnnm.js.map} +7 -7
- package/dist/cli/{chunk-w1r4vase.js → chunk-s0bdnc7v.js} +2 -2
- package/dist/cli/{chunk-fdrkhpjy.js → chunk-sc93apfs.js} +14 -14
- package/dist/cli/{chunk-dzwehpy7.js → chunk-tfycddbt.js} +2 -2
- package/dist/cli/{chunk-3x6x3pph.js → chunk-twy7psdz.js} +7 -7
- package/dist/cli/{chunk-e9q7a2ak.js → chunk-y4msrwm9.js} +15 -15
- package/dist/cli/{chunk-tdqhyxfz.js → chunk-ydp3065p.js} +5 -5
- package/dist/cli/index.js +18 -18
- package/dist/types/ai/ask.d.ts +24 -5
- package/dist/types/analytics/clics.d.ts +37 -0
- package/dist/types/analytics/index.d.ts +2 -0
- package/dist/types/analytics/schema.d.ts +18 -0
- package/dist/types/astro/integration.d.ts +6 -0
- package/dist/types/core/config-input.d.ts +7 -3
- package/dist/types/core/powered-by.d.ts +12 -0
- package/dist/types/core/schema.d.ts +109 -3
- package/dist/types/core/sources/lower.d.ts +10 -2
- package/dist/types/core/sources/notion.d.ts +8 -0
- package/dist/types/deploy/cloudflare-negotiation.d.ts +5 -2
- package/dist/types/deploy/node-headers.d.ts +12 -4
- package/dist/types/deploy/platforms/vercel.d.ts +2 -2
- package/dist/types/deploy/vercel-negotiation.d.ts +4 -2
- package/dist/types/narration/build.d.ts +34 -12
- package/dist/types/narration/provider.d.ts +96 -8
- package/dist/types/openapi/sentence.d.ts +11 -1
- package/dist/types/seo/jsonld.d.ts +11 -6
- package/dist/types/sources/notion.d.ts +8 -0
- package/dist/types/sources/registry.d.ts +9 -0
- package/docs/02-deployment.mdx +29 -1
- package/docs/03-upgrading.mdx +2 -1
- package/docs/04-migrating.mdx +9 -9
- package/docs/08-faq.mdx +12 -12
- package/docs/advanced/changelog.mdx +19 -2
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/cli/doctor.mdx +1 -1
- package/docs/configuration/analytics.mdx +19 -1
- package/docs/configuration/index.mdx +1 -0
- package/docs/configuration/narration.mdx +43 -6
- package/docs/content/navigation.mdx +4 -2
- package/docs/content/sources/contentful.mdx +2 -0
- package/docs/content/sources/github-releases.mdx +2 -0
- package/docs/content/sources/notion.mdx +16 -1
- package/docs/content/sources/obsidian.mdx +2 -0
- package/docs/content/sources/payload.mdx +2 -0
- package/docs/content/sources/remote-mdx.mdx +2 -0
- package/docs/content/sources/sanity.mdx +2 -0
- package/docs/content/sources/strapi.mdx +2 -0
- package/docs/discoverability/mcp.mdx +2 -0
- package/docs/discoverability/structured-data.mdx +8 -6
- package/docs/references/asyncapi.mdx +2 -0
- package/docs/references/graphql.mdx +2 -0
- package/docs/references/openapi.mdx +14 -0
- package/package.json +1 -1
- package/skills/blume/SKILL.md +1 -1
- package/skills/blume-migrate/SKILL.md +1 -1
- package/skills/blume-migrate/assets/{oxfmt@0.67.0.patch → oxfmt@0.71.0.patch} +7 -7
- package/skills/blume-migrate/references/monorepo.md +5 -5
- package/src/ai/ask.ts +24 -6
- package/src/analytics/clics.ts +58 -0
- package/src/analytics/head.ts +5 -0
- package/src/analytics/index.ts +2 -0
- package/src/analytics/schema.ts +2 -0
- package/src/astro/generate.ts +13 -8
- package/src/astro/integration.ts +56 -9
- package/src/astro/render-deps.ts +10 -5
- package/src/astro/runtime-deps.ts +23 -2
- package/src/astro/templates.ts +4 -1
- package/src/cli/init/scaffold.ts +6 -3
- package/src/cli/required-secrets.ts +2 -1
- package/src/components/content/view-switcher.ts +7 -4
- package/src/components/layout/NavTree.astro +49 -5
- package/src/components/layout/NavTreeScript.astro +18 -0
- package/src/components/layout/PageLayout.astro +13 -3
- package/src/components/layout/RootLayout.astro +17 -12
- package/src/components/layout/analytics-client.ts +8 -8
- package/src/components/openapi/playground-client.ts +53 -53
- package/src/components/powered-by-middleware.ts +33 -0
- package/src/core/config-input.ts +11 -3
- package/src/core/powered-by.ts +12 -0
- package/src/core/schema.ts +5 -4
- package/src/core/sources/lower.ts +19 -3
- package/src/core/sources/notion.ts +27 -9
- package/src/deploy/cloudflare-negotiation.ts +36 -7
- package/src/deploy/headers.ts +15 -1
- package/src/deploy/node-headers.ts +32 -20
- package/src/deploy/platforms/cloudflare.ts +1 -0
- package/src/deploy/platforms/vercel.ts +4 -3
- package/src/deploy/vercel-negotiation.ts +38 -4
- package/src/markdown/base-links.ts +15 -14
- package/src/markdown/package-commands.ts +9 -8
- package/src/narration/build.ts +218 -40
- package/src/narration/provider.ts +89 -9
- package/src/openapi/render-mdx.ts +68 -8
- package/src/openapi/sentence.ts +29 -2
- package/src/registry/eject.ts +5 -1
- package/src/seo/jsonld.ts +38 -20
- package/src/sources/notion.ts +7 -0
- package/src/theme/color.ts +9 -10
- package/src/theme/entry.ts +14 -3
- package/dist/cli/chunk-313vnn40.js.map +0 -10
- package/dist/cli/chunk-gb420vvz.js.map +0 -17
- /package/dist/cli/{chunk-nq1d20w2.js.map → chunk-0w6hs479.js.map} +0 -0
- /package/dist/cli/{chunk-kma9f6sf.js.map → chunk-1f882900.js.map} +0 -0
- /package/dist/cli/{chunk-e8xw9qkh.js.map → chunk-297ajdcv.js.map} +0 -0
- /package/dist/cli/{chunk-e6atmn4h.js.map → chunk-32khwcb6.js.map} +0 -0
- /package/dist/cli/{chunk-073rdvgb.js.map → chunk-36bxs9c7.js.map} +0 -0
- /package/dist/cli/{chunk-mrcccgnh.js.map → chunk-53jqw8z0.js.map} +0 -0
- /package/dist/cli/{chunk-ft9vfys6.js.map → chunk-58bt3hag.js.map} +0 -0
- /package/dist/cli/{chunk-sds923fa.js.map → chunk-5cmts6dv.js.map} +0 -0
- /package/dist/cli/{chunk-cfyg47zg.js.map → chunk-60hz265r.js.map} +0 -0
- /package/dist/cli/{chunk-j134vrp2.js.map → chunk-74wfhv79.js.map} +0 -0
- /package/dist/cli/{chunk-nrns10mj.js.map → chunk-9qzgg6mj.js.map} +0 -0
- /package/dist/cli/{chunk-tphac74f.js.map → chunk-a57yh0h9.js.map} +0 -0
- /package/dist/cli/{chunk-hebn0tpx.js.map → chunk-ave1wcbr.js.map} +0 -0
- /package/dist/cli/{chunk-htrwgabr.js.map → chunk-bqxaqn5p.js.map} +0 -0
- /package/dist/cli/{chunk-5vjtgs6e.js.map → chunk-bxwhtvw8.js.map} +0 -0
- /package/dist/cli/{chunk-n0v7m8ry.js.map → chunk-desvm72z.js.map} +0 -0
- /package/dist/cli/{chunk-3v3ffbrj.js.map → chunk-dhwtqnnd.js.map} +0 -0
- /package/dist/cli/{chunk-c1y538kd.js.map → chunk-dqhg54mr.js.map} +0 -0
- /package/dist/cli/{chunk-42t4jxdx.js.map → chunk-dv2z0rzf.js.map} +0 -0
- /package/dist/cli/{chunk-hx3xqnk9.js.map → chunk-gfc5fqvf.js.map} +0 -0
- /package/dist/cli/{chunk-7ppf1rva.js.map → chunk-gxtg6ed2.js.map} +0 -0
- /package/dist/cli/{chunk-8y2kmjv8.js.map → chunk-hvcxng5f.js.map} +0 -0
- /package/dist/cli/{chunk-54ztw1nj.js.map → chunk-j1x92mx9.js.map} +0 -0
- /package/dist/cli/{chunk-c887fj0s.js.map → chunk-kdpzw38a.js.map} +0 -0
- /package/dist/cli/{chunk-w1r4vase.js.map → chunk-s0bdnc7v.js.map} +0 -0
- /package/dist/cli/{chunk-fdrkhpjy.js.map → chunk-sc93apfs.js.map} +0 -0
- /package/dist/cli/{chunk-dzwehpy7.js.map → chunk-tfycddbt.js.map} +0 -0
- /package/dist/cli/{chunk-3x6x3pph.js.map → chunk-twy7psdz.js.map} +0 -0
- /package/dist/cli/{chunk-e9q7a2ak.js.map → chunk-y4msrwm9.js.map} +0 -0
- /package/dist/cli/{chunk-tdqhyxfz.js.map → chunk-ydp3065p.js.map} +0 -0
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
image,
|
|
23
23
|
linkParts,
|
|
24
24
|
renderInline,
|
|
25
|
+
renderInlineMdx,
|
|
25
26
|
} from "./lower.ts";
|
|
26
27
|
import { slugify, slugifyPath } from "./normalize.ts";
|
|
27
28
|
import { missingSecretError } from "./remote.ts";
|
|
@@ -143,6 +144,14 @@ export interface NotionSourceOptions {
|
|
|
143
144
|
* Status/select property are always imported as published.
|
|
144
145
|
*/
|
|
145
146
|
publishedValue?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Read the text of text blocks (paragraphs, headings, list items, quotes,
|
|
149
|
+
* callouts) as MDX source, so a component, expression, or Markdown typed in
|
|
150
|
+
* Notion renders instead of showing as written. MDX runs code at build
|
|
151
|
+
* time, so this hands everyone who can edit the database the site's build:
|
|
152
|
+
* set it only when you trust them with that. Default false.
|
|
153
|
+
*/
|
|
154
|
+
mdx?: boolean;
|
|
146
155
|
/** Opt-in dev polling interval (seconds); omit to freeze for the session. */
|
|
147
156
|
pollInterval?: number;
|
|
148
157
|
/** Injected for tests; otherwise built from `@notionhq/client`. */
|
|
@@ -174,9 +183,10 @@ interface LinkedRuns {
|
|
|
174
183
|
* itself instead of opening a JSX expression, a tag, or emphasis, a code run
|
|
175
184
|
* keeps its text verbatim inside a long-enough code span, neighbors share
|
|
176
185
|
* their marks' delimiters, and neighbors that link to one `href` share one
|
|
177
|
-
* link.
|
|
186
|
+
* link. With `mdx`, the text goes in as typed instead (see
|
|
187
|
+
* `NotionSourceOptions.mdx`).
|
|
178
188
|
*/
|
|
179
|
-
const richToMarkdown = (rich: NotionRichText[] = []): string => {
|
|
189
|
+
const richToMarkdown = (rich: NotionRichText[] = [], mdx = false): string => {
|
|
180
190
|
const linked: LinkedRuns[] = [];
|
|
181
191
|
for (const { annotations = {}, href, plain_text: text } of rich) {
|
|
182
192
|
const run: InlineRun = {
|
|
@@ -195,11 +205,18 @@ const richToMarkdown = (rich: NotionRichText[] = []): string => {
|
|
|
195
205
|
linked.push({ href: href ?? undefined, runs: [run] });
|
|
196
206
|
}
|
|
197
207
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
208
|
+
const parts = linked.flatMap(({ href, runs }) => linkParts(runs, href, mdx));
|
|
209
|
+
return mdx ? renderInlineMdx(parts) : renderInline(parts);
|
|
201
210
|
};
|
|
202
211
|
|
|
212
|
+
/**
|
|
213
|
+
* A text block's rich text: Markdown that renders as written, guarded at its
|
|
214
|
+
* block start — or, with `mdx`, MDX source as typed, which an `.mdx` file's
|
|
215
|
+
* own block starts (`import`, `#`, indentation) read the same way.
|
|
216
|
+
*/
|
|
217
|
+
const blockText = (rich: NotionRichText[] | undefined, mdx: boolean): string =>
|
|
218
|
+
mdx ? richToMarkdown(rich, true) : guardBlockStart(richToMarkdown(rich));
|
|
219
|
+
|
|
203
220
|
const isBlockPayload = (
|
|
204
221
|
value: NotionBlockPayload | boolean | string | undefined
|
|
205
222
|
): value is NotionBlockPayload => typeof value === "object";
|
|
@@ -340,9 +357,9 @@ const renderVideo = (data: NotionBlockPayload): string => {
|
|
|
340
357
|
};
|
|
341
358
|
|
|
342
359
|
/** Render a leaf (non-container) block to Markdown, or null for containers. */
|
|
343
|
-
const renderLeaf = (block: NotionBlock): string | null => {
|
|
360
|
+
const renderLeaf = (block: NotionBlock, mdx: boolean): string | null => {
|
|
344
361
|
const data = payloadOf(block) ?? {};
|
|
345
|
-
const text =
|
|
362
|
+
const text = blockText(blockField(block), mdx);
|
|
346
363
|
switch (block.type) {
|
|
347
364
|
case "paragraph": {
|
|
348
365
|
return text;
|
|
@@ -398,6 +415,7 @@ export const notionSource = (
|
|
|
398
415
|
ctx?: SourceContext
|
|
399
416
|
): ContentSource => {
|
|
400
417
|
const props = options.properties ?? {};
|
|
418
|
+
const textAsMdx = options.mdx ?? false;
|
|
401
419
|
// A FIFO semaphore: at most N calls run at once, the rest queue. Notion's
|
|
402
420
|
// rate limit is per-integration (an average of 3 req/s), and a large
|
|
403
421
|
// database fans out one block-children request per page plus one per nested
|
|
@@ -481,7 +499,7 @@ export const notionSource = (
|
|
|
481
499
|
|
|
482
500
|
if (block.type === "callout") {
|
|
483
501
|
const body = [
|
|
484
|
-
|
|
502
|
+
blockText(blockField(block), textAsMdx),
|
|
485
503
|
await children(block),
|
|
486
504
|
]
|
|
487
505
|
.filter(Boolean)
|
|
@@ -509,7 +527,7 @@ export const notionSource = (
|
|
|
509
527
|
): Promise<string> => {
|
|
510
528
|
const parts = await Promise.all(
|
|
511
529
|
blocks.map(async (block) => {
|
|
512
|
-
const leaf = renderLeaf(block);
|
|
530
|
+
const leaf = renderLeaf(block, textAsMdx);
|
|
513
531
|
if (leaf === null) {
|
|
514
532
|
return renderContainer(client, block, renderBlocks);
|
|
515
533
|
}
|
|
@@ -40,8 +40,9 @@
|
|
|
40
40
|
* Cloudflare does not apply `_headers` to worker-first routes, so the wrapper
|
|
41
41
|
* also re-stamps what the static layer would otherwise add on the routes it
|
|
42
42
|
* takes over: the homepage agent-discovery `Link` header, the Markdown
|
|
43
|
-
* `charset=utf-8`,
|
|
44
|
-
*
|
|
43
|
+
* `charset=utf-8`, the sandbox on the SVGs a content source downloaded (see
|
|
44
|
+
* `deploy/headers.ts`), and `X-Powered-By` unless `poweredBy` is `false`. The
|
|
45
|
+
* raw `.md`/`.mdx` URLs are
|
|
45
46
|
* exempted from worker-first routing with negative rules, keeping their
|
|
46
47
|
* `_headers` treatment and their zero-Worker serving path.
|
|
47
48
|
*
|
|
@@ -85,6 +86,7 @@ import {
|
|
|
85
86
|
OPENAPI_PATH,
|
|
86
87
|
} from "../ai/api/paths.ts";
|
|
87
88
|
import { normalizeBasePath, normalizePath } from "../core/base-path.ts";
|
|
89
|
+
import { POWERED_BY_HEADERS } from "../core/powered-by.ts";
|
|
88
90
|
import { compileRedirects, isPatternPath } from "../core/redirect-patterns.ts";
|
|
89
91
|
import { CONTENT_ASSETS_ROOT, SVG_ASSET_HEADERS } from "./headers.ts";
|
|
90
92
|
import type { NotFoundVariants } from "./vercel-negotiation.ts";
|
|
@@ -244,6 +246,8 @@ export interface NegotiationWorkerOptions {
|
|
|
244
246
|
base?: string;
|
|
245
247
|
/** Homepage agent-discovery `Link` header (see `ai/link-headers.ts`). */
|
|
246
248
|
homeLinkHeader?: string | null;
|
|
249
|
+
/** Whether responses identify Blume with `X-Powered-By`. Defaults to `true`. */
|
|
250
|
+
poweredBy?: boolean;
|
|
247
251
|
/** Estimated token count of the homepage Markdown mirror. */
|
|
248
252
|
homeTokens?: number;
|
|
249
253
|
/** Configured redirects the wrapper answers with their exact status. */
|
|
@@ -277,6 +281,9 @@ export const buildNegotiationWorker = (
|
|
|
277
281
|
const binding = JSON.stringify(options.assetsBinding);
|
|
278
282
|
const prefix = JSON.stringify(encodeURI(normalizeBasePath(options.base)));
|
|
279
283
|
const homeLinkHeader = JSON.stringify(options.homeLinkHeader ?? null);
|
|
284
|
+
const poweredBy = JSON.stringify(
|
|
285
|
+
options.poweredBy === false ? {} : POWERED_BY_HEADERS
|
|
286
|
+
);
|
|
280
287
|
const homeTokens = JSON.stringify(
|
|
281
288
|
options.homeTokens === undefined ? null : String(options.homeTokens)
|
|
282
289
|
);
|
|
@@ -331,6 +338,7 @@ const PATTERN_REDIRECTS = ${patternRedirects}.map(
|
|
|
331
338
|
);
|
|
332
339
|
const NOT_FOUND = ${notFound};
|
|
333
340
|
const SVG_ASSET_HEADERS = ${JSON.stringify(SVG_ASSET_HEADERS)};
|
|
341
|
+
const POWERED_BY = ${poweredBy};
|
|
334
342
|
|
|
335
343
|
// An SVG a content source downloaded: a document that could run script as the
|
|
336
344
|
// docs site if opened directly, so it goes out sandboxed. Matched on the
|
|
@@ -542,12 +550,30 @@ const isHomePath = (pathname) => {
|
|
|
542
550
|
return rest === "" || rest === "/";
|
|
543
551
|
};
|
|
544
552
|
|
|
553
|
+
// Blume's X-Powered-By, on every answer the wrapper gives unless the
|
|
554
|
+
// response already names its own (the project's middleware, say).
|
|
555
|
+
const identify = (headers) => {
|
|
556
|
+
for (const [name, value] of Object.entries(POWERED_BY)) {
|
|
557
|
+
if (!headers.has(name)) {
|
|
558
|
+
headers.set(name, value);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
545
563
|
const withHeaders = (response, apply) => {
|
|
546
564
|
const patched = new Response(response.body, response);
|
|
547
565
|
apply(patched.headers);
|
|
566
|
+
identify(patched.headers);
|
|
548
567
|
return patched;
|
|
549
568
|
};
|
|
550
569
|
|
|
570
|
+
// A response that already carries the header (Blume's runtime middleware sets
|
|
571
|
+
// it on every page Astro renders) goes out as it is.
|
|
572
|
+
const withPoweredBy = (response) =>
|
|
573
|
+
Object.keys(POWERED_BY).every((name) => response.headers.has(name))
|
|
574
|
+
? response
|
|
575
|
+
: withHeaders(response, () => {});
|
|
576
|
+
|
|
551
577
|
export default {
|
|
552
578
|
async fetch(request, env, context) {
|
|
553
579
|
const url = new URL(request.url);
|
|
@@ -556,7 +582,10 @@ export default {
|
|
|
556
582
|
const redirect = redirectFor(url.pathname);
|
|
557
583
|
if (redirect !== null) {
|
|
558
584
|
return new Response(null, {
|
|
559
|
-
headers: {
|
|
585
|
+
headers: {
|
|
586
|
+
...POWERED_BY,
|
|
587
|
+
location: redirectLocation(redirect[0], url.search),
|
|
588
|
+
},
|
|
560
589
|
status: redirect[1],
|
|
561
590
|
});
|
|
562
591
|
}
|
|
@@ -568,7 +597,7 @@ export default {
|
|
|
568
597
|
});
|
|
569
598
|
}
|
|
570
599
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
571
|
-
return server.fetch(request, env, context);
|
|
600
|
+
return withPoweredBy(await server.fetch(request, env, context));
|
|
572
601
|
}
|
|
573
602
|
const assets = env[ASSETS_BINDING];
|
|
574
603
|
// The request goes through untouched, so a conditional revalidation
|
|
@@ -577,7 +606,7 @@ export default {
|
|
|
577
606
|
if (assets !== undefined && isPageJson(url.pathname)) {
|
|
578
607
|
const asset = await assets.fetch(request);
|
|
579
608
|
if (asset.status !== 404) {
|
|
580
|
-
return asset;
|
|
609
|
+
return withPoweredBy(asset);
|
|
581
610
|
}
|
|
582
611
|
}
|
|
583
612
|
const variant = markdownVariantUrl(url.pathname + url.search);
|
|
@@ -608,10 +637,10 @@ export default {
|
|
|
608
637
|
const response = await server.fetch(request, env, context);
|
|
609
638
|
const twin = await notFoundTwin(request, url, response, assets);
|
|
610
639
|
if (twin !== null) {
|
|
611
|
-
return twin;
|
|
640
|
+
return withPoweredBy(twin);
|
|
612
641
|
}
|
|
613
642
|
if (variant === null && !(home && HOME_LINK_HEADER !== null)) {
|
|
614
|
-
return response;
|
|
643
|
+
return withPoweredBy(response);
|
|
615
644
|
}
|
|
616
645
|
return withHeaders(response, (headers) => {
|
|
617
646
|
if (variant !== null) {
|
package/src/deploy/headers.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
SIGNATURES_DIRECTORY_TYPE,
|
|
11
11
|
} from "../ai/web-bot-auth.ts";
|
|
12
12
|
import { normalizeBasePath } from "../core/base-path.ts";
|
|
13
|
+
import { POWERED_BY_HEADERS } from "../core/powered-by.ts";
|
|
13
14
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -98,6 +99,13 @@ export const headerRules = (
|
|
|
98
99
|
path: `${deployBase}/*.${rule.ext}`,
|
|
99
100
|
value: rule.contentType,
|
|
100
101
|
}));
|
|
102
|
+
// One rule for the whole site: `/*` matches the homepage too, and a host
|
|
103
|
+
// that joins the values of overlapping rules would send a second one twice.
|
|
104
|
+
if (config.poweredBy) {
|
|
105
|
+
for (const [name, value] of Object.entries(POWERED_BY_HEADERS)) {
|
|
106
|
+
rules.push({ name, path: `${deployBase}/*`, value });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
101
109
|
if (homeLinkHeader) {
|
|
102
110
|
rules.push({ name: "Link", path: `${deployBase}/`, value: homeLinkHeader });
|
|
103
111
|
}
|
|
@@ -212,9 +220,15 @@ export const escapeVercelSource = (path: string): string =>
|
|
|
212
220
|
* A `_headers` path as a `vercel.json` `source`, which `path-to-regexp` reads:
|
|
213
221
|
* the literal parts are escaped, and a `*` glob becomes the `(.*)` group that
|
|
214
222
|
* spans path segments as the glob does. An exact directory path (the homepage
|
|
215
|
-
* rule's `/docs/`) drops its trailing slash, the URL Vercel serves it at.
|
|
223
|
+
* rule's `/docs/`) drops its trailing slash, the URL Vercel serves it at. A
|
|
224
|
+
* whole-directory glob (`/docs/*`) becomes an optional group,
|
|
225
|
+
* `/docs/:path(.*)?`, so it matches that bare URL as well as every path
|
|
226
|
+
* beneath it.
|
|
216
227
|
*/
|
|
217
228
|
const vercelSource = (path: string): string => {
|
|
229
|
+
if (path.endsWith("/*")) {
|
|
230
|
+
return `${escapeVercelSource(path.slice(0, -2))}/:path(.*)?`;
|
|
231
|
+
}
|
|
218
232
|
const trimmed =
|
|
219
233
|
path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
220
234
|
return trimmed.split("*").map(escapeVercelSource).join("(.*)");
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
SIGNATURES_DIRECTORY_TYPE,
|
|
15
15
|
} from "../ai/web-bot-auth.ts";
|
|
16
16
|
import { normalizeBasePath, normalizePath } from "../core/base-path.ts";
|
|
17
|
+
import { POWERED_BY_HEADERS } from "../core/powered-by.ts";
|
|
17
18
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
18
19
|
import { compileRedirects, isPatternPath } from "../core/redirect-patterns.ts";
|
|
19
20
|
import type { CompiledRedirect } from "../core/redirect-patterns.ts";
|
|
@@ -159,6 +160,8 @@ export const nodePatternRedirects = (
|
|
|
159
160
|
export interface NodeEntryOptions {
|
|
160
161
|
/** `deployment.base`, normalized (`""` at the root). */
|
|
161
162
|
base?: string;
|
|
163
|
+
/** Whether responses identify Blume with `X-Powered-By`. Defaults to `true`. */
|
|
164
|
+
poweredBy?: boolean;
|
|
162
165
|
/** The configured redirects, from {@link nodeRedirects}. */
|
|
163
166
|
redirects?: NodeRedirects;
|
|
164
167
|
/** The configured pattern redirects, from {@link nodePatternRedirects}. */
|
|
@@ -169,7 +172,12 @@ export interface NodeEntryOptions {
|
|
|
169
172
|
* The entry that replaces Astro's: it answers a configured redirect with its
|
|
170
173
|
* exact status, and otherwise stamps the rules' headers on the response and
|
|
171
174
|
* hands the request to Astro's handler. `send` leaves a Content-Type that is
|
|
172
|
-
* already set alone, so the registered media type wins.
|
|
175
|
+
* already set alone, so the registered media type wins. Unless `poweredBy` is
|
|
176
|
+
* `false`, every answer also names Blume in `X-Powered-By` — the redirects,
|
|
177
|
+
* and the prerendered pages and files the static handler serves, which
|
|
178
|
+
* Blume's runtime middleware never sees. A middleware-mode entry leaves that
|
|
179
|
+
* header to the middleware: Astro hands a request it has no route for back to
|
|
180
|
+
* the host app through `next()`, and that response is the host's own.
|
|
173
181
|
*
|
|
174
182
|
* A rule matches the file the static handler will serve, not the raw URL:
|
|
175
183
|
* Astro strips the base when it leads the path and serves the rest from the
|
|
@@ -194,10 +202,11 @@ export const nodeEntryWrapper = (
|
|
|
194
202
|
// This wrapper answers the configured redirects with their exact status and
|
|
195
203
|
// sets the headers the standalone server's static handler can't (the media
|
|
196
204
|
// types and CORS headers of the .well-known discovery files, the sandbox on
|
|
197
|
-
// downloaded SVGs), then hands every other request to Astro.
|
|
205
|
+
// downloaded SVGs, X-Powered-By), then hands every other request to Astro.
|
|
198
206
|
import { posix } from "node:path";
|
|
199
207
|
|
|
200
208
|
const BASE = ${JSON.stringify(options.base ?? "")};
|
|
209
|
+
const POWERED_BY = ${JSON.stringify(options.poweredBy === false ? {} : POWERED_BY_HEADERS)};
|
|
201
210
|
const RULES = new Map(${JSON.stringify(exactRules(rules))});
|
|
202
211
|
const PATTERNS = ${JSON.stringify(patternRules(rules))};
|
|
203
212
|
const REDIRECTS = ${JSON.stringify(options.redirects ?? {})};
|
|
@@ -221,20 +230,20 @@ const servedPath = (req) => {
|
|
|
221
230
|
: path;
|
|
222
231
|
return BASE + (rest || "/");
|
|
223
232
|
};
|
|
224
|
-
const applyHeaders = (req, res) => {
|
|
233
|
+
const applyHeaders = (req, res, identify) => {
|
|
225
234
|
const path = servedPath(req);
|
|
226
235
|
const lower = path.toLowerCase();
|
|
227
|
-
const headers =
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
const headers = {
|
|
237
|
+
...(identify ? POWERED_BY : {}),
|
|
238
|
+
...(RULES.get(path) ??
|
|
239
|
+
PATTERNS.find(
|
|
240
|
+
([prefix, suffix]) =>
|
|
241
|
+
lower.startsWith(prefix.toLowerCase()) &&
|
|
242
|
+
lower.endsWith(suffix.toLowerCase())
|
|
243
|
+
)?.[2]),
|
|
244
|
+
};
|
|
245
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
246
|
+
res.setHeader(name, value);
|
|
238
247
|
}
|
|
239
248
|
};
|
|
240
249
|
// A pattern redirect's Location: its template, each $n filled with that
|
|
@@ -268,7 +277,7 @@ const answerRedirect = (req, res) => {
|
|
|
268
277
|
return false;
|
|
269
278
|
}
|
|
270
279
|
const [location, status] = redirect;
|
|
271
|
-
res.writeHead(status, { location });
|
|
280
|
+
res.writeHead(status, { ...POWERED_BY, location });
|
|
272
281
|
res.end();
|
|
273
282
|
return true;
|
|
274
283
|
};
|
|
@@ -285,7 +294,7 @@ export const handler = (req, res, ...rest) => {
|
|
|
285
294
|
if (answerRedirect(req, res)) {
|
|
286
295
|
return;
|
|
287
296
|
}
|
|
288
|
-
applyHeaders(req, res);
|
|
297
|
+
applyHeaders(req, res, options.mode !== "middleware");
|
|
289
298
|
return astro.handler(req, res, ...rest);
|
|
290
299
|
};
|
|
291
300
|
export const startServer = () => {
|
|
@@ -301,7 +310,7 @@ export const startServer = () => {
|
|
|
301
310
|
if (answerRedirect(req, res)) {
|
|
302
311
|
return;
|
|
303
312
|
}
|
|
304
|
-
applyHeaders(req, res);
|
|
313
|
+
applyHeaders(req, res, true);
|
|
305
314
|
for (const listener of astroListeners) {
|
|
306
315
|
listener.call(httpServer, req, res);
|
|
307
316
|
}
|
|
@@ -315,9 +324,10 @@ if (options.mode === "standalone" && previous !== "disabled") {
|
|
|
315
324
|
`;
|
|
316
325
|
|
|
317
326
|
/**
|
|
318
|
-
* Put the wrapper in front of a Node server build's entry. A build with
|
|
319
|
-
* rule (no API catalog, AI catalog, MCP server,
|
|
320
|
-
* downloaded content assets) and no redirect leaves
|
|
327
|
+
* Put the wrapper in front of a Node server build's entry. A build with
|
|
328
|
+
* `poweredBy` off, no rule (no API catalog, AI catalog, MCP server,
|
|
329
|
+
* signatures directory, or downloaded content assets), and no redirect leaves
|
|
330
|
+
* Astro's entry alone.
|
|
321
331
|
*/
|
|
322
332
|
export const wrapNodeEntry = async (
|
|
323
333
|
project: BlumeProject,
|
|
@@ -341,6 +351,7 @@ export const wrapNodeEntry = async (
|
|
|
341
351
|
const redirects = nodeRedirects(project);
|
|
342
352
|
const patternRedirects = nodePatternRedirects(project);
|
|
343
353
|
if (
|
|
354
|
+
!project.config.poweredBy &&
|
|
344
355
|
rules.length === 0 &&
|
|
345
356
|
Object.keys(redirects).length === 0 &&
|
|
346
357
|
patternRedirects.length === 0
|
|
@@ -365,6 +376,7 @@ export const wrapNodeEntry = async (
|
|
|
365
376
|
nodeEntryWrapper(rules, {
|
|
366
377
|
base: normalizeBasePath(project.config.deployment.options.base),
|
|
367
378
|
patternRedirects,
|
|
379
|
+
poweredBy: project.config.poweredBy,
|
|
368
380
|
redirects,
|
|
369
381
|
}),
|
|
370
382
|
"utf-8"
|
|
@@ -122,6 +122,7 @@ export const emitCloudflareNegotiation = async (
|
|
|
122
122
|
)
|
|
123
123
|
.map((route) => pageJsonPath(route.path))
|
|
124
124
|
: [],
|
|
125
|
+
poweredBy: config.poweredBy,
|
|
125
126
|
// The wrapper Worker matches full served URLs, so the redirects are
|
|
126
127
|
// based the same way the platform files are — it answers any the
|
|
127
128
|
// worker-first rules claim, where `_redirects` is never consulted and
|
|
@@ -151,7 +151,8 @@ export const emitVercelNegotiation = async (
|
|
|
151
151
|
// Downloaded content assets are prerendered static files; only a build
|
|
152
152
|
// that has them needs the SVG sandbox route.
|
|
153
153
|
existsSync(join(builtDir, "blume-assets")),
|
|
154
|
-
base
|
|
154
|
+
base,
|
|
155
|
+
config.poweredBy
|
|
155
156
|
);
|
|
156
157
|
if (injected === null) {
|
|
157
158
|
log.warn(
|
|
@@ -269,8 +270,8 @@ export const rebaseVercelRoutes = async (
|
|
|
269
270
|
* hidden runtime silently drops the function's chunks and `node_modules`.
|
|
270
271
|
* Static assets are served from the tree's `static/` half, so the deploy
|
|
271
272
|
* artifacts are written there; headers (the discovery files', the sandbox on
|
|
272
|
-
* downloaded SVGs) arrive through the routing config rather
|
|
273
|
-
* `_headers` file, which Vercel never reads. The adapter ignores
|
|
273
|
+
* downloaded SVGs, `X-Powered-By`) arrive through the routing config rather
|
|
274
|
+
* than a `_headers` file, which Vercel never reads. The adapter ignores
|
|
274
275
|
* `deployment.base`, so under one the build moves the static files and the
|
|
275
276
|
* routes beneath it.
|
|
276
277
|
*/
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { mountBasePath } from "../core/base-path.ts";
|
|
24
|
+
import { POWERED_BY_HEADERS } from "../core/powered-by.ts";
|
|
24
25
|
import { SVG_ASSET_HEADERS } from "./headers.ts";
|
|
25
26
|
|
|
26
27
|
/*
|
|
@@ -374,6 +375,31 @@ const svgAssetRoute = (base: string): VercelRoute => ({
|
|
|
374
375
|
src: svgAssetSource(base),
|
|
375
376
|
});
|
|
376
377
|
|
|
378
|
+
/** The `src` of the `X-Powered-By` route: the base and every path beneath it. */
|
|
379
|
+
const poweredBySource = (base: string): string =>
|
|
380
|
+
`^${routePattern(base)}(?:/.*)?$`;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* A main-phase `continue` route naming Blume in `X-Powered-By` (see
|
|
384
|
+
* `core/powered-by.ts`) on every response: the prerendered pages and files
|
|
385
|
+
* the static layer serves, which Blume's runtime middleware never sees, the
|
|
386
|
+
* functions, and the redirects.
|
|
387
|
+
*/
|
|
388
|
+
const poweredByRoute = (base: string): VercelRoute => ({
|
|
389
|
+
continue: true,
|
|
390
|
+
headers: Object.fromEntries(
|
|
391
|
+
Object.entries(POWERED_BY_HEADERS).map(([name, value]) => [
|
|
392
|
+
name.toLowerCase(),
|
|
393
|
+
value,
|
|
394
|
+
])
|
|
395
|
+
),
|
|
396
|
+
src: poweredBySource(base),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
/** Whether a route is the {@link poweredByRoute}. */
|
|
400
|
+
const isPoweredByRoute = (route: VercelRoute, base: string): boolean =>
|
|
401
|
+
route.continue === true && route.src === poweredBySource(base);
|
|
402
|
+
|
|
377
403
|
/** Whether a route is a `corsRoute` — the same three-field shape test as the others. */
|
|
378
404
|
const isCorsRoute = (route: VercelRoute): boolean =>
|
|
379
405
|
route.continue === true &&
|
|
@@ -389,7 +415,8 @@ const isCorsRoute = (route: VercelRoute): boolean =>
|
|
|
389
415
|
* the one re-added); the homepage `Link` route by its three-field
|
|
390
416
|
* continue-with-link shape (the Build Output config is adapter-generated, so
|
|
391
417
|
* no user-authored route competes in this file); the Markdown 404 routes by
|
|
392
|
-
* their `/404.md` destination
|
|
418
|
+
* their `/404.md` destination; the SVG and `X-Powered-By` routes by their
|
|
419
|
+
* `src`.
|
|
393
420
|
*/
|
|
394
421
|
const isNegotiationRoute = (route: VercelRoute, base: string): boolean =>
|
|
395
422
|
route.has?.some((condition) => MARKDOWN_VALUES.has(condition.value)) ===
|
|
@@ -407,7 +434,8 @@ const isNegotiationRoute = (route: VercelRoute, base: string): boolean =>
|
|
|
407
434
|
route.src === homeSource(base) &&
|
|
408
435
|
Object.keys(route).length === 3) ||
|
|
409
436
|
isCorsRoute(route) ||
|
|
410
|
-
(route.continue === true && route.src === svgAssetSource(base))
|
|
437
|
+
(route.continue === true && route.src === svgAssetSource(base)) ||
|
|
438
|
+
isPoweredByRoute(route, base);
|
|
411
439
|
|
|
412
440
|
/**
|
|
413
441
|
* Splice the negotiation routes into a Build Output `config.json`, plus — when
|
|
@@ -426,7 +454,9 @@ const isNegotiationRoute = (route: VercelRoute, base: string): boolean =>
|
|
|
426
454
|
* phase right before the adapter's `/404.html` fallback — and nowhere when
|
|
427
455
|
* that fallback is absent, since a `dest` with no file behind it would serve
|
|
428
456
|
* nothing. With `svgAssets`, a main-phase route also sandboxes the SVGs a
|
|
429
|
-
* content source downloaded (see {@link svgAssetRoute}).
|
|
457
|
+
* content source downloaded (see {@link svgAssetRoute}). With `poweredBy`, a
|
|
458
|
+
* route at the head of the config, ahead of the adapter's redirects, names
|
|
459
|
+
* Blume on every response (see {@link poweredByRoute}). Under a `base`, every
|
|
430
460
|
* route matches and names paths beneath it, and the fallback anchor is the
|
|
431
461
|
* one {@link rebaseAdapterRoutes} moved there. Returns the updated JSON
|
|
432
462
|
* text (tab-indented, like the adapter's own output), or `null` when there is
|
|
@@ -442,7 +472,8 @@ export const injectNegotiationRoutes = (
|
|
|
442
472
|
notFound: NotFoundVariants = {},
|
|
443
473
|
corsPaths: readonly string[] = [],
|
|
444
474
|
svgAssets = false,
|
|
445
|
-
base = ""
|
|
475
|
+
base = "",
|
|
476
|
+
poweredBy = false
|
|
446
477
|
): string | null => {
|
|
447
478
|
const overrideEntries = Object.entries(contentTypeOverrides ?? {});
|
|
448
479
|
let config: {
|
|
@@ -491,6 +522,9 @@ export const injectNegotiationRoutes = (
|
|
|
491
522
|
// route then terminates (Markdown negotiation on the homepage) still carries
|
|
492
523
|
// the Link header.
|
|
493
524
|
routes.splice(filesystemIndex, 0, ...headerRoutes, ...rewriteRoutes);
|
|
525
|
+
if (poweredBy) {
|
|
526
|
+
routes.unshift(poweredByRoute(base));
|
|
527
|
+
}
|
|
494
528
|
const notFoundRoutes = [
|
|
495
529
|
...(notFound.markdown ? notFoundMarkdownRoutes(base) : []),
|
|
496
530
|
...(notFound.json ? notFoundJsonRoutes(base) : []),
|
|
@@ -24,6 +24,21 @@ interface MdastUrlContext {
|
|
|
24
24
|
const isUrl = (url: string | null | undefined): url is string =>
|
|
25
25
|
typeof url === "string";
|
|
26
26
|
|
|
27
|
+
/** Record `based(url)` on the node when its root-relative URL changes. */
|
|
28
|
+
const rebase = (
|
|
29
|
+
node: UrlNode,
|
|
30
|
+
ctx: MdastUrlContext,
|
|
31
|
+
based: (url: string) => string
|
|
32
|
+
): void => {
|
|
33
|
+
const { url } = node;
|
|
34
|
+
if (isUrl(url) && isInternalPath(url)) {
|
|
35
|
+
const next = based(url);
|
|
36
|
+
if (next !== url) {
|
|
37
|
+
ctx.setProperty(node, "url", next);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
27
42
|
export interface BaseLinksPluginOptions {
|
|
28
43
|
/**
|
|
29
44
|
* The `blume:data` JSON file, for an ejected app: with no CLI in the process
|
|
@@ -72,20 +87,6 @@ export const baseLinksPlugin = (
|
|
|
72
87
|
const servesPage = (route: string): boolean =>
|
|
73
88
|
servesRoute(servedRoutes(), route);
|
|
74
89
|
|
|
75
|
-
/** Record `based(url)` on the node when its root-relative URL changes. */
|
|
76
|
-
const rebase = (
|
|
77
|
-
node: UrlNode,
|
|
78
|
-
ctx: MdastUrlContext,
|
|
79
|
-
based: (url: string) => string
|
|
80
|
-
): void => {
|
|
81
|
-
const { url } = node;
|
|
82
|
-
if (isUrl(url) && isInternalPath(url)) {
|
|
83
|
-
const next = based(url);
|
|
84
|
-
if (next !== url) {
|
|
85
|
-
ctx.setProperty(node, "url", next);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
90
|
const rebaseLink = (node: UrlNode, ctx: MdastUrlContext): void =>
|
|
90
91
|
rebase(node, ctx, (url) =>
|
|
91
92
|
withAuthoredBasePath(deployBase, basePath, url, servesPage)
|
|
@@ -205,6 +205,9 @@ const buildCommand = (manager: PackageManager, intent: Intent): string => {
|
|
|
205
205
|
return words.join(" ");
|
|
206
206
|
};
|
|
207
207
|
|
|
208
|
+
const normalizeCommand = (command: string): string =>
|
|
209
|
+
command.replaceAll(WHITESPACE_RUN, " ").trim();
|
|
210
|
+
|
|
208
211
|
/**
|
|
209
212
|
* Convert an install command into the equivalent for every supported package
|
|
210
213
|
* manager. Accepts a bare package list (`react`) or a full command
|
|
@@ -212,14 +215,12 @@ const buildCommand = (manager: PackageManager, intent: Intent): string => {
|
|
|
212
215
|
*/
|
|
213
216
|
export const toPackageCommands = (input: string) => {
|
|
214
217
|
const intent = parseIntent(input);
|
|
215
|
-
const normalize = (command: string): string =>
|
|
216
|
-
command.replaceAll(WHITESPACE_RUN, " ").trim();
|
|
217
218
|
return {
|
|
218
|
-
aube:
|
|
219
|
-
bun:
|
|
220
|
-
npm:
|
|
221
|
-
nub:
|
|
222
|
-
pnpm:
|
|
223
|
-
yarn:
|
|
219
|
+
aube: normalizeCommand(buildCommand("aube", intent)),
|
|
220
|
+
bun: normalizeCommand(buildCommand("bun", intent)),
|
|
221
|
+
npm: normalizeCommand(buildCommand("npm", intent)),
|
|
222
|
+
nub: normalizeCommand(buildCommand("nub", intent)),
|
|
223
|
+
pnpm: normalizeCommand(buildCommand("pnpm", intent)),
|
|
224
|
+
yarn: normalizeCommand(buildCommand("yarn", intent)),
|
|
224
225
|
} satisfies Record<PackageManager, string>;
|
|
225
226
|
};
|