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,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal Portable Text → Markdown serializer. Covers the standard block,
|
|
3
|
+
* list, and mark types Sanity emits; custom block/mark types fall through to a
|
|
4
|
+
* user-supplied serializer or are skipped with a noted comment. Output is
|
|
5
|
+
* Markdown text that flows through Blume's normal pipeline.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A single Portable Text node (block, image, or a custom type). */
|
|
9
|
+
export interface PortableTextBlock {
|
|
10
|
+
_type: string;
|
|
11
|
+
_key?: string;
|
|
12
|
+
style?: string;
|
|
13
|
+
listItem?: string;
|
|
14
|
+
level?: number;
|
|
15
|
+
children?: PortableTextSpan[];
|
|
16
|
+
markDefs?: PortableTextMarkDef[];
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PortableTextSpan {
|
|
21
|
+
_type: string;
|
|
22
|
+
text?: string;
|
|
23
|
+
marks?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PortableTextMarkDef {
|
|
27
|
+
_key: string;
|
|
28
|
+
_type: string;
|
|
29
|
+
href?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PortableTextOptions {
|
|
33
|
+
/** Resolve an image block to a URL (the adapter knows project/dataset). */
|
|
34
|
+
imageUrl?: (block: PortableTextBlock) => string | null;
|
|
35
|
+
/** Custom block-type serializers, keyed by `_type`; return Markdown/MDX. */
|
|
36
|
+
serializers?: Record<string, (block: PortableTextBlock) => string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const HEADING_STYLES: Record<string, string> = {
|
|
40
|
+
h1: "# ",
|
|
41
|
+
h2: "## ",
|
|
42
|
+
h3: "### ",
|
|
43
|
+
h4: "#### ",
|
|
44
|
+
h5: "##### ",
|
|
45
|
+
h6: "###### ",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Wrap a span's text in Markdown for its marks (decorators + link defs). */
|
|
49
|
+
const renderSpan = (
|
|
50
|
+
span: PortableTextSpan,
|
|
51
|
+
defs: Map<string, PortableTextMarkDef>
|
|
52
|
+
): string => {
|
|
53
|
+
let text = span.text ?? "";
|
|
54
|
+
if (!span.marks || span.marks.length === 0) {
|
|
55
|
+
return text;
|
|
56
|
+
}
|
|
57
|
+
// Decorators wrap inline; a link def wraps last so its label keeps emphasis.
|
|
58
|
+
let link: PortableTextMarkDef | undefined;
|
|
59
|
+
for (const mark of span.marks) {
|
|
60
|
+
switch (mark) {
|
|
61
|
+
case "strong": {
|
|
62
|
+
text = `**${text}**`;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "em": {
|
|
66
|
+
text = `*${text}*`;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "code": {
|
|
70
|
+
text = `\`${text}\``;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "strike-through": {
|
|
74
|
+
text = `~~${text}~~`;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
default: {
|
|
78
|
+
const def = defs.get(mark);
|
|
79
|
+
if (def?._type === "link") {
|
|
80
|
+
link = def;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return link?.href ? `[${text}](${link.href})` : text;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Render the inline children of a block to a single Markdown string. */
|
|
89
|
+
const renderChildren = (block: PortableTextBlock): string => {
|
|
90
|
+
const defs = new Map(
|
|
91
|
+
(block.markDefs ?? []).map((def) => [def._key, def] as const)
|
|
92
|
+
);
|
|
93
|
+
return (block.children ?? []).map((span) => renderSpan(span, defs)).join("");
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const renderBlock = (
|
|
97
|
+
block: PortableTextBlock,
|
|
98
|
+
options: PortableTextOptions
|
|
99
|
+
): string => {
|
|
100
|
+
const custom = options.serializers?.[block._type];
|
|
101
|
+
if (custom) {
|
|
102
|
+
return custom(block);
|
|
103
|
+
}
|
|
104
|
+
if (block._type === "image") {
|
|
105
|
+
const url = options.imageUrl?.(block);
|
|
106
|
+
const alt = typeof block.alt === "string" ? block.alt : "";
|
|
107
|
+
return url ? `` : "";
|
|
108
|
+
}
|
|
109
|
+
if (block._type !== "block") {
|
|
110
|
+
return `<!-- unsupported Portable Text block: ${block._type} -->`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const inline = renderChildren(block);
|
|
114
|
+
if (block.listItem) {
|
|
115
|
+
const indent = " ".repeat(Math.max(0, (block.level ?? 1) - 1));
|
|
116
|
+
const marker = block.listItem === "number" ? "1." : "-";
|
|
117
|
+
return `${indent}${marker} ${inline}`;
|
|
118
|
+
}
|
|
119
|
+
if (block.style === "blockquote") {
|
|
120
|
+
return `> ${inline}`;
|
|
121
|
+
}
|
|
122
|
+
return `${HEADING_STYLES[block.style ?? "normal"] ?? ""}${inline}`;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** Serialize a Portable Text array into a Markdown string. */
|
|
126
|
+
export const portableTextToMarkdown = (
|
|
127
|
+
blocks: PortableTextBlock[],
|
|
128
|
+
options: PortableTextOptions = {}
|
|
129
|
+
): string => {
|
|
130
|
+
const lines = blocks.map((block) => renderBlock(block, options));
|
|
131
|
+
// List items are single-newline separated; everything else gets a blank line.
|
|
132
|
+
const out: string[] = [];
|
|
133
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
134
|
+
const line = lines[i] ?? "";
|
|
135
|
+
const prevList = Boolean(blocks[i - 1]?.listItem);
|
|
136
|
+
const thisList = Boolean(blocks[i]?.listItem);
|
|
137
|
+
if (i > 0) {
|
|
138
|
+
out.push(prevList && thisList ? "\n" : "\n\n");
|
|
139
|
+
}
|
|
140
|
+
out.push(line);
|
|
141
|
+
}
|
|
142
|
+
return `${out.join("")}\n`;
|
|
143
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import type { PageRecord } from "../types.ts";
|
|
4
|
+
import type { ContentSource } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
/** The subset of a scanned project the entry reader needs. */
|
|
7
|
+
export interface EntryReadContext {
|
|
8
|
+
/** The instantiated sources, keyed for `read()` lookups. */
|
|
9
|
+
sources?: ContentSource[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read an entry's raw body text without assuming a filesystem path. Prefers the
|
|
14
|
+
* body captured at scan time (staged sources), then the owning source's lazy
|
|
15
|
+
* `read()`, then the back-compat `sourcePath` (filesystem). Returns `""` when an
|
|
16
|
+
* entry can't be resolved, so callers degrade gracefully.
|
|
17
|
+
*/
|
|
18
|
+
export const readEntryText = async (
|
|
19
|
+
ctx: EntryReadContext,
|
|
20
|
+
page: PageRecord
|
|
21
|
+
): Promise<string> => {
|
|
22
|
+
if (page.body) {
|
|
23
|
+
return page.body.text;
|
|
24
|
+
}
|
|
25
|
+
const name = page.source?.name;
|
|
26
|
+
const source = name
|
|
27
|
+
? ctx.sources?.find((candidate) => candidate.name === name)
|
|
28
|
+
: undefined;
|
|
29
|
+
if (source?.read) {
|
|
30
|
+
return await source.read(page.source.ref);
|
|
31
|
+
}
|
|
32
|
+
if (page.sourcePath) {
|
|
33
|
+
return await readFile(page.sourcePath, "utf-8");
|
|
34
|
+
}
|
|
35
|
+
return "";
|
|
36
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { join } from "pathe";
|
|
2
|
+
|
|
3
|
+
import type { ContentSourceConfig, ResolvedConfig } from "../schema.ts";
|
|
4
|
+
import type { ProjectContext } from "../types.ts";
|
|
5
|
+
import { filesystemSource } from "./filesystem.ts";
|
|
6
|
+
import { mdxRemoteSource } from "./mdx-remote.ts";
|
|
7
|
+
import { mintlifySource } from "./mintlify.ts";
|
|
8
|
+
import { notionSource } from "./notion.ts";
|
|
9
|
+
import { sanitySource } from "./sanity.ts";
|
|
10
|
+
import type { ContentSource, SourceContext } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
/** Allocate a unique, stable source name from a base (prefix or type). */
|
|
13
|
+
const uniqueNamer = (): ((base: string) => string) => {
|
|
14
|
+
const used = new Set<string>();
|
|
15
|
+
return (base) => {
|
|
16
|
+
let name = base;
|
|
17
|
+
let n = 2;
|
|
18
|
+
while (used.has(name)) {
|
|
19
|
+
name = `${base}-${n}`;
|
|
20
|
+
n += 1;
|
|
21
|
+
}
|
|
22
|
+
used.add(name);
|
|
23
|
+
return name;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Runtime knobs for a scan: dev/build mode, preview, and cache refresh. */
|
|
28
|
+
export interface SourceRuntime {
|
|
29
|
+
mode: "dev" | "build";
|
|
30
|
+
preview?: boolean;
|
|
31
|
+
refresh?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sourceContext = (
|
|
35
|
+
context: ProjectContext,
|
|
36
|
+
name: string,
|
|
37
|
+
runtime: SourceRuntime
|
|
38
|
+
): SourceContext => ({
|
|
39
|
+
assetsBaseUrl: `/blume-assets/${name}`,
|
|
40
|
+
assetsDir: join(context.outDir, "public", "blume-assets", name),
|
|
41
|
+
cacheDir: join(context.outDir, "cache", name),
|
|
42
|
+
mode: runtime.mode,
|
|
43
|
+
preview: runtime.preview,
|
|
44
|
+
projectRoot: context.root,
|
|
45
|
+
refresh: runtime.refresh ?? runtime.mode === "build",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const buildSource = (
|
|
49
|
+
def: ContentSourceConfig,
|
|
50
|
+
name: string,
|
|
51
|
+
context: ProjectContext,
|
|
52
|
+
runtime: SourceRuntime
|
|
53
|
+
): ContentSource => {
|
|
54
|
+
if (def.type === "filesystem") {
|
|
55
|
+
return filesystemSource({
|
|
56
|
+
exclude: def.exclude,
|
|
57
|
+
include: def.include,
|
|
58
|
+
name,
|
|
59
|
+
prefix: def.prefix,
|
|
60
|
+
projectRoot: context.root,
|
|
61
|
+
root: def.root,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (def.type === "mintlify") {
|
|
65
|
+
return mintlifySource({
|
|
66
|
+
configFile: def.configFile,
|
|
67
|
+
exclude: def.exclude,
|
|
68
|
+
include: def.include,
|
|
69
|
+
name,
|
|
70
|
+
prefix: def.prefix,
|
|
71
|
+
projectRoot: context.root,
|
|
72
|
+
root: def.root,
|
|
73
|
+
variables: def.variables,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (def.type === "custom") {
|
|
77
|
+
// A user-provided instance manages its own context/caching; we only ensure
|
|
78
|
+
// its name is unique across the project for id namespacing.
|
|
79
|
+
return def.source.name === name ? def.source : { ...def.source, name };
|
|
80
|
+
}
|
|
81
|
+
if (def.type === "sanity") {
|
|
82
|
+
return sanitySource(
|
|
83
|
+
{
|
|
84
|
+
apiVersion: def.apiVersion,
|
|
85
|
+
dataset: def.dataset,
|
|
86
|
+
fields: def.fields,
|
|
87
|
+
name,
|
|
88
|
+
pollInterval: def.pollInterval,
|
|
89
|
+
prefix: def.prefix,
|
|
90
|
+
projectId: def.projectId,
|
|
91
|
+
query: def.query,
|
|
92
|
+
},
|
|
93
|
+
sourceContext(context, name, runtime)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (def.type === "notion") {
|
|
97
|
+
return notionSource(
|
|
98
|
+
{
|
|
99
|
+
database: def.database,
|
|
100
|
+
name,
|
|
101
|
+
pollInterval: def.pollInterval,
|
|
102
|
+
prefix: def.prefix,
|
|
103
|
+
properties: def.properties,
|
|
104
|
+
publishedValue: def.publishedValue,
|
|
105
|
+
},
|
|
106
|
+
sourceContext(context, name, runtime)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return mdxRemoteSource(
|
|
110
|
+
{
|
|
111
|
+
files: def.files,
|
|
112
|
+
github: def.github,
|
|
113
|
+
include: def.include,
|
|
114
|
+
name,
|
|
115
|
+
pollInterval: def.pollInterval,
|
|
116
|
+
prefix: def.prefix,
|
|
117
|
+
url: def.url,
|
|
118
|
+
},
|
|
119
|
+
sourceContext(context, name, runtime)
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** The base name to allocate for a source config (before deduplication). */
|
|
124
|
+
const baseName = (def: ContentSourceConfig): string => {
|
|
125
|
+
if (def.type === "custom") {
|
|
126
|
+
return def.source.name;
|
|
127
|
+
}
|
|
128
|
+
return def.prefix ?? def.type;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build the ordered list of content sources for a project. With no
|
|
133
|
+
* `content.sources` configured, the top-level `root`/`include`/`exclude` desugar
|
|
134
|
+
* to a single implicit filesystem source, so existing projects are untouched.
|
|
135
|
+
*/
|
|
136
|
+
export const resolveSources = (
|
|
137
|
+
config: ResolvedConfig,
|
|
138
|
+
context: ProjectContext,
|
|
139
|
+
runtime: SourceRuntime
|
|
140
|
+
): ContentSource[] => {
|
|
141
|
+
const defs = config.content.sources;
|
|
142
|
+
if (!defs || defs.length === 0) {
|
|
143
|
+
return [
|
|
144
|
+
filesystemSource({
|
|
145
|
+
exclude: config.content.exclude,
|
|
146
|
+
include: config.content.include,
|
|
147
|
+
name: "filesystem",
|
|
148
|
+
projectRoot: context.root,
|
|
149
|
+
root: config.content.root,
|
|
150
|
+
}),
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const nameFor = uniqueNamer();
|
|
155
|
+
return defs.map((def) =>
|
|
156
|
+
buildSource(def, nameFor(baseName(def)), context, runtime)
|
|
157
|
+
);
|
|
158
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import matter from "gray-matter";
|
|
2
|
+
import { join } from "pathe";
|
|
3
|
+
|
|
4
|
+
import { BlumeError } from "../diagnostics.ts";
|
|
5
|
+
import {
|
|
6
|
+
hashText,
|
|
7
|
+
loadWithCache,
|
|
8
|
+
pollingWatch,
|
|
9
|
+
snapshotCache,
|
|
10
|
+
} from "./cache.ts";
|
|
11
|
+
import { slugify } from "./normalize.ts";
|
|
12
|
+
import { portableTextToMarkdown } from "./portable-text.ts";
|
|
13
|
+
import type { PortableTextBlock } from "./portable-text.ts";
|
|
14
|
+
import type {
|
|
15
|
+
ContentSource,
|
|
16
|
+
SourceContext,
|
|
17
|
+
SourceEntry,
|
|
18
|
+
SourceLoadResult,
|
|
19
|
+
} from "./types.ts";
|
|
20
|
+
|
|
21
|
+
/** The slice of `@sanity/client` this adapter relies on (so it's mockable). */
|
|
22
|
+
export interface SanityClientLike {
|
|
23
|
+
fetch: <T = unknown>(query: string) => Promise<T>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Field paths mapping a Sanity document onto Blume meta + body. */
|
|
27
|
+
export interface SanityFieldMap {
|
|
28
|
+
/** Frontmatter title; default `title`. */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Frontmatter description; default `description`. */
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Route slug (dot path); default `slug.current`. */
|
|
33
|
+
slug?: string;
|
|
34
|
+
/** Portable Text body field; default `body`. */
|
|
35
|
+
body?: string;
|
|
36
|
+
/** Last-modified ISO date; default `_updatedAt`. */
|
|
37
|
+
lastModified?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SanitySourceOptions {
|
|
41
|
+
name: string;
|
|
42
|
+
prefix?: string;
|
|
43
|
+
projectId: string;
|
|
44
|
+
dataset: string;
|
|
45
|
+
/** Sanity API version (a date); default `2024-01-01`. */
|
|
46
|
+
apiVersion?: string;
|
|
47
|
+
/** GROQ query selecting the documents to import. */
|
|
48
|
+
query: string;
|
|
49
|
+
fields?: SanityFieldMap;
|
|
50
|
+
/** Custom Portable Text block serializers, keyed by `_type`. */
|
|
51
|
+
serializers?: Record<string, (block: PortableTextBlock) => string>;
|
|
52
|
+
/** Read token for private datasets; defaults to `SANITY_TOKEN`. */
|
|
53
|
+
token?: string;
|
|
54
|
+
/** Opt-in dev polling interval (seconds); omit to freeze for the session. */
|
|
55
|
+
pollInterval?: number;
|
|
56
|
+
/** Injected for tests; otherwise built from `@sanity/client`. */
|
|
57
|
+
client?: SanityClientLike;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const IMAGE_REF = /^image-(?<id>[a-f0-9]+)-(?<dims>\d+x\d+)-(?<ext>\w+)$/u;
|
|
61
|
+
|
|
62
|
+
/** Resolve a dot path (`slug.current`) against a document. */
|
|
63
|
+
const getPath = (doc: Record<string, unknown>, path: string): unknown => {
|
|
64
|
+
let current: unknown = doc;
|
|
65
|
+
for (const key of path.split(".")) {
|
|
66
|
+
if (current && typeof current === "object") {
|
|
67
|
+
current = (current as Record<string, unknown>)[key];
|
|
68
|
+
} else {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return current;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const asString = (value: unknown): string | undefined =>
|
|
76
|
+
typeof value === "string" ? value : undefined;
|
|
77
|
+
|
|
78
|
+
/** Build a Sanity CDN URL from an image asset `_ref`. */
|
|
79
|
+
const imageUrlFromRef = (
|
|
80
|
+
ref: string,
|
|
81
|
+
projectId: string,
|
|
82
|
+
dataset: string
|
|
83
|
+
): string | null => {
|
|
84
|
+
const match = ref.match(IMAGE_REF);
|
|
85
|
+
if (!match?.groups) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const { id, dims, ext } = match.groups;
|
|
89
|
+
return `https://cdn.sanity.io/images/${projectId}/${dataset}/${id}-${dims}.${ext}`;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const resolveClient = async (
|
|
93
|
+
options: SanitySourceOptions,
|
|
94
|
+
preview: boolean
|
|
95
|
+
): Promise<SanityClientLike> => {
|
|
96
|
+
if (options.client) {
|
|
97
|
+
return options.client;
|
|
98
|
+
}
|
|
99
|
+
let createClient: (config: Record<string, unknown>) => SanityClientLike;
|
|
100
|
+
try {
|
|
101
|
+
({ createClient } = (await import("@sanity/client")) as {
|
|
102
|
+
createClient: (config: Record<string, unknown>) => SanityClientLike;
|
|
103
|
+
});
|
|
104
|
+
} catch {
|
|
105
|
+
throw new BlumeError({
|
|
106
|
+
code: "BLUME_SOURCE_SDK_MISSING",
|
|
107
|
+
message: `Source "${options.name}" needs "@sanity/client". Install it (e.g. \`npm install @sanity/client\`).`,
|
|
108
|
+
severity: "error",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return createClient({
|
|
112
|
+
apiVersion: options.apiVersion ?? "2024-01-01",
|
|
113
|
+
dataset: options.dataset,
|
|
114
|
+
// Preview reads draft documents through the API; published builds use the CDN.
|
|
115
|
+
perspective: preview ? "previewDrafts" : "published",
|
|
116
|
+
projectId: options.projectId,
|
|
117
|
+
token: options.token ?? process.env.SANITY_TOKEN,
|
|
118
|
+
useCdn: !preview,
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Sanity content source. Runs a GROQ query, maps each document's fields to Blume
|
|
124
|
+
* frontmatter and its Portable Text body to Markdown, and stages the result.
|
|
125
|
+
*/
|
|
126
|
+
export const sanitySource = (
|
|
127
|
+
options: SanitySourceOptions,
|
|
128
|
+
ctx?: SourceContext
|
|
129
|
+
): ContentSource => {
|
|
130
|
+
const fields = options.fields ?? {};
|
|
131
|
+
// When constructed directly (custom-source SPI) without a context, cache under
|
|
132
|
+
// a name-derived dir relative to the project; the built-in type passes a ctx.
|
|
133
|
+
const cache = snapshotCache(
|
|
134
|
+
ctx?.cacheDir ?? join(".blume", "cache", options.name)
|
|
135
|
+
);
|
|
136
|
+
let snapshot = new Map<string, SourceEntry>();
|
|
137
|
+
|
|
138
|
+
const toEntry = (doc: Record<string, unknown>): SourceEntry => {
|
|
139
|
+
const slugValue =
|
|
140
|
+
asString(getPath(doc, fields.slug ?? "slug.current")) ??
|
|
141
|
+
asString(doc._id) ??
|
|
142
|
+
"untitled";
|
|
143
|
+
const slug = slugify(slugValue) || "untitled";
|
|
144
|
+
|
|
145
|
+
const data: Record<string, unknown> = {};
|
|
146
|
+
const title = asString(getPath(doc, fields.title ?? "title"));
|
|
147
|
+
const description = asString(
|
|
148
|
+
getPath(doc, fields.description ?? "description")
|
|
149
|
+
);
|
|
150
|
+
if (title) {
|
|
151
|
+
data.title = title;
|
|
152
|
+
}
|
|
153
|
+
if (description) {
|
|
154
|
+
data.description = description;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const blocks = (getPath(doc, fields.body ?? "body") ??
|
|
158
|
+
[]) as PortableTextBlock[];
|
|
159
|
+
const markdown = Array.isArray(blocks)
|
|
160
|
+
? portableTextToMarkdown(blocks, {
|
|
161
|
+
imageUrl: (block) => {
|
|
162
|
+
const ref = (block.asset as { _ref?: string } | undefined)?._ref;
|
|
163
|
+
return ref
|
|
164
|
+
? imageUrlFromRef(ref, options.projectId, options.dataset)
|
|
165
|
+
: null;
|
|
166
|
+
},
|
|
167
|
+
serializers: options.serializers,
|
|
168
|
+
})
|
|
169
|
+
: "";
|
|
170
|
+
|
|
171
|
+
const raw = matter.stringify(markdown, data);
|
|
172
|
+
return {
|
|
173
|
+
body: { format: "md", text: markdown },
|
|
174
|
+
data,
|
|
175
|
+
hash: hashText(raw),
|
|
176
|
+
lastModified: asString(getPath(doc, fields.lastModified ?? "_updatedAt")),
|
|
177
|
+
raw,
|
|
178
|
+
ref: `${slug}.md`,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const load = async (): Promise<SourceLoadResult> => {
|
|
183
|
+
const result = await loadWithCache(
|
|
184
|
+
options.name,
|
|
185
|
+
cache,
|
|
186
|
+
async () => {
|
|
187
|
+
const client = await resolveClient(options, ctx?.preview ?? false);
|
|
188
|
+
const docs = await client.fetch<Record<string, unknown>[]>(
|
|
189
|
+
options.query
|
|
190
|
+
);
|
|
191
|
+
return docs.map(toEntry);
|
|
192
|
+
},
|
|
193
|
+
ctx?.refresh ?? true
|
|
194
|
+
);
|
|
195
|
+
snapshot = new Map(result.entries.map((entry) => [entry.ref, entry]));
|
|
196
|
+
return result;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const read = async (ref: string): Promise<string> => {
|
|
200
|
+
const cached = snapshot.get(ref);
|
|
201
|
+
if (cached) {
|
|
202
|
+
return cached.raw ?? cached.body.text;
|
|
203
|
+
}
|
|
204
|
+
const all = await cache.read();
|
|
205
|
+
return all.find((e) => e.ref === ref)?.raw ?? "";
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
load,
|
|
210
|
+
name: options.name,
|
|
211
|
+
prefix: options.prefix,
|
|
212
|
+
read,
|
|
213
|
+
staged: true,
|
|
214
|
+
watch: options.pollInterval
|
|
215
|
+
? pollingWatch(load, options.pollInterval)
|
|
216
|
+
: undefined,
|
|
217
|
+
};
|
|
218
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ResolvedI18nConfig } from "../schema.ts";
|
|
2
|
+
import type { Diagnostic } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A single content item, normalized by a source adapter. Adapters lower their
|
|
6
|
+
* native shape (files, Portable Text, Notion blocks, remote HTML) to Markdown/MDX
|
|
7
|
+
* *text* so Blume's markdown processors and component set apply uniformly.
|
|
8
|
+
*/
|
|
9
|
+
export interface SourceEntry {
|
|
10
|
+
/** Source-local stable id, e.g. `api/auth.mdx` or a CMS document id. */
|
|
11
|
+
ref: string;
|
|
12
|
+
/** Logical route input; defaults to `ref` if omitted. May include slashes. */
|
|
13
|
+
slug?: string;
|
|
14
|
+
/** Frontmatter-equivalent metadata, validated against the Blume meta schema. */
|
|
15
|
+
data: Record<string, unknown>;
|
|
16
|
+
/** The renderable body as Markdown/MDX source text (frontmatter stripped). */
|
|
17
|
+
body: { format: "md" | "mdx"; text: string };
|
|
18
|
+
/**
|
|
19
|
+
* Full source text including frontmatter, written verbatim to the staging
|
|
20
|
+
* dir so Astro re-parses the same frontmatter. Staged (non-filesystem) sources
|
|
21
|
+
* set this; filesystem entries (read from disk) omit it.
|
|
22
|
+
*/
|
|
23
|
+
raw?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Absolute filesystem path when the entry originates from disk. Populated by
|
|
26
|
+
* the filesystem adapter only; powers git last-modified and edit URLs and the
|
|
27
|
+
* `sourcePath` back-compat window. Omitted by remote/CMS adapters.
|
|
28
|
+
*/
|
|
29
|
+
sourcePath?: string;
|
|
30
|
+
/** Optional provenance for "edit this page". */
|
|
31
|
+
editUrl?: string;
|
|
32
|
+
/** Optional last-modified ISO date supplied by the adapter (non-filesystem). */
|
|
33
|
+
lastModified?: string;
|
|
34
|
+
/** Content hash for cache invalidation / HMR; adapter-computed when cheap. */
|
|
35
|
+
hash?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The result of a single `ContentSource.load()` call. */
|
|
39
|
+
export interface SourceLoadResult {
|
|
40
|
+
entries: SourceEntry[];
|
|
41
|
+
/** Source-level diagnostics (e.g. an offline cache fallback warning). */
|
|
42
|
+
diagnostics: Diagnostic[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Per-source runtime context, handed to an adapter factory at construction so
|
|
47
|
+
* `load`/`read`/`watch` can close over it without re-threading on every call.
|
|
48
|
+
*/
|
|
49
|
+
export interface SourceContext {
|
|
50
|
+
projectRoot: string;
|
|
51
|
+
/** Per-source cache dir under `.blume/cache/<source>/`. */
|
|
52
|
+
cacheDir: string;
|
|
53
|
+
mode: "dev" | "build";
|
|
54
|
+
/** Dir for downloaded assets (served from the site's public dir). */
|
|
55
|
+
assetsDir?: string;
|
|
56
|
+
/** Public URL prefix the downloaded assets are served under. */
|
|
57
|
+
assetsBaseUrl?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Re-fetch remote content instead of serving the cached snapshot. True for
|
|
60
|
+
* builds and `blume sync`; false in dev (cache-first for fast, offline-tolerant
|
|
61
|
+
* restarts — refresh with `blume sync` or an opt-in `pollInterval`).
|
|
62
|
+
*/
|
|
63
|
+
refresh?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Preview unpublished content: drafts are kept and CMS adapters fetch draft
|
|
66
|
+
* documents (Sanity's `previewDrafts` perspective). Off for production builds.
|
|
67
|
+
*/
|
|
68
|
+
preview?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A pluggable content source. Adapters enumerate normalized entries and
|
|
73
|
+
* (optionally) re-read a single entry, validate themselves, and watch for
|
|
74
|
+
* changes. Identity is the opaque `SourceEntry.ref`, never a filesystem path.
|
|
75
|
+
*/
|
|
76
|
+
export interface ContentSource {
|
|
77
|
+
/** Unique, stable name; used for id namespacing and diagnostics. */
|
|
78
|
+
readonly name: string;
|
|
79
|
+
/**
|
|
80
|
+
* Whether entries render through the staging collection. Filesystem sources
|
|
81
|
+
* render through Astro's existing `docs` glob collection (`false`); every
|
|
82
|
+
* other source materializes MDX into `.blume/content` (`true`).
|
|
83
|
+
*/
|
|
84
|
+
readonly staged: boolean;
|
|
85
|
+
/** Optional route prefix; the source's routes namespace under `/<prefix>/`. */
|
|
86
|
+
readonly prefix?: string;
|
|
87
|
+
/** Pull every entry. Called once per scan. */
|
|
88
|
+
load: () => Promise<SourceLoadResult>;
|
|
89
|
+
/** Validate the source is usable; throws a BlumeError when not. */
|
|
90
|
+
validate?: () => void;
|
|
91
|
+
/** Read a single entry's body lazily (search / AI / raw export). */
|
|
92
|
+
read?: (ref: string) => Promise<string>;
|
|
93
|
+
/**
|
|
94
|
+
* Notify on change in dev. Returns a disposer. Filesystem uses `fs.watch`;
|
|
95
|
+
* remote/static sources omit it (content is frozen for the session).
|
|
96
|
+
*/
|
|
97
|
+
watch?: (onChange: () => void) => () => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Context passed to `normalizeEntry`, describing the owning source. */
|
|
101
|
+
export interface NormalizeContext {
|
|
102
|
+
source: { name: string; prefix?: string; staged: boolean };
|
|
103
|
+
defaultType: string;
|
|
104
|
+
i18n?: ResolvedI18nConfig;
|
|
105
|
+
}
|