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,392 @@
|
|
|
1
|
+
import { extname } from "pathe";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
FolderMeta,
|
|
5
|
+
SidebarDisplay,
|
|
6
|
+
SidebarItemConfig,
|
|
7
|
+
} from "./schema.ts";
|
|
8
|
+
import type {
|
|
9
|
+
NavChromeVariant,
|
|
10
|
+
NavNode,
|
|
11
|
+
NavSidebarVariant,
|
|
12
|
+
Navigation,
|
|
13
|
+
NavSelector,
|
|
14
|
+
NavTab,
|
|
15
|
+
PageRecord,
|
|
16
|
+
} from "./types.ts";
|
|
17
|
+
|
|
18
|
+
const NUMERIC_PREFIX = /^(?<order>\d+)[-_.]/u;
|
|
19
|
+
const GROUP_FOLDER = /^\((?<label>.+)\)$/u;
|
|
20
|
+
const WORD_SPLIT = /[-_]/u;
|
|
21
|
+
|
|
22
|
+
const humanize = (segment: string): string =>
|
|
23
|
+
segment
|
|
24
|
+
.replace(NUMERIC_PREFIX, "")
|
|
25
|
+
.split(WORD_SPLIT)
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
28
|
+
.join(" ");
|
|
29
|
+
|
|
30
|
+
const numericOrder = (segment: string): number => {
|
|
31
|
+
const value = segment.match(NUMERIC_PREFIX)?.groups?.order;
|
|
32
|
+
return value ? Number.parseInt(value, 10) : Number.POSITIVE_INFINITY;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The nav key of a raw path segment: group label or numeric-stripped name. */
|
|
36
|
+
const segmentKey = (raw: string): string => {
|
|
37
|
+
const group = raw.match(GROUP_FOLDER)?.groups?.label;
|
|
38
|
+
return (group ?? raw).replace(NUMERIC_PREFIX, "");
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface MutablePage {
|
|
42
|
+
kind: "page";
|
|
43
|
+
key: string;
|
|
44
|
+
label: string;
|
|
45
|
+
route: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
icon?: string;
|
|
48
|
+
badge?: string;
|
|
49
|
+
deprecated?: boolean;
|
|
50
|
+
pageId: string;
|
|
51
|
+
order: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface MutableGroup {
|
|
55
|
+
kind: "group";
|
|
56
|
+
key: string;
|
|
57
|
+
path: string;
|
|
58
|
+
label: string;
|
|
59
|
+
icon?: string;
|
|
60
|
+
collapsed?: boolean;
|
|
61
|
+
display?: SidebarDisplay;
|
|
62
|
+
order: number;
|
|
63
|
+
children: MutableNode[];
|
|
64
|
+
index: Map<string, MutableGroup>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type MutableNode = MutablePage | MutableGroup;
|
|
68
|
+
|
|
69
|
+
const createGroup = (
|
|
70
|
+
key: string,
|
|
71
|
+
path: string,
|
|
72
|
+
label: string,
|
|
73
|
+
order: number
|
|
74
|
+
): MutableGroup => ({
|
|
75
|
+
children: [],
|
|
76
|
+
index: new Map(),
|
|
77
|
+
key,
|
|
78
|
+
kind: "group",
|
|
79
|
+
label,
|
|
80
|
+
order,
|
|
81
|
+
path,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const ensureGroup = (
|
|
85
|
+
parent: MutableGroup,
|
|
86
|
+
rawSegment: string
|
|
87
|
+
): MutableGroup => {
|
|
88
|
+
const existing = parent.index.get(rawSegment);
|
|
89
|
+
if (existing) {
|
|
90
|
+
return existing;
|
|
91
|
+
}
|
|
92
|
+
const path = parent.path ? `${parent.path}/${rawSegment}` : rawSegment;
|
|
93
|
+
const group = createGroup(
|
|
94
|
+
segmentKey(rawSegment),
|
|
95
|
+
path,
|
|
96
|
+
humanize(rawSegment.match(GROUP_FOLDER)?.groups?.label ?? rawSegment),
|
|
97
|
+
numericOrder(rawSegment)
|
|
98
|
+
);
|
|
99
|
+
parent.index.set(rawSegment, group);
|
|
100
|
+
parent.children.push(group);
|
|
101
|
+
return group;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const pageOrder = (page: PageRecord, filename: string): number => {
|
|
105
|
+
if (page.meta.sidebar.order !== undefined) {
|
|
106
|
+
return page.meta.sidebar.order;
|
|
107
|
+
}
|
|
108
|
+
if (filename.replace(extname(filename), "") === "index") {
|
|
109
|
+
return Number.NEGATIVE_INFINITY;
|
|
110
|
+
}
|
|
111
|
+
return numericOrder(filename);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Folder-meta lookup key for a group path. Under i18n the meta files live in
|
|
116
|
+
* the locale directory (`fr/guides/meta.ts` -> key `fr/guides`) while the
|
|
117
|
+
* nav group path is locale-stripped (`guides`), so prepend the locale prefix.
|
|
118
|
+
*/
|
|
119
|
+
const metaKey = (path: string, metaPrefix: string): string => {
|
|
120
|
+
if (!metaPrefix) {
|
|
121
|
+
return path;
|
|
122
|
+
}
|
|
123
|
+
return path ? `${metaPrefix}/${path}` : metaPrefix;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/** Apply folder meta (title/order/icon/collapsed and explicit page order). */
|
|
127
|
+
const applyFolderMeta = (
|
|
128
|
+
group: MutableGroup,
|
|
129
|
+
folderMeta: Map<string, FolderMeta>,
|
|
130
|
+
sharedMeta: Map<string, FolderMeta>,
|
|
131
|
+
metaPrefix: string
|
|
132
|
+
): void => {
|
|
133
|
+
// Locale-specific meta wins; a shared `meta.$.*` (keyed by the locale-stripped
|
|
134
|
+
// group path) applies to every locale otherwise.
|
|
135
|
+
const meta =
|
|
136
|
+
folderMeta.get(metaKey(group.path, metaPrefix)) ??
|
|
137
|
+
sharedMeta.get(group.path);
|
|
138
|
+
if (meta) {
|
|
139
|
+
group.label = meta.title ?? group.label;
|
|
140
|
+
group.icon = meta.icon ?? group.icon;
|
|
141
|
+
group.order = meta.order ?? group.order;
|
|
142
|
+
group.collapsed = meta.collapsed ?? group.collapsed;
|
|
143
|
+
group.display = meta.display ?? group.display;
|
|
144
|
+
|
|
145
|
+
if (meta.pages) {
|
|
146
|
+
const rank = new Map(meta.pages.map((key, i) => [key, i]));
|
|
147
|
+
for (const child of group.children) {
|
|
148
|
+
const position = rank.get(child.key);
|
|
149
|
+
if (position !== undefined) {
|
|
150
|
+
child.order = position;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const child of group.children) {
|
|
157
|
+
if (child.kind === "group") {
|
|
158
|
+
applyFolderMeta(child, folderMeta, sharedMeta, metaPrefix);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const sortNodes = (nodes: MutableNode[]): void => {
|
|
164
|
+
nodes.sort((a, b) => {
|
|
165
|
+
if (a.order !== b.order) {
|
|
166
|
+
return a.order - b.order;
|
|
167
|
+
}
|
|
168
|
+
return a.label.localeCompare(b.label);
|
|
169
|
+
});
|
|
170
|
+
for (const node of nodes) {
|
|
171
|
+
if (node.kind === "group") {
|
|
172
|
+
sortNodes(node.children);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const toNavNode = (node: MutableNode): NavNode => {
|
|
178
|
+
if (node.kind === "page") {
|
|
179
|
+
return {
|
|
180
|
+
badge: node.badge,
|
|
181
|
+
deprecated: node.deprecated || undefined,
|
|
182
|
+
description: node.description,
|
|
183
|
+
icon: node.icon,
|
|
184
|
+
kind: "page",
|
|
185
|
+
label: node.label,
|
|
186
|
+
pageId: node.pageId,
|
|
187
|
+
route: node.route,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
children: node.children.map(toNavNode),
|
|
192
|
+
collapsed: node.collapsed,
|
|
193
|
+
display: node.display,
|
|
194
|
+
icon: node.icon,
|
|
195
|
+
kind: "group",
|
|
196
|
+
label: node.label,
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** Build the sidebar tree from the file system and folder meta. */
|
|
201
|
+
const buildFileSystemSidebar = (
|
|
202
|
+
pages: PageRecord[],
|
|
203
|
+
folderMeta: Map<string, FolderMeta>,
|
|
204
|
+
sharedMeta: Map<string, FolderMeta>,
|
|
205
|
+
metaPrefix: string
|
|
206
|
+
): NavNode[] => {
|
|
207
|
+
const root = createGroup("", "", "", 0);
|
|
208
|
+
|
|
209
|
+
for (const page of pages) {
|
|
210
|
+
if (page.meta.sidebar.hidden) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// Group by the locale-stripped path so the locale dir is not a nav group.
|
|
214
|
+
const parts = page.navPath.split("/");
|
|
215
|
+
const filename = parts.at(-1) ?? page.navPath;
|
|
216
|
+
const dirs = parts.slice(0, -1);
|
|
217
|
+
|
|
218
|
+
let parent = root;
|
|
219
|
+
for (const dir of dirs) {
|
|
220
|
+
parent = ensureGroup(parent, dir);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
parent.children.push({
|
|
224
|
+
badge: page.meta.sidebar.badge,
|
|
225
|
+
deprecated: page.meta.deprecated || undefined,
|
|
226
|
+
description: page.description,
|
|
227
|
+
icon: page.meta.sidebar.icon,
|
|
228
|
+
key: segmentKey(filename.replace(extname(filename), "")),
|
|
229
|
+
kind: "page",
|
|
230
|
+
label: page.meta.sidebar.label ?? page.title,
|
|
231
|
+
order: pageOrder(page, filename),
|
|
232
|
+
pageId: page.id,
|
|
233
|
+
route: page.route,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
applyFolderMeta(root, folderMeta, sharedMeta, metaPrefix);
|
|
238
|
+
sortNodes(root.children);
|
|
239
|
+
return root.children.map(toNavNode);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const normalizeRef = (ref: string): string => {
|
|
243
|
+
if (ref === "index") {
|
|
244
|
+
return "/";
|
|
245
|
+
}
|
|
246
|
+
const withSlash = ref.startsWith("/") ? ref : `/${ref}`;
|
|
247
|
+
return withSlash.endsWith("/index") ? withSlash.slice(0, -6) : withSlash;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const routeForRef = (
|
|
251
|
+
ref: string | undefined,
|
|
252
|
+
byRoute: Map<string, PageRecord>
|
|
253
|
+
): string | undefined => {
|
|
254
|
+
if (!ref) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
const normalized = normalizeRef(ref);
|
|
258
|
+
return byRoute.get(normalized)?.route ?? normalized;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/** Build the sidebar tree from an explicit config spec. */
|
|
262
|
+
const buildConfigSidebar = (
|
|
263
|
+
items: SidebarItemConfig[],
|
|
264
|
+
byRoute: Map<string, PageRecord>
|
|
265
|
+
): NavNode[] => {
|
|
266
|
+
const nodes: NavNode[] = [];
|
|
267
|
+
|
|
268
|
+
for (const item of items) {
|
|
269
|
+
if (typeof item === "string") {
|
|
270
|
+
const page = byRoute.get(normalizeRef(item));
|
|
271
|
+
if (page) {
|
|
272
|
+
nodes.push({
|
|
273
|
+
badge: page.meta.sidebar.badge,
|
|
274
|
+
deprecated: page.meta.deprecated || undefined,
|
|
275
|
+
description: page.description,
|
|
276
|
+
icon: page.meta.sidebar.icon,
|
|
277
|
+
kind: "page",
|
|
278
|
+
label: page.meta.sidebar.label ?? page.title,
|
|
279
|
+
pageId: page.id,
|
|
280
|
+
route: page.route,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (item.items) {
|
|
287
|
+
nodes.push({
|
|
288
|
+
badge: item.badge,
|
|
289
|
+
children: buildConfigSidebar(item.items, byRoute),
|
|
290
|
+
collapsed: item.collapsed,
|
|
291
|
+
directory: item.directory,
|
|
292
|
+
display: item.display,
|
|
293
|
+
icon: item.icon,
|
|
294
|
+
kind: "group",
|
|
295
|
+
label: item.label,
|
|
296
|
+
route: routeForRef(item.root, byRoute),
|
|
297
|
+
});
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (item.root) {
|
|
302
|
+
const page = byRoute.get(normalizeRef(item.root));
|
|
303
|
+
nodes.push({
|
|
304
|
+
badge: item.badge,
|
|
305
|
+
deprecated: page?.meta.deprecated || undefined,
|
|
306
|
+
icon: item.icon,
|
|
307
|
+
kind: "page",
|
|
308
|
+
label: item.label,
|
|
309
|
+
pageId: page?.id ?? "",
|
|
310
|
+
route: page?.route ?? normalizeRef(item.root),
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (item.href) {
|
|
316
|
+
nodes.push({
|
|
317
|
+
badge: item.badge,
|
|
318
|
+
icon: item.icon,
|
|
319
|
+
kind: "page",
|
|
320
|
+
label: item.label,
|
|
321
|
+
pageId: "",
|
|
322
|
+
route: item.href,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return nodes;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/** Build the complete navigation model from pages, meta, and config. */
|
|
331
|
+
export const buildNavigation = (
|
|
332
|
+
pages: PageRecord[],
|
|
333
|
+
options: {
|
|
334
|
+
chromeVariants?: NavChromeVariant[];
|
|
335
|
+
folderMeta: Map<string, FolderMeta>;
|
|
336
|
+
selectors?: NavSelector[];
|
|
337
|
+
tabs?: NavTab[];
|
|
338
|
+
sidebar?: SidebarItemConfig[];
|
|
339
|
+
sidebarVariants?: { path: string; items: SidebarItemConfig[] }[];
|
|
340
|
+
/** Locale dir prefix for folder-meta lookup (`""` for the default locale). */
|
|
341
|
+
metaPrefix?: string;
|
|
342
|
+
/**
|
|
343
|
+
* Resolve explicit-sidebar references against each page's locale-agnostic
|
|
344
|
+
* `translationKey` instead of its localized `route`. Used under i18n so a
|
|
345
|
+
* single authored sidebar maps onto every locale's pages.
|
|
346
|
+
*/
|
|
347
|
+
refByLogical?: boolean;
|
|
348
|
+
/** Shared `meta.$.*` meta, keyed by locale-stripped dir path. */
|
|
349
|
+
sharedFolderMeta?: Map<string, FolderMeta>;
|
|
350
|
+
}
|
|
351
|
+
): Navigation => {
|
|
352
|
+
const chromeVariants = options.chromeVariants ?? [];
|
|
353
|
+
const selectors = options.selectors ?? [];
|
|
354
|
+
const tabs = options.tabs ?? [];
|
|
355
|
+
const metaPrefix = options.metaPrefix ?? "";
|
|
356
|
+
const sharedFolderMeta = options.sharedFolderMeta ?? new Map();
|
|
357
|
+
const byRoute = new Map(
|
|
358
|
+
pages.map((page) => [
|
|
359
|
+
options.refByLogical ? page.translationKey : page.route,
|
|
360
|
+
page,
|
|
361
|
+
])
|
|
362
|
+
);
|
|
363
|
+
const sidebarVariants: NavSidebarVariant[] = (
|
|
364
|
+
options.sidebarVariants ?? []
|
|
365
|
+
).map((variant) => ({
|
|
366
|
+
path: variant.path,
|
|
367
|
+
sidebar: buildConfigSidebar(variant.items, byRoute),
|
|
368
|
+
}));
|
|
369
|
+
|
|
370
|
+
if (options.sidebar) {
|
|
371
|
+
return {
|
|
372
|
+
chromeVariants,
|
|
373
|
+
selectors,
|
|
374
|
+
sidebar: buildConfigSidebar(options.sidebar, byRoute),
|
|
375
|
+
sidebarVariants,
|
|
376
|
+
tabs,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
chromeVariants,
|
|
382
|
+
selectors,
|
|
383
|
+
sidebar: buildFileSystemSidebar(
|
|
384
|
+
pages,
|
|
385
|
+
options.folderMeta,
|
|
386
|
+
sharedFolderMeta,
|
|
387
|
+
metaPrefix
|
|
388
|
+
),
|
|
389
|
+
sidebarVariants,
|
|
390
|
+
tabs,
|
|
391
|
+
};
|
|
392
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { dirname, join } from "pathe";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Walk up from `start` until a directory containing a `package.json` is found.
|
|
7
|
+
*
|
|
8
|
+
* Exported for testing; call sites should use {@link packageRoot}.
|
|
9
|
+
*/
|
|
10
|
+
export const findPackageRoot = (start: string): string => {
|
|
11
|
+
let dir = start;
|
|
12
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
13
|
+
const parent = dirname(dir);
|
|
14
|
+
if (parent === dir) {
|
|
15
|
+
throw new Error("blume: unable to locate the package root");
|
|
16
|
+
}
|
|
17
|
+
dir = parent;
|
|
18
|
+
}
|
|
19
|
+
return dir;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let cached: string | undefined;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Absolute path to the installed Blume package root (the directory holding its
|
|
26
|
+
* `package.json`), found by walking up from this module.
|
|
27
|
+
*
|
|
28
|
+
* Anchoring here — rather than at a fixed offset from `import.meta` — keeps the
|
|
29
|
+
* package's own `src/`, assets, and `node_modules` locatable whether the code
|
|
30
|
+
* runs from source under Bun (`src/...`) or from the published, bundled CLI
|
|
31
|
+
* (`dist/cli/index.js`). The two layouts sit at different depths, so a relative
|
|
32
|
+
* `../..` resolves to different places; locating `package.json` does not.
|
|
33
|
+
*/
|
|
34
|
+
export const packageRoot = (): string => {
|
|
35
|
+
cached ??= findPackageRoot(import.meta.dirname);
|
|
36
|
+
return cached;
|
|
37
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { loadConfig } from "./config.ts";
|
|
2
|
+
import type { ConfigBridge } from "./config.ts";
|
|
3
|
+
import { buildContentGraph } from "./graph.ts";
|
|
4
|
+
import { i18nDiagnostics } from "./i18n.ts";
|
|
5
|
+
import {
|
|
6
|
+
gitLastModifiedTimes,
|
|
7
|
+
resolveLastModifiedConfig,
|
|
8
|
+
} from "./last-modified.ts";
|
|
9
|
+
import { buildManifest } from "./manifest.ts";
|
|
10
|
+
import { discoverFolderMeta } from "./meta.ts";
|
|
11
|
+
import { resolveProjectContext } from "./project.ts";
|
|
12
|
+
import type { ResolvedConfig } from "./schema.ts";
|
|
13
|
+
import { normalizeEntry } from "./sources/normalize.ts";
|
|
14
|
+
import { resolveSources } from "./sources/resolve.ts";
|
|
15
|
+
import type { ContentSource } from "./sources/types.ts";
|
|
16
|
+
import type {
|
|
17
|
+
BlumeManifest,
|
|
18
|
+
ContentGraph,
|
|
19
|
+
Diagnostic,
|
|
20
|
+
PageRecord,
|
|
21
|
+
ProjectContext,
|
|
22
|
+
} from "./types.ts";
|
|
23
|
+
|
|
24
|
+
/** Build mode: drafts are kept in `dev` and dropped in `build`. */
|
|
25
|
+
export type BuildMode = "dev" | "build";
|
|
26
|
+
|
|
27
|
+
/** Everything Blume knows about a project after a full scan. */
|
|
28
|
+
export interface BlumeProject {
|
|
29
|
+
mode: BuildMode;
|
|
30
|
+
context: ProjectContext;
|
|
31
|
+
config: ResolvedConfig;
|
|
32
|
+
graph: ContentGraph;
|
|
33
|
+
manifest: BlumeManifest;
|
|
34
|
+
diagnostics: Diagnostic[];
|
|
35
|
+
/** The instantiated content sources, for lazy entry reads (search/AI/raw). */
|
|
36
|
+
sources: ContentSource[];
|
|
37
|
+
/** Set when serving a foreign docs config (e.g. Mintlify) without migrating. */
|
|
38
|
+
bridge: ConfigBridge | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Run the full core pipeline for a project root: load config, resolve paths,
|
|
43
|
+
* discover content and folder meta, build the graph, and assemble the manifest.
|
|
44
|
+
* Collects all diagnostics without throwing on content-level problems so
|
|
45
|
+
* callers can decide how strict to be.
|
|
46
|
+
*/
|
|
47
|
+
export const scanProject = async (
|
|
48
|
+
root: string,
|
|
49
|
+
options: {
|
|
50
|
+
devServerUrl?: string;
|
|
51
|
+
mode?: BuildMode;
|
|
52
|
+
preview?: boolean;
|
|
53
|
+
refresh?: boolean;
|
|
54
|
+
} = {}
|
|
55
|
+
): Promise<BlumeProject> => {
|
|
56
|
+
const mode = options.mode ?? "dev";
|
|
57
|
+
const preview = options.preview ?? false;
|
|
58
|
+
const { bridge, config } = await loadConfig(root, {
|
|
59
|
+
devServerUrl: options.devServerUrl,
|
|
60
|
+
});
|
|
61
|
+
const context = resolveProjectContext(root, config);
|
|
62
|
+
|
|
63
|
+
// Each source validates itself (e.g. the filesystem source checks its root
|
|
64
|
+
// exists), replacing the single hard `contentRoot` check.
|
|
65
|
+
const sources = resolveSources(config, context, {
|
|
66
|
+
mode,
|
|
67
|
+
preview,
|
|
68
|
+
refresh: options.refresh,
|
|
69
|
+
});
|
|
70
|
+
for (const source of sources) {
|
|
71
|
+
source.validate?.();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Run every source's `load()` in parallel, then funnel each entry through the
|
|
75
|
+
// shared `normalizeEntry` so route mapping is identical regardless of origin.
|
|
76
|
+
const [loaded, folderMeta] = await Promise.all([
|
|
77
|
+
Promise.all(
|
|
78
|
+
sources.map(async (source) => ({ source, ...(await source.load()) }))
|
|
79
|
+
),
|
|
80
|
+
discoverFolderMeta(context.contentRoot),
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const allPages: PageRecord[] = [];
|
|
84
|
+
const contentDiagnostics: Diagnostic[] = [];
|
|
85
|
+
for (const { source, entries, diagnostics } of loaded) {
|
|
86
|
+
contentDiagnostics.push(...diagnostics);
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const normalized = normalizeEntry(entry, {
|
|
89
|
+
defaultType: config.content.defaultType,
|
|
90
|
+
i18n: config.i18n,
|
|
91
|
+
source: {
|
|
92
|
+
name: source.name,
|
|
93
|
+
prefix: source.prefix,
|
|
94
|
+
staged: source.staged,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
allPages.push(...normalized.pages);
|
|
98
|
+
contentDiagnostics.push(...normalized.diagnostics);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Drafts render in dev and in preview, but are excluded from production builds.
|
|
103
|
+
const pages =
|
|
104
|
+
mode === "build" && !preview
|
|
105
|
+
? allPages.filter((page) => !page.meta.draft)
|
|
106
|
+
: allPages;
|
|
107
|
+
|
|
108
|
+
// Resolve "last updated" dates before the graph is built so the manifest
|
|
109
|
+
// (which shares these page objects) picks them up. Frontmatter always wins;
|
|
110
|
+
// git applies to filesystem entries, other sources supply dates on the entry.
|
|
111
|
+
const lastModified = resolveLastModifiedConfig(config.lastModified);
|
|
112
|
+
if (lastModified.enabled && lastModified.source === "git") {
|
|
113
|
+
const fsPaths = pages
|
|
114
|
+
.map((page) => page.sourcePath)
|
|
115
|
+
.filter((path): path is string => path !== undefined);
|
|
116
|
+
const gitTimes = gitLastModifiedTimes(
|
|
117
|
+
context.root,
|
|
118
|
+
context.contentRoot,
|
|
119
|
+
fsPaths
|
|
120
|
+
);
|
|
121
|
+
for (const page of pages) {
|
|
122
|
+
if (!page.lastModified && page.sourcePath) {
|
|
123
|
+
page.lastModified = gitTimes.get(page.sourcePath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const graph = buildContentGraph(pages, {
|
|
129
|
+
folderMeta: folderMeta.meta,
|
|
130
|
+
i18n: config.i18n,
|
|
131
|
+
navigation: config.navigation,
|
|
132
|
+
sharedFolderMeta: folderMeta.shared,
|
|
133
|
+
});
|
|
134
|
+
const manifest = buildManifest({ config, context, graph });
|
|
135
|
+
|
|
136
|
+
const i18nWarnings = config.i18n ? i18nDiagnostics(pages, config.i18n) : [];
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
bridge,
|
|
140
|
+
config,
|
|
141
|
+
context,
|
|
142
|
+
diagnostics: [
|
|
143
|
+
...contentDiagnostics,
|
|
144
|
+
...folderMeta.diagnostics,
|
|
145
|
+
...graph.diagnostics,
|
|
146
|
+
...i18nWarnings,
|
|
147
|
+
],
|
|
148
|
+
graph,
|
|
149
|
+
manifest,
|
|
150
|
+
mode,
|
|
151
|
+
sources,
|
|
152
|
+
};
|
|
153
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { isAbsolute, join, resolve } from "pathe";
|
|
4
|
+
|
|
5
|
+
import type { ResolvedConfig } from "./schema.ts";
|
|
6
|
+
import type { ProjectContext } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILENAMES = [
|
|
9
|
+
"blume.config.ts",
|
|
10
|
+
"blume.config.mjs",
|
|
11
|
+
"blume.config.js",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const THEME_FILENAMES = ["theme.css"];
|
|
15
|
+
const COMPONENTS_FILENAMES = ["components.tsx", "components.ts"];
|
|
16
|
+
|
|
17
|
+
const firstExisting = (root: string, names: string[]): string | null => {
|
|
18
|
+
for (const name of names) {
|
|
19
|
+
const candidate = join(root, name);
|
|
20
|
+
if (existsSync(candidate)) {
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Locate the Blume config file for a project root, if any. */
|
|
28
|
+
export const findConfigFile = (root: string): string | null =>
|
|
29
|
+
firstExisting(root, CONFIG_FILENAMES);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve every path Blume needs from a project root and its resolved config.
|
|
33
|
+
* Paths are absolute and normalized.
|
|
34
|
+
*/
|
|
35
|
+
export const resolveProjectContext = (
|
|
36
|
+
root: string,
|
|
37
|
+
config: ResolvedConfig
|
|
38
|
+
): ProjectContext => {
|
|
39
|
+
const absoluteRoot = resolve(root);
|
|
40
|
+
const contentRoot = isAbsolute(config.content.root)
|
|
41
|
+
? config.content.root
|
|
42
|
+
: join(absoluteRoot, config.content.root);
|
|
43
|
+
|
|
44
|
+
const pagesPath = join(absoluteRoot, config.content.pages);
|
|
45
|
+
const pagesRoot = existsSync(pagesPath) ? pagesPath : null;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
componentsFile: firstExisting(absoluteRoot, COMPONENTS_FILENAMES),
|
|
49
|
+
configFile: findConfigFile(absoluteRoot),
|
|
50
|
+
contentRoot,
|
|
51
|
+
outDir: join(absoluteRoot, ".blume"),
|
|
52
|
+
pagesRoot,
|
|
53
|
+
root: absoluteRoot,
|
|
54
|
+
themeFile: firstExisting(absoluteRoot, THEME_FILENAMES),
|
|
55
|
+
};
|
|
56
|
+
};
|