blume 0.5.2 → 0.5.4
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/dist/cli/index.js +2235 -5942
- package/dist/cli/index.js.map +36 -65
- package/dist/types/core/config.d.ts +0 -8
- package/dist/types/core/data.d.ts +2 -2
- package/dist/types/core/schema.d.ts +58 -453
- package/dist/types/core/types.d.ts +1 -7
- package/docs/advanced/meta.ts +1 -8
- package/docs/advanced/skills.mdx +28 -0
- package/docs/configuration/ai.mdx +3 -1
- package/docs/configuration/index.mdx +13 -17
- package/docs/configuration/theming.mdx +3 -11
- package/docs/content/components.mdx +2 -53
- package/docs/content/i18n.mdx +1 -5
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +1 -1
- package/docs/index.mdx +0 -1
- package/docs/reference/cli.mdx +12 -13
- package/package.json +4 -6
- package/skills/blume/SKILL.md +71 -0
- package/skills/blume-update-docs/SKILL.md +52 -0
- package/skills/blume-update-docs/references/audit-checklist.md +46 -0
- package/src/ai/mcp/discovery.ts +3 -1
- package/src/ai/mcp/server.ts +3 -1
- package/src/astro/component-slots.ts +10 -2
- package/src/astro/generate.ts +15 -12
- package/src/astro/integration.ts +0 -21
- package/src/astro/templates.ts +62 -37
- package/src/cli/coalesce.ts +43 -0
- package/src/cli/commands/dev.ts +30 -22
- package/src/cli/commands/validate.ts +0 -2
- package/src/cli/dev-lock.ts +4 -2
- package/src/cli/index.ts +0 -2
- package/src/components/BlumePage.astro +0 -6
- package/src/components/Icon.astro +1 -12
- package/src/components/content/AccordionItem.astro +3 -6
- package/src/components/content/Badge.astro +1 -3
- package/src/components/content/Callout.astro +3 -9
- package/src/components/content/Card.astro +2 -3
- package/src/components/content/ColorItem.astro +8 -5
- package/src/components/content/Column.astro +1 -1
- package/src/components/content/Prompt.astro +8 -4
- package/src/components/content/Step.astro +3 -4
- package/src/components/content/Tab.astro +2 -3
- package/src/components/content/Tabs.astro +13 -2
- package/src/components/content/TypeTable.astro +5 -5
- package/src/components/content/Update.astro +1 -1
- package/src/components/content/mermaid-element.ts +20 -2
- package/src/components/islands/ask-ai.tsx +4 -8
- package/src/components/islands/base-path.ts +30 -0
- package/src/components/islands/hooks.ts +12 -8
- package/src/components/layout/Header.astro +26 -3
- package/src/components/layout/Logo.astro +5 -1
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/NavTree.astro +14 -14
- package/src/components/layout/PageActions.astro +17 -11
- package/src/components/layout/PageLayout.astro +42 -0
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +31 -2
- package/src/components/layout/Search.astro +9 -6
- package/src/components/layout/nav-utils.ts +9 -4
- package/src/components/layout/search/types.ts +16 -5
- package/src/components/openapi/ApiOverview.astro +4 -50
- package/src/components/openapi/ApiTagOperations.astro +42 -0
- package/src/components/openapi/ParametersTable.astro +1 -1
- package/src/components/openapi/SchemaProperty.astro +1 -1
- package/src/components/openapi/SchemaTable.astro +3 -3
- package/src/components/openapi/helpers.ts +17 -8
- package/src/components/openapi/snippets.ts +17 -4
- package/src/core/builtin-tags.ts +1 -3
- package/src/core/config.ts +19 -33
- package/src/core/data.ts +2 -2
- package/src/core/graph.ts +7 -5
- package/src/core/links.ts +5 -19
- package/src/core/meta.ts +1 -1
- package/src/core/navigation.ts +5 -6
- package/src/core/project-graph.ts +0 -5
- package/src/core/schema.ts +13 -90
- package/src/core/sources/filesystem.ts +19 -1
- package/src/core/sources/mdx-remote.ts +20 -4
- package/src/core/sources/normalize.ts +28 -6
- package/src/core/sources/resolve.ts +0 -13
- package/src/core/sources/watch.ts +45 -0
- package/src/core/types.ts +1 -8
- package/src/markdown/code-title.ts +6 -3
- package/src/markdown/package-install.ts +3 -1
- package/src/og/card.ts +16 -4
- package/src/openapi/parse.ts +197 -14
- package/src/openapi/render-mdx.ts +54 -11
- package/src/openapi/source.ts +19 -2
- package/src/search/sync/orama-cloud.ts +2 -0
- package/src/search/sync/typesense.ts +4 -0
- package/src/theme/entry.ts +45 -17
- package/src/theme/icons.ts +22 -104
- package/src/theme/palette.ts +35 -47
- package/src/theme/twoslash.ts +6 -1
- package/dist/types/core/bridge.d.ts +0 -24
- package/dist/types/migrate/mintlify/assets.d.ts +0 -8
- package/dist/types/migrate/mintlify/config.d.ts +0 -16
- package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
- package/docs/advanced/bridge.mdx +0 -76
- package/docs/advanced/migrate.mdx +0 -123
- package/src/astro/static-assets.ts +0 -117
- package/src/cli/commands/migrate.ts +0 -39
- package/src/components/content/ApiField.astro +0 -75
- package/src/components/content/ParamField.astro +0 -39
- package/src/components/content/RequestField.astro +0 -23
- package/src/components/content/ResponseField.astro +0 -23
- package/src/components/content/Warning.astro +0 -9
- package/src/core/assets.ts +0 -31
- package/src/core/bridge.ts +0 -102
- package/src/core/sources/mintlify.ts +0 -204
- package/src/migrate/fumadocs/config.ts +0 -155
- package/src/migrate/fumadocs/content.ts +0 -378
- package/src/migrate/fumadocs/frontmatter.ts +0 -18
- package/src/migrate/fumadocs/groups.ts +0 -237
- package/src/migrate/fumadocs/index.ts +0 -340
- package/src/migrate/fumadocs/meta.ts +0 -244
- package/src/migrate/migrate.ts +0 -53
- package/src/migrate/mintlify/assets.ts +0 -46
- package/src/migrate/mintlify/config.ts +0 -958
- package/src/migrate/mintlify/content.ts +0 -120
- package/src/migrate/mintlify/frontmatter.ts +0 -126
- package/src/migrate/mintlify/i18n.ts +0 -51
- package/src/migrate/mintlify/icons.ts +0 -128
- package/src/migrate/mintlify/index.ts +0 -372
- package/src/migrate/mintlify/snippets.ts +0 -306
- package/src/migrate/mintlify/transform.ts +0 -82
- package/src/migrate/nextra/content.ts +0 -46
- package/src/migrate/nextra/frontmatter.ts +0 -40
- package/src/migrate/nextra/index.ts +0 -374
- package/src/migrate/nextra/meta.ts +0 -266
- package/src/migrate/shared.ts +0 -728
- package/src/migrate/starlight/config.ts +0 -455
- package/src/migrate/starlight/content.ts +0 -78
- package/src/migrate/starlight/frontmatter.ts +0 -111
- package/src/migrate/starlight/i18n.ts +0 -54
- package/src/migrate/starlight/index.ts +0 -131
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
2
|
-
import type { Stats } from "node:fs";
|
|
3
|
-
import { cp } from "node:fs/promises";
|
|
4
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
5
|
-
|
|
6
|
-
import { extname, join, relative, resolve, sep } from "pathe";
|
|
7
|
-
|
|
8
|
-
import type { AssetMount } from "../core/assets.ts";
|
|
9
|
-
|
|
10
|
-
/** Content types for the asset extensions a docs project commonly serves. */
|
|
11
|
-
const MIME_TYPES: Record<string, string> = {
|
|
12
|
-
".avif": "image/avif",
|
|
13
|
-
".css": "text/css; charset=utf-8",
|
|
14
|
-
".gif": "image/gif",
|
|
15
|
-
".ico": "image/x-icon",
|
|
16
|
-
".jpeg": "image/jpeg",
|
|
17
|
-
".jpg": "image/jpeg",
|
|
18
|
-
".js": "text/javascript; charset=utf-8",
|
|
19
|
-
".json": "application/json; charset=utf-8",
|
|
20
|
-
".mp4": "video/mp4",
|
|
21
|
-
".otf": "font/otf",
|
|
22
|
-
".pdf": "application/pdf",
|
|
23
|
-
".png": "image/png",
|
|
24
|
-
".svg": "image/svg+xml",
|
|
25
|
-
".ttf": "font/ttf",
|
|
26
|
-
".txt": "text/plain; charset=utf-8",
|
|
27
|
-
".webm": "video/webm",
|
|
28
|
-
".webp": "image/webp",
|
|
29
|
-
".woff": "font/woff",
|
|
30
|
-
".woff2": "font/woff2",
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const mimeType = (file: string): string =>
|
|
34
|
-
MIME_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
35
|
-
|
|
36
|
-
/** Whether `child` is `parent` itself or a path nested under it. */
|
|
37
|
-
const isContained = (parent: string, child: string): boolean => {
|
|
38
|
-
const rel = relative(parent, child);
|
|
39
|
-
return rel === "" || (!rel.startsWith("..") && !rel.startsWith(sep));
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/** Resolve a request URL to an on-disk file within one of the mounts, if any. */
|
|
43
|
-
const resolveRequest = (url: string, mounts: AssetMount[]): string | null => {
|
|
44
|
-
const pathname = decodeURIComponent(
|
|
45
|
-
(url.split("?")[0] ?? "").split("#")[0] ?? ""
|
|
46
|
-
);
|
|
47
|
-
for (const mount of mounts) {
|
|
48
|
-
if (pathname !== mount.url && !pathname.startsWith(`${mount.url}/`)) {
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
// `.` + rel keeps the join relative so an absolute-looking suffix can't
|
|
52
|
-
// escape the mount; the containment check rejects `..` traversal outright.
|
|
53
|
-
const file = resolve(mount.dir, `.${pathname.slice(mount.url.length)}`);
|
|
54
|
-
if (isContained(mount.dir, file)) {
|
|
55
|
-
return file;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/** `statSync` that returns null instead of throwing on a missing file. */
|
|
62
|
-
const statFile = (file: string): Stats | null => {
|
|
63
|
-
try {
|
|
64
|
-
return statSync(file);
|
|
65
|
-
} catch {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* A dev-server middleware that serves `content.assets` mounts (top-level dirs
|
|
72
|
-
* kept in place, e.g. a migrated `images/`) at their site URL. Astro only serves
|
|
73
|
-
* `publicDir` in dev, so without this those references would 404. Non-matching
|
|
74
|
-
* requests, and any path that isn't a real file, fall through to Astro. Stat is
|
|
75
|
-
* synchronous — this is dev-only middleware and mirrors how sirv serves statics.
|
|
76
|
-
*/
|
|
77
|
-
export const serveAssetMounts =
|
|
78
|
-
(mounts: AssetMount[]) =>
|
|
79
|
-
(req: IncomingMessage, res: ServerResponse, next: () => void): void => {
|
|
80
|
-
if ((req.method !== "GET" && req.method !== "HEAD") || !req.url) {
|
|
81
|
-
next();
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const file = resolveRequest(req.url, mounts);
|
|
85
|
-
const stats = file ? statFile(file) : null;
|
|
86
|
-
if (!(file && stats?.isFile())) {
|
|
87
|
-
next();
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
res.setHeader("Content-Type", mimeType(file));
|
|
91
|
-
res.setHeader("Content-Length", String(stats.size));
|
|
92
|
-
if (req.method === "HEAD") {
|
|
93
|
-
res.end();
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
createReadStream(file).pipe(res);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Copy every asset mount into the build output, mirroring what Astro does with
|
|
101
|
-
* `publicDir`. Runs on `astro:build:done` so in-place asset dirs ship in the
|
|
102
|
-
* final `dist/`. A missing source dir is skipped (it may be referenced but
|
|
103
|
-
* absent); `cp` handles both directories and single files.
|
|
104
|
-
*/
|
|
105
|
-
export const copyAssetMounts = async (
|
|
106
|
-
mounts: AssetMount[],
|
|
107
|
-
outDir: string
|
|
108
|
-
): Promise<void> => {
|
|
109
|
-
for (const mount of mounts) {
|
|
110
|
-
if (!existsSync(mount.dir)) {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
const dest = join(outDir, mount.url.replace(/^\/+/u, ""));
|
|
114
|
-
// oxlint-disable-next-line no-await-in-loop -- sequential fs copies
|
|
115
|
-
await cp(mount.dir, dest, { recursive: true });
|
|
116
|
-
}
|
|
117
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { defineCommand } from "citty";
|
|
2
|
-
|
|
3
|
-
import { migrators } from "../../migrate/migrate.ts";
|
|
4
|
-
import { logger } from "../log.ts";
|
|
5
|
-
|
|
6
|
-
const makeMigrateCommand = (source: keyof typeof migrators) =>
|
|
7
|
-
defineCommand({
|
|
8
|
-
meta: {
|
|
9
|
-
description: `Migrate a ${source} project to Blume.`,
|
|
10
|
-
name: source,
|
|
11
|
-
},
|
|
12
|
-
async run() {
|
|
13
|
-
const root = process.cwd();
|
|
14
|
-
logger.start(`Migrating ${source} project`);
|
|
15
|
-
const result = await migrators[source]?.(root);
|
|
16
|
-
if (!result) {
|
|
17
|
-
logger.error(`No migrator for ${source}.`);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
logger.success(`Migrated ${result.moved} content file(s).`);
|
|
21
|
-
for (const warning of result.warnings) {
|
|
22
|
-
logger.warn(warning);
|
|
23
|
-
}
|
|
24
|
-
logger.box("Review blume.config.ts and run `blume dev`.");
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
export const migrateCommand = defineCommand({
|
|
29
|
-
meta: {
|
|
30
|
-
description: "Migrate from another docs tool to Blume.",
|
|
31
|
-
name: "migrate",
|
|
32
|
-
},
|
|
33
|
-
subCommands: {
|
|
34
|
-
fumadocs: makeMigrateCommand("fumadocs"),
|
|
35
|
-
mintlify: makeMigrateCommand("mintlify"),
|
|
36
|
-
nextra: makeMigrateCommand("nextra"),
|
|
37
|
-
starlight: makeMigrateCommand("starlight"),
|
|
38
|
-
},
|
|
39
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
// Shared field row behind the Mintlify-compat <ParamField>/<ResponseField>/
|
|
3
|
-
// <RequestField> components: a labeled name + type + badges, with the field's
|
|
4
|
-
// description rendered from the slot (which may hold rich MDX, including nested
|
|
5
|
-
// <Expandable> or further fields). Styled to match the native OpenAPI reference
|
|
6
|
-
// rows (src/components/openapi/), so a migrated site reads consistently.
|
|
7
|
-
interface Props {
|
|
8
|
-
default?: unknown;
|
|
9
|
-
deprecated?: unknown;
|
|
10
|
-
location?: string;
|
|
11
|
-
name?: string;
|
|
12
|
-
required?: unknown;
|
|
13
|
-
type?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const {
|
|
17
|
-
default: defaultValue,
|
|
18
|
-
deprecated,
|
|
19
|
-
location,
|
|
20
|
-
name,
|
|
21
|
-
required,
|
|
22
|
-
type,
|
|
23
|
-
} = Astro.props;
|
|
24
|
-
|
|
25
|
-
// Booleans arrive as MDX shorthand (`required`) or strings (`required="true"`).
|
|
26
|
-
const isTrue = (value: unknown): boolean => value === true || value === "true";
|
|
27
|
-
const hasDefault =
|
|
28
|
-
defaultValue !== undefined && defaultValue !== null && defaultValue !== "";
|
|
29
|
-
const hasDescription = Astro.slots.has("default");
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
<div class="not-prose my-2 rounded-blume border border-border px-4 py-3">
|
|
33
|
-
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
34
|
-
{name && <code class="font-mono text-foreground text-sm">{name}</code>}
|
|
35
|
-
{
|
|
36
|
-
location && (
|
|
37
|
-
<span class="rounded bg-muted px-1.5 py-0.5 font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide">
|
|
38
|
-
{location}
|
|
39
|
-
</span>
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
{type && <span class="text-muted-foreground text-xs">{type}</span>}
|
|
43
|
-
{
|
|
44
|
-
isTrue(required) && (
|
|
45
|
-
<span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
|
|
46
|
-
required
|
|
47
|
-
</span>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
{
|
|
51
|
-
isTrue(deprecated) && (
|
|
52
|
-
<span class="font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide line-through">
|
|
53
|
-
deprecated
|
|
54
|
-
</span>
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
</div>
|
|
58
|
-
{
|
|
59
|
-
hasDefault && (
|
|
60
|
-
<div class="mt-1 text-muted-foreground text-xs">
|
|
61
|
-
Default:{" "}
|
|
62
|
-
<code class="rounded bg-muted px-1 py-0.5 text-foreground">
|
|
63
|
-
{String(defaultValue)}
|
|
64
|
-
</code>
|
|
65
|
-
</div>
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
{
|
|
69
|
-
hasDescription && (
|
|
70
|
-
<div class="mt-1.5 text-muted-foreground text-sm [&>:first-child]:mt-0 [&>:last-child]:mb-0">
|
|
71
|
-
<slot />
|
|
72
|
-
</div>
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
</div>
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
// Mintlify-compat <ParamField>. Mintlify encodes the parameter's location in the
|
|
3
|
-
// attribute name (`<ParamField path="id" />`, `query`, `header`, `body`); the
|
|
4
|
-
// attribute's value is the field name. Falls back to a plain `name` prop.
|
|
5
|
-
import ApiField from "./ApiField.astro";
|
|
6
|
-
|
|
7
|
-
const {
|
|
8
|
-
body,
|
|
9
|
-
default: defaultValue,
|
|
10
|
-
deprecated,
|
|
11
|
-
header,
|
|
12
|
-
name,
|
|
13
|
-
path,
|
|
14
|
-
query,
|
|
15
|
-
required,
|
|
16
|
-
type,
|
|
17
|
-
} = Astro.props;
|
|
18
|
-
|
|
19
|
-
const locations = [
|
|
20
|
-
{ key: "path", value: path },
|
|
21
|
-
{ key: "query", value: query },
|
|
22
|
-
{ key: "header", value: header },
|
|
23
|
-
{ key: "body", value: body },
|
|
24
|
-
] as const;
|
|
25
|
-
const located = locations.find((entry) => typeof entry.value === "string");
|
|
26
|
-
const fieldName = located ? located.value : name;
|
|
27
|
-
const location = located?.key;
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
<ApiField
|
|
31
|
-
default={defaultValue}
|
|
32
|
-
deprecated={deprecated}
|
|
33
|
-
location={location}
|
|
34
|
-
name={fieldName}
|
|
35
|
-
required={required}
|
|
36
|
-
type={type}
|
|
37
|
-
>
|
|
38
|
-
<slot />
|
|
39
|
-
</ApiField>
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
// Mintlify-compat <RequestField>: same shape as <ResponseField> — a named field
|
|
3
|
-
// (`name`, `type`, `required`, `default`, `deprecated`) with a slotted body.
|
|
4
|
-
import ApiField from "./ApiField.astro";
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
default: defaultValue,
|
|
8
|
-
deprecated,
|
|
9
|
-
name,
|
|
10
|
-
required,
|
|
11
|
-
type,
|
|
12
|
-
} = Astro.props;
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
<ApiField
|
|
16
|
-
default={defaultValue}
|
|
17
|
-
deprecated={deprecated}
|
|
18
|
-
name={name}
|
|
19
|
-
required={required}
|
|
20
|
-
type={type}
|
|
21
|
-
>
|
|
22
|
-
<slot />
|
|
23
|
-
</ApiField>
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
// Mintlify-compat <ResponseField>: a named field (`name`, `type`, `required`,
|
|
3
|
-
// `default`, `deprecated`) with its description in the slot.
|
|
4
|
-
import ApiField from "./ApiField.astro";
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
default: defaultValue,
|
|
8
|
-
deprecated,
|
|
9
|
-
name,
|
|
10
|
-
required,
|
|
11
|
-
type,
|
|
12
|
-
} = Astro.props;
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
<ApiField
|
|
16
|
-
default={defaultValue}
|
|
17
|
-
deprecated={deprecated}
|
|
18
|
-
name={name}
|
|
19
|
-
required={required}
|
|
20
|
-
type={type}
|
|
21
|
-
>
|
|
22
|
-
<slot />
|
|
23
|
-
</ApiField>
|
package/src/core/assets.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { join } from "pathe";
|
|
2
|
-
|
|
3
|
-
/** A static directory served at a URL prefix, in addition to `public/`. */
|
|
4
|
-
export interface AssetMount {
|
|
5
|
-
/** Absolute filesystem path to the source directory (or file). */
|
|
6
|
-
dir: string;
|
|
7
|
-
/** URL path prefix the source is served at, e.g. `/images`. */
|
|
8
|
-
url: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Resolve `content.assets` entries (top-level dirs served at the site root,
|
|
13
|
-
* alongside `public/`) to `{ dir, url }` mounts. Shared by the generated Astro
|
|
14
|
-
* runtime (dev middleware + build copy) and by link validation, so all three
|
|
15
|
-
* agree on where a `/images/foo.png` reference resolves on disk.
|
|
16
|
-
*
|
|
17
|
-
* Each entry is normalized to a leading-slash URL and joined to the project
|
|
18
|
-
* root; leading `./` or `/` and any `..` segments are stripped so a mount can't
|
|
19
|
-
* escape the root or collide with the site's own routing prefix.
|
|
20
|
-
*/
|
|
21
|
-
export const resolveAssetMounts = (
|
|
22
|
-
root: string,
|
|
23
|
-
assets: string[]
|
|
24
|
-
): AssetMount[] =>
|
|
25
|
-
assets.map((entry) => {
|
|
26
|
-
const rel = entry
|
|
27
|
-
.replace(/^[./]+/u, "")
|
|
28
|
-
.replaceAll(/\.\.\/?/gu, "")
|
|
29
|
-
.replace(/\/+$/u, "");
|
|
30
|
-
return { dir: join(root, rel), url: `/${rel}` };
|
|
31
|
-
});
|
package/src/core/bridge.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
|
|
4
|
-
import { join } from "pathe";
|
|
5
|
-
|
|
6
|
-
import { assetSegments } from "../migrate/mintlify/assets.ts";
|
|
7
|
-
import { loadMintlifyConfig } from "../migrate/mintlify/config.ts";
|
|
8
|
-
import { mintlifyI18n } from "../migrate/mintlify/i18n.ts";
|
|
9
|
-
import type { BlumeConfig } from "./schema.ts";
|
|
10
|
-
|
|
11
|
-
/** A detected docs-tool config that Blume can serve without a migration. */
|
|
12
|
-
export interface BridgeDetection {
|
|
13
|
-
/** Which foreign docs tool was detected. */
|
|
14
|
-
tool: "mintlify";
|
|
15
|
-
/** Absolute path of the detected config file (`docs.json`/`mint.json`). */
|
|
16
|
-
configFile: string;
|
|
17
|
-
/** A Blume config synthesized from the foreign config, ready to validate. */
|
|
18
|
-
raw: BlumeConfig;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const MINTLIFY_CONFIG_FILES = ["docs.json", "mint.json"];
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Detect a Mintlify project at `root` and synthesize an equivalent Blume config.
|
|
25
|
-
*
|
|
26
|
-
* "Bridge mode" lets a team swap `mintlify dev` for `blume dev` with no file
|
|
27
|
-
* changes: `docs.json` is translated to Blume config (`loadMintlifyConfig`) and
|
|
28
|
-
* its content block is rewired to a single `mintlify` content source, which
|
|
29
|
-
* transforms each MDX page to Blume idiom at scan time. The original
|
|
30
|
-
* `content.root`/`exclude` and `variables` move onto the source (Blume has no
|
|
31
|
-
* runtime variable substitution, so globals are inlined into content there).
|
|
32
|
-
*
|
|
33
|
-
* Returns `null` when no Mintlify config is present. Only called when no
|
|
34
|
-
* `blume.config.*` exists, so an explicit Blume config always takes precedence.
|
|
35
|
-
*/
|
|
36
|
-
export const detectMintlifyBridge = async (
|
|
37
|
-
root: string
|
|
38
|
-
): Promise<BridgeDetection | null> => {
|
|
39
|
-
const configFile = MINTLIFY_CONFIG_FILES.map((name) => join(root, name)).find(
|
|
40
|
-
(candidate) => existsSync(candidate)
|
|
41
|
-
);
|
|
42
|
-
if (!configFile) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const config = await loadMintlifyConfig(root, configFile);
|
|
47
|
-
|
|
48
|
-
// i18n is derived from the raw spec (language dirs), mirroring the migrator.
|
|
49
|
-
const spec = JSON.parse(await readFile(configFile, "utf-8")) as Record<
|
|
50
|
-
string,
|
|
51
|
-
unknown
|
|
52
|
-
>;
|
|
53
|
-
const i18n = mintlifyI18n(spec);
|
|
54
|
-
if (i18n) {
|
|
55
|
-
config.i18n = i18n;
|
|
56
|
-
// Language switching is handled by Blume i18n, not a nav selector.
|
|
57
|
-
if (config.navigation?.selectors) {
|
|
58
|
-
config.navigation.selectors = config.navigation.selectors.filter(
|
|
59
|
-
(selector) => selector.kind !== "language"
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const variables = (config.variables as Record<string, string>) ?? {};
|
|
65
|
-
const root_ = config.content?.root ?? ".";
|
|
66
|
-
const exclude = config.content?.exclude ?? [];
|
|
67
|
-
|
|
68
|
-
// Mintlify serves assets from the project root; the bridge never moves files,
|
|
69
|
-
// so referenced root-level asset folders (e.g. `images/`) are served in place
|
|
70
|
-
// via `content.assets` instead. This is the read-only twin of the migrator's
|
|
71
|
-
// relocation — same referenced segments, just no `public/` move.
|
|
72
|
-
const assets = assetSegments(config).filter(
|
|
73
|
-
(segment) => segment !== "public" && existsSync(join(root, segment))
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
configFile,
|
|
78
|
-
raw: {
|
|
79
|
-
...config,
|
|
80
|
-
content: {
|
|
81
|
-
assets,
|
|
82
|
-
// Mirror the excludes onto `content.exclude` too: the generated Astro
|
|
83
|
-
// `docs` collection globs `content.root` (here the project root) and
|
|
84
|
-
// must skip node_modules/snippets just like the source does.
|
|
85
|
-
exclude,
|
|
86
|
-
root: root_,
|
|
87
|
-
sources: [
|
|
88
|
-
{
|
|
89
|
-
configFile,
|
|
90
|
-
exclude,
|
|
91
|
-
root: root_,
|
|
92
|
-
type: "mintlify",
|
|
93
|
-
variables,
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
},
|
|
97
|
-
// Globals are inlined into content by the source; drop the top-level copy.
|
|
98
|
-
variables: undefined,
|
|
99
|
-
},
|
|
100
|
-
tool: "mintlify",
|
|
101
|
-
};
|
|
102
|
-
};
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { existsSync, watch as fsWatch } from "node:fs";
|
|
2
|
-
import type { WatchListener } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
|
|
5
|
-
import { isAbsolute, join, relative, resolve } from "pathe";
|
|
6
|
-
import { glob } from "tinyglobby";
|
|
7
|
-
|
|
8
|
-
import { transformMintlifyContent } from "../../migrate/mintlify/transform.ts";
|
|
9
|
-
import { BlumeError } from "../diagnostics.ts";
|
|
10
|
-
import matter from "../frontmatter.ts";
|
|
11
|
-
import type { Diagnostic } from "../types.ts";
|
|
12
|
-
import type { ContentSource, SourceEntry, SourceLoadResult } from "./types.ts";
|
|
13
|
-
|
|
14
|
-
/** Options for the Mintlify bridge content source. */
|
|
15
|
-
export interface MintlifySourceOptions {
|
|
16
|
-
/** Stable source name; namespaces ids and diagnostics. */
|
|
17
|
-
name: string;
|
|
18
|
-
/** Optional route prefix. */
|
|
19
|
-
prefix?: string;
|
|
20
|
-
/** Content root, absolute or relative to `projectRoot` (Mintlify: `.`). */
|
|
21
|
-
root: string;
|
|
22
|
-
include: string[];
|
|
23
|
-
exclude: string[];
|
|
24
|
-
/** `docs.json` variables, inlined into content (`{{name}}`) at scan time. */
|
|
25
|
-
variables: Record<string, string>;
|
|
26
|
-
/** Absolute path of the `docs.json`/`mint.json`, watched for changes in dev. */
|
|
27
|
-
configFile?: string;
|
|
28
|
-
/** Absolute project root, used to resolve a relative `root`. */
|
|
29
|
-
projectRoot: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Folders Mintlify projects keep alongside content that are never pages:
|
|
34
|
-
* snippets are inlined as includes, and build/tooling dirs are noise. Merged
|
|
35
|
-
* with the user's `exclude` so bridge mode behaves like the one-shot migrator.
|
|
36
|
-
*/
|
|
37
|
-
const MINTLIFY_SOURCE_IGNORES = [
|
|
38
|
-
"node_modules/**",
|
|
39
|
-
".blume/**",
|
|
40
|
-
"dist/**",
|
|
41
|
-
"build/**",
|
|
42
|
-
"public/**",
|
|
43
|
-
"snippets/**",
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Directory names the recursive dev watcher must ignore. In bridge mode the
|
|
48
|
-
* content root is the project root, so a naive recursive `fs.watch` also sees
|
|
49
|
-
* Blume's own `.blume/` output — which the dev server rewrites on every request
|
|
50
|
-
* (`.blume/.astro/data-store.json`). Left unfiltered, each such write re-triggers
|
|
51
|
-
* a full rescan + runtime regeneration, whose writes land back under `.blume/`
|
|
52
|
-
* and fire the watcher again: a self-sustaining storm that stalls page renders
|
|
53
|
-
* and floods the console. `fs.watch` has no ignore option, so we filter by the
|
|
54
|
-
* changed path in the callback. Derived from {@link MINTLIFY_SOURCE_IGNORES}
|
|
55
|
-
* (dir prefixes) plus VCS metadata.
|
|
56
|
-
*/
|
|
57
|
-
const WATCH_IGNORE_DIRS = new Set([
|
|
58
|
-
...MINTLIFY_SOURCE_IGNORES.map((pattern) => pattern.replace(/\/\*\*$/u, "")),
|
|
59
|
-
".git",
|
|
60
|
-
]);
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Build the recursive-watch listener: fire `onChange` for content changes but
|
|
64
|
-
* ignore events whose path crosses a {@link WATCH_IGNORE_DIRS} segment (Blume's
|
|
65
|
-
* own `.blume/` output, `node_modules`, VCS metadata, …). A missing `filename`
|
|
66
|
-
* — rare; the platform couldn't name the changed path — falls through to
|
|
67
|
-
* regenerate rather than silently dropping a real edit. Exported for testing.
|
|
68
|
-
*/
|
|
69
|
-
export const mintlifyWatchListener =
|
|
70
|
-
(onChange: () => void): WatchListener<string> =>
|
|
71
|
-
(_event, filename) => {
|
|
72
|
-
if (
|
|
73
|
-
typeof filename === "string" &&
|
|
74
|
-
filename.split(/[/\\]/u).some((segment) => WATCH_IGNORE_DIRS.has(segment))
|
|
75
|
-
) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
onChange();
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* The Mintlify bridge content source. Reads an unconverted Mintlify project in
|
|
83
|
-
* place and transforms each page to Blume MDX at scan time (callouts → `:::`
|
|
84
|
-
* directives, snippet/variable inlining, etc.) via `transformMintlifyContent`.
|
|
85
|
-
* Staged: the transformed bodies are materialized under `.blume/content` and
|
|
86
|
-
* rendered through Astro's `staged` collection, so the rewrites actually reach
|
|
87
|
-
* the output. Components Blume already ships (Card, Tabs, Steps, …) render as-is.
|
|
88
|
-
*/
|
|
89
|
-
export const mintlifySource = (
|
|
90
|
-
options: MintlifySourceOptions
|
|
91
|
-
): ContentSource & { readonly contentRoot: string } => {
|
|
92
|
-
const contentRoot = isAbsolute(options.root)
|
|
93
|
-
? options.root
|
|
94
|
-
: join(resolve(options.projectRoot), options.root);
|
|
95
|
-
const ignore = [...new Set([...options.exclude, ...MINTLIFY_SOURCE_IGNORES])];
|
|
96
|
-
|
|
97
|
-
const transform = (
|
|
98
|
-
raw: string,
|
|
99
|
-
file: string
|
|
100
|
-
): ReturnType<typeof transformMintlifyContent> =>
|
|
101
|
-
transformMintlifyContent(raw, {
|
|
102
|
-
filePath: file,
|
|
103
|
-
root: resolve(options.projectRoot),
|
|
104
|
-
variables: options.variables,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const load = async (): Promise<SourceLoadResult> => {
|
|
108
|
-
const files = await glob(options.include, {
|
|
109
|
-
absolute: true,
|
|
110
|
-
cwd: contentRoot,
|
|
111
|
-
ignore,
|
|
112
|
-
onlyFiles: true,
|
|
113
|
-
});
|
|
114
|
-
files.sort();
|
|
115
|
-
|
|
116
|
-
const unsupported = new Set<string>();
|
|
117
|
-
const entries = await Promise.all(
|
|
118
|
-
files.map(async (file): Promise<SourceEntry> => {
|
|
119
|
-
const result = await transform(await readFile(file, "utf-8"), file);
|
|
120
|
-
for (const name of result.unsupported) {
|
|
121
|
-
unsupported.add(name);
|
|
122
|
-
}
|
|
123
|
-
const parsed = matter(result.content);
|
|
124
|
-
// Force MDX: Mintlify pages are MDX-authored and the rewrites emit `:::`
|
|
125
|
-
// directives + JSX, neither of which the plain `.md` processor expands.
|
|
126
|
-
return {
|
|
127
|
-
body: { format: "mdx", text: parsed.content },
|
|
128
|
-
data: parsed.data,
|
|
129
|
-
raw: result.content,
|
|
130
|
-
ref: relative(contentRoot, file),
|
|
131
|
-
sourcePath: file,
|
|
132
|
-
};
|
|
133
|
-
})
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const diagnostics: Diagnostic[] =
|
|
137
|
-
unsupported.size > 0
|
|
138
|
-
? [
|
|
139
|
-
{
|
|
140
|
-
code: "BLUME_MINTLIFY_UNSUPPORTED",
|
|
141
|
-
message: `Mintlify components without a Blume equivalent were left as-is: ${[...unsupported].toSorted().join(", ")}. Replace them by hand or provide a matching component.`,
|
|
142
|
-
severity: "warning",
|
|
143
|
-
},
|
|
144
|
-
]
|
|
145
|
-
: [];
|
|
146
|
-
|
|
147
|
-
return { diagnostics, entries };
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const validate = (): void => {
|
|
151
|
-
if (!existsSync(contentRoot)) {
|
|
152
|
-
throw new BlumeError({
|
|
153
|
-
code: "BLUME_CONTENT_ROOT_MISSING",
|
|
154
|
-
file: contentRoot,
|
|
155
|
-
message: `Content root not found: ${options.root}`,
|
|
156
|
-
severity: "error",
|
|
157
|
-
suggestion: `Run "blume dev" from the directory that contains docs.json.`,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const watch = (onChange: () => void): (() => void) => {
|
|
163
|
-
const disposers: (() => void)[] = [];
|
|
164
|
-
if (existsSync(contentRoot)) {
|
|
165
|
-
// Recursively watch the content root, but skip Blume's own output and
|
|
166
|
-
// other non-content trees so the dev server's `.blume/` writes don't feed
|
|
167
|
-
// a regeneration loop (`fs.watch` has no ignore option, so filter here).
|
|
168
|
-
const watcher = fsWatch(
|
|
169
|
-
contentRoot,
|
|
170
|
-
{ recursive: true },
|
|
171
|
-
mintlifyWatchListener(onChange)
|
|
172
|
-
);
|
|
173
|
-
disposers.push(() => watcher.close());
|
|
174
|
-
}
|
|
175
|
-
// Watch docs.json directly: it lives at the content root but a non-recursive
|
|
176
|
-
// single-file watch fires reliably on edits that recursive dir-watch can miss.
|
|
177
|
-
if (options.configFile && existsSync(options.configFile)) {
|
|
178
|
-
const watcher = fsWatch(options.configFile, onChange);
|
|
179
|
-
disposers.push(() => watcher.close());
|
|
180
|
-
}
|
|
181
|
-
return () => {
|
|
182
|
-
for (const dispose of disposers) {
|
|
183
|
-
dispose();
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const read = async (ref: string): Promise<string> => {
|
|
189
|
-
const file = join(contentRoot, ref);
|
|
190
|
-
const result = await transform(await readFile(file, "utf-8"), file);
|
|
191
|
-
return result.content;
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
contentRoot,
|
|
196
|
-
load,
|
|
197
|
-
name: options.name,
|
|
198
|
-
prefix: options.prefix,
|
|
199
|
-
read,
|
|
200
|
-
staged: true,
|
|
201
|
-
validate,
|
|
202
|
-
watch,
|
|
203
|
-
};
|
|
204
|
-
};
|