blume 2.0.2 → 2.0.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/CHANGELOG.md +65 -0
- package/dist/cli/{chunk-6k4ftwze.js → chunk-1d7ve1dm.js} +1 -1
- package/dist/cli/{chunk-zp79m0ts.js → chunk-2eytanqx.js} +2 -2
- package/dist/cli/{chunk-8g8ytmgx.js → chunk-2hsdwb9n.js} +19 -19
- package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-2hsdwb9n.js.map} +1 -1
- package/dist/cli/{chunk-g698a744.js → chunk-2z928egk.js} +5 -5
- package/dist/cli/{chunk-ppzjqwx2.js → chunk-35d4wj9f.js} +17 -18
- package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-35d4wj9f.js.map} +3 -3
- package/dist/cli/{chunk-mqc662a6.js → chunk-364znk6q.js} +2 -2
- package/dist/cli/{chunk-gs7r695n.js → chunk-3em5wd2y.js} +21 -8
- package/dist/cli/{chunk-gs7r695n.js.map → chunk-3em5wd2y.js.map} +3 -3
- package/dist/cli/{chunk-k7pj68a8.js → chunk-5f86nr5m.js} +15 -15
- package/dist/cli/{chunk-hr8ne106.js → chunk-5m5nmvyq.js} +68 -43
- package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
- package/dist/cli/{chunk-91ws1n6j.js → chunk-5xvm6tfj.js} +14 -14
- package/dist/cli/{chunk-w4bxdvsa.js → chunk-6dsbexzp.js} +14 -14
- package/dist/cli/{chunk-00gs3wqs.js → chunk-8ktnccpt.js} +1 -1
- package/dist/cli/{chunk-j85scx15.js → chunk-9t7a85s3.js} +130 -44
- package/dist/cli/chunk-9t7a85s3.js.map +10 -0
- package/dist/cli/{chunk-bbnwccaz.js → chunk-a58773jm.js} +2 -2
- package/dist/cli/{chunk-nfcyttvj.js → chunk-acanzt5p.js} +9 -9
- package/dist/cli/{chunk-nfcyttvj.js.map → chunk-acanzt5p.js.map} +1 -1
- package/dist/cli/{chunk-wdrt2k2v.js → chunk-akbpwfxc.js} +90 -26
- package/dist/cli/chunk-akbpwfxc.js.map +10 -0
- package/dist/cli/{chunk-sqw4ekg1.js → chunk-b07cmahc.js} +2 -2
- package/dist/cli/{chunk-xh43dwgw.js → chunk-c8chx29p.js} +42 -29
- package/dist/cli/{chunk-xh43dwgw.js.map → chunk-c8chx29p.js.map} +9 -9
- package/dist/cli/{chunk-7mbqtmgb.js → chunk-crgn1q09.js} +21 -12
- package/dist/cli/chunk-crgn1q09.js.map +10 -0
- package/dist/cli/chunk-e04dxsz1.js +39 -0
- package/dist/cli/chunk-e04dxsz1.js.map +10 -0
- package/dist/cli/{chunk-4e9b9ra6.js → chunk-ey84smr6.js} +3 -3
- package/dist/cli/{chunk-d1v5rhy0.js → chunk-g4hq16wv.js} +13 -13
- package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
- package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
- package/dist/cli/{chunk-pbg5a4s3.js → chunk-hm3vjy5s.js} +64 -45
- package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
- package/dist/cli/{chunk-5r8g91qn.js → chunk-j85vccga.js} +306 -107
- package/dist/cli/chunk-j85vccga.js.map +36 -0
- package/dist/cli/{chunk-n1yg3tj3.js → chunk-p3v96n38.js} +6 -6
- package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-p3v96n38.js.map} +3 -3
- package/dist/cli/{chunk-7vtckvaw.js → chunk-p73c0m7w.js} +14 -14
- package/dist/cli/{chunk-7vtckvaw.js.map → chunk-p73c0m7w.js.map} +1 -1
- package/dist/cli/{chunk-qkb5a8sa.js → chunk-pehfxfta.js} +3 -3
- package/dist/cli/{chunk-v6ya5kcb.js → chunk-pv29h0wf.js} +1223 -978
- package/dist/cli/{chunk-v6ya5kcb.js.map → chunk-pv29h0wf.js.map} +47 -46
- package/dist/cli/{chunk-6dtt0zfn.js → chunk-q58y5e6a.js} +10 -10
- package/dist/cli/{chunk-6dtt0zfn.js.map → chunk-q58y5e6a.js.map} +3 -3
- package/dist/cli/{chunk-fsmrqk8a.js → chunk-r20tn01b.js} +1 -1
- package/dist/cli/{chunk-bfwp9vp6.js → chunk-r9rcc4w7.js} +7 -7
- package/dist/cli/{chunk-h7k3nq3v.js → chunk-tkacnehg.js} +2 -2
- package/dist/cli/{chunk-h2ez8dzb.js → chunk-tzmab476.js} +4 -4
- package/dist/cli/{chunk-3yce002v.js → chunk-vg9r4eb9.js} +6 -6
- package/dist/cli/{chunk-3yce002v.js.map → chunk-vg9r4eb9.js.map} +4 -4
- package/dist/cli/{chunk-hqp2ajnh.js → chunk-wrr3j9w9.js} +16 -17
- package/dist/cli/{chunk-hqp2ajnh.js.map → chunk-wrr3j9w9.js.map} +7 -7
- package/dist/cli/{chunk-ddndchfr.js → chunk-yfyb25rh.js} +38 -26
- package/dist/cli/chunk-yfyb25rh.js.map +14 -0
- package/dist/cli/index.js +20 -18
- package/dist/cli/index.js.map +3 -3
- package/dist/types/ai/link-headers.d.ts +8 -1
- package/dist/types/ai/openapi-components.d.ts +5 -2
- package/dist/types/ai/relative-links.d.ts +9 -7
- package/dist/types/ai/skills.d.ts +4 -1
- package/dist/types/ai/tar.d.ts +1 -3
- package/dist/types/analytics/index.d.ts +2 -0
- package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
- package/dist/types/analytics/schema.d.ts +14 -0
- package/dist/types/astro/integration.d.ts +3 -2
- package/dist/types/core/base-path.d.ts +21 -7
- package/dist/types/core/config-input.d.ts +9 -7
- package/dist/types/core/directive-diagnostics.d.ts +12 -0
- package/dist/types/core/heading-markers.d.ts +5 -7
- package/dist/types/core/i18n.d.ts +2 -0
- package/dist/types/core/last-modified.d.ts +10 -0
- package/dist/types/core/meta.d.ts +8 -0
- package/dist/types/core/schema.d.ts +7 -0
- package/dist/types/core/sources/lower.d.ts +29 -16
- package/dist/types/core/sources/normalize.d.ts +13 -1
- package/dist/types/core/sources/watch.d.ts +12 -5
- package/dist/types/core/standard-schema.d.ts +5 -0
- package/dist/types/deploy/artifacts.d.ts +6 -4
- package/dist/types/deploy/headers.d.ts +5 -0
- package/dist/types/deploy/platforms/types.d.ts +8 -0
- package/dist/types/deploy/redirects.d.ts +24 -13
- package/dist/types/markdown/directives.d.ts +62 -0
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/markdown/mdast.d.ts +63 -0
- package/dist/types/openapi/asyncapi.d.ts +4 -2
- package/docs/02-deployment.mdx +4 -2
- package/docs/08-faq.mdx +1 -1
- package/docs/advanced/custom-pages.mdx +3 -3
- package/docs/cli/audit.mdx +17 -1
- package/docs/cli/evals.mdx +3 -3
- package/docs/cli/translate.mdx +3 -3
- package/docs/cli/version.mdx +1 -1
- package/docs/configuration/analytics.mdx +20 -1
- package/docs/configuration/customization.mdx +2 -0
- package/docs/configuration/search.mdx +1 -1
- package/docs/content/components.mdx +1 -1
- package/docs/content/frontmatter.mdx +1 -1
- package/docs/content/i18n.mdx +2 -0
- package/docs/content/index.mdx +4 -2
- package/docs/content/islands.mdx +1 -1
- package/docs/content/meta.mdx +3 -1
- package/docs/content/sources.mdx +1 -5
- package/docs/content/syntax.mdx +14 -0
- package/docs/content/versioning.mdx +1 -1
- package/docs/discoverability/agent-discovery.mdx +1 -1
- package/docs/discoverability/index.mdx +2 -1
- package/docs/discoverability/markdown.mdx +2 -2
- package/docs/discoverability/open-graph.mdx +5 -3
- package/docs/discoverability/rss.mdx +1 -1
- package/docs/references/asyncapi.mdx +1 -1
- package/docs/references/graphql.mdx +1 -1
- package/docs/references/openapi.mdx +5 -3
- package/package.json +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/nextra.md +1 -1
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +7 -2
- package/src/ai/agent-readability.ts +2 -2
- package/src/ai/ai-catalog.ts +6 -2
- package/src/ai/api/handlers.ts +2 -2
- package/src/ai/api/spec.ts +2 -2
- package/src/ai/api-catalog.ts +6 -2
- package/src/ai/changelog-markdown.ts +2 -2
- package/src/ai/link-headers.ts +14 -5
- package/src/ai/llms.ts +18 -7
- package/src/ai/mcp/discovery.ts +2 -2
- package/src/ai/mcp/query.ts +2 -2
- package/src/ai/mcp/server.ts +2 -2
- package/src/ai/openapi-components.ts +22 -6
- package/src/ai/relative-links.ts +42 -26
- package/src/ai/serializers.ts +2 -1
- package/src/ai/skills.ts +14 -1
- package/src/ai/tar.ts +139 -12
- package/src/analytics/head.ts +4 -0
- package/src/analytics/index.ts +5 -0
- package/src/analytics/one-dollar-stats.ts +86 -0
- package/src/analytics/schema.ts +2 -0
- package/src/astro/generate.ts +1 -1
- package/src/astro/include-hmr.ts +31 -12
- package/src/astro/include-refresh.ts +19 -8
- package/src/astro/integration.ts +108 -14
- package/src/astro/runtime-modules.ts +28 -13
- package/src/astro/templates.ts +81 -37
- package/src/audit/checks/links.ts +8 -1
- package/src/audit/graph.ts +3 -1
- package/src/audit/run.ts +12 -9
- package/src/audit/snapshot.ts +5 -0
- package/src/cli/args.ts +32 -0
- package/src/cli/commands/audit.ts +4 -1
- package/src/cli/commands/doctor.ts +3 -1
- package/src/cli/commands/eval.ts +19 -11
- package/src/cli/commands/translate.ts +9 -12
- package/src/cli/commands/validate.ts +3 -1
- package/src/cli/dev-lock.ts +157 -37
- package/src/cli/report-format.ts +11 -6
- package/src/components/colors.ts +19 -0
- package/src/components/content/Badge.astro +5 -3
- package/src/components/content/Component.astro +2 -2
- package/src/components/content/Tab.astro +0 -1
- package/src/components/content/Tabs.astro +4 -0
- package/src/components/content/badge-color.ts +5 -2
- package/src/components/content/base-href.ts +13 -36
- package/src/components/islands/assistant.tsx +21 -4
- package/src/components/islands/base-path.ts +47 -10
- package/src/components/islands/hooks.ts +35 -19
- package/src/components/islands/webmcp.ts +4 -2
- package/src/components/layout/Breadcrumbs.astro +5 -2
- package/src/components/layout/DiscoveryLinks.astro +9 -5
- package/src/components/layout/Header.astro +2 -2
- package/src/components/layout/LanguageSwitcher.astro +2 -2
- package/src/components/layout/NavSelector.astro +2 -2
- package/src/components/layout/NavTabMenu.astro +3 -3
- package/src/components/layout/NavTree.astro +16 -6
- package/src/components/layout/PageActions.astro +4 -3
- package/src/components/layout/PageLayout.astro +7 -6
- package/src/components/layout/Pagination.astro +3 -3
- package/src/components/layout/RootLayout.astro +5 -5
- package/src/components/layout/Search.astro +33 -13
- package/src/components/layout/VersionBanner.astro +2 -2
- package/src/components/layout/analytics-client.ts +12 -0
- package/src/components/layout/toc-active.ts +41 -0
- package/src/components/layout/toc-element.ts +8 -14
- package/src/components/openapi/ApiTagOperations.astro +2 -2
- package/src/components/openapi/AsyncApiOperation.astro +7 -4
- package/src/components/openapi/GraphqlChip.astro +2 -2
- package/src/components/openapi/Operation.astro +2 -0
- package/src/components/openapi/Playground.astro +4 -4
- package/src/components/openapi/RequestPanel.astro +5 -2
- package/src/components/openapi/SchemaTable.astro +7 -0
- package/src/components/openapi/async.ts +38 -6
- package/src/components/openapi/helpers.ts +19 -9
- package/src/components/openapi/message-composer.ts +6 -1
- package/src/components/openapi/message.ts +17 -2
- package/src/components/openapi/operation-model.ts +81 -11
- package/src/components/openapi/panel.ts +4 -2
- package/src/components/openapi/param-style.ts +181 -0
- package/src/components/openapi/playground-client.ts +73 -16
- package/src/components/openapi/request.ts +190 -26
- package/src/components/openapi/schema-tree.ts +30 -24
- package/src/components/openapi/snippets.ts +85 -6
- package/src/components/openapi/ws-client.ts +18 -2
- package/src/core/base-path.ts +62 -16
- package/src/core/config-input.ts +9 -7
- package/src/core/diagnostics.ts +2 -0
- package/src/core/directive-diagnostics.ts +99 -0
- package/src/core/frontmatter.ts +21 -18
- package/src/core/heading-markers.ts +5 -18
- package/src/core/i18n.ts +31 -3
- package/src/core/last-modified.ts +25 -3
- package/src/core/locale-links.ts +5 -1
- package/src/core/meta.ts +33 -19
- package/src/core/navigation.ts +28 -8
- package/src/core/project-graph.ts +27 -5
- package/src/core/schema.ts +24 -4
- package/src/core/sources/contentful-rich-text.ts +27 -20
- package/src/core/sources/filesystem.ts +20 -2
- package/src/core/sources/github-releases.ts +25 -6
- package/src/core/sources/lexical.ts +23 -18
- package/src/core/sources/lower.ts +201 -34
- package/src/core/sources/mdx-remote.ts +51 -16
- package/src/core/sources/normalize.ts +325 -90
- package/src/core/sources/notion.ts +52 -25
- package/src/core/sources/obsidian.ts +23 -6
- package/src/core/sources/portable-text.ts +38 -21
- package/src/core/sources/strapi-blocks.ts +20 -16
- package/src/core/sources/watch.ts +20 -7
- package/src/core/standard-schema.ts +10 -6
- package/src/core/version-cut.ts +52 -15
- package/src/deploy/artifacts.ts +27 -6
- package/src/deploy/cloudflare-negotiation.ts +4 -12
- package/src/deploy/headers.ts +8 -4
- package/src/deploy/node-headers.ts +1 -1
- package/src/deploy/platforms/cloudflare.ts +12 -3
- package/src/deploy/platforms/netlify.ts +1 -0
- package/src/deploy/platforms/node.ts +1 -0
- package/src/deploy/platforms/static.ts +1 -0
- package/src/deploy/platforms/types.ts +8 -0
- package/src/deploy/platforms/vercel.ts +1 -0
- package/src/deploy/redirects.ts +54 -18
- package/src/deploy/robots.ts +2 -2
- package/src/deploy/rss.ts +4 -3
- package/src/deploy/sitemap.ts +7 -5
- package/src/markdown/base-links.ts +34 -36
- package/src/markdown/directives.ts +242 -36
- package/src/markdown/features.ts +17 -0
- package/src/markdown/index.ts +7 -6
- package/src/markdown/mdast.ts +5 -2
- package/src/markdown/relative-links.ts +12 -4
- package/src/og/card.ts +129 -5
- package/src/og/derive.ts +41 -31
- package/src/og/index.ts +1 -0
- package/src/openapi/asyncapi.ts +4 -2
- package/src/openapi/model.ts +22 -14
- package/src/openapi/proxy.ts +63 -10
- package/src/openapi/render-mdx.ts +41 -2
- package/src/registry/eject.ts +181 -24
- package/src/search/adapters/version-scope.ts +30 -0
- package/src/search/documents.ts +67 -26
- package/src/search/popular.ts +2 -1
- package/src/seo/jsonld.ts +7 -3
- package/src/translate/meta.ts +68 -24
- package/src/translate/run.ts +3 -3
- package/src/translate/validate.ts +4 -1
- package/src/translate/work-list.ts +56 -17
- package/dist/cli/chunk-5r8g91qn.js.map +0 -34
- package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
- package/dist/cli/chunk-ddndchfr.js.map +0 -14
- package/dist/cli/chunk-esh98wmb.js +0 -23
- package/dist/cli/chunk-esh98wmb.js.map +0 -10
- package/dist/cli/chunk-hr8ne106.js.map +0 -13
- package/dist/cli/chunk-j85scx15.js.map +0 -10
- package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
- package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
- /package/dist/cli/{chunk-6k4ftwze.js.map → chunk-1d7ve1dm.js.map} +0 -0
- /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-2eytanqx.js.map} +0 -0
- /package/dist/cli/{chunk-g698a744.js.map → chunk-2z928egk.js.map} +0 -0
- /package/dist/cli/{chunk-mqc662a6.js.map → chunk-364znk6q.js.map} +0 -0
- /package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5f86nr5m.js.map} +0 -0
- /package/dist/cli/{chunk-91ws1n6j.js.map → chunk-5xvm6tfj.js.map} +0 -0
- /package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-6dsbexzp.js.map} +0 -0
- /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-8ktnccpt.js.map} +0 -0
- /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-a58773jm.js.map} +0 -0
- /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-b07cmahc.js.map} +0 -0
- /package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-ey84smr6.js.map} +0 -0
- /package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-g4hq16wv.js.map} +0 -0
- /package/dist/cli/{chunk-qkb5a8sa.js.map → chunk-pehfxfta.js.map} +0 -0
- /package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-r20tn01b.js.map} +0 -0
- /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-r9rcc4w7.js.map} +0 -0
- /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-tkacnehg.js.map} +0 -0
- /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-tzmab476.js.map} +0 -0
|
@@ -15,14 +15,15 @@ import {
|
|
|
15
15
|
pollingWatch,
|
|
16
16
|
snapshotCache,
|
|
17
17
|
} from "./cache.ts";
|
|
18
|
+
import type { InlineRun } from "./lower.ts";
|
|
18
19
|
import {
|
|
19
20
|
codeFence,
|
|
20
21
|
guardBlockStart,
|
|
21
22
|
image,
|
|
23
|
+
linkParts,
|
|
22
24
|
renderInline,
|
|
23
|
-
renderLink,
|
|
24
25
|
} from "./lower.ts";
|
|
25
|
-
import { slugifyPath } from "./normalize.ts";
|
|
26
|
+
import { slugify, slugifyPath } from "./normalize.ts";
|
|
26
27
|
import type {
|
|
27
28
|
ContentSource,
|
|
28
29
|
SourceContext,
|
|
@@ -160,26 +161,43 @@ interface NotionFrontmatter {
|
|
|
160
161
|
[key: string]: boolean | string | { order: number } | undefined;
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
/** Neighboring rich-text runs that link to one `href` (or none), and it. */
|
|
165
|
+
interface LinkedRuns {
|
|
166
|
+
href?: string;
|
|
167
|
+
runs: InlineRun[];
|
|
168
|
+
}
|
|
169
|
+
|
|
163
170
|
/**
|
|
164
|
-
* Rich text as Markdown for an MDX body.
|
|
171
|
+
* Rich text as Markdown for an MDX body. The runs go through the shared
|
|
165
172
|
* lowering, so a literal `{`, `<`, `*`, or `[` typed in Notion renders as
|
|
166
|
-
* itself instead of opening a JSX expression, a tag, or emphasis,
|
|
167
|
-
*
|
|
173
|
+
* itself instead of opening a JSX expression, a tag, or emphasis, a code run
|
|
174
|
+
* keeps its text verbatim inside a long-enough code span, neighbors share
|
|
175
|
+
* their marks' delimiters, and neighbors that link to one `href` share one
|
|
176
|
+
* link.
|
|
168
177
|
*/
|
|
169
|
-
const richToMarkdown = (rich: NotionRichText[] = []): string =>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
.
|
|
178
|
+
const richToMarkdown = (rich: NotionRichText[] = []): string => {
|
|
179
|
+
const linked: LinkedRuns[] = [];
|
|
180
|
+
for (const { annotations = {}, href, plain_text: text } of rich) {
|
|
181
|
+
const run: InlineRun = {
|
|
182
|
+
marks: {
|
|
183
|
+
bold: annotations.bold,
|
|
184
|
+
code: annotations.code,
|
|
185
|
+
italic: annotations.italic,
|
|
186
|
+
strike: annotations.strikethrough,
|
|
187
|
+
},
|
|
188
|
+
text,
|
|
189
|
+
};
|
|
190
|
+
const last = linked.at(-1);
|
|
191
|
+
if (last && last.href === (href ?? undefined)) {
|
|
192
|
+
last.runs.push(run);
|
|
193
|
+
} else {
|
|
194
|
+
linked.push({ href: href ?? undefined, runs: [run] });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return renderInline(
|
|
198
|
+
linked.flatMap(({ href, runs }) => linkParts(runs, href))
|
|
199
|
+
);
|
|
200
|
+
};
|
|
183
201
|
|
|
184
202
|
const isBlockPayload = (
|
|
185
203
|
value: NotionBlockPayload | boolean | string | undefined
|
|
@@ -307,10 +325,14 @@ const renderVideo = (data: NotionBlockPayload): string => {
|
|
|
307
325
|
const title = caption ? ` title=${jsxString(richToPlain(data.caption))}` : "";
|
|
308
326
|
// Everything else (a Notion upload, or a direct link to a media file) is a
|
|
309
327
|
// real video file: `materializeAssets` rewrites the `src`, which matters most
|
|
310
|
-
// for uploads, whose Notion URLs are signed and expire.
|
|
328
|
+
// for uploads, whose Notion URLs are signed and expire. It finds the file by
|
|
329
|
+
// that quoted `src`, where MDX decodes no backslash escapes — a JSON
|
|
330
|
+
// string's `\\` and `\"` would corrupt the URL or end the attribute — so the
|
|
331
|
+
// URL goes in as written, with a double quote as the `%22` it means anyway.
|
|
332
|
+
// The embed's `url` takes the expression form, like every string prop here.
|
|
311
333
|
const media = isYouTubeUrl(url)
|
|
312
|
-
? `<YouTube${title} url=${
|
|
313
|
-
: `<video controls src
|
|
334
|
+
? `<YouTube${title} url=${jsxString(url)} />`
|
|
335
|
+
: `<video controls src="${url.replaceAll('"', "%22")}" />`;
|
|
314
336
|
return caption
|
|
315
337
|
? `<Frame caption=${jsxString(caption.replaceAll(FRAME_ESCAPED, ""))}>\n${media}\n</Frame>`
|
|
316
338
|
: media;
|
|
@@ -577,9 +599,14 @@ export const notionSource = (
|
|
|
577
599
|
const slugProp = richToPlain(
|
|
578
600
|
page.properties[props.slug ?? "Slug"]?.rich_text
|
|
579
601
|
);
|
|
580
|
-
//
|
|
581
|
-
// instead of mashing into `guidessetup`.
|
|
582
|
-
|
|
602
|
+
// A Slug property is path-aware: `guides/setup` keeps its `/` (per-segment
|
|
603
|
+
// slugging) instead of mashing into `guidessetup`. A title is one name, so
|
|
604
|
+
// its slashes become hyphens: "CI/CD Setup" routes to `ci-cd-setup`, not
|
|
605
|
+
// to `cd-setup` inside an invented `ci` group.
|
|
606
|
+
const slug =
|
|
607
|
+
(slugProp
|
|
608
|
+
? slugifyPath(slugProp)
|
|
609
|
+
: slugify(title.replaceAll("/", "-"))) || page.id;
|
|
583
610
|
return { data, slug };
|
|
584
611
|
};
|
|
585
612
|
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
resolveEntryRoute,
|
|
22
22
|
slugifyPath,
|
|
23
23
|
strippedLineOffset,
|
|
24
|
+
unloadablePathDiagnostic,
|
|
24
25
|
} from "./normalize.ts";
|
|
25
26
|
import type {
|
|
26
27
|
ContentSource,
|
|
@@ -587,10 +588,11 @@ const transformBody = (
|
|
|
587
588
|
* `[#custom-id]` pin and the `setup-1` a later collision gets are honored
|
|
588
589
|
* rather than re-slugged. When no heading holds anything the rewrite changes,
|
|
589
590
|
* the raw body's headings are the staged body's; otherwise the body is lowered
|
|
590
|
-
* once more so the headings are scanned exactly as they ship. A link
|
|
591
|
-
* heading is rewritten here before
|
|
592
|
-
*
|
|
593
|
-
*
|
|
591
|
+
* once more so the headings are scanned exactly as they ship. A heading link
|
|
592
|
+
* inside a heading is rewritten here before every note's anchors are known,
|
|
593
|
+
* so its href may still lack the anchor it ships with. That moves no id: a
|
|
594
|
+
* heading is slugged by its rendered text, where the link is its label alone,
|
|
595
|
+
* so the heading stays as addressable as any other.
|
|
594
596
|
*/
|
|
595
597
|
const fillAnchors = (pair: IndexedPair, index: LinkIndex): void => {
|
|
596
598
|
const raw = extractHeadings(pair.note.content);
|
|
@@ -958,7 +960,19 @@ export const obsidianSource = (
|
|
|
958
960
|
const exclude = new Set([...BLUME_IGNORE_DIRS, ...(options.exclude ?? [])]);
|
|
959
961
|
|
|
960
962
|
const load = async (): Promise<SourceLoadResult> => {
|
|
961
|
-
|
|
963
|
+
// A note is staged under its vault path, where Astro's content loader
|
|
964
|
+
// can't read a name holding `#` or `?`; such a note is reported and left
|
|
965
|
+
// out, and links to it degrade like links to any missing note.
|
|
966
|
+
const skipped: Diagnostic[] = [];
|
|
967
|
+
const files: string[] = [];
|
|
968
|
+
for (const rel of await walkVault(vaultDir, vaultDir, exclude)) {
|
|
969
|
+
const unloadable = unloadablePathDiagnostic(rel, join(vaultDir, rel));
|
|
970
|
+
if (unloadable) {
|
|
971
|
+
skipped.push(unloadable);
|
|
972
|
+
} else {
|
|
973
|
+
files.push(rel);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
962
976
|
const read = await Promise.all(
|
|
963
977
|
files.map((rel) => readNote(vaultDir, rel, options))
|
|
964
978
|
);
|
|
@@ -971,7 +985,10 @@ export const obsidianSource = (
|
|
|
971
985
|
noteToEntry(pair, index, options, unresolved)
|
|
972
986
|
);
|
|
973
987
|
return {
|
|
974
|
-
diagnostics:
|
|
988
|
+
diagnostics: [
|
|
989
|
+
...skipped,
|
|
990
|
+
...unresolvedDiagnostics(options.name, unresolved),
|
|
991
|
+
],
|
|
975
992
|
entries,
|
|
976
993
|
};
|
|
977
994
|
};
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* user-supplied serializer or are skipped with a noted comment. Output is
|
|
5
5
|
* Markdown text that flows through Blume's normal pipeline.
|
|
6
6
|
*/
|
|
7
|
+
import type { InlineRun } from "./lower.ts";
|
|
7
8
|
import {
|
|
8
9
|
guardBlockStart,
|
|
9
10
|
image,
|
|
11
|
+
linkParts,
|
|
10
12
|
renderInline,
|
|
11
|
-
renderLink,
|
|
12
13
|
unsupported,
|
|
13
14
|
writesMdx,
|
|
14
15
|
} from "./lower.ts";
|
|
@@ -73,38 +74,54 @@ const HEADING_STYLES = new Map([
|
|
|
73
74
|
["h6", "###### "],
|
|
74
75
|
]);
|
|
75
76
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
* uses: prose is escaped, a code span outruns the backticks inside it, edge
|
|
79
|
-
* whitespace stays outside emphasis, and a link def wraps last (so its label
|
|
80
|
-
* keeps emphasis) only when its destination is safe to click.
|
|
81
|
-
*/
|
|
82
|
-
const renderSpan = (
|
|
83
|
-
span: PortableTextSpan,
|
|
84
|
-
defs: Map<string, PortableTextMarkDef>
|
|
85
|
-
): string => {
|
|
77
|
+
/** A span's text and the marks Markdown has syntax for. */
|
|
78
|
+
const spanRun = (span: PortableTextSpan): InlineRun => {
|
|
86
79
|
const marks = span.marks ?? [];
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.findLast((def) => def?._type === "link");
|
|
90
|
-
return renderLink(
|
|
91
|
-
renderInline(span.text ?? "", {
|
|
80
|
+
return {
|
|
81
|
+
marks: {
|
|
92
82
|
bold: marks.includes("strong"),
|
|
93
83
|
code: marks.includes("code"),
|
|
94
84
|
italic: marks.includes("em"),
|
|
95
85
|
strike: marks.includes("strike-through"),
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
},
|
|
87
|
+
text: span.text ?? "",
|
|
88
|
+
};
|
|
99
89
|
};
|
|
100
90
|
|
|
101
|
-
/**
|
|
91
|
+
/** Neighboring spans under one link def (or none), and that def. */
|
|
92
|
+
interface LinkedSpans {
|
|
93
|
+
link?: PortableTextMarkDef;
|
|
94
|
+
runs: InlineRun[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Render the inline children of a block to a single Markdown string, through
|
|
99
|
+
* the shared lowering every CMS uses: prose is escaped, a code span outruns
|
|
100
|
+
* the backticks inside it, edge whitespace stays outside emphasis, neighbors
|
|
101
|
+
* share their marks' delimiters, and neighbors under one link def share one
|
|
102
|
+
* link — wrapped last (so its label keeps emphasis), and only when its
|
|
103
|
+
* destination is safe to click.
|
|
104
|
+
*/
|
|
102
105
|
const renderChildren = (block: PortableTextBlock): string => {
|
|
103
106
|
const defs = new Map(
|
|
104
107
|
(block.markDefs ?? []).map((def) => [def._key, def] as const)
|
|
105
108
|
);
|
|
109
|
+
const linked: LinkedSpans[] = [];
|
|
110
|
+
for (const span of block.children ?? []) {
|
|
111
|
+
const link = (span.marks ?? [])
|
|
112
|
+
.map((mark) => defs.get(mark))
|
|
113
|
+
.findLast((def) => def?._type === "link");
|
|
114
|
+
const last = linked.at(-1);
|
|
115
|
+
if (last && last.link === link) {
|
|
116
|
+
last.runs.push(spanRun(span));
|
|
117
|
+
} else {
|
|
118
|
+
linked.push({ link, runs: [spanRun(span)] });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
106
121
|
return guardBlockStart(
|
|
107
|
-
(
|
|
122
|
+
renderInline(
|
|
123
|
+
linked.flatMap(({ link, runs }) => linkParts(runs, link?.href))
|
|
124
|
+
)
|
|
108
125
|
);
|
|
109
126
|
};
|
|
110
127
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { JsonObject, JsonValue } from "./json.ts";
|
|
7
7
|
import { asNumber, asObject, asString, objectsIn } from "./json.ts";
|
|
8
|
+
import type { InlinePart } from "./lower.ts";
|
|
8
9
|
import {
|
|
9
10
|
absoluteUrl,
|
|
10
11
|
blockquote,
|
|
@@ -12,10 +13,10 @@ import {
|
|
|
12
13
|
guardBlockStart,
|
|
13
14
|
headingPrefix,
|
|
14
15
|
image,
|
|
16
|
+
linkParts,
|
|
15
17
|
listItem,
|
|
16
18
|
markdownDocument,
|
|
17
19
|
renderInline,
|
|
18
|
-
renderLink,
|
|
19
20
|
unsupported,
|
|
20
21
|
} from "./lower.ts";
|
|
21
22
|
|
|
@@ -28,34 +29,37 @@ const children = (node: JsonObject): JsonObject[] => objectsIn(node.children);
|
|
|
28
29
|
|
|
29
30
|
const typeOf = (node: JsonObject): string => asString(node.type) ?? "";
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.map((node) => renderInlineNode(node))
|
|
35
|
-
.join("");
|
|
32
|
+
const inlineParts = (nodes: JsonObject[]): InlinePart[] =>
|
|
33
|
+
// oxlint-disable-next-line no-use-before-define -- mutual recursion: a link's label is itself inline content
|
|
34
|
+
nodes.flatMap((node) => inlineNodeParts(node));
|
|
36
35
|
|
|
37
|
-
const
|
|
36
|
+
const inlineNodeParts = (node: JsonObject): InlinePart[] => {
|
|
38
37
|
switch (typeOf(node)) {
|
|
39
38
|
case "text": {
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
marks: {
|
|
42
|
+
bold: node.bold === true,
|
|
43
|
+
code: node.code === true,
|
|
44
|
+
italic: node.italic === true,
|
|
45
|
+
strike: node.strikethrough === true,
|
|
46
|
+
},
|
|
47
|
+
text: asString(node.text) ?? "",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
46
50
|
}
|
|
47
51
|
case "link": {
|
|
48
|
-
return
|
|
52
|
+
return linkParts(inlineParts(children(node)), asString(node.url));
|
|
49
53
|
}
|
|
50
54
|
default: {
|
|
51
|
-
return
|
|
55
|
+
return inlineParts(children(node));
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
};
|
|
55
59
|
|
|
56
60
|
/** A text block's inline nodes, joined and guarded as one block. */
|
|
57
61
|
const blockText = (nodes: JsonObject[]): string =>
|
|
58
|
-
guardBlockStart(
|
|
62
|
+
guardBlockStart(renderInline(inlineParts(nodes)));
|
|
59
63
|
|
|
60
64
|
const renderList = (node: JsonObject): string => {
|
|
61
65
|
const ordered = asString(node.format) === "ordered";
|
|
@@ -51,18 +51,26 @@ export const baselineScanIgnore = (): string[] =>
|
|
|
51
51
|
*/
|
|
52
52
|
export const BLUME_WATCH_IGNORE_DIRS = BLUME_IGNORE_DIRS;
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Extract root-anchored ignore dirs (`/foo`) from `foo/**`-style excludes. The
|
|
56
|
+
* scan's glob `foo/**` matches `foo` at the source root only, so the watcher
|
|
57
|
+
* skips that one directory: a nested `guides/foo/` is still content, and an
|
|
58
|
+
* edit there must still reload.
|
|
59
|
+
*/
|
|
55
60
|
export const excludeDirSegments = (patterns: readonly string[]): string[] =>
|
|
56
61
|
patterns
|
|
57
62
|
.map((pattern) => /^(?<dir>[^*/]+)\/\*\*$/u.exec(pattern)?.groups?.dir)
|
|
58
|
-
.filter((dir): dir is string => dir !== undefined)
|
|
63
|
+
.filter((dir): dir is string => dir !== undefined)
|
|
64
|
+
.map((dir) => `/${dir}`);
|
|
59
65
|
|
|
60
66
|
/**
|
|
61
67
|
* Build a recursive-watch listener that fires `onChange` for content changes but
|
|
62
|
-
* ignores events whose path crosses an ignored directory segment
|
|
63
|
-
* `ignoreDirs
|
|
64
|
-
* a shape (every dot-name) rather than a list.
|
|
65
|
-
*
|
|
68
|
+
* ignores events whose path crosses an ignored directory — a segment named in
|
|
69
|
+
* `ignoreDirs` at any depth, or one `ignoreSegment` rejects, for a scan whose
|
|
70
|
+
* skip rule is a shape (every dot-name) rather than a list. An entry written
|
|
71
|
+
* with a leading slash (`/drafts`, see {@link excludeDirSegments}) names a
|
|
72
|
+
* directory at the watched root only. A missing `filename` — rare; the
|
|
73
|
+
* platform couldn't name the changed path — falls through to `onChange`
|
|
66
74
|
* rather than silently dropping a real edit. Exported for testing.
|
|
67
75
|
*/
|
|
68
76
|
export const ignoringWatchListener = (
|
|
@@ -76,7 +84,12 @@ export const ignoringWatchListener = (
|
|
|
76
84
|
filename !== null &&
|
|
77
85
|
filename
|
|
78
86
|
.split(/[/\\]/u)
|
|
79
|
-
.some(
|
|
87
|
+
.some(
|
|
88
|
+
(segment, index) =>
|
|
89
|
+
ignore.has(segment) ||
|
|
90
|
+
(index === 0 && ignore.has(`/${segment}`)) ||
|
|
91
|
+
ignoreSegment(segment)
|
|
92
|
+
)
|
|
80
93
|
) {
|
|
81
94
|
return;
|
|
82
95
|
}
|
|
@@ -50,11 +50,15 @@ export interface StandardSchema<Input = unknown, Output = Input> {
|
|
|
50
50
|
* Whether a config-supplied value implements the `~standard` contract. Generic
|
|
51
51
|
* so it can decode any input at the config boundary (`z.custom` hands it the
|
|
52
52
|
* raw config value) while narrowing whatever type the caller holds.
|
|
53
|
+
*
|
|
54
|
+
* The contract is the `~standard` property, whatever carries it: Zod and
|
|
55
|
+
* Valibot schemas are objects, but an ArkType type is a callable function. The
|
|
56
|
+
* optional-chained probe reads the property off either, and off `null`,
|
|
57
|
+
* `undefined`, or a primitive it finds nothing.
|
|
53
58
|
*/
|
|
54
59
|
export const isStandardSchema = <T>(value: T): value is T & StandardSchema =>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
?.validate === "function";
|
|
60
|
+
// SAFETY: the assertion only widens the value for property probing; the
|
|
61
|
+
// typeof check is what verifies `~standard.validate` exists.
|
|
62
|
+
typeof (
|
|
63
|
+
value as { "~standard"?: { validate?: unknown } } | null | undefined
|
|
64
|
+
)?.["~standard"]?.validate === "function";
|
package/src/core/version-cut.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { cp, readdir, readFile, rm } from "node:fs/promises";
|
|
|
3
3
|
|
|
4
4
|
import { join, relative } from "pathe";
|
|
5
5
|
|
|
6
|
-
import { stripBasePath } from "./base-path.ts";
|
|
6
|
+
import { mountBasePath, stripBasePath } from "./base-path.ts";
|
|
7
7
|
import { nextFenceState } from "./code-fences.ts";
|
|
8
8
|
import type { FenceState } from "./code-fences.ts";
|
|
9
9
|
import { writeTextAtomic } from "./fs-atomic.ts";
|
|
@@ -45,18 +45,22 @@ export class CutError extends Error {
|
|
|
45
45
|
// matching so replacements can splice into the real line by index (the mask
|
|
46
46
|
// preserves length). Mirrors the content-assets rewriter.
|
|
47
47
|
const INLINE_CODE = /`[^`]*`/gu;
|
|
48
|
-
// A markdown link/image target
|
|
49
|
-
//
|
|
50
|
-
|
|
48
|
+
// A root-absolute markdown link/image target, reference-style link definition
|
|
49
|
+
// (`[setup]: /guides/setup`, up to three spaces in, never a `[^note]:`
|
|
50
|
+
// footnote), or HTML href/src attribute in either quote style. Named groups
|
|
51
|
+
// carry the prefix (kept) and target (rewritten).
|
|
52
|
+
const ROOT_LINK =
|
|
53
|
+
/(?<prefix>\]\(|^ {0,3}\[(?!\^)[^\]]+\]:[ \t]*<?|(?:href|src)=["'])(?<target>\/[^\s"'<>)]*)/gu;
|
|
51
54
|
|
|
52
55
|
/**
|
|
53
56
|
* Build the link-rewrite table: every current-version route (basePath
|
|
54
|
-
* stripped, since authors write root-absolute links as if mounted at root
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* stripped, since authors write root-absolute links as if mounted at root;
|
|
58
|
+
* a link that spells the base out is looked up without it, see
|
|
59
|
+
* `snapshotTarget`) mapped to the same page's route inside the new snapshot.
|
|
60
|
+
* Only pages the snapshot actually contains qualify — filesystem pages under
|
|
61
|
+
* the content root. Spec-rendered references (`/api`, `/events`) and remote
|
|
62
|
+
* sources aren't copied, so links to them keep pointing at the live pages
|
|
63
|
+
* instead of a 404 inside the snapshot.
|
|
60
64
|
*/
|
|
61
65
|
const buildRouteRewrites = (
|
|
62
66
|
project: BlumeProject,
|
|
@@ -105,8 +109,35 @@ const buildRouteRewrites = (
|
|
|
105
109
|
return rewrites;
|
|
106
110
|
};
|
|
107
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The snapshot link a root-absolute path rewrites to, or undefined when the
|
|
114
|
+
* snapshot has no copy of its page. A path written with the base path by hand
|
|
115
|
+
* (`/docs/guides/x` under `basePath: "/docs"`) names the same page as
|
|
116
|
+
* `/guides/x` — rendering leaves an already-based link alone (see
|
|
117
|
+
* `withBasePath`) — so it is looked up without the base and keeps it.
|
|
118
|
+
*/
|
|
119
|
+
const snapshotTarget = (
|
|
120
|
+
path: string,
|
|
121
|
+
rewrites: Map<string, string>,
|
|
122
|
+
basePath: string
|
|
123
|
+
): string | undefined => {
|
|
124
|
+
const based =
|
|
125
|
+
basePath !== "" && (path === basePath || path.startsWith(`${basePath}/`));
|
|
126
|
+
if (!based) {
|
|
127
|
+
return rewrites.get(path);
|
|
128
|
+
}
|
|
129
|
+
const replacement = rewrites.get(stripBasePath(basePath, path));
|
|
130
|
+
return replacement === undefined
|
|
131
|
+
? undefined
|
|
132
|
+
: mountBasePath(basePath, replacement);
|
|
133
|
+
};
|
|
134
|
+
|
|
108
135
|
/** Rewrite one line's root-absolute internal links via the rewrite table. */
|
|
109
|
-
const rewriteLine = (
|
|
136
|
+
const rewriteLine = (
|
|
137
|
+
line: string,
|
|
138
|
+
rewrites: Map<string, string>,
|
|
139
|
+
basePath: string
|
|
140
|
+
): string => {
|
|
110
141
|
const masked = line.replaceAll(INLINE_CODE, (span) =>
|
|
111
142
|
" ".repeat(span.length)
|
|
112
143
|
);
|
|
@@ -120,7 +151,7 @@ const rewriteLine = (line: string, rewrites: Map<string, string>): string => {
|
|
|
120
151
|
const path = hash === -1 ? target : target.slice(0, hash);
|
|
121
152
|
const suffix = hash === -1 ? "" : target.slice(hash);
|
|
122
153
|
const bare = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
123
|
-
const replacement =
|
|
154
|
+
const replacement = snapshotTarget(bare, rewrites, basePath);
|
|
124
155
|
if (replacement === undefined) {
|
|
125
156
|
continue;
|
|
126
157
|
}
|
|
@@ -142,10 +173,12 @@ export interface SnapshotRewrite {
|
|
|
142
173
|
* Rewrite a copied page's root-absolute internal links to their snapshot
|
|
143
174
|
* equivalents, skipping fenced and inline code. Relative links need no
|
|
144
175
|
* rewriting — the whole tree copies together, so they stay self-contained.
|
|
176
|
+
* `basePath` is the site's, for links that spell it out by hand.
|
|
145
177
|
*/
|
|
146
178
|
export const rewriteSnapshotLinks = (
|
|
147
179
|
source: string,
|
|
148
|
-
rewrites: Map<string, string
|
|
180
|
+
rewrites: Map<string, string>,
|
|
181
|
+
basePath = ""
|
|
149
182
|
): SnapshotRewrite => {
|
|
150
183
|
let fence: FenceState = null;
|
|
151
184
|
let count = 0;
|
|
@@ -156,7 +189,7 @@ export const rewriteSnapshotLinks = (
|
|
|
156
189
|
if (inFence) {
|
|
157
190
|
return line;
|
|
158
191
|
}
|
|
159
|
-
const rewrittenLine = rewriteLine(line, rewrites);
|
|
192
|
+
const rewrittenLine = rewriteLine(line, rewrites, basePath);
|
|
160
193
|
if (rewrittenLine !== line) {
|
|
161
194
|
count += 1;
|
|
162
195
|
}
|
|
@@ -416,7 +449,11 @@ export const cutVersion = async (
|
|
|
416
449
|
return;
|
|
417
450
|
}
|
|
418
451
|
const source = await readFile(abs, "utf-8");
|
|
419
|
-
const { text, count } = rewriteSnapshotLinks(
|
|
452
|
+
const { text, count } = rewriteSnapshotLinks(
|
|
453
|
+
source,
|
|
454
|
+
rewrites,
|
|
455
|
+
project.config.basePath
|
|
456
|
+
);
|
|
420
457
|
if (count > 0) {
|
|
421
458
|
await writeTextAtomic(abs, text);
|
|
422
459
|
rewritten.push({ count, file: relative(dir, abs) });
|
package/src/deploy/artifacts.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
buildSignaturesDirectory,
|
|
26
26
|
SIGNATURES_DIRECTORY_PATH,
|
|
27
27
|
} from "../ai/web-bot-auth.ts";
|
|
28
|
+
import { normalizeBasePath } from "../core/base-path.ts";
|
|
28
29
|
import { discoverPagesSync } from "../core/custom-pages.ts";
|
|
29
30
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
30
31
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
@@ -51,10 +52,12 @@ import { buildSitemapFiles, describeSitemapFiles } from "./sitemap.ts";
|
|
|
51
52
|
* Written from the integration's `astro:build:done` hook — Astro's channel
|
|
52
53
|
* for exactly this kind of post-build work — into the directory Astro
|
|
53
54
|
* reports as the client output (`dist/`, or `dist/client` for a server
|
|
54
|
-
* build
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
* build, or `dist/client/<base>/` on Cloudflare, whose `_headers` goes to
|
|
56
|
+
* the root the platform serves above it). The Vercel adapter copies that
|
|
57
|
+
* directory into its Build Output static tree in a later hook, so the
|
|
58
|
+
* artifacts ride along; every other adapter serves it directly. Running
|
|
59
|
+
* inside the hook rather than after `astro build` returns means an ejected
|
|
60
|
+
* project keeps producing them.
|
|
58
61
|
*
|
|
59
62
|
* Every file yields to one the user ships in `public/` (Astro copied it into
|
|
60
63
|
* the output before this runs).
|
|
@@ -88,7 +91,7 @@ const emitRedirectFiles = async (
|
|
|
88
91
|
return;
|
|
89
92
|
}
|
|
90
93
|
const platform = deployPlatform(config.deployment);
|
|
91
|
-
const redirects = platformRedirects(
|
|
94
|
+
const redirects = platformRedirects(project);
|
|
92
95
|
const vercelHeaders = platform.redirectFiles.includes(VERCEL_JSON_FILE)
|
|
93
96
|
? buildVercelHeaders(
|
|
94
97
|
config,
|
|
@@ -127,6 +130,24 @@ const emitRedirectFiles = async (
|
|
|
127
130
|
logger.info(`Emitted redirect files for ${redirects.length} redirect(s)`);
|
|
128
131
|
};
|
|
129
132
|
|
|
133
|
+
/**
|
|
134
|
+
* The root of the static assets the platform serves, where it reads
|
|
135
|
+
* `_headers`: `distDir` itself, except on a server build whose adapter moved
|
|
136
|
+
* the client output under `deployment.base` (`@astrojs/cloudflare` hands
|
|
137
|
+
* `astro:build:done` `dist/client/<base>/` but serves `dist/client`). A file
|
|
138
|
+
* left in `distDir` there would never be read, and would be served publicly
|
|
139
|
+
* at `<base>/_headers`.
|
|
140
|
+
*/
|
|
141
|
+
const assetsRoot = (config: ResolvedConfig, distDir: string): string => {
|
|
142
|
+
const base = normalizeBasePath(config.deployment.options.base);
|
|
143
|
+
const nested =
|
|
144
|
+
config.deployment.options.output === "server" &&
|
|
145
|
+
deployPlatform(config.deployment).serverClientUnderBase;
|
|
146
|
+
// One `..` per base segment climbs back out of `dist/client/<base>/`.
|
|
147
|
+
const depth = base.split("/").length - 1;
|
|
148
|
+
return nested && base ? join(distDir, "../".repeat(depth)) : distDir;
|
|
149
|
+
};
|
|
150
|
+
|
|
130
151
|
/**
|
|
131
152
|
* Emit a `_headers` file so Netlify / Cloudflare serve the raw AI-ready
|
|
132
153
|
* endpoints (`*.md`, `*.mdx`, `*.txt`) with an explicit `charset=utf-8`. Without
|
|
@@ -179,7 +200,7 @@ export const emitHeaderFiles = async (
|
|
|
179
200
|
// An adapter may have written its own rules here already (Cloudflare adds an
|
|
180
201
|
// immutable Cache-Control for /_astro/*). Keep them and append ours: both
|
|
181
202
|
// sets are wanted, and `_headers` has no merge semantics beyond order.
|
|
182
|
-
const target = join(distDir, "_headers");
|
|
203
|
+
const target = join(assetsRoot(config, distDir), "_headers");
|
|
183
204
|
const existing = existsSync(target) ? await readFile(target, "utf-8") : "";
|
|
184
205
|
await writeFile(
|
|
185
206
|
target,
|
|
@@ -84,7 +84,7 @@ import {
|
|
|
84
84
|
API_PAGES_PATH,
|
|
85
85
|
OPENAPI_PATH,
|
|
86
86
|
} from "../ai/api/paths.ts";
|
|
87
|
-
import { normalizePath } from "../core/base-path.ts";
|
|
87
|
+
import { normalizeBasePath, normalizePath } from "../core/base-path.ts";
|
|
88
88
|
import { CONTENT_ASSETS_ROOT, SVG_ASSET_HEADERS } from "./headers.ts";
|
|
89
89
|
import type { NotFoundVariants } from "./vercel-negotiation.ts";
|
|
90
90
|
|
|
@@ -132,14 +132,6 @@ const STATIC_EXEMPTIONS = [
|
|
|
132
132
|
API_NAVIGATION_PATH,
|
|
133
133
|
];
|
|
134
134
|
|
|
135
|
-
/**
|
|
136
|
-
* The deployment base as a rule/URL prefix: trailing slash stripped, empty
|
|
137
|
-
* for a root deploy — the same normalization the dev middleware applies in
|
|
138
|
-
* `astro/markdown-negotiation.ts`.
|
|
139
|
-
*/
|
|
140
|
-
const basePrefix = (base?: string): string =>
|
|
141
|
-
base && base !== "/" ? base.replace(/\/$/u, "") : "";
|
|
142
|
-
|
|
143
135
|
/** A value as `JSON.parse` produces it (the wrangler config's value space). */
|
|
144
136
|
type JsonValue =
|
|
145
137
|
| string
|
|
@@ -180,7 +172,7 @@ const ruleBody = (rule: string): string =>
|
|
|
180
172
|
* status (see the module comment).
|
|
181
173
|
*/
|
|
182
174
|
export const buildRunWorkerFirstRules = (base?: string): string[] => {
|
|
183
|
-
const prefix = encodeURI(
|
|
175
|
+
const prefix = encodeURI(normalizeBasePath(base));
|
|
184
176
|
const exemptions = STATIC_EXEMPTIONS.map((path) => `!${prefix}${path}`);
|
|
185
177
|
return prefix
|
|
186
178
|
? [prefix, `${prefix}/*`, ...exemptions]
|
|
@@ -282,7 +274,7 @@ export const buildNegotiationWorker = (
|
|
|
282
274
|
const routes = JSON.stringify(options.routePaths);
|
|
283
275
|
const pageJson = JSON.stringify(options.pageJsonPaths ?? []);
|
|
284
276
|
const binding = JSON.stringify(options.assetsBinding);
|
|
285
|
-
const prefix = JSON.stringify(encodeURI(
|
|
277
|
+
const prefix = JSON.stringify(encodeURI(normalizeBasePath(options.base)));
|
|
286
278
|
const homeLinkHeader = JSON.stringify(options.homeLinkHeader ?? null);
|
|
287
279
|
const homeTokens = JSON.stringify(
|
|
288
280
|
options.homeTokens === undefined ? null : String(options.homeTokens)
|
|
@@ -680,7 +672,7 @@ export const injectWorkerNegotiation = (
|
|
|
680
672
|
// The content-route guard compares against redirect `from`s, which carry the
|
|
681
673
|
// full `{deployment.base}{basePath}` stack; the routes carry only
|
|
682
674
|
// `basePath`, so the deployment base is applied here.
|
|
683
|
-
const deployPrefix =
|
|
675
|
+
const deployPrefix = normalizeBasePath(options.base);
|
|
684
676
|
const guardRoutes = new Set(
|
|
685
677
|
contentRoutePaths.map((route) =>
|
|
686
678
|
normalizePath(
|