blume 2.1.0 → 2.1.1
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 +94 -0
- package/dist/cli/{chunk-5cg2rzgf.js → chunk-073rdvgb.js} +19 -48
- package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-073rdvgb.js.map} +4 -5
- package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
- package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
- package/dist/cli/chunk-150jk7km.js +52 -0
- package/dist/cli/chunk-150jk7km.js.map +10 -0
- package/dist/cli/{chunk-rekqz4qg.js → chunk-313vnn40.js} +16 -15
- package/dist/cli/chunk-313vnn40.js.map +10 -0
- package/dist/cli/{chunk-gp84za0h.js → chunk-3v3ffbrj.js} +2 -2
- package/dist/cli/{chunk-9xr4dgee.js → chunk-3x6x3pph.js} +29 -16
- package/dist/cli/chunk-3x6x3pph.js.map +10 -0
- package/dist/cli/{chunk-0gz36akr.js → chunk-42t4jxdx.js} +21 -5
- package/dist/cli/chunk-42t4jxdx.js.map +12 -0
- package/dist/cli/{chunk-ta27feev.js → chunk-54ztw1nj.js} +21 -2
- package/dist/cli/chunk-54ztw1nj.js.map +10 -0
- package/dist/cli/{chunk-jnwj5kjj.js → chunk-5vjtgs6e.js} +20 -7
- package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-5vjtgs6e.js.map} +3 -3
- package/dist/cli/chunk-66w9j7p6.js +322 -0
- package/dist/cli/chunk-66w9j7p6.js.map +14 -0
- package/dist/cli/{chunk-nd1821sk.js → chunk-7ppf1rva.js} +32 -18
- package/dist/cli/chunk-7ppf1rva.js.map +10 -0
- package/dist/cli/{chunk-p804f3pe.js → chunk-8y2kmjv8.js} +6 -6
- package/dist/cli/{chunk-2f7n4qzt.js → chunk-c1y538kd.js} +1 -1
- package/dist/cli/{chunk-1n228xqb.js → chunk-c887fj0s.js} +2 -2
- package/dist/cli/{chunk-y5h4r2y0.js → chunk-cfyg47zg.js} +15 -5
- package/dist/cli/chunk-cfyg47zg.js.map +10 -0
- package/dist/cli/{chunk-cmtcnh9f.js → chunk-dzwehpy7.js} +2 -2
- package/dist/cli/{chunk-h2a48yhd.js → chunk-e6atmn4h.js} +119 -15
- package/dist/cli/{chunk-h2a48yhd.js.map → chunk-e6atmn4h.js.map} +10 -10
- package/dist/cli/{chunk-5ffywsmb.js → chunk-e8xw9qkh.js} +8 -6
- package/dist/cli/{chunk-5ffywsmb.js.map → chunk-e8xw9qkh.js.map} +3 -3
- package/dist/cli/{chunk-ntv4qrt7.js → chunk-e9q7a2ak.js} +14 -14
- package/dist/cli/{chunk-xkz5xbtv.js → chunk-ehweg4fr.js} +2366 -1114
- package/dist/cli/chunk-ehweg4fr.js.map +218 -0
- package/dist/cli/{chunk-zf748avv.js → chunk-ep6nd0rn.js} +176 -281
- package/dist/cli/{chunk-zf748avv.js.map → chunk-ep6nd0rn.js.map} +11 -12
- package/dist/cli/{chunk-yxmk4pec.js → chunk-fdrkhpjy.js} +16 -15
- package/dist/cli/{chunk-yxmk4pec.js.map → chunk-fdrkhpjy.js.map} +3 -3
- package/dist/cli/{chunk-5wj2qwn2.js → chunk-ft9vfys6.js} +4 -4
- package/dist/cli/{chunk-4h8z9vv5.js → chunk-gb420vvz.js} +89 -46
- package/dist/cli/chunk-gb420vvz.js.map +17 -0
- package/dist/cli/{chunk-713mngdq.js → chunk-hebn0tpx.js} +76 -10
- package/dist/cli/chunk-hebn0tpx.js.map +14 -0
- package/dist/cli/{chunk-2n4ps1b3.js → chunk-htrwgabr.js} +40 -22
- package/dist/cli/chunk-htrwgabr.js.map +15 -0
- package/dist/cli/{chunk-zhvd6adm.js → chunk-hx3xqnk9.js} +13 -13
- package/dist/cli/chunk-j134vrp2.js +65 -0
- package/dist/cli/chunk-j134vrp2.js.map +10 -0
- package/dist/cli/{chunk-y4ykbark.js → chunk-kma9f6sf.js} +2 -2
- package/dist/cli/{chunk-py62fewh.js → chunk-mrcccgnh.js} +3 -3
- package/dist/cli/{chunk-2f3ndkpw.js → chunk-n0v7m8ry.js} +3 -3
- package/dist/cli/{chunk-43zcf4gz.js → chunk-ncj7w596.js} +522 -353
- package/dist/cli/chunk-ncj7w596.js.map +35 -0
- package/dist/cli/{chunk-9geymqns.js → chunk-nq1d20w2.js} +14 -14
- package/dist/cli/{chunk-g2z9x01v.js → chunk-nrns10mj.js} +3 -3
- package/dist/cli/{chunk-e7vvk5ya.js → chunk-sds923fa.js} +3 -3
- package/dist/cli/{chunk-cy5bm0j0.js → chunk-t2m5j4pr.js} +2 -2
- package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-t2m5j4pr.js.map} +1 -1
- package/dist/cli/{chunk-yj17vs39.js → chunk-tdqhyxfz.js} +5 -5
- package/dist/cli/{chunk-q3wxxq39.js → chunk-tphac74f.js} +5 -5
- package/dist/cli/{chunk-6bm0xpgd.js → chunk-w1r4vase.js} +2 -2
- package/dist/cli/index.js +24 -19
- package/dist/cli/index.js.map +3 -3
- package/dist/types/ai/ask-context.d.ts +6 -2
- package/dist/types/ai/component-markdown.d.ts +2 -0
- package/dist/types/ai/openapi-components.d.ts +4 -1
- package/dist/types/astro/integration.d.ts +5 -4
- package/dist/types/astro/markdown-negotiation.d.ts +3 -0
- package/dist/types/astro/pages.d.ts +10 -0
- package/dist/types/cli/init/scaffold.d.ts +1 -1
- package/dist/types/components/content/api-field.d.ts +4 -1
- package/dist/types/components/content/api-page.d.ts +6 -1
- package/dist/types/components/openapi/code-samples.d.ts +29 -0
- package/dist/types/components/openapi/helpers.d.ts +22 -1
- package/dist/types/components/openapi/security.d.ts +10 -0
- package/dist/types/components/openapi/snippets.d.ts +60 -0
- package/dist/types/core/changelog-index.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +30 -3
- package/dist/types/core/data.d.ts +4 -0
- package/dist/types/core/i18n-ui.d.ts +2 -0
- package/dist/types/core/links.d.ts +8 -0
- package/dist/types/core/probe.d.ts +2 -0
- package/dist/types/core/project-graph.d.ts +6 -0
- package/dist/types/core/redirect-patterns.d.ts +11 -3
- package/dist/types/core/schema.d.ts +119 -0
- package/dist/types/core/sources/cache.d.ts +3 -0
- package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
- package/dist/types/core/sources/contentful.d.ts +17 -1
- package/dist/types/core/sources/lexical.d.ts +5 -0
- package/dist/types/core/sources/normalize.d.ts +20 -1
- package/dist/types/core/sources/remote.d.ts +37 -10
- package/dist/types/core/sources/sanity.d.ts +1 -1
- package/dist/types/core/types.d.ts +17 -0
- package/dist/types/deploy/adapter-output.d.ts +2 -1
- package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
- package/dist/types/deploy/adapters/node.d.ts +58 -23
- package/dist/types/deploy/adapters/registry.d.ts +10 -0
- package/dist/types/deploy/adapters/types.d.ts +3 -2
- package/dist/types/deploy/platforms/cloudflare.d.ts +10 -7
- package/dist/types/deploy/platforms/node.d.ts +3 -1
- package/dist/types/deploy/platforms/types.d.ts +18 -7
- package/dist/types/deploy/platforms/vercel.d.ts +20 -1
- package/dist/types/deploy/redirects.d.ts +17 -1
- package/dist/types/deploy/sitemap.d.ts +2 -1
- package/dist/types/deploy/vercel-negotiation.d.ts +42 -21
- package/dist/types/openapi/checks.d.ts +27 -0
- package/dist/types/openapi/graphql.d.ts +6 -0
- package/dist/types/openapi/model.d.ts +15 -4
- package/dist/types/openapi/parse.d.ts +3 -0
- package/dist/types/openapi/references.d.ts +16 -0
- package/dist/types/ratelimit/index.d.ts +1 -1
- package/dist/types/ratelimit/schema.d.ts +8 -0
- package/dist/types/ratelimit/upstash.d.ts +39 -14
- package/dist/types/reference/graphql.d.ts +76 -1
- package/dist/types/reference/index.d.ts +1 -1
- package/dist/types/reference/openapi.d.ts +2 -2
- package/dist/types/reference/options.d.ts +37 -1
- package/dist/types/reference/schema.d.ts +84 -0
- package/dist/types/search/adapters/registry.d.ts +3 -0
- package/dist/types/search/adapters/typesense.d.ts +14 -4
- package/dist/types/search/build.d.ts +3 -1
- package/dist/types/search/orama-index.d.ts +9 -9
- package/dist/types/search/sync/algolia.d.ts +5 -2
- package/dist/types/search/sync/index.d.ts +3 -1
- package/dist/types/search/sync/typesense.d.ts +11 -7
- package/dist/types/sources/contentful.d.ts +16 -0
- package/dist/types/sources/github-releases.d.ts +8 -1
- package/dist/types/sources/payload.d.ts +2 -0
- package/dist/types/sources/registry.d.ts +54 -0
- package/dist/types/sources/shared.d.ts +3 -0
- package/dist/types/sources/strapi.d.ts +2 -0
- package/docs/01-quickstart.mdx +4 -2
- package/docs/02-deployment.mdx +31 -13
- package/docs/advanced/changelog.mdx +11 -0
- package/docs/advanced/skills.mdx +1 -1
- package/docs/cli/audit.mdx +30 -7
- package/docs/cli/doctor.mdx +5 -1
- package/docs/cli/evals.mdx +4 -4
- package/docs/cli/translate.mdx +1 -0
- package/docs/cli/validate.mdx +4 -3
- package/docs/configuration/analytics.mdx +1 -1
- package/docs/configuration/assistant.mdx +6 -2
- package/docs/configuration/customization.mdx +4 -2
- package/docs/configuration/export.mdx +5 -3
- package/docs/configuration/index.mdx +3 -2
- package/docs/configuration/rate-limiting.mdx +23 -2
- package/docs/configuration/search.mdx +37 -14
- package/docs/content/components.mdx +2 -2
- package/docs/content/frontmatter.mdx +5 -3
- package/docs/content/i18n.mdx +2 -2
- package/docs/content/includes.mdx +1 -1
- package/docs/content/navigation.mdx +4 -4
- package/docs/content/sources/contentful.mdx +7 -4
- package/docs/content/sources/custom.mdx +7 -3
- package/docs/content/sources/github-releases.mdx +10 -2
- package/docs/content/sources/index.mdx +3 -3
- package/docs/content/sources/notion.mdx +1 -1
- package/docs/content/sources/obsidian.mdx +1 -1
- package/docs/content/sources/payload.mdx +3 -2
- package/docs/content/sources/remote-mdx.mdx +4 -0
- package/docs/content/sources/sanity.mdx +2 -2
- package/docs/content/sources/strapi.mdx +3 -2
- package/docs/content/syntax.mdx +2 -2
- package/docs/discoverability/agent-discovery.mdx +1 -1
- package/docs/discoverability/index.mdx +1 -1
- package/docs/discoverability/json-api.mdx +2 -0
- package/docs/discoverability/llms-txt.mdx +1 -1
- package/docs/discoverability/markdown.mdx +3 -1
- package/docs/discoverability/mcp.mdx +4 -0
- package/docs/discoverability/sitemap-and-robots.mdx +1 -1
- package/docs/references/api-pages.mdx +1 -1
- package/docs/references/asyncapi.mdx +3 -1
- package/docs/references/graphql.mdx +13 -1
- package/docs/references/openapi.mdx +50 -8
- package/package.json +9 -9
- package/skills/blume-migrate/SKILL.md +1 -1
- package/skills/blume-migrate/references/docusaurus.md +4 -4
- package/skills/blume-migrate/references/fumadocs.md +17 -13
- package/skills/blume-migrate/references/nextra.md +8 -5
- package/skills/blume-migrate/references/starlight.md +9 -7
- package/src/ai/ask-context.ts +33 -3
- package/src/ai/ask-limits.ts +13 -0
- package/src/ai/changelog-markdown.ts +11 -6
- package/src/ai/component-markdown.ts +1 -1
- package/src/ai/cors.ts +12 -3
- package/src/ai/mcp/query.ts +14 -6
- package/src/ai/openapi-components.ts +28 -4
- package/src/ai/relative-links.ts +4 -38
- package/src/astro/generate.ts +78 -48
- package/src/astro/integration.ts +16 -13
- package/src/astro/markdown-negotiation.ts +3 -0
- package/src/astro/module-types.ts +3 -1
- package/src/astro/pages.ts +23 -0
- package/src/astro/templates.ts +125 -80
- package/src/audit/catalog.ts +19 -3
- package/src/audit/checks/links.ts +14 -1
- package/src/audit/checks/network.ts +110 -6
- package/src/audit/checks/redirects.ts +6 -4
- package/src/audit/checks/robots.ts +58 -2
- package/src/audit/run.ts +3 -0
- package/src/audit/types.ts +2 -0
- package/src/audit/url.ts +14 -5
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/args.ts +51 -0
- package/src/cli/build-failure.ts +23 -0
- package/src/cli/commands/audit.ts +10 -1
- package/src/cli/commands/dev.ts +18 -2
- package/src/cli/commands/doctor.ts +45 -12
- package/src/cli/commands/eval.ts +36 -4
- package/src/cli/commands/init.ts +6 -4
- package/src/cli/commands/validate.ts +10 -1
- package/src/cli/init/scaffold.ts +3 -3
- package/src/cli/prepare.ts +6 -3
- package/src/cli/required-secrets.ts +1 -1
- package/src/components/content/ApiPlayground.astro +6 -2
- package/src/components/content/api-field.ts +4 -1
- package/src/components/content/api-page.ts +31 -7
- package/src/components/content/mermaid-element.ts +15 -1
- package/src/components/islands/hooks.ts +27 -1
- package/src/components/layout/PageActions.astro +139 -24
- package/src/components/layout/RootLayout.astro +10 -2
- package/src/components/layout/Search.astro +17 -3
- package/src/components/layout/TableOfContents.astro +24 -17
- package/src/components/layout/head-scripts.ts +6 -1
- package/src/components/layout/search/algolia.ts +10 -1
- package/src/components/layout/search/analytics.ts +42 -14
- package/src/components/layout/search/endpoint.ts +48 -5
- package/src/components/layout/search/pagefind.ts +114 -5
- package/src/components/layout/search/types.ts +12 -0
- package/src/components/layout/search/typesense.ts +18 -6
- package/src/components/openapi/AsyncApiOperation.astro +14 -7
- package/src/components/openapi/Authorization.astro +10 -0
- package/src/components/openapi/GraphqlOperation.astro +5 -0
- package/src/components/openapi/Operation.astro +24 -1
- package/src/components/openapi/ParametersTable.astro +8 -4
- package/src/components/openapi/Playground.astro +7 -1
- package/src/components/openapi/RequestBody.astro +19 -2
- package/src/components/openapi/Responses.astro +1 -0
- package/src/components/openapi/SchemaTable.astro +15 -3
- package/src/components/openapi/async-snippets.ts +61 -5
- package/src/components/openapi/async.ts +123 -29
- package/src/components/openapi/graphql-helpers.ts +35 -12
- package/src/components/openapi/helpers.ts +36 -2
- package/src/components/openapi/message-model.ts +54 -11
- package/src/components/openapi/message.ts +14 -4
- package/src/components/openapi/operation-model.ts +16 -7
- package/src/components/openapi/playground-client.ts +24 -5
- package/src/components/openapi/playground-schema.ts +1 -1
- package/src/components/openapi/schema-tree.ts +44 -14
- package/src/components/openapi/security.ts +10 -0
- package/src/components/openapi/snippets.ts +5 -3
- package/src/components/openapi/validate-json.ts +4 -3
- package/src/core/changelog-index.ts +31 -1
- package/src/core/component-diagnostics.ts +53 -0
- package/src/core/component-overrides.ts +32 -0
- package/src/core/config-input.ts +31 -3
- package/src/core/data.ts +2 -0
- package/src/core/diagnostics.ts +5 -0
- package/src/core/i18n-ui.ts +2 -0
- package/src/core/includes.ts +1 -1
- package/src/core/links.ts +47 -17
- package/src/core/probe.ts +26 -6
- package/src/core/project-graph.ts +59 -18
- package/src/core/redirect-patterns.ts +36 -7
- package/src/core/schema.ts +21 -2
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/contentful-rich-text.ts +29 -6
- package/src/core/sources/contentful.ts +135 -25
- package/src/core/sources/filesystem.ts +26 -4
- package/src/core/sources/github-releases.ts +12 -1
- package/src/core/sources/lexical.ts +15 -10
- package/src/core/sources/mdx-remote.ts +62 -10
- package/src/core/sources/normalize.ts +227 -41
- package/src/core/sources/notion.ts +7 -1
- package/src/core/sources/obsidian.ts +16 -3
- package/src/core/sources/payload.ts +38 -15
- package/src/core/sources/remote.ts +72 -15
- package/src/core/sources/resolve.ts +11 -1
- package/src/core/sources/sanity.ts +43 -25
- package/src/core/sources/strapi.ts +2 -2
- package/src/core/types.ts +18 -0
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +1 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/deploy/adapter-output.ts +7 -2
- package/src/deploy/adapters/cloudflare.ts +7 -5
- package/src/deploy/adapters/node.ts +47 -9
- package/src/deploy/adapters/types.ts +7 -3
- package/src/deploy/artifacts.ts +2 -1
- package/src/deploy/platforms/cloudflare.ts +13 -9
- package/src/deploy/platforms/netlify.ts +2 -1
- package/src/deploy/platforms/node.ts +12 -2
- package/src/deploy/platforms/types.ts +21 -7
- package/src/deploy/platforms/vercel.ts +113 -16
- package/src/deploy/redirects.ts +66 -6
- package/src/deploy/rss.ts +3 -1
- package/src/deploy/sitemap.ts +39 -5
- package/src/deploy/vercel-negotiation.ts +290 -102
- package/src/eval/agents.ts +18 -1
- package/src/eval/prompts.ts +6 -3
- package/src/eval/report.ts +34 -13
- package/src/eval/run.ts +5 -0
- package/src/markdown/relative-links.ts +3 -2
- package/src/openapi/checks.ts +273 -0
- package/src/openapi/graphql-build.ts +21 -0
- package/src/openapi/graphql.ts +11 -0
- package/src/openapi/model.ts +15 -3
- package/src/openapi/parse.ts +127 -2
- package/src/openapi/references.ts +43 -3
- package/src/openapi/source.ts +39 -14
- package/src/ratelimit/index.ts +1 -1
- package/src/ratelimit/runtime.ts +37 -19
- package/src/ratelimit/upstash.ts +40 -10
- package/src/reference/graphql.ts +15 -1
- package/src/reference/index.ts +1 -0
- package/src/reference/openapi.ts +2 -2
- package/src/reference/options.ts +29 -1
- package/src/registry/eject.ts +3 -1
- package/src/search/adapters/locale-scope.ts +30 -0
- package/src/search/adapters/typesense.ts +14 -4
- package/src/search/build.ts +21 -3
- package/src/search/orama-index.ts +163 -31
- package/src/search/source-pages.ts +29 -0
- package/src/search/sync/algolia.ts +99 -9
- package/src/search/sync/index.ts +52 -13
- package/src/search/sync/typesense.ts +72 -27
- package/src/sources/contentful.ts +12 -0
- package/src/sources/github-releases.ts +7 -1
- package/src/sources/shared.ts +3 -0
- package/src/theme/entry.ts +29 -3
- package/src/translate/validate.ts +23 -1
- package/dist/cli/chunk-0gz36akr.js.map +0 -12
- package/dist/cli/chunk-2n4ps1b3.js.map +0 -15
- package/dist/cli/chunk-43zcf4gz.js.map +0 -36
- package/dist/cli/chunk-4ejxbzee.js +0 -39
- package/dist/cli/chunk-4ejxbzee.js.map +0 -10
- package/dist/cli/chunk-4h8z9vv5.js.map +0 -16
- package/dist/cli/chunk-713mngdq.js.map +0 -14
- package/dist/cli/chunk-9xr4dgee.js.map +0 -10
- package/dist/cli/chunk-amrb6hgh.js +0 -145
- package/dist/cli/chunk-amrb6hgh.js.map +0 -11
- package/dist/cli/chunk-nd1821sk.js.map +0 -10
- package/dist/cli/chunk-rekqz4qg.js.map +0 -10
- package/dist/cli/chunk-ta27feev.js.map +0 -10
- package/dist/cli/chunk-xkz5xbtv.js.map +0 -213
- package/dist/cli/chunk-y5h4r2y0.js.map +0 -10
- /package/dist/cli/{chunk-gp84za0h.js.map → chunk-3v3ffbrj.js.map} +0 -0
- /package/dist/cli/{chunk-p804f3pe.js.map → chunk-8y2kmjv8.js.map} +0 -0
- /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-c1y538kd.js.map} +0 -0
- /package/dist/cli/{chunk-1n228xqb.js.map → chunk-c887fj0s.js.map} +0 -0
- /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-dzwehpy7.js.map} +0 -0
- /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-e9q7a2ak.js.map} +0 -0
- /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-ft9vfys6.js.map} +0 -0
- /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-hx3xqnk9.js.map} +0 -0
- /package/dist/cli/{chunk-y4ykbark.js.map → chunk-kma9f6sf.js.map} +0 -0
- /package/dist/cli/{chunk-py62fewh.js.map → chunk-mrcccgnh.js.map} +0 -0
- /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-n0v7m8ry.js.map} +0 -0
- /package/dist/cli/{chunk-9geymqns.js.map → chunk-nq1d20w2.js.map} +0 -0
- /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-nrns10mj.js.map} +0 -0
- /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-sds923fa.js.map} +0 -0
- /package/dist/cli/{chunk-yj17vs39.js.map → chunk-tdqhyxfz.js.map} +0 -0
- /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-tphac74f.js.map} +0 -0
- /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-w1r4vase.js.map} +0 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { normalizeBasePath } from "../../core/base-path.ts";
|
|
1
|
+
import { normalizeBasePath, stripBasePath } from "../../core/base-path.ts";
|
|
2
2
|
import { gradeExternal, probeAll } from "../../core/probe.ts";
|
|
3
3
|
import type { ProbeResult } from "../../core/probe.ts";
|
|
4
4
|
import type { Diagnostic } from "../../core/types.ts";
|
|
5
5
|
import { finding } from "../catalog.ts";
|
|
6
6
|
import { pageSite } from "../locate.ts";
|
|
7
|
-
import type {
|
|
8
|
-
|
|
7
|
+
import type {
|
|
8
|
+
AuditContext,
|
|
9
|
+
CheckModule,
|
|
10
|
+
PageSnapshot,
|
|
11
|
+
RedirectResolution,
|
|
12
|
+
} from "../types.ts";
|
|
13
|
+
import { decodePath, normalizePath, resolveHref, siteOrigin } from "../url.ts";
|
|
9
14
|
|
|
10
15
|
const CLIENT_ERROR = 400;
|
|
11
16
|
const SERVER_ERROR = 500;
|
|
@@ -135,6 +140,79 @@ export const servedPageChecks = (
|
|
|
135
140
|
return found;
|
|
136
141
|
};
|
|
137
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The configured redirects the live site should answer: only those the static
|
|
145
|
+
* checks passed. A pattern (`/beta/:slug*`) names no single URL to request, a
|
|
146
|
+
* loop never lands, and a redirect whose source is also a page never fires.
|
|
147
|
+
*/
|
|
148
|
+
const liveRedirects = (context: AuditContext): RedirectResolution[] =>
|
|
149
|
+
context.redirects.filter(
|
|
150
|
+
(redirect) =>
|
|
151
|
+
(redirect.outcome === "ok" || redirect.outcome === "chain") &&
|
|
152
|
+
!context.byUrl.has(normalizePath(redirect.from))
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* What requesting a redirect's old URL on the live site got wrong, or null
|
|
157
|
+
* when it redirected to the configured destination.
|
|
158
|
+
*
|
|
159
|
+
* The build writes the host's redirect file, but a host that doesn't read it
|
|
160
|
+
* answers with the meta-refresh page Astro writes at the old URL (a 200:
|
|
161
|
+
* readers still get there, so it's a warning), and a missing redirect 404s (an
|
|
162
|
+
* error). An external destination isn't compared: its site may redirect again.
|
|
163
|
+
*/
|
|
164
|
+
export const liveRedirectCheck = (
|
|
165
|
+
context: AuditContext,
|
|
166
|
+
redirect: RedirectResolution,
|
|
167
|
+
result: ProbeResult,
|
|
168
|
+
deployBase: string
|
|
169
|
+
): Diagnostic | null => {
|
|
170
|
+
const from = normalizePath(redirect.from);
|
|
171
|
+
const site = {
|
|
172
|
+
file: context.project.context.configFile ?? undefined,
|
|
173
|
+
url: from,
|
|
174
|
+
};
|
|
175
|
+
const landed =
|
|
176
|
+
result.redirected && result.finalUrl
|
|
177
|
+
? normalizePath(
|
|
178
|
+
stripBasePath(
|
|
179
|
+
deployBase,
|
|
180
|
+
decodePath(new URL(result.finalUrl).pathname)
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
: from;
|
|
184
|
+
|
|
185
|
+
// A host that only adds a trailing slash (`/old` → `/old/`) still didn't
|
|
186
|
+
// redirect to the destination.
|
|
187
|
+
if (landed === from) {
|
|
188
|
+
let answer = `could not be reached (${result.error ?? "no response"})`;
|
|
189
|
+
if (result.timedOut) {
|
|
190
|
+
answer = "did not respond in time";
|
|
191
|
+
} else if (result.status !== undefined) {
|
|
192
|
+
answer = `answered it with HTTP ${result.status} instead`;
|
|
193
|
+
}
|
|
194
|
+
const found = finding(
|
|
195
|
+
"BLUME_AUDIT_REDIRECT_NOT_SERVED",
|
|
196
|
+
site,
|
|
197
|
+
`${from} should redirect to ${redirect.to}, but the live site ${answer}.`
|
|
198
|
+
);
|
|
199
|
+
return result.ok ? { ...found, severity: "warning" } : found;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const destination = redirect.chain.at(-1) ?? from;
|
|
203
|
+
if (/^https?:\/\//iu.test(destination) || landed === destination) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
...finding(
|
|
208
|
+
"BLUME_AUDIT_REDIRECT_NOT_SERVED",
|
|
209
|
+
site,
|
|
210
|
+
`${from} redirects to ${landed} on the live site, not to ${destination}.`
|
|
211
|
+
),
|
|
212
|
+
severity: "warning",
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
138
216
|
/**
|
|
139
217
|
* The built site, checked against a live deployment.
|
|
140
218
|
*
|
|
@@ -164,8 +242,23 @@ export const networkChecks: CheckModule = {
|
|
|
164
242
|
// build output, which the host serves under the deployment base.
|
|
165
243
|
const robotsUrl = new URL(`${deployBase}/robots.txt`, origin).toString();
|
|
166
244
|
const sitemapUrl = new URL(`${deployBase}/sitemap.xml`, origin).toString();
|
|
245
|
+
// Each configured redirect is requested at its old URL, the way a reader
|
|
246
|
+
// following an old link arrives. Its `from` carries `basePath`; the host
|
|
247
|
+
// serves it under the deployment base too.
|
|
248
|
+
const redirects = liveRedirects(context).map(
|
|
249
|
+
(redirect) =>
|
|
250
|
+
[
|
|
251
|
+
redirect,
|
|
252
|
+
new URL(`${deployBase}${redirect.from}`, origin).toString(),
|
|
253
|
+
] as const
|
|
254
|
+
);
|
|
167
255
|
|
|
168
|
-
const results = await probeAll([
|
|
256
|
+
const results = await probeAll([
|
|
257
|
+
...targets,
|
|
258
|
+
robotsUrl,
|
|
259
|
+
sitemapUrl,
|
|
260
|
+
...redirects.map(([, url]) => url),
|
|
261
|
+
]);
|
|
169
262
|
|
|
170
263
|
for (const page of context.pages) {
|
|
171
264
|
const result = results.get(liveUrl(origin, page, deployBase));
|
|
@@ -180,6 +273,16 @@ export const networkChecks: CheckModule = {
|
|
|
180
273
|
found.push(...servedPageChecks(context, page, result, origin));
|
|
181
274
|
}
|
|
182
275
|
|
|
276
|
+
for (const [redirect, url] of redirects) {
|
|
277
|
+
// SAFETY: every redirect URL was in the probed list, and `probeAll`
|
|
278
|
+
// returns a result for each URL it was given.
|
|
279
|
+
const result = results.get(url) as ProbeResult;
|
|
280
|
+
const wrong = liveRedirectCheck(context, redirect, result, deployBase);
|
|
281
|
+
if (wrong) {
|
|
282
|
+
found.push(wrong);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
183
286
|
// With `seo.robots: false` Blume writes no robots.txt, so its absence on
|
|
184
287
|
// the live site is the configured outcome, not a defect.
|
|
185
288
|
const robots = results.get(robotsUrl);
|
|
@@ -210,7 +313,8 @@ export const networkChecks: CheckModule = {
|
|
|
210
313
|
};
|
|
211
314
|
|
|
212
315
|
/**
|
|
213
|
-
* Outbound links, probed over the network (`--external`)
|
|
316
|
+
* Outbound links, probed over the network (`--external`), except those an
|
|
317
|
+
* `--ignore` glob matches.
|
|
214
318
|
*
|
|
215
319
|
* Severity is graded rather than flat: a 404 is the author's bug, but a 403 or a
|
|
216
320
|
* 5xx is usually rate limiting or someone else's outage, and failing a build on
|
|
@@ -229,7 +333,7 @@ export const externalChecks: CheckModule = {
|
|
|
229
333
|
for (const page of context.pages) {
|
|
230
334
|
for (const link of page.links) {
|
|
231
335
|
const resolved = resolveHref(page.url, link.href, origin, deployBase);
|
|
232
|
-
if (resolved.kind !== "external") {
|
|
336
|
+
if (resolved.kind !== "external" || context.ignore(resolved.url)) {
|
|
233
337
|
continue;
|
|
234
338
|
}
|
|
235
339
|
const pages = linkers.get(resolved.url);
|
|
@@ -23,15 +23,17 @@ export const redirectChecks: CheckModule = {
|
|
|
23
23
|
for (const redirect of context.redirects) {
|
|
24
24
|
const from = normalizePath(redirect.from);
|
|
25
25
|
|
|
26
|
-
// A redirect whose source is also a built page
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// A redirect whose source is also a built page: one of the two never
|
|
27
|
+
// serves, and which depends on the host (a static layer that answers
|
|
28
|
+
// files first keeps the page; a router that runs redirects first keeps
|
|
29
|
+
// the redirect). Ahrefs can't see this: it only observes the served
|
|
30
|
+
// response, which looks perfectly healthy.
|
|
29
31
|
if (context.byUrl.has(from)) {
|
|
30
32
|
found.push(
|
|
31
33
|
finding(
|
|
32
34
|
"BLUME_AUDIT_REDIRECT_SOURCE_IS_PAGE",
|
|
33
35
|
{ ...site, url: from },
|
|
34
|
-
`A redirect is configured from ${from}, but ${from} is also a real page — the
|
|
36
|
+
`A redirect is configured from ${from}, but ${from} is also a real page — readers get one or the other, depending on the host.`
|
|
35
37
|
)
|
|
36
38
|
);
|
|
37
39
|
continue;
|
|
@@ -12,9 +12,30 @@ import { normalizePath } from "../url.ts";
|
|
|
12
12
|
*/
|
|
13
13
|
const MATCH_ORIGIN = "https://robots-audit.invalid";
|
|
14
14
|
|
|
15
|
+
const USER_AGENT_LINE = /^\s*user-agent\s*:\s*(?<agent>[^#]*)/iu;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The crawlers robots.txt names in a `User-agent` line, other than `*`, as
|
|
19
|
+
* written (first spelling wins). robots-parser matches a group by the token
|
|
20
|
+
* before any `/version`, case-insensitively, so that's what dedupes them.
|
|
21
|
+
*/
|
|
22
|
+
const namedAgents = (raw: string): string[] => {
|
|
23
|
+
const agents = new Map<string, string>();
|
|
24
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
25
|
+
const agent = USER_AGENT_LINE.exec(line)?.groups?.agent?.trim() ?? "";
|
|
26
|
+
const key = agent.toLowerCase().split("/")[0]?.trim() ?? "";
|
|
27
|
+
if (key && key !== "*" && !agents.has(key)) {
|
|
28
|
+
agents.set(key, agent);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [...agents.values()];
|
|
32
|
+
};
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
35
|
* robots.txt: is it there, is it well-formed, does it point at the sitemap, and
|
|
17
36
|
* — the one that matters — does it block a page the sitemap is advertising?
|
|
37
|
+
* Blocking every crawler (`User-agent: *`) is an error; a group aimed at one
|
|
38
|
+
* crawler is a warning, since shutting out an AI crawler is often on purpose.
|
|
18
39
|
*
|
|
19
40
|
* Ahrefs also tracks "robots.txt has too many redirects". A static host serves
|
|
20
41
|
* the file directly, so that is effectively unreachable here and isn't checked.
|
|
@@ -70,6 +91,14 @@ export const robotsChecks: CheckModule = {
|
|
|
70
91
|
// consecutive User-agent lines form one group as the spec requires.
|
|
71
92
|
const parser = robotsParser(`${MATCH_ORIGIN}/robots.txt`, robots.raw);
|
|
72
93
|
const lines = robots.raw.split(/\r?\n/u);
|
|
94
|
+
const ruleAt = (url: string, agent: string) => {
|
|
95
|
+
const line = parser.getMatchingLineNumber(url, agent);
|
|
96
|
+
return { line, rule: line > 0 ? lines[line - 1]?.trim() : undefined };
|
|
97
|
+
};
|
|
98
|
+
// A crawler named in its own group follows only that group, so a rule
|
|
99
|
+
// there can block it from a page every other crawler may read.
|
|
100
|
+
const agents = namedAgents(robots.raw);
|
|
101
|
+
const blockedFor = new Map<string, string[]>();
|
|
73
102
|
for (const loc of context.sitemap?.urls ?? []) {
|
|
74
103
|
let pathname: string;
|
|
75
104
|
try {
|
|
@@ -82,8 +111,7 @@ export const robotsChecks: CheckModule = {
|
|
|
82
111
|
const path = normalizePath(pathname);
|
|
83
112
|
const url = `${MATCH_ORIGIN}${pathname}`;
|
|
84
113
|
if (parser.isDisallowed(url, "*")) {
|
|
85
|
-
const
|
|
86
|
-
const rule = line > 0 ? lines[line - 1]?.trim() : undefined;
|
|
114
|
+
const { rule } = ruleAt(url, "*");
|
|
87
115
|
found.push(
|
|
88
116
|
finding(
|
|
89
117
|
"BLUME_AUDIT_ROBOTS_DISALLOWS_INDEXABLE",
|
|
@@ -91,7 +119,35 @@ export const robotsChecks: CheckModule = {
|
|
|
91
119
|
`robots.txt "${rule ?? "Disallow"}" blocks ${path}, which sitemap.xml advertises.`
|
|
92
120
|
)
|
|
93
121
|
);
|
|
122
|
+
continue;
|
|
94
123
|
}
|
|
124
|
+
for (const agent of agents) {
|
|
125
|
+
if (!parser.isDisallowed(url, agent)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const blocked = blockedFor.get(agent);
|
|
129
|
+
if (blocked) {
|
|
130
|
+
blocked.push(url);
|
|
131
|
+
} else {
|
|
132
|
+
blockedFor.set(agent, [url]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// One finding per crawler, at the rule that blocks its first page: a
|
|
138
|
+
// `Disallow: /` aimed at one bot would otherwise list every page.
|
|
139
|
+
for (const [agent, urls] of blockedFor) {
|
|
140
|
+
const [first = ""] = urls;
|
|
141
|
+
const { line, rule } = ruleAt(first, agent);
|
|
142
|
+
const path = normalizePath(new URL(first).pathname);
|
|
143
|
+
const more = urls.length > 1 ? ` and ${urls.length - 1} more` : "";
|
|
144
|
+
found.push(
|
|
145
|
+
finding(
|
|
146
|
+
"BLUME_AUDIT_ROBOTS_BLOCKS_CRAWLER",
|
|
147
|
+
{ file: robots.file, line, url: "/robots.txt" },
|
|
148
|
+
`robots.txt "${rule ?? "Disallow"}" blocks ${agent} from ${path}${more}, which sitemap.xml advertises.`
|
|
149
|
+
)
|
|
150
|
+
);
|
|
95
151
|
}
|
|
96
152
|
|
|
97
153
|
return found;
|
package/src/audit/run.ts
CHANGED
|
@@ -69,6 +69,8 @@ export interface AuditOptions {
|
|
|
69
69
|
origin?: string;
|
|
70
70
|
/** Probe outbound links (`--external`). */
|
|
71
71
|
external?: boolean;
|
|
72
|
+
/** Outbound URLs not to probe (`--ignore`). */
|
|
73
|
+
ignore?: (url: string) => boolean;
|
|
72
74
|
/** Only report these check ids or categories. */
|
|
73
75
|
only?: string[];
|
|
74
76
|
/** Suppress these check ids or categories. */
|
|
@@ -159,6 +161,7 @@ export const runAudit = async (options: AuditOptions): Promise<AuditResult> => {
|
|
|
159
161
|
siteOrigin(project.config.deployment.options.site),
|
|
160
162
|
normalizeBasePath(project.config.deployment.options.base)
|
|
161
163
|
),
|
|
164
|
+
ignore: options.ignore ?? (() => false),
|
|
162
165
|
llms: crawl.llms,
|
|
163
166
|
origin,
|
|
164
167
|
pages: crawl.pages,
|
package/src/audit/types.ts
CHANGED
|
@@ -249,6 +249,8 @@ export interface AuditContext {
|
|
|
249
249
|
robots: RobotsDoc | null;
|
|
250
250
|
llms: LlmsDoc | null;
|
|
251
251
|
thresholds: AuditThresholds;
|
|
252
|
+
/** Outbound URLs the external tier doesn't probe (`--ignore`). */
|
|
253
|
+
ignore: (url: string) => boolean;
|
|
252
254
|
}
|
|
253
255
|
|
|
254
256
|
/** One category's checks. Modules, not per-check closures — see catalog.ts. */
|
package/src/audit/url.ts
CHANGED
|
@@ -45,10 +45,11 @@ export type ResolvedHref =
|
|
|
45
45
|
| { kind: "self-origin"; path: string; hash: string }
|
|
46
46
|
/**
|
|
47
47
|
* A path on this origin that lacks `deployment.base`. The host serves the
|
|
48
|
-
* whole build under the base, so nothing answers here — `path`
|
|
49
|
-
* as requested, base missing.
|
|
48
|
+
* whole build under the base, so nothing in the build answers here — `path`
|
|
49
|
+
* is the path as requested, base missing. `absolute` when the href spelled
|
|
50
|
+
* out the origin: a full URL can name another app on the same host.
|
|
50
51
|
*/
|
|
51
|
-
| { kind: "outside-base"; path: string }
|
|
52
|
+
| { kind: "outside-base"; path: string; absolute: boolean }
|
|
52
53
|
/** An absolute URL on another origin. */
|
|
53
54
|
| { kind: "external"; url: string }
|
|
54
55
|
/** In-page anchor, `mailto:`, `tel:`, `javascript:`, data URI — not a page link. */
|
|
@@ -139,7 +140,11 @@ export const resolveHref = (
|
|
|
139
140
|
if (origin && parsed.origin === origin) {
|
|
140
141
|
const served = decodePath(parsed.pathname);
|
|
141
142
|
if (!underBase(deployBase, served)) {
|
|
142
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
absolute: true,
|
|
145
|
+
kind: "outside-base",
|
|
146
|
+
path: normalizePath(served),
|
|
147
|
+
};
|
|
143
148
|
}
|
|
144
149
|
return {
|
|
145
150
|
hash: parsed.hash.slice(1),
|
|
@@ -171,7 +176,11 @@ export const resolveHref = (
|
|
|
171
176
|
// (`./auth`) was resolved against the base-less page URL above, so it
|
|
172
177
|
// inherits the page's base by construction.
|
|
173
178
|
if (target.startsWith("/") && !underBase(deployBase, served)) {
|
|
174
|
-
return {
|
|
179
|
+
return {
|
|
180
|
+
absolute: false,
|
|
181
|
+
kind: "outside-base",
|
|
182
|
+
path: normalizePath(served),
|
|
183
|
+
};
|
|
175
184
|
}
|
|
176
185
|
return {
|
|
177
186
|
hash: resolved.hash.slice(1),
|
package/src/blume-modules.d.ts
CHANGED
|
@@ -12,7 +12,11 @@ declare module "blume:search-client" {
|
|
|
12
12
|
// biome-ignore lint/style/useImportType: ambient module must stay a global script
|
|
13
13
|
// oxlint-disable-next-line typescript/consistent-type-imports
|
|
14
14
|
type Fn = import("./components/layout/search/types.ts").SearchFn;
|
|
15
|
-
|
|
15
|
+
type Options =
|
|
16
|
+
// biome-ignore lint/style/useImportType: ambient module must stay a global script
|
|
17
|
+
// oxlint-disable-next-line typescript/consistent-type-imports
|
|
18
|
+
import("./components/layout/search/types.ts").SearchClientOptions;
|
|
19
|
+
export const createSearch: (options?: Options) => Fn | Promise<Fn>;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
declare module "blume:consent-client" {
|
package/src/cli/args.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import picomatch from "picomatch";
|
|
2
|
+
|
|
1
3
|
import { logger } from "./log.ts";
|
|
2
4
|
|
|
3
5
|
const MAX_PORT = 65_535;
|
|
@@ -22,6 +24,55 @@ export const parsePort = (value?: string): number | undefined => {
|
|
|
22
24
|
return port;
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Every value a repeatable string flag was given (`--ignore a --ignore b`), in
|
|
29
|
+
* order. citty 0.2 parses with `node:util.parseArgs` without its `multiple`
|
|
30
|
+
* option, so the parsed `args` keep only the last value; this reads the raw
|
|
31
|
+
* arguments the way that parser does instead: `--name=value`, or `--name`
|
|
32
|
+
* followed by the next token whatever it looks like (`""` when there is
|
|
33
|
+
* none), and nothing after a bare `--`.
|
|
34
|
+
*/
|
|
35
|
+
export const repeatedFlag = (
|
|
36
|
+
rawArgs: readonly string[],
|
|
37
|
+
name: string
|
|
38
|
+
): string[] => {
|
|
39
|
+
const flag = `--${name}`;
|
|
40
|
+
const values: string[] = [];
|
|
41
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
42
|
+
const arg = rawArgs[index];
|
|
43
|
+
if (arg === "--") {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (arg === flag) {
|
|
47
|
+
values.push(rawArgs[index + 1] ?? "");
|
|
48
|
+
index += 1;
|
|
49
|
+
} else if (arg?.startsWith(`${flag}=`)) {
|
|
50
|
+
values.push(arg.slice(flag.length + 1));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return values;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The `--ignore` globs of `blume validate` and `blume audit`, as a test for
|
|
58
|
+
* the external URLs they skip: any glob matching the full URL skips it, with
|
|
59
|
+
* `dot` set so `**` also crosses a segment like `/.well-known/`. No globs skip
|
|
60
|
+
* nothing. An empty one (`--ignore` with nothing after it) exits with an
|
|
61
|
+
* error: it names no URL, and picomatch throws on it.
|
|
62
|
+
*/
|
|
63
|
+
export const parseIgnoreFlag = (
|
|
64
|
+
rawArgs: readonly string[]
|
|
65
|
+
): ((url: string) => boolean) => {
|
|
66
|
+
const globs = repeatedFlag(rawArgs, "ignore");
|
|
67
|
+
if (globs.includes("")) {
|
|
68
|
+
logger.error(
|
|
69
|
+
'Invalid --ignore "": pass a glob for the URLs to skip (e.g. --ignore "https://example.com/**").'
|
|
70
|
+
);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
return picomatch(globs, { dot: true });
|
|
74
|
+
};
|
|
75
|
+
|
|
25
76
|
/**
|
|
26
77
|
* The longest `--timeout` a timer can hold, in seconds. Node clamps a
|
|
27
78
|
* `setTimeout` delay above 2^31−1 ms (about 24.8 days) to 1 ms, so a larger
|
package/src/cli/build-failure.ts
CHANGED
|
@@ -20,8 +20,31 @@ const buildErrorSchema = z.looseObject({
|
|
|
20
20
|
.optional(),
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The diagnostic a `BlumeError` thrown inside the build carries, such as a
|
|
25
|
+
* failed hosted search sync in `astro:build:done`. The hook runs Blume's
|
|
26
|
+
* source rather than the CLI bundle, so its error is another copy of the class
|
|
27
|
+
* that the command's `instanceof BlumeError` doesn't recognize.
|
|
28
|
+
*/
|
|
29
|
+
const blumeErrorSchema = z.looseObject({
|
|
30
|
+
diagnostic: z.object({
|
|
31
|
+
code: z.string(),
|
|
32
|
+
column: z.number().optional(),
|
|
33
|
+
docsUrl: z.string().optional(),
|
|
34
|
+
file: z.string().optional(),
|
|
35
|
+
line: z.number().optional(),
|
|
36
|
+
message: z.string(),
|
|
37
|
+
severity: z.enum(["error", "info", "warning"]),
|
|
38
|
+
suggestion: z.string().optional(),
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
|
|
23
42
|
/** One failure as a diagnostic at the file (and position) it names. */
|
|
24
43
|
const failureDiagnostic = (error: Error): Diagnostic => {
|
|
44
|
+
const raised = blumeErrorSchema.safeParse(error);
|
|
45
|
+
if (raised.success) {
|
|
46
|
+
return raised.data.diagnostic;
|
|
47
|
+
}
|
|
25
48
|
const parsed = buildErrorSchema.safeParse(error);
|
|
26
49
|
const { id, loc } = parsed.success ? parsed.data : {};
|
|
27
50
|
// A named error (`MDXError`, `CompilerError`) says which step failed.
|
|
@@ -15,6 +15,7 @@ import { isHttpUrl } from "../../audit/url.ts";
|
|
|
15
15
|
import { BlumeError } from "../../core/diagnostics.ts";
|
|
16
16
|
import { scanProject } from "../../core/project-graph.ts";
|
|
17
17
|
import type { DiagnosticSeverity } from "../../core/types.ts";
|
|
18
|
+
import { parseIgnoreFlag } from "../args.ts";
|
|
18
19
|
import { commandMeta } from "../command-meta.ts";
|
|
19
20
|
import { reportInternalError } from "../internal-error.ts";
|
|
20
21
|
import { flushStdout, logger, reportDiagnostics } from "../log.ts";
|
|
@@ -103,6 +104,12 @@ export const auditCommand = defineCommand({
|
|
|
103
104
|
"Exit non-zero at this severity or above: error | warning | info. Defaults to error.",
|
|
104
105
|
type: "string",
|
|
105
106
|
},
|
|
107
|
+
ignore: {
|
|
108
|
+
description:
|
|
109
|
+
'With --external, skip outbound links whose URL matches this glob (e.g. "https://example.com/**"). Repeat it for more; internal links are always checked.',
|
|
110
|
+
type: "string",
|
|
111
|
+
valueHint: "glob",
|
|
112
|
+
},
|
|
106
113
|
json: {
|
|
107
114
|
description: "Emit the report as JSON on stdout (for CI/editors).",
|
|
108
115
|
type: "boolean",
|
|
@@ -134,7 +141,7 @@ export const auditCommand = defineCommand({
|
|
|
134
141
|
},
|
|
135
142
|
},
|
|
136
143
|
meta: commandMeta.audit,
|
|
137
|
-
async run({ args }) {
|
|
144
|
+
async run({ args, rawArgs }) {
|
|
138
145
|
if (args["list-checks"]) {
|
|
139
146
|
process.stdout.write(formatCatalog());
|
|
140
147
|
return;
|
|
@@ -172,6 +179,7 @@ export const auditCommand = defineCommand({
|
|
|
172
179
|
const only = splitTerms(args.only);
|
|
173
180
|
const skip = splitTerms(args.skip);
|
|
174
181
|
rejectUnknownTerms([...only, ...skip]);
|
|
182
|
+
const ignore = parseIgnoreFlag(rawArgs);
|
|
175
183
|
let result: AuditResult;
|
|
176
184
|
try {
|
|
177
185
|
// `scanProject`, not `prepareProject`: the audit reads the *existing*
|
|
@@ -180,6 +188,7 @@ export const auditCommand = defineCommand({
|
|
|
180
188
|
const project = await scanProject(root, { mode: "build" });
|
|
181
189
|
result = await runAudit({
|
|
182
190
|
external: args.external,
|
|
191
|
+
ignore,
|
|
183
192
|
only,
|
|
184
193
|
origin: args.url,
|
|
185
194
|
project,
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
import { BlumeError } from "../../core/diagnostics.ts";
|
|
12
12
|
import { scanProject } from "../../core/project-graph.ts";
|
|
13
13
|
import { resolveRuntimeDir } from "../../core/project.ts";
|
|
14
|
+
import { referenceSpecFiles } from "../../openapi/references.ts";
|
|
14
15
|
import { parsePort } from "../args.ts";
|
|
16
|
+
import { astroBuildDiagnostics } from "../build-failure.ts";
|
|
15
17
|
import { commandMeta } from "../command-meta.ts";
|
|
16
18
|
import {
|
|
17
19
|
acquireDevLock,
|
|
@@ -140,7 +142,19 @@ export const devCommand = defineCommand({
|
|
|
140
142
|
server: { host: normalizeHost(args.host), open, port: listenPort },
|
|
141
143
|
});
|
|
142
144
|
|
|
143
|
-
let server
|
|
145
|
+
let server: Awaited<ReturnType<typeof createServer>>;
|
|
146
|
+
try {
|
|
147
|
+
server = await createServer(explicitPort, args.open ?? false);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// Astro's first content sync rejects on a page it can't load (front
|
|
150
|
+
// matter that isn't valid YAML): report it at the file it names, as
|
|
151
|
+
// `blume build` does, not as an internal error blaming Blume.
|
|
152
|
+
if (error instanceof BlumeError || !(error instanceof Error)) {
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
reportDiagnostics(astroBuildDiagnostics(error), root);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
144
158
|
|
|
145
159
|
// Vite bumps to the next free port when the default is taken, so record
|
|
146
160
|
// the port the server actually bound — the lock's URL is what a refused
|
|
@@ -229,7 +243,8 @@ export const devCommand = defineCommand({
|
|
|
229
243
|
|
|
230
244
|
// Content is watched per source (filesystem uses fs.watch; remote sources
|
|
231
245
|
// are frozen for the session). The remaining project inputs — user pages,
|
|
232
|
-
// config, theme,
|
|
246
|
+
// config, theme, component overrides, and the local specs and overlays API
|
|
247
|
+
// references read — are watched directly.
|
|
233
248
|
const dirTargets = [project.context.pagesRoot].filter(
|
|
234
249
|
(target) => target !== null
|
|
235
250
|
);
|
|
@@ -237,6 +252,7 @@ export const devCommand = defineCommand({
|
|
|
237
252
|
project.context.configFile,
|
|
238
253
|
project.context.themeFile,
|
|
239
254
|
project.context.componentsFile,
|
|
255
|
+
...referenceSpecFiles(project.config, root),
|
|
240
256
|
].filter((target) => target !== null);
|
|
241
257
|
|
|
242
258
|
// chokidar handles what raw fs.watch made us hand-roll: recursive
|
|
@@ -5,13 +5,17 @@ import { defineCommand } from "citty";
|
|
|
5
5
|
import { join } from "pathe";
|
|
6
6
|
import { satisfies } from "semver";
|
|
7
7
|
|
|
8
|
+
import { discoverExamples } from "../../astro/examples.ts";
|
|
8
9
|
import { discoverIslands } from "../../astro/islands.ts";
|
|
10
|
+
import { discoverPages, navTargetRoutes } from "../../astro/pages.ts";
|
|
9
11
|
import { missingDependencyDiagnostic } from "../../astro/runtime-deps.ts";
|
|
12
|
+
import { missingExampleDiagnostics } from "../../core/component-diagnostics.ts";
|
|
10
13
|
import {
|
|
11
14
|
analyzeComponentOverrides,
|
|
12
15
|
ComponentOverridesError,
|
|
13
16
|
} from "../../core/component-overrides.ts";
|
|
14
17
|
import { BlumeError } from "../../core/diagnostics.ts";
|
|
18
|
+
import { validateNavTargets } from "../../core/nav-diagnostics.ts";
|
|
15
19
|
import { packageRoot } from "../../core/package-root.ts";
|
|
16
20
|
import { scanProject } from "../../core/project-graph.ts";
|
|
17
21
|
import type { ResolvedConfig } from "../../core/schema.ts";
|
|
@@ -35,23 +39,24 @@ const FALLBACK_NODE_RANGE = ">=22.12.0";
|
|
|
35
39
|
* Plan `components.ts` the way `blume dev`/`build` do, reporting each override
|
|
36
40
|
* that can't be planned — a non-static form, or an import of a file that
|
|
37
41
|
* doesn't exist — with its own line. The scan alone never reads the file, so
|
|
38
|
-
* without this doctor would pass a project whose build fails.
|
|
42
|
+
* without this doctor would pass a project whose build fails. Also returns
|
|
43
|
+
* the MDX tags the file overrides.
|
|
39
44
|
*/
|
|
40
45
|
const componentsDiagnostics = async (
|
|
41
46
|
componentsFile: string | null
|
|
42
|
-
): Promise<Diagnostic[]> => {
|
|
47
|
+
): Promise<{ issues: Diagnostic[]; mdx: string[] }> => {
|
|
43
48
|
if (!componentsFile) {
|
|
44
|
-
return [];
|
|
49
|
+
return { issues: [], mdx: [] };
|
|
45
50
|
}
|
|
46
51
|
try {
|
|
47
|
-
analyzeComponentOverrides(
|
|
52
|
+
const { mdx } = analyzeComponentOverrides(
|
|
48
53
|
await readFile(componentsFile, "utf-8"),
|
|
49
54
|
componentsFile
|
|
50
55
|
);
|
|
51
|
-
return [];
|
|
56
|
+
return { issues: [], mdx: mdx.map((entry) => entry.key) };
|
|
52
57
|
} catch (error) {
|
|
53
58
|
if (error instanceof ComponentOverridesError) {
|
|
54
|
-
return error.issues;
|
|
59
|
+
return { issues: error.issues, mdx: [] };
|
|
55
60
|
}
|
|
56
61
|
throw error;
|
|
57
62
|
}
|
|
@@ -117,15 +122,33 @@ export const doctorCommand = defineCommand({
|
|
|
117
122
|
loadEnvFiles(root);
|
|
118
123
|
|
|
119
124
|
try {
|
|
120
|
-
const project = await scanProject(root, {
|
|
125
|
+
const project = await scanProject(root, {
|
|
126
|
+
// A missing secret fails at the first request. Checked before the
|
|
127
|
+
// sources fetch, so it's still reported when one of them fails the
|
|
128
|
+
// scan for want of it.
|
|
129
|
+
beforeSources: (config) => {
|
|
130
|
+
diagnostics.push(...checkRequiredSecrets(config));
|
|
131
|
+
},
|
|
132
|
+
mode: "build",
|
|
133
|
+
});
|
|
134
|
+
// Tabs, selector items, and featured links can point at custom pages
|
|
135
|
+
// and generated routes too, so they're checked against every route
|
|
136
|
+
// the site serves, as `blume dev`/`build` check them.
|
|
137
|
+
const userPages = project.context.pagesRoot
|
|
138
|
+
? await discoverPages(project.context.pagesRoot)
|
|
139
|
+
: [];
|
|
121
140
|
diagnostics.push(
|
|
122
141
|
...project.diagnostics,
|
|
123
|
-
...unregisteredSnapshotDiagnostics(project)
|
|
142
|
+
...unregisteredSnapshotDiagnostics(project),
|
|
143
|
+
...validateNavTargets(
|
|
144
|
+
project.graph.navigation,
|
|
145
|
+
navTargetRoutes(project, userPages)
|
|
146
|
+
)
|
|
124
147
|
);
|
|
125
148
|
|
|
126
149
|
const { config } = project;
|
|
127
|
-
// The packages
|
|
128
|
-
//
|
|
150
|
+
// The packages a build would need: an adapter's missing SDK fails the
|
|
151
|
+
// build.
|
|
129
152
|
const { islands } = await discoverIslands(root);
|
|
130
153
|
const dependencies = await missingDependencyDiagnostic(
|
|
131
154
|
config,
|
|
@@ -136,9 +159,19 @@ export const doctorCommand = defineCommand({
|
|
|
136
159
|
if (dependencies) {
|
|
137
160
|
diagnostics.push(dependencies);
|
|
138
161
|
}
|
|
162
|
+
const overrides = await componentsDiagnostics(
|
|
163
|
+
project.context.componentsFile
|
|
164
|
+
);
|
|
139
165
|
diagnostics.push(
|
|
140
|
-
...
|
|
141
|
-
|
|
166
|
+
...overrides.issues,
|
|
167
|
+
// A `<Component path>` naming no example renders a "No example
|
|
168
|
+
// found" box, which dev and build warn about too.
|
|
169
|
+
...missingExampleDiagnostics(
|
|
170
|
+
project.graph.pages,
|
|
171
|
+
await discoverExamples(root, config.examples.source),
|
|
172
|
+
root,
|
|
173
|
+
new Set([...islands.map((island) => island.name), ...overrides.mdx])
|
|
174
|
+
)
|
|
142
175
|
);
|
|
143
176
|
const features = serverFeatures(config);
|
|
144
177
|
if (
|