blume 0.0.0 → 0.1.0
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/bin/blume.mjs +18 -0
- package/dist/cli/index.js +11989 -0
- package/dist/cli/index.js.map +141 -0
- package/docs/01-quickstart.mdx +99 -0
- package/docs/02-deployment.mdx +129 -0
- package/docs/advanced/api-reference.mdx +114 -0
- package/docs/advanced/blog.mdx +121 -0
- package/docs/advanced/changelog.mdx +113 -0
- package/docs/advanced/custom-pages.mdx +187 -0
- package/docs/advanced/meta.ts +7 -0
- package/docs/changelog/v0-1-0.mdx +12 -0
- package/docs/changelog/v0-2-0.mdx +16 -0
- package/docs/configuration/ai.mdx +228 -0
- package/docs/configuration/analytics.mdx +98 -0
- package/docs/configuration/customization.mdx +91 -0
- package/docs/configuration/export.mdx +70 -0
- package/docs/configuration/index.mdx +271 -0
- package/docs/configuration/meta.ts +15 -0
- package/docs/configuration/search.mdx +172 -0
- package/docs/configuration/seo.mdx +196 -0
- package/docs/configuration/theming.mdx +178 -0
- package/docs/content/components.mdx +565 -0
- package/docs/content/i18n.mdx +205 -0
- package/docs/content/index.mdx +161 -0
- package/docs/content/islands.mdx +94 -0
- package/docs/content/meta.mdx +119 -0
- package/docs/content/meta.ts +15 -0
- package/docs/content/navigation.mdx +168 -0
- package/docs/content/sources.mdx +216 -0
- package/docs/content/syntax.mdx +445 -0
- package/docs/index.mdx +112 -0
- package/docs/reference/cli.mdx +43 -0
- package/docs/reference/frontmatter.mdx +74 -0
- package/docs/reference/meta.ts +7 -0
- package/package.json +140 -6
- package/src/ai/ask.ts +93 -0
- package/src/ai/llms.ts +65 -0
- package/src/ai/markdown.ts +31 -0
- package/src/ai/mcp/data.ts +74 -0
- package/src/ai/mcp/discovery.ts +49 -0
- package/src/ai/mcp/server.ts +225 -0
- package/src/ai/mcp/tools.ts +47 -0
- package/src/assets/icon.png +0 -0
- package/src/astro/generate.ts +878 -0
- package/src/astro/index.ts +4 -0
- package/src/astro/integration.ts +74 -0
- package/src/astro/islands.ts +131 -0
- package/src/astro/markdown-negotiation.ts +68 -0
- package/src/astro/pages.ts +28 -0
- package/src/astro/templates.ts +1199 -0
- package/src/cli/commands/add.ts +81 -0
- package/src/cli/commands/build.ts +103 -0
- package/src/cli/commands/dev.ts +108 -0
- package/src/cli/commands/doctor.ts +74 -0
- package/src/cli/commands/eject.ts +57 -0
- package/src/cli/commands/init.ts +98 -0
- package/src/cli/commands/migrate.ts +39 -0
- package/src/cli/commands/preview.ts +39 -0
- package/src/cli/commands/sync.ts +52 -0
- package/src/cli/commands/validate.ts +60 -0
- package/src/cli/index.ts +35 -0
- package/src/cli/log.ts +37 -0
- package/src/cli/prepare.ts +80 -0
- package/src/components/Icon.astro +99 -0
- package/src/components/content/Accordion.astro +8 -0
- package/src/components/content/AccordionItem.astro +121 -0
- package/src/components/content/AutoTypeTable.astro +51 -0
- package/src/components/content/Badge.astro +124 -0
- package/src/components/content/Callout.astro +73 -0
- package/src/components/content/Card.astro +104 -0
- package/src/components/content/CardGroup.astro +14 -0
- package/src/components/content/CodeGroup.astro +13 -0
- package/src/components/content/Color.astro +15 -0
- package/src/components/content/ColorItem.astro +87 -0
- package/src/components/content/ColorRow.astro +10 -0
- package/src/components/content/Column.astro +6 -0
- package/src/components/content/Columns.astro +9 -0
- package/src/components/content/Expandable.astro +11 -0
- package/src/components/content/FileTree.astro +8 -0
- package/src/components/content/Frame.astro +70 -0
- package/src/components/content/GithubInfo.astro +110 -0
- package/src/components/content/Math.astro +24 -0
- package/src/components/content/Panel.astro +20 -0
- package/src/components/content/Prompt.astro +129 -0
- package/src/components/content/Step.astro +34 -0
- package/src/components/content/Steps.astro +20 -0
- package/src/components/content/Tab.astro +40 -0
- package/src/components/content/Tabs.astro +273 -0
- package/src/components/content/Tile.astro +42 -0
- package/src/components/content/Tooltip.astro +68 -0
- package/src/components/content/Tree.astro +300 -0
- package/src/components/content/TreeFile.astro +15 -0
- package/src/components/content/TreeFolder.astro +62 -0
- package/src/components/content/TypeTable.astro +106 -0
- package/src/components/content/Update.astro +66 -0
- package/src/components/content/Visibility.astro +12 -0
- package/src/components/content/Warning.astro +9 -0
- package/src/components/content/auto-type-table.ts +141 -0
- package/src/components/content/github-info.ts +79 -0
- package/src/components/content/mermaid-element.ts +68 -0
- package/src/components/github-mark.ts +9 -0
- package/src/components/index.ts +14 -0
- package/src/components/islands/AskAI.astro +12 -0
- package/src/components/islands/ask-ai.tsx +156 -0
- package/src/components/layout/Analytics.astro +63 -0
- package/src/components/layout/Banner.astro +50 -0
- package/src/components/layout/Breadcrumbs.astro +31 -0
- package/src/components/layout/Favicon.astro +15 -0
- package/src/components/layout/Fonts.astro +14 -0
- package/src/components/layout/Header.astro +188 -0
- package/src/components/layout/LanguageSwitcher.astro +56 -0
- package/src/components/layout/NavTree.astro +462 -0
- package/src/components/layout/PageActions.astro +438 -0
- package/src/components/layout/PageFeedback.astro +58 -0
- package/src/components/layout/Pagination.astro +56 -0
- package/src/components/layout/ReferenceLayout.astro +102 -0
- package/src/components/layout/RootLayout.astro +533 -0
- package/src/components/layout/Search.astro +608 -0
- package/src/components/layout/TableOfContents.astro +68 -0
- package/src/components/layout/analytics-client.ts +38 -0
- package/src/components/layout/nav-utils.ts +87 -0
- package/src/components/layout/overrides.ts +32 -0
- package/src/components/layout/search/algolia.ts +43 -0
- package/src/components/layout/search/endpoint.ts +22 -0
- package/src/components/layout/search/flexsearch.ts +52 -0
- package/src/components/layout/search/orama-cloud.ts +41 -0
- package/src/components/layout/search/orama.ts +26 -0
- package/src/components/layout/search/pagefind.ts +43 -0
- package/src/components/layout/search/types.ts +163 -0
- package/src/components/layout/search/typesense.ts +60 -0
- package/src/components/layout/toc-element.ts +108 -0
- package/src/core/bridge.ts +92 -0
- package/src/core/config.ts +112 -0
- package/src/core/content.ts +50 -0
- package/src/core/define-components.ts +34 -0
- package/src/core/define-meta.ts +20 -0
- package/src/core/deployment-env.ts +73 -0
- package/src/core/diagnostics.ts +104 -0
- package/src/core/graph.ts +128 -0
- package/src/core/i18n-ui.ts +171 -0
- package/src/core/i18n.ts +169 -0
- package/src/core/last-modified.ts +88 -0
- package/src/core/links.ts +336 -0
- package/src/core/load-module.ts +15 -0
- package/src/core/manifest.ts +126 -0
- package/src/core/meta.ts +97 -0
- package/src/core/navigation.ts +392 -0
- package/src/core/package-root.ts +37 -0
- package/src/core/project-graph.ts +153 -0
- package/src/core/project.ts +56 -0
- package/src/core/schema.ts +1057 -0
- package/src/core/server-features.ts +23 -0
- package/src/core/sources/assets.ts +77 -0
- package/src/core/sources/cache.ts +122 -0
- package/src/core/sources/filesystem.ts +99 -0
- package/src/core/sources/mdx-remote.ts +216 -0
- package/src/core/sources/mintlify.ts +161 -0
- package/src/core/sources/normalize.ts +227 -0
- package/src/core/sources/notion.ts +440 -0
- package/src/core/sources/portable-text.ts +143 -0
- package/src/core/sources/read.ts +36 -0
- package/src/core/sources/resolve.ts +158 -0
- package/src/core/sources/sanity.ts +218 -0
- package/src/core/sources/types.ts +105 -0
- package/src/core/types.ts +261 -0
- package/src/core/ui-packs/ar.ts +47 -0
- package/src/core/ui-packs/bg.ts +47 -0
- package/src/core/ui-packs/bn.ts +47 -0
- package/src/core/ui-packs/ca.ts +47 -0
- package/src/core/ui-packs/cs.ts +47 -0
- package/src/core/ui-packs/da.ts +47 -0
- package/src/core/ui-packs/de.ts +47 -0
- package/src/core/ui-packs/el.ts +47 -0
- package/src/core/ui-packs/es.ts +47 -0
- package/src/core/ui-packs/fa.ts +47 -0
- package/src/core/ui-packs/fi.ts +47 -0
- package/src/core/ui-packs/fr.ts +47 -0
- package/src/core/ui-packs/he.ts +47 -0
- package/src/core/ui-packs/hi.ts +47 -0
- package/src/core/ui-packs/hr.ts +47 -0
- package/src/core/ui-packs/hu.ts +47 -0
- package/src/core/ui-packs/id.ts +47 -0
- package/src/core/ui-packs/index.ts +87 -0
- package/src/core/ui-packs/it.ts +47 -0
- package/src/core/ui-packs/ja.ts +47 -0
- package/src/core/ui-packs/ko.ts +47 -0
- package/src/core/ui-packs/nl.ts +47 -0
- package/src/core/ui-packs/no.ts +47 -0
- package/src/core/ui-packs/pl.ts +47 -0
- package/src/core/ui-packs/pt-br.ts +47 -0
- package/src/core/ui-packs/pt.ts +47 -0
- package/src/core/ui-packs/ro.ts +47 -0
- package/src/core/ui-packs/ru.ts +47 -0
- package/src/core/ui-packs/sk.ts +47 -0
- package/src/core/ui-packs/sr.ts +47 -0
- package/src/core/ui-packs/sv.ts +47 -0
- package/src/core/ui-packs/th.ts +47 -0
- package/src/core/ui-packs/tr.ts +47 -0
- package/src/core/ui-packs/uk.ts +47 -0
- package/src/core/ui-packs/vi.ts +47 -0
- package/src/core/ui-packs/zh-tw.ts +47 -0
- package/src/core/ui-packs/zh.ts +47 -0
- package/src/core/version.ts +23 -0
- package/src/deploy/robots.ts +20 -0
- package/src/deploy/rss.ts +128 -0
- package/src/deploy/sitemap.ts +28 -0
- package/src/index.ts +27 -0
- package/src/markdown/code-title.ts +71 -0
- package/src/markdown/directives.ts +83 -0
- package/src/markdown/heading-anchors.ts +137 -0
- package/src/markdown/index.ts +159 -0
- package/src/markdown/inline-code.ts +108 -0
- package/src/markdown/language-icon.ts +172 -0
- package/src/markdown/math.ts +32 -0
- package/src/markdown/mdast.ts +48 -0
- package/src/markdown/mermaid.ts +37 -0
- package/src/markdown/package-commands.ts +159 -0
- package/src/markdown/package-install.ts +40 -0
- package/src/migrate/fumadocs/config.ts +106 -0
- package/src/migrate/fumadocs/content.ts +365 -0
- package/src/migrate/fumadocs/frontmatter.ts +18 -0
- package/src/migrate/fumadocs/index.ts +252 -0
- package/src/migrate/fumadocs/meta.ts +114 -0
- package/src/migrate/migrate.ts +53 -0
- package/src/migrate/mintlify/config.ts +1040 -0
- package/src/migrate/mintlify/content.ts +98 -0
- package/src/migrate/mintlify/frontmatter.ts +126 -0
- package/src/migrate/mintlify/i18n.ts +51 -0
- package/src/migrate/mintlify/icons.ts +128 -0
- package/src/migrate/mintlify/index.ts +266 -0
- package/src/migrate/mintlify/snippets.ts +305 -0
- package/src/migrate/mintlify/transform.ts +81 -0
- package/src/migrate/nextra/content.ts +46 -0
- package/src/migrate/nextra/frontmatter.ts +40 -0
- package/src/migrate/nextra/index.ts +374 -0
- package/src/migrate/nextra/meta.ts +266 -0
- package/src/migrate/shared.ts +623 -0
- package/src/migrate/starlight/config.ts +459 -0
- package/src/migrate/starlight/content.ts +78 -0
- package/src/migrate/starlight/frontmatter.ts +111 -0
- package/src/migrate/starlight/i18n.ts +54 -0
- package/src/migrate/starlight/index.ts +131 -0
- package/src/og/card.ts +92 -0
- package/src/og/index.ts +2 -0
- package/src/openapi/scalar.ts +246 -0
- package/src/registry/eject.ts +263 -0
- package/src/registry/registry.ts +100 -0
- package/src/registry/rewrite-imports.ts +39 -0
- package/src/runtime/index.ts +14 -0
- package/src/search/build.ts +23 -0
- package/src/search/documents.ts +165 -0
- package/src/search/orama-index.ts +66 -0
- package/src/search/providers.ts +91 -0
- package/src/search/sync/algolia.ts +30 -0
- package/src/search/sync/index.ts +50 -0
- package/src/search/sync/orama-cloud.ts +40 -0
- package/src/search/sync/typesense.ts +65 -0
- package/src/seo/jsonld.ts +113 -0
- package/src/theme/entry.ts +608 -0
- package/src/theme/fonts.ts +198 -0
- package/src/theme/icons.ts +184 -0
- package/src/theme/palette.ts +143 -0
- package/src/theme/twoslash.ts +81 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client behaviour for the `<blume-toc>` custom element wrapping each "On this
|
|
3
|
+
* page" list. As the reader scrolls, it marks the link for the section currently
|
|
4
|
+
* in view with `aria-current="location"` — styled via Tailwind `aria-[current]`
|
|
5
|
+
* variants in RootLayout — giving the table of contents a live scrollspy.
|
|
6
|
+
*
|
|
7
|
+
* The active heading is chosen by rect (the last heading at or above a trigger
|
|
8
|
+
* line just below the sticky header), so it stays correct for sections taller
|
|
9
|
+
* than the viewport and at the bottom of the page. An IntersectionObserver is the
|
|
10
|
+
* cheap primary trigger — it only fires as headings cross the band near the top —
|
|
11
|
+
* and a passive, rAF-throttled scroll listener covers the one case it can't: a
|
|
12
|
+
* final section too short to push its heading past the trigger line.
|
|
13
|
+
*
|
|
14
|
+
* Imported for its side effect (registers the element) from RootLayout's script.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface TocEntry {
|
|
18
|
+
heading: HTMLElement;
|
|
19
|
+
link: HTMLAnchorElement;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Matches the theme's `scroll-padding-top: 4.5rem`, so the highlighted heading
|
|
23
|
+
// agrees with where a clicked anchor lands beneath the sticky header.
|
|
24
|
+
const TRIGGER_OFFSET = 72;
|
|
25
|
+
|
|
26
|
+
class BlumeToc extends HTMLElement {
|
|
27
|
+
#entries: TocEntry[] = [];
|
|
28
|
+
#observer: IntersectionObserver | null = null;
|
|
29
|
+
#current: HTMLAnchorElement | null = null;
|
|
30
|
+
#ticking = false;
|
|
31
|
+
|
|
32
|
+
connectedCallback() {
|
|
33
|
+
for (const link of this.querySelectorAll<HTMLAnchorElement>(
|
|
34
|
+
'a[href^="#"]'
|
|
35
|
+
)) {
|
|
36
|
+
const id = decodeURIComponent(link.hash.slice(1));
|
|
37
|
+
const heading = id
|
|
38
|
+
? document.querySelector<HTMLElement>(`#${CSS.escape(id)}`)
|
|
39
|
+
: null;
|
|
40
|
+
if (heading) {
|
|
41
|
+
this.#entries.push({ heading, link });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (this.#entries.length === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.#observer = new IntersectionObserver(() => this.#update(), {
|
|
49
|
+
rootMargin: `-${TRIGGER_OFFSET}px 0px -70% 0px`,
|
|
50
|
+
threshold: 0,
|
|
51
|
+
});
|
|
52
|
+
for (const { heading } of this.#entries) {
|
|
53
|
+
this.#observer.observe(heading);
|
|
54
|
+
}
|
|
55
|
+
window.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
56
|
+
this.#update();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
disconnectedCallback() {
|
|
60
|
+
this.#observer?.disconnect();
|
|
61
|
+
this.#observer = null;
|
|
62
|
+
window.removeEventListener("scroll", this.#onScroll);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#onScroll = () => {
|
|
66
|
+
if (this.#ticking) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.#ticking = true;
|
|
70
|
+
requestAnimationFrame(() => {
|
|
71
|
+
this.#ticking = false;
|
|
72
|
+
this.#update();
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
#update() {
|
|
77
|
+
const active = this.#activeLink();
|
|
78
|
+
if (active === this.#current) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.#current?.removeAttribute("aria-current");
|
|
82
|
+
active?.setAttribute("aria-current", "location");
|
|
83
|
+
this.#current = active;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#activeLink(): HTMLAnchorElement | null {
|
|
87
|
+
const scrolledToBottom =
|
|
88
|
+
window.innerHeight + window.scrollY >=
|
|
89
|
+
document.documentElement.scrollHeight - 2;
|
|
90
|
+
if (scrolledToBottom) {
|
|
91
|
+
return this.#entries.at(-1)?.link ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Headings are in document order, so the last one whose top has reached the
|
|
95
|
+
// trigger line is the section currently being read; default to the first.
|
|
96
|
+
let active = this.#entries[0]?.link ?? null;
|
|
97
|
+
for (const { heading, link } of this.#entries) {
|
|
98
|
+
if (heading.getBoundingClientRect().top <= TRIGGER_OFFSET) {
|
|
99
|
+
active = link;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return active;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!customElements.get("blume-toc")) {
|
|
107
|
+
customElements.define("blume-toc", BlumeToc);
|
|
108
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import { join } from "pathe";
|
|
5
|
+
|
|
6
|
+
import { loadMintlifyConfig } from "../migrate/mintlify/config.ts";
|
|
7
|
+
import { mintlifyI18n } from "../migrate/mintlify/i18n.ts";
|
|
8
|
+
import type { BlumeConfig } from "./schema.ts";
|
|
9
|
+
|
|
10
|
+
/** A detected docs-tool config that Blume can serve without a migration. */
|
|
11
|
+
export interface BridgeDetection {
|
|
12
|
+
/** Which foreign docs tool was detected. */
|
|
13
|
+
tool: "mintlify";
|
|
14
|
+
/** Absolute path of the detected config file (`docs.json`/`mint.json`). */
|
|
15
|
+
configFile: string;
|
|
16
|
+
/** A Blume config synthesized from the foreign config, ready to validate. */
|
|
17
|
+
raw: BlumeConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MINTLIFY_CONFIG_FILES = ["docs.json", "mint.json"];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detect a Mintlify project at `root` and synthesize an equivalent Blume config.
|
|
24
|
+
*
|
|
25
|
+
* "Bridge mode" lets a team swap `mintlify dev` for `blume dev` with no file
|
|
26
|
+
* changes: `docs.json` is translated to Blume config (`loadMintlifyConfig`) and
|
|
27
|
+
* its content block is rewired to a single `mintlify` content source, which
|
|
28
|
+
* transforms each MDX page to Blume idiom at scan time. The original
|
|
29
|
+
* `content.root`/`exclude` and `variables` move onto the source (Blume has no
|
|
30
|
+
* runtime variable substitution, so globals are inlined into content there).
|
|
31
|
+
*
|
|
32
|
+
* Returns `null` when no Mintlify config is present. Only called when no
|
|
33
|
+
* `blume.config.*` exists, so an explicit Blume config always takes precedence.
|
|
34
|
+
*/
|
|
35
|
+
export const detectMintlifyBridge = async (
|
|
36
|
+
root: string
|
|
37
|
+
): Promise<BridgeDetection | null> => {
|
|
38
|
+
const configFile = MINTLIFY_CONFIG_FILES.map((name) => join(root, name)).find(
|
|
39
|
+
(candidate) => existsSync(candidate)
|
|
40
|
+
);
|
|
41
|
+
if (!configFile) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const config = await loadMintlifyConfig(root, configFile);
|
|
46
|
+
|
|
47
|
+
// i18n is derived from the raw spec (language dirs), mirroring the migrator.
|
|
48
|
+
const spec = JSON.parse(await readFile(configFile, "utf-8")) as Record<
|
|
49
|
+
string,
|
|
50
|
+
unknown
|
|
51
|
+
>;
|
|
52
|
+
const i18n = mintlifyI18n(spec);
|
|
53
|
+
if (i18n) {
|
|
54
|
+
config.i18n = i18n;
|
|
55
|
+
// Language switching is handled by Blume i18n, not a nav selector.
|
|
56
|
+
if (config.navigation?.selectors) {
|
|
57
|
+
config.navigation.selectors = config.navigation.selectors.filter(
|
|
58
|
+
(selector) => selector.kind !== "language"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const variables = (config.variables as Record<string, string>) ?? {};
|
|
64
|
+
const root_ = config.content?.root ?? ".";
|
|
65
|
+
const exclude = config.content?.exclude ?? [];
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
configFile,
|
|
69
|
+
raw: {
|
|
70
|
+
...config,
|
|
71
|
+
content: {
|
|
72
|
+
// Mirror the excludes onto `content.exclude` too: the generated Astro
|
|
73
|
+
// `docs` collection globs `content.root` (here the project root) and
|
|
74
|
+
// must skip node_modules/snippets just like the source does.
|
|
75
|
+
exclude,
|
|
76
|
+
root: root_,
|
|
77
|
+
sources: [
|
|
78
|
+
{
|
|
79
|
+
configFile,
|
|
80
|
+
exclude,
|
|
81
|
+
root: root_,
|
|
82
|
+
type: "mintlify",
|
|
83
|
+
variables,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
// Globals are inlined into content by the source; drop the top-level copy.
|
|
88
|
+
variables: undefined,
|
|
89
|
+
},
|
|
90
|
+
tool: "mintlify",
|
|
91
|
+
};
|
|
92
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { detectMintlifyBridge } from "./bridge.ts";
|
|
2
|
+
import type { BridgeDetection } from "./bridge.ts";
|
|
3
|
+
import { applyDeploymentEnv } from "./deployment-env.ts";
|
|
4
|
+
import { BlumeError, diagnosticsFromZod } from "./diagnostics.ts";
|
|
5
|
+
import { createModuleLoader } from "./load-module.ts";
|
|
6
|
+
import { findConfigFile } from "./project.ts";
|
|
7
|
+
import { blumeConfigSchema } from "./schema.ts";
|
|
8
|
+
import type { BlumeConfig, ResolvedConfig } from "./schema.ts";
|
|
9
|
+
import type { Diagnostic } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Identity helper for authoring `blume.config.ts`. Exists for type inference
|
|
13
|
+
* and a stable future home for plugin hooks; it does not transform input.
|
|
14
|
+
*/
|
|
15
|
+
export const defineConfig = (config: BlumeConfig): BlumeConfig => config;
|
|
16
|
+
|
|
17
|
+
/** Bridge mode info: a foreign docs tool Blume is serving without migrating. */
|
|
18
|
+
export interface ConfigBridge {
|
|
19
|
+
tool: "mintlify";
|
|
20
|
+
/** Absolute path of the foreign config file (`docs.json`/`mint.json`). */
|
|
21
|
+
configFile: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Result of loading + validating a project config. */
|
|
25
|
+
export interface ConfigLoadResult {
|
|
26
|
+
config: ResolvedConfig;
|
|
27
|
+
/** Absolute path of the config file used, or null when defaults were used. */
|
|
28
|
+
configFile: string | null;
|
|
29
|
+
/** Set when a foreign docs config (e.g. Mintlify) is being bridged. */
|
|
30
|
+
bridge: ConfigBridge | null;
|
|
31
|
+
diagnostics: Diagnostic[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const importConfigModule = createModuleLoader();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load and validate the project config. When no config file exists, schema
|
|
38
|
+
* defaults produce a fully resolved config so the zero-boilerplate path works.
|
|
39
|
+
*/
|
|
40
|
+
export const loadConfig = async (
|
|
41
|
+
root: string,
|
|
42
|
+
/**
|
|
43
|
+
* Supplied only by `blume dev`: the local dev server URL, used as the
|
|
44
|
+
* `deployment.site` fallback when none is configured or detected. Builds
|
|
45
|
+
* never pass it, so production output can't end up pointing at localhost.
|
|
46
|
+
*/
|
|
47
|
+
options: { devServerUrl?: string } = {}
|
|
48
|
+
): Promise<ConfigLoadResult> => {
|
|
49
|
+
const configFile = findConfigFile(root);
|
|
50
|
+
|
|
51
|
+
// With no Blume config, a Mintlify `docs.json` activates bridge mode: serve
|
|
52
|
+
// the unconverted project by synthesizing config + a `mintlify` content source.
|
|
53
|
+
let bridge: BridgeDetection | null = null;
|
|
54
|
+
let raw: unknown = {};
|
|
55
|
+
if (configFile) {
|
|
56
|
+
try {
|
|
57
|
+
raw = await importConfigModule(configFile);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new BlumeError({
|
|
60
|
+
code: "BLUME_CONFIG_LOAD_FAILED",
|
|
61
|
+
file: configFile,
|
|
62
|
+
message: `Failed to load config: ${(error as Error).message}`,
|
|
63
|
+
severity: "error",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
bridge = await detectMintlifyBridge(root);
|
|
68
|
+
if (bridge) {
|
|
69
|
+
({ raw } = bridge);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sourceFile = bridge?.configFile ?? configFile;
|
|
74
|
+
const parsed = blumeConfigSchema.safeParse(raw ?? {});
|
|
75
|
+
if (!parsed.success) {
|
|
76
|
+
const diagnostics = diagnosticsFromZod(parsed.error, {
|
|
77
|
+
code: "BLUME_CONFIG_INVALID",
|
|
78
|
+
file: sourceFile ?? undefined,
|
|
79
|
+
});
|
|
80
|
+
throw new BlumeError(
|
|
81
|
+
diagnostics[0] ?? {
|
|
82
|
+
code: "BLUME_CONFIG_INVALID",
|
|
83
|
+
file: sourceFile ?? undefined,
|
|
84
|
+
message: "Invalid Blume config.",
|
|
85
|
+
severity: "error",
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Resolve the canonical site URL, then SEO defaults that depend on it.
|
|
91
|
+
// Precedence: explicit config > platform env (Vercel/Netlify/Cloudflare, via
|
|
92
|
+
// applyDeploymentEnv) > the local dev server URL (dev only).
|
|
93
|
+
const config = applyDeploymentEnv(parsed.data);
|
|
94
|
+
const site = config.deployment.site ?? options.devServerUrl;
|
|
95
|
+
|
|
96
|
+
// OG images need an absolute `og:image`, so they default on once a site URL
|
|
97
|
+
// is known and off otherwise. An explicit `seo.og.enabled` always wins.
|
|
98
|
+
const ogEnabled = config.seo.og.enabled ?? Boolean(site);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
bridge: bridge
|
|
102
|
+
? { configFile: bridge.configFile, tool: bridge.tool }
|
|
103
|
+
: null,
|
|
104
|
+
config: {
|
|
105
|
+
...config,
|
|
106
|
+
deployment: { ...config.deployment, site },
|
|
107
|
+
seo: { ...config.seo, og: { ...config.seo.og, enabled: ogEnabled } },
|
|
108
|
+
},
|
|
109
|
+
configFile: sourceFile,
|
|
110
|
+
diagnostics: [],
|
|
111
|
+
};
|
|
112
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ResolvedI18nConfig } from "./schema.ts";
|
|
2
|
+
import { filesystemSource } from "./sources/filesystem.ts";
|
|
3
|
+
import { normalizeEntry } from "./sources/normalize.ts";
|
|
4
|
+
import type { Diagnostic, PageRecord } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
// Re-export the shared parsing helpers from their new home so existing importers
|
|
7
|
+
// (and tests) keep resolving them from `core/content.ts`.
|
|
8
|
+
export {
|
|
9
|
+
extractHeadings,
|
|
10
|
+
extractLinks,
|
|
11
|
+
normalizeEntry,
|
|
12
|
+
slugify,
|
|
13
|
+
} from "./sources/normalize.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Discover and normalize all content pages under a content root. Thin wrapper
|
|
17
|
+
* around the filesystem `ContentSource` + `normalizeEntry`, kept for callers and
|
|
18
|
+
* tests that scan a single directory directly.
|
|
19
|
+
*/
|
|
20
|
+
export const discoverContent = async (options: {
|
|
21
|
+
contentRoot: string;
|
|
22
|
+
include: string[];
|
|
23
|
+
exclude: string[];
|
|
24
|
+
defaultType: string;
|
|
25
|
+
i18n?: ResolvedI18nConfig;
|
|
26
|
+
}): Promise<{ pages: PageRecord[]; diagnostics: Diagnostic[] }> => {
|
|
27
|
+
const source = filesystemSource({
|
|
28
|
+
exclude: options.exclude,
|
|
29
|
+
include: options.include,
|
|
30
|
+
name: "filesystem",
|
|
31
|
+
projectRoot: options.contentRoot,
|
|
32
|
+
root: options.contentRoot,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { entries, diagnostics: loadDiagnostics } = await source.load();
|
|
36
|
+
const pages: PageRecord[] = [];
|
|
37
|
+
const diagnostics: Diagnostic[] = [...loadDiagnostics];
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const normalized = normalizeEntry(entry, {
|
|
41
|
+
defaultType: options.defaultType,
|
|
42
|
+
i18n: options.i18n,
|
|
43
|
+
source: { name: source.name, prefix: source.prefix, staged: false },
|
|
44
|
+
});
|
|
45
|
+
pages.push(...normalized.pages);
|
|
46
|
+
diagnostics.push(...normalized.diagnostics);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { diagnostics, pages };
|
|
50
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { HydrationMode } from "./schema.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A reference to a component. Either an imported component (preferred, for type
|
|
5
|
+
* safety) or a string path resolved relative to the project root.
|
|
6
|
+
*/
|
|
7
|
+
export type ComponentReference = unknown | string;
|
|
8
|
+
|
|
9
|
+
/** An interactive component plus its hydration strategy. */
|
|
10
|
+
export interface IslandDescriptor {
|
|
11
|
+
component: ComponentReference;
|
|
12
|
+
client: HydrationMode;
|
|
13
|
+
/** Required when `client` is `"media"`. */
|
|
14
|
+
media?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A component override: a static component or a hydrated island. */
|
|
18
|
+
export type ComponentOverride = ComponentReference | IslandDescriptor;
|
|
19
|
+
|
|
20
|
+
/** User-authored component overrides, grouped by surface. */
|
|
21
|
+
export interface ComponentOverrides {
|
|
22
|
+
/** MDX component map overrides (`Callout`, `Card`, ...). */
|
|
23
|
+
mdx?: Record<string, ComponentOverride>;
|
|
24
|
+
/** Layout slot overrides (`Header`, `Sidebar`, `Search`, ...). */
|
|
25
|
+
layout?: Record<string, ComponentOverride>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Identity helper for authoring `components.ts`. Provides type inference and a
|
|
30
|
+
* stable home for future normalization; it does not transform input.
|
|
31
|
+
*/
|
|
32
|
+
export const defineComponents = (
|
|
33
|
+
overrides: ComponentOverrides
|
|
34
|
+
): ComponentOverrides => overrides;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FolderMeta } from "./schema.ts";
|
|
2
|
+
|
|
3
|
+
/** A function that computes folder meta, optionally asynchronously. */
|
|
4
|
+
export type FolderMetaFactory = () => FolderMeta | Promise<FolderMeta>;
|
|
5
|
+
|
|
6
|
+
/** What a `meta.ts` may default-export: a plain object or a (async) factory. */
|
|
7
|
+
export type FolderMetaDefinition = FolderMeta | FolderMetaFactory;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Identity helper for authoring a folder's `meta.ts`. Provides type inference;
|
|
11
|
+
* it does not transform input. Pass an object, or a function (sync or async)
|
|
12
|
+
* that returns one when you need to compute the meta:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* export default defineMeta({ title: "Guides", order: 2 });
|
|
16
|
+
* export default defineMeta(async () => ({ pages: await orderPages() }));
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const defineMeta = (meta: FolderMetaDefinition): FolderMetaDefinition =>
|
|
20
|
+
meta;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "./schema.ts";
|
|
2
|
+
|
|
3
|
+
/** The deployment platforms Blume can infer from runtime/CI env vars. */
|
|
4
|
+
type DeploymentAdapter = NonNullable<ResolvedConfig["deployment"]["adapter"]>;
|
|
5
|
+
|
|
6
|
+
interface Platform {
|
|
7
|
+
/** Astro adapter to use when building for server output on this platform. */
|
|
8
|
+
adapter: DeploymentAdapter;
|
|
9
|
+
/** True when the env indicates the build is running on this platform. */
|
|
10
|
+
detect: (env: NodeJS.ProcessEnv) => boolean;
|
|
11
|
+
/** Resolve the canonical site URL from the platform's env vars, or null. */
|
|
12
|
+
site: (env: NodeJS.ProcessEnv) => string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Prefix a bare host with `https://`; pass values that are already absolute. */
|
|
16
|
+
const toUrl = (value: string | undefined): string | null => {
|
|
17
|
+
const trimmed = value?.trim();
|
|
18
|
+
if (!trimmed) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return /^https?:\/\//u.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Platforms checked in order; the first whose `detect` matches wins. Site URLs
|
|
26
|
+
* prefer the stable production domain over per-deployment preview URLs so the
|
|
27
|
+
* inferred origin (sitemap, OG, RSS) stays put across deploys.
|
|
28
|
+
*/
|
|
29
|
+
const PLATFORMS: Platform[] = [
|
|
30
|
+
{
|
|
31
|
+
adapter: "vercel",
|
|
32
|
+
detect: (env) => Boolean(env.VERCEL),
|
|
33
|
+
site: (env) => toUrl(env.VERCEL_PROJECT_PRODUCTION_URL ?? env.VERCEL_URL),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
adapter: "netlify",
|
|
37
|
+
detect: (env) => Boolean(env.NETLIFY),
|
|
38
|
+
site: (env) => toUrl(env.URL ?? env.DEPLOY_PRIME_URL ?? env.DEPLOY_URL),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
adapter: "cloudflare",
|
|
42
|
+
detect: (env) => Boolean(env.CF_PAGES),
|
|
43
|
+
site: (env) => toUrl(env.CF_PAGES_URL),
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fill in `deployment.adapter` and `deployment.site` from platform env vars
|
|
49
|
+
* (Vercel, Netlify, Cloudflare Pages) when the user hasn't set them. Explicit
|
|
50
|
+
* config always wins, and the adapter is only inferred for server output (it
|
|
51
|
+
* has no effect on static builds). Mirrors Astro's platform auto-detection so a
|
|
52
|
+
* project deployed to a known host gets a working canonical origin for free.
|
|
53
|
+
*/
|
|
54
|
+
export const applyDeploymentEnv = (
|
|
55
|
+
config: ResolvedConfig,
|
|
56
|
+
env: NodeJS.ProcessEnv = process.env
|
|
57
|
+
): ResolvedConfig => {
|
|
58
|
+
const platform = PLATFORMS.find((candidate) => candidate.detect(env));
|
|
59
|
+
if (!platform) {
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { deployment } = config;
|
|
64
|
+
const adapter =
|
|
65
|
+
deployment.adapter ??
|
|
66
|
+
(deployment.output === "server" ? platform.adapter : null);
|
|
67
|
+
const site = deployment.site ?? platform.site(env) ?? undefined;
|
|
68
|
+
|
|
69
|
+
if (adapter === deployment.adapter && site === deployment.site) {
|
|
70
|
+
return config;
|
|
71
|
+
}
|
|
72
|
+
return { ...config, deployment: { ...deployment, adapter, site } };
|
|
73
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { relative } from "pathe";
|
|
2
|
+
import type { ZodError } from "zod";
|
|
3
|
+
|
|
4
|
+
import type { Diagnostic } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
/** A recoverable error carrying a structured diagnostic. */
|
|
7
|
+
export class BlumeError extends Error {
|
|
8
|
+
readonly diagnostic: Diagnostic;
|
|
9
|
+
|
|
10
|
+
constructor(diagnostic: Diagnostic) {
|
|
11
|
+
super(diagnostic.message);
|
|
12
|
+
this.name = "BlumeError";
|
|
13
|
+
this.diagnostic = diagnostic;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const createDiagnostic = (diagnostic: Diagnostic): Diagnostic =>
|
|
18
|
+
diagnostic;
|
|
19
|
+
|
|
20
|
+
/** Convert a ZodError into Blume diagnostics, anchored to a file. */
|
|
21
|
+
export const diagnosticsFromZod = (
|
|
22
|
+
error: ZodError,
|
|
23
|
+
options: { code: string; file?: string }
|
|
24
|
+
): Diagnostic[] =>
|
|
25
|
+
error.issues.map((issue) => {
|
|
26
|
+
const schemaPath = issue.path.join(".");
|
|
27
|
+
const received =
|
|
28
|
+
"received" in issue
|
|
29
|
+
? ` (received: ${JSON.stringify(issue.received)})`
|
|
30
|
+
: "";
|
|
31
|
+
return {
|
|
32
|
+
code: options.code,
|
|
33
|
+
file: options.file,
|
|
34
|
+
message: schemaPath
|
|
35
|
+
? `${schemaPath}: ${issue.message}${received}`
|
|
36
|
+
: `${issue.message}${received}`,
|
|
37
|
+
schemaPath: schemaPath || undefined,
|
|
38
|
+
severity: "error",
|
|
39
|
+
} satisfies Diagnostic;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const ESC = String.fromCodePoint(27);
|
|
43
|
+
const COLORS = {
|
|
44
|
+
blue: `${ESC}[34m`,
|
|
45
|
+
bold: `${ESC}[1m`,
|
|
46
|
+
cyan: `${ESC}[36m`,
|
|
47
|
+
dim: `${ESC}[2m`,
|
|
48
|
+
red: `${ESC}[31m`,
|
|
49
|
+
reset: `${ESC}[0m`,
|
|
50
|
+
yellow: `${ESC}[33m`,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const severityColor = (severity: Diagnostic["severity"]): string => {
|
|
54
|
+
if (severity === "error") {
|
|
55
|
+
return COLORS.red;
|
|
56
|
+
}
|
|
57
|
+
if (severity === "warning") {
|
|
58
|
+
return COLORS.yellow;
|
|
59
|
+
}
|
|
60
|
+
return COLORS.blue;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Format a single diagnostic for terminal output. */
|
|
64
|
+
export const formatDiagnostic = (
|
|
65
|
+
diagnostic: Diagnostic,
|
|
66
|
+
root?: string
|
|
67
|
+
): string => {
|
|
68
|
+
const color = severityColor(diagnostic.severity);
|
|
69
|
+
const lines: string[] = [
|
|
70
|
+
`${color}${COLORS.bold}${diagnostic.code}${COLORS.reset} ${diagnostic.message}`,
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
if (diagnostic.file) {
|
|
74
|
+
const location = root ? relative(root, diagnostic.file) : diagnostic.file;
|
|
75
|
+
const column =
|
|
76
|
+
diagnostic.column === undefined ? "" : `:${diagnostic.column}`;
|
|
77
|
+
const position =
|
|
78
|
+
diagnostic.line === undefined ? "" : `:${diagnostic.line}${column}`;
|
|
79
|
+
lines.push(` ${COLORS.dim}at ${location}${position}${COLORS.reset}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (diagnostic.suggestion) {
|
|
83
|
+
lines.push(` ${COLORS.cyan}fix: ${diagnostic.suggestion}${COLORS.reset}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (diagnostic.docsUrl) {
|
|
87
|
+
lines.push(` ${COLORS.dim}docs: ${diagnostic.docsUrl}${COLORS.reset}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const hasErrors = (diagnostics: Diagnostic[]): boolean =>
|
|
94
|
+
diagnostics.some((d) => d.severity === "error");
|
|
95
|
+
|
|
96
|
+
export const countBySeverity = (
|
|
97
|
+
diagnostics: Diagnostic[]
|
|
98
|
+
): Record<Diagnostic["severity"], number> => {
|
|
99
|
+
const counts = { error: 0, info: 0, warning: 0 };
|
|
100
|
+
for (const diagnostic of diagnostics) {
|
|
101
|
+
counts[diagnostic.severity] += 1;
|
|
102
|
+
}
|
|
103
|
+
return counts;
|
|
104
|
+
};
|