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
package/src/cli/commands/eval.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
|
-
import {
|
|
4
|
+
import { resolve } from "pathe";
|
|
5
5
|
|
|
6
6
|
import { AGENTS, launchAgent } from "../../audit/agent.ts";
|
|
7
7
|
import type { AgentKind } from "../../audit/agent.ts";
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { passFraction, runEval } from "../../eval/run.ts";
|
|
23
23
|
import type { EvalResult } from "../../eval/run.ts";
|
|
24
24
|
import { EvalsFileError, loadEvalsFile } from "../../eval/schema.ts";
|
|
25
|
+
import { parseTimeoutSeconds } from "../args.ts";
|
|
25
26
|
import { commandMeta } from "../command-meta.ts";
|
|
26
27
|
import { reportInternalError } from "../internal-error.ts";
|
|
27
28
|
import { flushStdout, logger, reportDiagnostics } from "../log.ts";
|
|
@@ -88,16 +89,18 @@ const parseFlags = (args: EvalFlags) => {
|
|
|
88
89
|
process.exit(1);
|
|
89
90
|
}
|
|
90
91
|
const threshold = args.threshold === undefined ? 1 : Number(args.threshold);
|
|
91
|
-
|
|
92
|
+
// A bare `--threshold` (or `--threshold $UNSET_VAR`) arrives as "", which
|
|
93
|
+
// `Number` reads as 0 — a gate that passes every run — so it's rejected.
|
|
94
|
+
if (
|
|
95
|
+
args.threshold?.trim() === "" ||
|
|
96
|
+
!Number.isFinite(threshold) ||
|
|
97
|
+
threshold < 0 ||
|
|
98
|
+
threshold > 1
|
|
99
|
+
) {
|
|
92
100
|
logger.error(`Invalid --threshold "${args.threshold}" (use 0..1).`);
|
|
93
101
|
process.exit(1);
|
|
94
102
|
}
|
|
95
|
-
const timeoutS =
|
|
96
|
-
args.timeout === undefined ? DEFAULT_TIMEOUT_S : Number(args.timeout);
|
|
97
|
-
if (!Number.isInteger(timeoutS) || timeoutS <= 0) {
|
|
98
|
-
logger.error(`Invalid --timeout "${args.timeout}" (whole seconds).`);
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
103
|
+
const timeoutS = parseTimeoutSeconds(args.timeout, DEFAULT_TIMEOUT_S);
|
|
101
104
|
return { agent: args.agent, threshold, timeoutS };
|
|
102
105
|
};
|
|
103
106
|
|
|
@@ -125,7 +128,7 @@ const runFixHandoff = async (
|
|
|
125
128
|
|
|
126
129
|
/** `blume eval init`: draft a starter evals file via the interactive agent. */
|
|
127
130
|
const runInit = async (agent: AgentKind, file: string): Promise<void> => {
|
|
128
|
-
const path =
|
|
131
|
+
const path = resolve(process.cwd(), file);
|
|
129
132
|
if (existsSync(path)) {
|
|
130
133
|
logger.error(
|
|
131
134
|
`${file} already exists — edit it directly, or pass --file to draft elsewhere.`
|
|
@@ -193,7 +196,9 @@ export const evalCommand = defineCommand({
|
|
|
193
196
|
// and never regenerates the runtime, so it doesn't contend with a
|
|
194
197
|
// running dev server. Same reasoning as `blume audit`.
|
|
195
198
|
const project = await scanProject(root, { mode: "build" });
|
|
196
|
-
|
|
199
|
+
// `resolve`, not `join`: an absolute `--file` (`/ci/evals.yaml`) is
|
|
200
|
+
// used as given instead of being nested under the project root.
|
|
201
|
+
const evalsPath = resolve(root, args.file);
|
|
197
202
|
const { evals, raw } = await loadEvalsFile(evalsPath);
|
|
198
203
|
|
|
199
204
|
process.stderr.write(`${headerLine(evals.questions.length, agent)}\n\n`);
|
|
@@ -270,7 +275,10 @@ export const evalCommand = defineCommand({
|
|
|
270
275
|
}
|
|
271
276
|
|
|
272
277
|
if (failed) {
|
|
273
|
-
process.exit
|
|
278
|
+
// Set the code and return rather than `process.exit`, which doesn't wait
|
|
279
|
+
// for a piped stderr: a long `--verbose` report was cut off mid-write, in
|
|
280
|
+
// exactly the CI logs that need to show every failure.
|
|
281
|
+
process.exitCode = 1;
|
|
274
282
|
}
|
|
275
283
|
},
|
|
276
284
|
});
|
|
@@ -28,9 +28,10 @@ import {
|
|
|
28
28
|
computeWorkList,
|
|
29
29
|
scanForTranslation,
|
|
30
30
|
} from "../../translate/work-list.ts";
|
|
31
|
+
import { parseTimeoutSeconds } from "../args.ts";
|
|
31
32
|
import { commandMeta } from "../command-meta.ts";
|
|
32
33
|
import { reportInternalError } from "../internal-error.ts";
|
|
33
|
-
import {
|
|
34
|
+
import { logger, reportDiagnostics } from "../log.ts";
|
|
34
35
|
|
|
35
36
|
/** Wall-clock ceiling per file, in seconds. */
|
|
36
37
|
const DEFAULT_TIMEOUT_S = DEFAULT_TRANSLATE_TIMEOUT_MS / 1000;
|
|
@@ -80,12 +81,7 @@ const parseFlags = (args: TranslateFlags): ParsedTranslateFlags => {
|
|
|
80
81
|
);
|
|
81
82
|
process.exit(1);
|
|
82
83
|
}
|
|
83
|
-
const timeoutS =
|
|
84
|
-
args.timeout === undefined ? DEFAULT_TIMEOUT_S : Number(args.timeout);
|
|
85
|
-
if (!Number.isInteger(timeoutS) || timeoutS <= 0) {
|
|
86
|
-
logger.error(`Invalid --timeout "${args.timeout}" (whole seconds).`);
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
84
|
+
const timeoutS = parseTimeoutSeconds(args.timeout, DEFAULT_TIMEOUT_S);
|
|
89
85
|
const concurrency =
|
|
90
86
|
args.concurrency === undefined
|
|
91
87
|
? DEFAULT_CONCURRENCY
|
|
@@ -228,8 +224,9 @@ export const translateCommand = defineCommand({
|
|
|
228
224
|
process.stdout.write(checkReportJson(workList));
|
|
229
225
|
}
|
|
230
226
|
if (hasDrift(workList)) {
|
|
231
|
-
|
|
232
|
-
|
|
227
|
+
// Set the code and return rather than `process.exit`, which waits for
|
|
228
|
+
// neither a piped stderr (the drift report) nor a piped stdout.
|
|
229
|
+
process.exitCode = 1;
|
|
233
230
|
}
|
|
234
231
|
return;
|
|
235
232
|
}
|
|
@@ -290,9 +287,9 @@ export const translateCommand = defineCommand({
|
|
|
290
287
|
}
|
|
291
288
|
if (result.counts.failed > 0 || result.counts.partial > 0) {
|
|
292
289
|
// The ledger write above already persisted every success, so a failed
|
|
293
|
-
// rerun only retries what actually failed.
|
|
294
|
-
|
|
295
|
-
process.
|
|
290
|
+
// rerun only retries what actually failed. Set the code rather than
|
|
291
|
+
// `process.exit`, which would cut off a piped stderr summary.
|
|
292
|
+
process.exitCode = 1;
|
|
296
293
|
}
|
|
297
294
|
} catch (error) {
|
|
298
295
|
if (error instanceof BlumeError) {
|
|
@@ -119,7 +119,9 @@ export const validateCommand = defineCommand({
|
|
|
119
119
|
logger.success("No broken links found.");
|
|
120
120
|
}
|
|
121
121
|
if (hadErrors || strictFailure) {
|
|
122
|
-
process.exit
|
|
122
|
+
// Set the code and return rather than `process.exit`, which doesn't wait
|
|
123
|
+
// for a piped stderr: a long diagnostic list would be cut off.
|
|
124
|
+
process.exitCode = 1;
|
|
123
125
|
}
|
|
124
126
|
},
|
|
125
127
|
});
|
package/src/cli/dev-lock.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
existsSync,
|
|
3
|
+
linkSync,
|
|
3
4
|
mkdirSync,
|
|
4
5
|
readFileSync,
|
|
6
|
+
renameSync,
|
|
5
7
|
rmSync,
|
|
8
|
+
statSync,
|
|
6
9
|
writeFileSync,
|
|
7
10
|
} from "node:fs";
|
|
8
11
|
|
|
@@ -29,6 +32,83 @@ export interface DevLockInfo {
|
|
|
29
32
|
|
|
30
33
|
const lockPath = (outDir: string): string => join(outDir, "dev.lock");
|
|
31
34
|
|
|
35
|
+
/** The mutex a process holds while it clears a stale lock. */
|
|
36
|
+
const reapPath = (outDir: string): string => join(outDir, "dev.lock.reap");
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How long a lock file that doesn't parse counts as one being written right
|
|
40
|
+
* now. Blume creates a lock with its whole content in place (see
|
|
41
|
+
* {@link createExclusive}), but one left empty or cut off — by a crash, or by
|
|
42
|
+
* an older Blume that opened the file before writing it — would otherwise
|
|
43
|
+
* read as stale the instant it appears. Past this age it's debris.
|
|
44
|
+
*/
|
|
45
|
+
const PARTIAL_LOCK_GRACE_MS = 2000;
|
|
46
|
+
|
|
47
|
+
/** Pause between claim attempts while another process is mid-claim. */
|
|
48
|
+
const RETRY_DELAY_MS = 5;
|
|
49
|
+
|
|
50
|
+
const pause = (ms: number): void => {
|
|
51
|
+
// A synchronous sleep: the claim is synchronous, and what it waits on is
|
|
52
|
+
// another process, so there's nothing in this one to yield to.
|
|
53
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Per-process counter that keeps concurrent temp file names apart. */
|
|
57
|
+
let tempSequence = 0;
|
|
58
|
+
|
|
59
|
+
/** A private sibling of `path` to stage a write in. */
|
|
60
|
+
const tempPath = (path: string): string => {
|
|
61
|
+
tempSequence += 1;
|
|
62
|
+
return `${path}.${process.pid}.${tempSequence}.tmp`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create `path` holding `payload`, atomically and exclusively. The payload
|
|
67
|
+
* goes to a private temp file first, which is then hard-linked into place:
|
|
68
|
+
* the link fails when `path` exists, exactly like a `wx` open, but the file
|
|
69
|
+
* appears with its whole content — no reader ever sees it empty or half
|
|
70
|
+
* written. Returns false when `path` already exists.
|
|
71
|
+
*/
|
|
72
|
+
const createExclusive = (path: string, payload: string): boolean => {
|
|
73
|
+
const temp = tempPath(path);
|
|
74
|
+
writeFileSync(temp, payload);
|
|
75
|
+
try {
|
|
76
|
+
linkSync(temp, path);
|
|
77
|
+
return true;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// SAFETY: `linkSync` failures are errno exceptions.
|
|
80
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
} finally {
|
|
85
|
+
rmSync(temp, { force: true });
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Replace `path` with `payload` in one step, so readers see old or new. */
|
|
90
|
+
const replaceFile = (path: string, payload: string): void => {
|
|
91
|
+
const temp = tempPath(path);
|
|
92
|
+
writeFileSync(temp, payload);
|
|
93
|
+
renameSync(temp, path);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** A file's text, or null when it doesn't exist (anymore). */
|
|
97
|
+
const readIfExists = (
|
|
98
|
+
path: string
|
|
99
|
+
): { raw: string; mtimeMs: number } | null => {
|
|
100
|
+
try {
|
|
101
|
+
const raw = readFileSync(path, "utf-8");
|
|
102
|
+
return { mtimeMs: statSync(path).mtimeMs, raw };
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// SAFETY: `readFileSync`/`statSync` failures are errno exceptions.
|
|
105
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
32
112
|
/** What `JSON.parse` can yield for a lock file body. */
|
|
33
113
|
type LockFileValue =
|
|
34
114
|
| string
|
|
@@ -85,17 +165,44 @@ const isProcessAlive = (pid: number): boolean => {
|
|
|
85
165
|
}
|
|
86
166
|
};
|
|
87
167
|
|
|
168
|
+
/** What the lock file on a dir says, read once. */
|
|
169
|
+
type LockState =
|
|
170
|
+
| { kind: "absent" }
|
|
171
|
+
/** Unparseable but brand new: another process is writing it right now. */
|
|
172
|
+
| { kind: "busy" }
|
|
173
|
+
| { kind: "live"; lock: DevLockInfo }
|
|
174
|
+
/** A dead holder's lock, or unparseable debris. */
|
|
175
|
+
| { kind: "stale" };
|
|
176
|
+
|
|
177
|
+
const inspectLock = (outDir: string): LockState => {
|
|
178
|
+
const file = readIfExists(lockPath(outDir));
|
|
179
|
+
if (!file) {
|
|
180
|
+
return { kind: "absent" };
|
|
181
|
+
}
|
|
182
|
+
const lock = parseLock(file.raw);
|
|
183
|
+
if (!lock) {
|
|
184
|
+
return Date.now() - file.mtimeMs < PARTIAL_LOCK_GRACE_MS
|
|
185
|
+
? { kind: "busy" }
|
|
186
|
+
: { kind: "stale" };
|
|
187
|
+
}
|
|
188
|
+
return isProcessAlive(lock.pid) ? { kind: "live", lock } : { kind: "stale" };
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Whether this process may clear the lock to claim the dir: a dead holder's
|
|
193
|
+
* lock and debris, or this process's own leftover.
|
|
194
|
+
*/
|
|
195
|
+
const isClaimable = (state: LockState): boolean =>
|
|
196
|
+
state.kind === "stale" ||
|
|
197
|
+
(state.kind === "live" && state.lock.pid === process.pid);
|
|
198
|
+
|
|
88
199
|
/**
|
|
89
200
|
* Read the lock on `outDir` held by a live `blume dev`, or null. A lock left
|
|
90
201
|
* by a process that has since exited (stale) is treated as absent.
|
|
91
202
|
*/
|
|
92
203
|
export const readDevLock = (outDir: string): DevLockInfo | null => {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
const lock = parseLock(readFileSync(path, "utf-8"));
|
|
98
|
-
return lock && isProcessAlive(lock.pid) ? lock : null;
|
|
204
|
+
const state = inspectLock(outDir);
|
|
205
|
+
return state.kind === "live" ? state.lock : null;
|
|
99
206
|
};
|
|
100
207
|
|
|
101
208
|
/** Whether another live `blume dev` holds the lock on `outDir`. */
|
|
@@ -110,10 +217,6 @@ const lockPayload = (port?: number): string => {
|
|
|
110
217
|
return JSON.stringify(info);
|
|
111
218
|
};
|
|
112
219
|
|
|
113
|
-
const writeLock = (outDir: string, port?: number): void => {
|
|
114
|
-
writeFileSync(lockPath(outDir), lockPayload(port));
|
|
115
|
-
};
|
|
116
|
-
|
|
117
220
|
/** Thrown when another live `blume dev` already holds the lock. */
|
|
118
221
|
export class DevLockHeldError extends Error {
|
|
119
222
|
readonly lock: DevLockInfo;
|
|
@@ -138,43 +241,58 @@ const ownsLock = (outDir: string): boolean => {
|
|
|
138
241
|
};
|
|
139
242
|
|
|
140
243
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
244
|
+
* Clear a claimable lock without racing anyone else doing the same. Two
|
|
245
|
+
* processes can both read the same stale lock; if each simply deleted the
|
|
246
|
+
* file, the slower one could delete the lock the faster one had claimed in
|
|
247
|
+
* between, and both would win. So removing a lock takes the reap mutex
|
|
248
|
+
* (created the same atomic way), and whoever holds it reads the lock again
|
|
249
|
+
* first: a live lock claimed since is left alone. A mutex whose holder died
|
|
250
|
+
* mid-reap is cleared for the next attempt.
|
|
145
251
|
*/
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
252
|
+
const reapStaleLock = (outDir: string): void => {
|
|
253
|
+
const mutex = reapPath(outDir);
|
|
254
|
+
if (!createExclusive(mutex, lockPayload())) {
|
|
255
|
+
const holder = readIfExists(mutex);
|
|
256
|
+
const pid = holder ? parseLock(holder.raw)?.pid : undefined;
|
|
257
|
+
if (pid !== undefined && pid !== process.pid && isProcessAlive(pid)) {
|
|
258
|
+
pause(RETRY_DELAY_MS);
|
|
259
|
+
} else if (holder) {
|
|
260
|
+
rmSync(mutex, { force: true });
|
|
154
261
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
if (isClaimable(inspectLock(outDir))) {
|
|
266
|
+
rmSync(lockPath(outDir), { force: true });
|
|
158
267
|
}
|
|
159
|
-
|
|
160
|
-
rmSync(
|
|
161
|
-
return false;
|
|
268
|
+
} finally {
|
|
269
|
+
rmSync(mutex, { force: true });
|
|
162
270
|
}
|
|
163
271
|
};
|
|
164
272
|
|
|
165
273
|
/**
|
|
166
274
|
* Write the current process's dev lock into `outDir` and return a release
|
|
167
|
-
* function.
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* this process's own leftover is
|
|
171
|
-
*
|
|
172
|
-
*
|
|
275
|
+
* function. Two `blume dev` processes starting at the same moment can't both
|
|
276
|
+
* win: the lock appears whole and exclusively (a hard link, which fails when
|
|
277
|
+
* the file exists), a lock still being written is waited on rather than read
|
|
278
|
+
* as stale, and a stale lock (dead pid) or this process's own leftover is only
|
|
279
|
+
* cleared under the reap mutex — the loser gets a {@link DevLockHeldError}
|
|
280
|
+
* naming the live holder. The release only removes the file if it's still
|
|
281
|
+
* ours, so a newer dev server's lock is never clobbered.
|
|
173
282
|
*/
|
|
174
283
|
export const acquireDevLock = (outDir: string, port?: number): (() => void) => {
|
|
175
284
|
mkdirSync(outDir, { recursive: true });
|
|
176
|
-
|
|
177
|
-
|
|
285
|
+
const payload = lockPayload(port);
|
|
286
|
+
while (!createExclusive(lockPath(outDir), payload)) {
|
|
287
|
+
const state = inspectLock(outDir);
|
|
288
|
+
if (isClaimable(state)) {
|
|
289
|
+
reapStaleLock(outDir);
|
|
290
|
+
} else if (state.kind === "live") {
|
|
291
|
+
throw new DevLockHeldError(state.lock);
|
|
292
|
+
} else if (state.kind === "busy") {
|
|
293
|
+
pause(RETRY_DELAY_MS);
|
|
294
|
+
}
|
|
295
|
+
// "absent": the holder released it just now — race for the claim again.
|
|
178
296
|
}
|
|
179
297
|
let released = false;
|
|
180
298
|
return () => {
|
|
@@ -199,7 +317,9 @@ export const acquireDevLock = (outDir: string, port?: number): (() => void) => {
|
|
|
199
317
|
*/
|
|
200
318
|
export const updateDevLockPort = (outDir: string, port: number): void => {
|
|
201
319
|
if (ownsLock(outDir)) {
|
|
202
|
-
|
|
320
|
+
// Replaced in one step: rewriting in place would leave a moment where a
|
|
321
|
+
// concurrent claim reads an empty lock.
|
|
322
|
+
replaceFile(lockPath(outDir), lockPayload(port));
|
|
203
323
|
}
|
|
204
324
|
};
|
|
205
325
|
|
package/src/cli/report-format.ts
CHANGED
|
@@ -11,12 +11,17 @@ export const seconds = (ms: number): string => `${(ms / 1000).toFixed(1)}s`;
|
|
|
11
11
|
export const money = (cost: number | undefined): string =>
|
|
12
12
|
cost === undefined ? "" : `$${cost.toFixed(2)}`;
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Milliseconds as `12.3s` under a minute, `4m 12s` from there up. Rounding
|
|
16
|
+
* happens before the split, so a value just under a boundary carries over
|
|
17
|
+
* (`59_990` → `1m 0s`, `119_700` → `2m 0s`) instead of printing `60.0s` or
|
|
18
|
+
* `1m 60s`.
|
|
19
|
+
*/
|
|
15
20
|
export const duration = (ms: number): string => {
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
const tenths = Math.round(ms / 100);
|
|
22
|
+
if (tenths < 600) {
|
|
23
|
+
return `${(tenths / 10).toFixed(1)}s`;
|
|
18
24
|
}
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
return `${minutes}m ${rest}s`;
|
|
25
|
+
const total = Math.round(ms / 1000);
|
|
26
|
+
return `${Math.floor(total / 60)}m ${total % 60}s`;
|
|
22
27
|
};
|
package/src/components/colors.ts
CHANGED
|
@@ -49,6 +49,25 @@ export const STROKE = {
|
|
|
49
49
|
yellow: "border-yellow-500/40 text-yellow-800 dark:text-yellow-300",
|
|
50
50
|
} satisfies Record<Hue, string>;
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The theme accent's label color: the accent mixed halfway toward the
|
|
54
|
+
* foreground. The accent is whatever the site configures (a preset, any CSS
|
|
55
|
+
* color, or the neutral black/white default), so no fixed shade can promise
|
|
56
|
+
* the contrast rule above, and the raw accent as text misses it (every preset
|
|
57
|
+
* sits under 3.5:1 over its tint in light mode). Pulling it toward the
|
|
58
|
+
* foreground darkens it in light mode and lightens it in dark, since the
|
|
59
|
+
* foreground contrasts with the page by construction: every preset clears
|
|
60
|
+
* 7:1 over its 15% tint in both modes.
|
|
61
|
+
*/
|
|
62
|
+
const ACCENT_LABEL =
|
|
63
|
+
"text-[color-mix(in_oklab,var(--blume-accent)_50%,var(--blume-foreground))]";
|
|
64
|
+
|
|
65
|
+
/** `TINT` for the theme accent (`<Badge variant="accent">`). */
|
|
66
|
+
export const ACCENT_TINT = `bg-accent/15 ${ACCENT_LABEL}`;
|
|
67
|
+
|
|
68
|
+
/** `STROKE` for the theme accent. */
|
|
69
|
+
export const ACCENT_STROKE = `border-accent/40 ${ACCENT_LABEL}`;
|
|
70
|
+
|
|
52
71
|
/** The neutral fallback every table falls through to. */
|
|
53
72
|
export const MUTED = "bg-muted text-muted-foreground";
|
|
54
73
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { STROKE, TINT } from "../colors.ts";
|
|
2
|
+
import { ACCENT_STROKE, ACCENT_TINT, STROKE, TINT } from "../colors.ts";
|
|
3
3
|
import Icon from "../Icon.astro";
|
|
4
4
|
import { badgeColorName } from "./badge-color.ts";
|
|
5
5
|
|
|
@@ -38,10 +38,11 @@ const isDisabled = disabled === true || disabled === "true";
|
|
|
38
38
|
const colorName = badgeColorName(variant, color);
|
|
39
39
|
const customColor = colorName.startsWith("#") ? colorName : null;
|
|
40
40
|
|
|
41
|
-
// The hue rows come from the shared table (see colors.ts for
|
|
42
|
-
// rule); the neutral and surface rows are Badge's own.
|
|
41
|
+
// The hue and theme-accent rows come from the shared table (see colors.ts for
|
|
42
|
+
// the contrast rule); the neutral and surface rows are Badge's own.
|
|
43
43
|
const filledColorClass: Record<string, string> = {
|
|
44
44
|
...TINT,
|
|
45
|
+
accent: ACCENT_TINT,
|
|
45
46
|
gray: "bg-muted text-muted-foreground",
|
|
46
47
|
surface: "bg-background text-muted-foreground ring-1 ring-border",
|
|
47
48
|
"surface-destructive":
|
|
@@ -53,6 +54,7 @@ const filledColorClass: Record<string, string> = {
|
|
|
53
54
|
|
|
54
55
|
const strokeColorClass: Record<string, string> = {
|
|
55
56
|
...STROKE,
|
|
57
|
+
accent: ACCENT_STROKE,
|
|
56
58
|
gray: "border-border text-muted-foreground",
|
|
57
59
|
surface: "border-border text-muted-foreground",
|
|
58
60
|
"surface-destructive": "border-red-500/40 text-red-700 dark:text-red-300",
|
|
@@ -13,7 +13,7 @@ import data from "blume:data";
|
|
|
13
13
|
import { CODE_PADDING_BLOCK_REM } from "../../theme/code-block-padding.ts";
|
|
14
14
|
|
|
15
15
|
import { highlightCode } from "../../markdown/index.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { withMountedBase } from "../islands/base-path.ts";
|
|
17
17
|
import { contentStrings } from "./content-strings.ts";
|
|
18
18
|
import { EXAMPLE_PANE_MIN_PX } from "./example-pane.ts";
|
|
19
19
|
import Tab from "./Tab.astro";
|
|
@@ -35,7 +35,7 @@ const { content: strings } = contentStrings(data, Astro.currentLocale);
|
|
|
35
35
|
// by their own design system (e.g. shadcn, via the configured `examples.css`).
|
|
36
36
|
// The frame syncs its theme with this document before first paint.
|
|
37
37
|
const previewSrc = entry
|
|
38
|
-
?
|
|
38
|
+
? withMountedBase(
|
|
39
39
|
`${examplesBase}/${path.split("/").map(encodeURIComponent).join("/")}`
|
|
40
40
|
)
|
|
41
41
|
: undefined;
|
|
@@ -237,6 +237,10 @@ const { content: strings } = contentStrings(data, Astro.currentLocale);
|
|
|
237
237
|
}
|
|
238
238
|
trigger.id = `blume-tab-${slug}-${instance}`;
|
|
239
239
|
trigger.setAttribute("aria-controls", panel.id);
|
|
240
|
+
// The role rides with the labelling tab: adopted panels (CodeGroup
|
|
241
|
+
// fences, ts2js pairs, nested groups) have none of their own, and
|
|
242
|
+
// dropdown mode renders no tabs, so its panels take no role.
|
|
243
|
+
panel.setAttribute("role", "tabpanel");
|
|
240
244
|
panel.setAttribute("aria-labelledby", trigger.id);
|
|
241
245
|
trigger.setAttribute(
|
|
242
246
|
"aria-selected",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* The named color each documented Badge `variant` paints with; `accent` is
|
|
3
|
+
* the site's theme accent rather than a fixed hue.
|
|
4
|
+
*/
|
|
2
5
|
const VARIANT_COLOR = new Map([
|
|
3
|
-
["accent", "
|
|
6
|
+
["accent", "accent"],
|
|
4
7
|
["danger", "red"],
|
|
5
8
|
["default", "gray"],
|
|
6
9
|
["success", "green"],
|
|
@@ -1,48 +1,25 @@
|
|
|
1
1
|
import data from "blume:data";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
isInternalPath,
|
|
5
4
|
normalizeBasePath,
|
|
6
|
-
|
|
7
|
-
withComposedBasePath,
|
|
5
|
+
withAuthoredBasePath,
|
|
8
6
|
} from "../../core/base-path.ts";
|
|
9
7
|
import { routeSetFor, servesRoute } from "../../core/locale-links.ts";
|
|
10
8
|
|
|
11
|
-
// Mirrors `markdown/base-links.ts`: a path whose final segment carries a file
|
|
12
|
-
// extension is a `public/` asset, served at the site root and never moved
|
|
13
|
-
// under `basePath` — unless a page is served there (a dotted route like
|
|
14
|
-
// `/releases/v1.2`).
|
|
15
|
-
const DOTTED_PATH = /\.[a-z0-9]+$/iu;
|
|
16
|
-
|
|
17
|
-
/** Strip any `#fragment`/`?query` so only the path is extension-tested. */
|
|
18
|
-
const pathOf = (url: string): string => url.replace(/[#?].*$/u, "");
|
|
19
|
-
|
|
20
|
-
/** Whether an internal `href` links a page rather than a public asset. */
|
|
21
|
-
const isPageHref = (href: string): boolean => {
|
|
22
|
-
const path = pathOf(href);
|
|
23
|
-
return (
|
|
24
|
-
!DOTTED_PATH.test(path) ||
|
|
25
|
-
servesRoute(
|
|
26
|
-
routeSetFor(data.routes),
|
|
27
|
-
withBasePath(data.config.basePath, path)
|
|
28
|
-
)
|
|
29
|
-
);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
9
|
/**
|
|
33
10
|
* Rebase a component-emitted `href` the way `markdown/base-links.ts` rebases
|
|
34
11
|
* `[x](/guide)`: a root-relative internal page link gains the composed
|
|
35
|
-
* `deployment.base` + `basePath` prefix,
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
12
|
+
* `deployment.base` + `basePath` prefix, and a public asset (`/spec.pdf`, with
|
|
13
|
+
* no page served there) the deployment base alone, so authors write component
|
|
14
|
+
* links (`<Card href="/guide">`) under the same "as if mounted at root"
|
|
15
|
+
* contract as markdown links (see `withAuthoredBasePath`). Idempotent per
|
|
16
|
+
* layer (a hand-written `/docs/x` isn't double-prefixed) and inert for
|
|
17
|
+
* external URLs, fragments, and relative paths.
|
|
40
18
|
*/
|
|
41
19
|
export const contentHref = (href: string): string =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
: href;
|
|
20
|
+
withAuthoredBasePath(
|
|
21
|
+
normalizeBasePath(import.meta.env.BASE_URL),
|
|
22
|
+
data.config.basePath,
|
|
23
|
+
href,
|
|
24
|
+
(route) => servesRoute(routeSetFor(data.routes), route)
|
|
25
|
+
);
|
|
@@ -6,7 +6,7 @@ import { createPortal } from "react-dom";
|
|
|
6
6
|
|
|
7
7
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
8
8
|
import { copyText } from "../copy-feedback.ts";
|
|
9
|
-
import { joinBase,
|
|
9
|
+
import { joinBase, mountBase } from "./base-path.ts";
|
|
10
10
|
import { useAssistant } from "./hooks.ts";
|
|
11
11
|
|
|
12
12
|
/** A resolved empty-state prompt; `icon` is ready-to-inline SVG (or null). */
|
|
@@ -65,11 +65,12 @@ const markdown = new Marked({
|
|
|
65
65
|
breaks: true,
|
|
66
66
|
gfm: true,
|
|
67
67
|
// The model cites pages as base-less logical routes (`[Title](/route)`); rewrite
|
|
68
|
-
// link targets to served URLs so citations resolve under `deployment.base
|
|
69
|
-
//
|
|
68
|
+
// link targets to served URLs so citations resolve under `deployment.base`,
|
|
69
|
+
// even a route that starts with the base's own name. `mountBase` leaves
|
|
70
|
+
// external URLs and fragments untouched.
|
|
70
71
|
walkTokens: (token) => {
|
|
71
72
|
if (token.type === "link") {
|
|
72
|
-
token.href =
|
|
73
|
+
token.href = mountBase(import.meta.env.BASE_URL, token.href);
|
|
73
74
|
}
|
|
74
75
|
},
|
|
75
76
|
});
|
|
@@ -262,6 +263,22 @@ const Assistant = ({
|
|
|
262
263
|
};
|
|
263
264
|
}, [open]);
|
|
264
265
|
|
|
266
|
+
// Stamp the incoming body before the swap installs it, as the theme and
|
|
267
|
+
// banner scripts do for <html>. Stamped only after the swap (below, in a
|
|
268
|
+
// passive effect), the new body would first paint without the push padding,
|
|
269
|
+
// replaying its transition on every navigation with the panel docked.
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
if (!open) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const onBeforeSwap = (event: DocumentEventMap["astro:before-swap"]) => {
|
|
275
|
+
event.newDocument.body.dataset.blumeAssistant = "open";
|
|
276
|
+
};
|
|
277
|
+
document.addEventListener("astro:before-swap", onBeforeSwap);
|
|
278
|
+
return () =>
|
|
279
|
+
document.removeEventListener("astro:before-swap", onBeforeSwap);
|
|
280
|
+
}, [open]);
|
|
281
|
+
|
|
265
282
|
// Re-stamp the push attribute after a swap while the panel is open — the new
|
|
266
283
|
// body arrives without it. Deliberately separate from the effect above: a
|
|
267
284
|
// navigation must not re-run the focus handling and yank focus out of the
|