blume 0.3.0 → 0.5.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/dist/cli/index.js +1631 -940
- package/dist/cli/index.js.map +62 -50
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/project.d.ts +12 -2
- package/dist/types/core/schema.d.ts +442 -292
- package/dist/types/core/types.d.ts +7 -0
- package/dist/types/migrate/mintlify/assets.d.ts +8 -0
- package/docs/01-quickstart.mdx +5 -16
- package/docs/02-deployment.mdx +21 -54
- package/docs/advanced/api-reference.mdx +34 -51
- package/docs/advanced/blog.mdx +9 -25
- package/docs/advanced/bridge.mdx +74 -0
- package/docs/advanced/changelog.mdx +10 -33
- package/docs/advanced/custom-pages.mdx +21 -78
- package/docs/advanced/meta.ts +8 -1
- package/docs/advanced/migrate.mdx +119 -0
- package/docs/configuration/ai.mdx +42 -103
- package/docs/configuration/analytics.mdx +20 -38
- package/docs/configuration/customization.mdx +40 -73
- package/docs/configuration/export.mdx +9 -34
- package/docs/configuration/index.mdx +67 -87
- package/docs/configuration/search.mdx +17 -54
- package/docs/configuration/seo.mdx +17 -48
- package/docs/configuration/theming.mdx +20 -42
- package/docs/content/components.mdx +95 -101
- package/docs/content/i18n.mdx +21 -72
- package/docs/content/index.mdx +18 -48
- package/docs/content/islands.mdx +25 -52
- package/docs/content/meta.mdx +23 -50
- package/docs/content/navigation.mdx +23 -62
- package/docs/content/sources.mdx +20 -83
- package/docs/content/syntax.mdx +37 -105
- package/docs/index.mdx +12 -41
- package/docs/reference/cli.mdx +47 -30
- package/docs/reference/frontmatter.mdx +7 -5
- package/package.json +11 -1
- package/src/astro/generate.ts +18 -8
- package/src/astro/integration.ts +26 -3
- package/src/astro/islands.ts +6 -2
- package/src/astro/markdown-negotiation.ts +17 -3
- package/src/astro/pages.ts +6 -1
- package/src/astro/static-assets.ts +117 -0
- package/src/astro/templates.ts +76 -30
- package/src/cli/args.ts +23 -0
- package/src/cli/commands/build.ts +129 -62
- package/src/cli/commands/check.ts +20 -0
- package/src/cli/commands/dev.ts +11 -2
- package/src/cli/commands/doctor.ts +10 -1
- package/src/cli/commands/eject.ts +3 -1
- package/src/cli/commands/init.ts +21 -1
- package/src/cli/commands/preview.ts +2 -1
- package/src/cli/commands/validate.ts +12 -1
- package/src/cli/dev-lock.ts +92 -0
- package/src/cli/log.ts +11 -0
- package/src/cli/prepare.ts +3 -0
- package/src/components/BlumePage.astro +8 -0
- package/src/components/Icon.astro +13 -10
- package/src/components/content/ApiField.astro +75 -0
- package/src/components/content/ParamField.astro +39 -0
- package/src/components/content/RequestField.astro +23 -0
- package/src/components/content/ResponseField.astro +23 -0
- package/src/components/content/Step.astro +1 -1
- package/src/components/content/YouTube.astro +35 -0
- package/src/components/content/youtube.ts +46 -0
- package/src/components/islands/ask-ai.tsx +14 -14
- package/src/components/layout/Breadcrumbs.astro +7 -2
- package/src/components/layout/NavTree.astro +24 -8
- package/src/components/layout/RootLayout.astro +56 -34
- package/src/components/layout/Search.astro +1 -1
- package/src/components/openapi/ApiOverview.astro +84 -0
- package/src/components/openapi/MethodBadge.astro +28 -0
- package/src/components/openapi/Operation.astro +140 -0
- package/src/components/openapi/ParametersTable.astro +97 -0
- package/src/components/openapi/RequestBody.astro +58 -0
- package/src/components/openapi/RequestPanel.astro +169 -0
- package/src/components/openapi/Responses.astro +91 -0
- package/src/components/openapi/SchemaProperty.astro +118 -0
- package/src/components/openapi/SchemaTable.astro +86 -0
- package/src/components/openapi/helpers.ts +238 -0
- package/src/components/openapi/panel.ts +59 -0
- package/src/components/openapi/snippets.ts +201 -0
- package/src/components/props.ts +3 -0
- package/src/core/assets.ts +31 -0
- package/src/core/bridge.ts +10 -0
- package/src/core/builtin-tags.ts +6 -0
- package/src/core/data.ts +2 -0
- package/src/core/diagnostics.ts +6 -1
- package/src/core/gitignore.ts +30 -0
- package/src/core/links.ts +60 -19
- package/src/core/project-graph.ts +5 -1
- package/src/core/project.ts +25 -3
- package/src/core/schema.ts +54 -6
- package/src/core/sources/mdx-remote.ts +54 -8
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/normalize.ts +6 -1
- package/src/core/sources/notion.ts +49 -5
- package/src/core/sources/resolve.ts +28 -6
- package/src/core/sources/sanity.ts +5 -1
- package/src/core/types.ts +7 -0
- package/src/deploy/rss.ts +1 -8
- package/src/deploy/sitemap.ts +20 -1
- package/src/deploy/xml.ts +8 -0
- package/src/markdown/directives.ts +15 -7
- package/src/markdown/package-commands.ts +26 -4
- package/src/migrate/fumadocs/content.ts +14 -1
- package/src/migrate/fumadocs/groups.ts +7 -0
- package/src/migrate/fumadocs/index.ts +5 -2
- package/src/migrate/mintlify/assets.ts +46 -0
- package/src/migrate/mintlify/config.ts +153 -1
- package/src/migrate/mintlify/content.ts +8 -2
- package/src/migrate/mintlify/index.ts +111 -46
- package/src/migrate/shared.ts +12 -27
- package/src/og/card.ts +14 -2
- package/src/openapi/model.ts +174 -0
- package/src/openapi/parse.ts +48 -0
- package/src/openapi/references.ts +164 -0
- package/src/openapi/render-mdx.ts +76 -0
- package/src/openapi/scalar.ts +15 -103
- package/src/openapi/source.ts +140 -0
- package/src/registry/eject.ts +28 -5
- package/src/registry/registry.ts +6 -0
- package/src/registry/rewrite-imports.ts +31 -19
- package/src/search/documents.ts +23 -5
- package/src/search/sync/algolia.ts +5 -1
- package/src/search/sync/typesense.ts +24 -16
- package/src/theme/chrome-icons.ts +22 -0
- package/src/theme/icons.ts +151 -161
- package/src/theme/palette.ts +26 -7
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BlumeConfig } from "../../core/schema.ts";
|
|
2
|
+
|
|
3
|
+
/** Asset paths referenced by the resolved config (logo, favicon, backgrounds). */
|
|
4
|
+
const assetRefs = (config: BlumeConfig): unknown[] => {
|
|
5
|
+
const refs: unknown[] = ["/images"];
|
|
6
|
+
const logo = config.logo as
|
|
7
|
+
| string
|
|
8
|
+
| { dark?: string; light?: string }
|
|
9
|
+
| undefined;
|
|
10
|
+
if (typeof logo === "string") {
|
|
11
|
+
refs.push(logo);
|
|
12
|
+
} else if (logo) {
|
|
13
|
+
refs.push(logo.light, logo.dark);
|
|
14
|
+
}
|
|
15
|
+
const favicon = config.favicon as
|
|
16
|
+
| string
|
|
17
|
+
| { dark?: string; light?: string }
|
|
18
|
+
| undefined;
|
|
19
|
+
if (typeof favicon === "string") {
|
|
20
|
+
refs.push(favicon);
|
|
21
|
+
} else if (favicon) {
|
|
22
|
+
refs.push(favicon.light, favicon.dark);
|
|
23
|
+
}
|
|
24
|
+
refs.push(config.theme?.backgroundImage, config.theme?.backgroundImageDark);
|
|
25
|
+
return refs;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Top-level path segments referenced as static assets by a Mintlify config
|
|
30
|
+
* (the conventional `/images`, plus logo/favicon/background paths). These are
|
|
31
|
+
* the root-served folders Mintlify exposes at the site root; Blume serves them
|
|
32
|
+
* via `content.assets` (bridge) or relocates them under `public/` (migrator).
|
|
33
|
+
*/
|
|
34
|
+
export const assetSegments = (config: BlumeConfig): string[] => {
|
|
35
|
+
const segments = new Set<string>();
|
|
36
|
+
for (const ref of assetRefs(config)) {
|
|
37
|
+
if (typeof ref !== "string" || !ref.startsWith("/")) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const [segment] = ref.replace(/^\/+/u, "").split("/");
|
|
41
|
+
if (segment) {
|
|
42
|
+
segments.add(segment);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...segments];
|
|
46
|
+
};
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ResolvedConfig,
|
|
10
10
|
SidebarItemConfig,
|
|
11
11
|
} from "../../core/schema.ts";
|
|
12
|
+
import { GOOGLE_FONTS } from "../../theme/fonts.ts";
|
|
12
13
|
|
|
13
14
|
type JsonObject = Record<string, unknown>;
|
|
14
15
|
type NavigationSelectors = ResolvedConfig["navigation"]["selectors"];
|
|
@@ -593,6 +594,16 @@ const mintignorePatterns = async (root: string): Promise<string[]> => {
|
|
|
593
594
|
}
|
|
594
595
|
};
|
|
595
596
|
|
|
597
|
+
// path-to-regexp param (:slug, :slug*, :id?) → Astro dynamic segment.
|
|
598
|
+
// *,+ (repeatable) → spread [...name]; bare/? → single [name]. Name must start
|
|
599
|
+
// with a letter/underscore so URL ports (:8080) and protocols (https:) are left alone.
|
|
600
|
+
const REDIRECT_PARAM = /:(?<name>[A-Za-z_]\w*)(?<modifier>[*+?])?/gu;
|
|
601
|
+
|
|
602
|
+
const toAstroRedirectPath = (path: string): string =>
|
|
603
|
+
path.replaceAll(REDIRECT_PARAM, (_match, name: string, modifier?: string) =>
|
|
604
|
+
modifier === "*" || modifier === "+" ? `[...${name}]` : `[${name}]`
|
|
605
|
+
);
|
|
606
|
+
|
|
596
607
|
const mintlifyRedirects = (
|
|
597
608
|
spec: JsonObject
|
|
598
609
|
): NonNullable<BlumeConfig["redirects"]> =>
|
|
@@ -609,9 +620,100 @@ const mintlifyRedirects = (
|
|
|
609
620
|
if (!from || !to) {
|
|
610
621
|
return [];
|
|
611
622
|
}
|
|
612
|
-
return [{ from, to }];
|
|
623
|
+
return [{ from: toAstroRedirectPath(from), to: toAstroRedirectPath(to) }];
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
interface OpenApiSourceDraft {
|
|
627
|
+
label?: string;
|
|
628
|
+
route?: string;
|
|
629
|
+
spec: string;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// A Mintlify `{ source, directory }` object's `directory` is the URL path its
|
|
633
|
+
// generated pages mount under; map it to a Blume per-source `route`.
|
|
634
|
+
const openapiRouteFromDirectory = (value: unknown): string | undefined => {
|
|
635
|
+
const directory = normalizeDirectory(asString(value) ?? "");
|
|
636
|
+
return directory.length > 0 ? `/${directory}` : undefined;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// Resolve a Mintlify `openapi` value (a spec string, an array, or a
|
|
640
|
+
// `{ source, directory }` object) into spec sources. Endpoint refs (`GET /path`)
|
|
641
|
+
// are skipped — Blume's native renderer generates those pages from the spec.
|
|
642
|
+
const openapiSourcesFromValue = (
|
|
643
|
+
value: unknown,
|
|
644
|
+
context: { label?: string; route?: string }
|
|
645
|
+
): OpenApiSourceDraft[] => {
|
|
646
|
+
if (typeof value === "string") {
|
|
647
|
+
if (value.length === 0 || API_ENDPOINT_REF.test(value)) {
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
650
|
+
return [
|
|
651
|
+
withoutUndefined({
|
|
652
|
+
label: context.label,
|
|
653
|
+
route: context.route,
|
|
654
|
+
spec: value,
|
|
655
|
+
}),
|
|
656
|
+
];
|
|
657
|
+
}
|
|
658
|
+
if (Array.isArray(value)) {
|
|
659
|
+
return value.flatMap((item) => openapiSourcesFromValue(item, context));
|
|
660
|
+
}
|
|
661
|
+
const object = asObject(value);
|
|
662
|
+
if (!object) {
|
|
663
|
+
return [];
|
|
664
|
+
}
|
|
665
|
+
return openapiSourcesFromValue(object.source ?? object.openapi, {
|
|
666
|
+
label: context.label,
|
|
667
|
+
route: openapiRouteFromDirectory(object.directory) ?? context.route,
|
|
668
|
+
});
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// Walk the navigation tree collecting every `openapi` source — a group or tab
|
|
672
|
+
// can declare one alongside its pages — then fold in the top-level specs
|
|
673
|
+
// (legacy `mint.json` `openapi`, newer `api.openapi`) and dedupe by spec so a
|
|
674
|
+
// Mintlify API reference maps to Blume's native renderer instead of dropping.
|
|
675
|
+
const mintlifyOpenapi = (spec: JsonObject): BlumeConfig["openapi"] => {
|
|
676
|
+
const drafts: OpenApiSourceDraft[] = [];
|
|
677
|
+
|
|
678
|
+
const visit = (node: unknown): void => {
|
|
679
|
+
if (Array.isArray(node)) {
|
|
680
|
+
for (const item of node) {
|
|
681
|
+
visit(item);
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const object = asObject(node);
|
|
686
|
+
if (!object) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (hasOwn(object, "openapi")) {
|
|
690
|
+
drafts.push(
|
|
691
|
+
...openapiSourcesFromValue(object.openapi, {
|
|
692
|
+
label: labelForNavItem(object),
|
|
693
|
+
})
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
for (const children of childNavigationArrays(object)) {
|
|
697
|
+
visit(children);
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
visit(spec.navigation);
|
|
702
|
+
drafts.push(...openapiSourcesFromValue(spec.openapi, {}));
|
|
703
|
+
drafts.push(...openapiSourcesFromValue(asObject(spec.api)?.openapi, {}));
|
|
704
|
+
|
|
705
|
+
const seen = new Set<string>();
|
|
706
|
+
const sources = drafts.flatMap((draft) => {
|
|
707
|
+
if (seen.has(draft.spec)) {
|
|
708
|
+
return [];
|
|
709
|
+
}
|
|
710
|
+
seen.add(draft.spec);
|
|
711
|
+
return [draft];
|
|
613
712
|
});
|
|
614
713
|
|
|
714
|
+
return sources.length > 0 ? { enabled: true, sources } : undefined;
|
|
715
|
+
};
|
|
716
|
+
|
|
615
717
|
const mintlifyLogo = (value: unknown): BlumeConfig["logo"] => {
|
|
616
718
|
if (typeof value === "string") {
|
|
617
719
|
return value;
|
|
@@ -645,6 +747,16 @@ const mintlifyFavicon = (value: unknown): BlumeConfig["favicon"] => {
|
|
|
645
747
|
return withoutUndefined({ dark, light });
|
|
646
748
|
};
|
|
647
749
|
|
|
750
|
+
// Mintlify defaults to Font Awesome, so a migrated site's bare `icon` names are
|
|
751
|
+
// FA names unless it opted into Lucide/Tabler. Set the default library to match.
|
|
752
|
+
const mintlifyIcons = (value: unknown): BlumeConfig["icons"] => {
|
|
753
|
+
const library = asString(asObject(value)?.library);
|
|
754
|
+
return {
|
|
755
|
+
library:
|
|
756
|
+
library === "lucide" || library === "tabler" ? library : "fontawesome",
|
|
757
|
+
};
|
|
758
|
+
};
|
|
759
|
+
|
|
648
760
|
const mintlifyBanner = (value: unknown): BlumeConfig["banner"] => {
|
|
649
761
|
const object = asObject(value);
|
|
650
762
|
const content = object ? asString(object.content) : undefined;
|
|
@@ -768,6 +880,43 @@ const mintlifyMarkdown = (
|
|
|
768
880
|
});
|
|
769
881
|
};
|
|
770
882
|
|
|
883
|
+
type ThemeFonts = NonNullable<NonNullable<BlumeConfig["theme"]>["fonts"]>;
|
|
884
|
+
|
|
885
|
+
// Blume's curated Google-font family names, indexed by lowercased family so a
|
|
886
|
+
// Mintlify `fonts.family: "Space Grotesk"` resolves to the `space-grotesk` slug.
|
|
887
|
+
const FAMILY_TO_SLUG: Record<string, string> = Object.fromEntries(
|
|
888
|
+
Object.entries(GOOGLE_FONTS).map(([slug, def]) => [
|
|
889
|
+
def.family.toLowerCase(),
|
|
890
|
+
slug,
|
|
891
|
+
])
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
const fontSlugForFamily = (value: unknown): string | undefined => {
|
|
895
|
+
const family = asString(value);
|
|
896
|
+
return family ? FAMILY_TO_SLUG[family.toLowerCase()] : undefined;
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Map Mintlify `fonts` onto Blume `theme.fonts`. Mintlify sets one family for
|
|
901
|
+
* everything (`fonts.family`) or splits heading/body (`fonts.heading.family`,
|
|
902
|
+
* `fonts.body.family`); each maps to a curated Blume slug when one matches.
|
|
903
|
+
* Families outside Blume's set are left unset (defaults) and warned about.
|
|
904
|
+
*/
|
|
905
|
+
const mintlifyFonts = (value: unknown): ThemeFonts | undefined => {
|
|
906
|
+
const object = asObject(value);
|
|
907
|
+
if (!object) {
|
|
908
|
+
return undefined;
|
|
909
|
+
}
|
|
910
|
+
const heading = asObject(object.heading);
|
|
911
|
+
const body = asObject(object.body);
|
|
912
|
+
const fonts = withoutUndefined({
|
|
913
|
+
body: fontSlugForFamily(body?.family) ?? fontSlugForFamily(object.family),
|
|
914
|
+
display:
|
|
915
|
+
fontSlugForFamily(heading?.family) ?? fontSlugForFamily(object.family),
|
|
916
|
+
});
|
|
917
|
+
return Object.keys(fonts).length > 0 ? (fonts as ThemeFonts) : undefined;
|
|
918
|
+
};
|
|
919
|
+
|
|
771
920
|
const mintlifySeo = (value: unknown): NonNullable<BlumeConfig["seo"]> => {
|
|
772
921
|
const object = asObject(value);
|
|
773
922
|
const metatags = asObject(object?.metatags);
|
|
@@ -825,6 +974,7 @@ export const loadMintlifyConfig = async (
|
|
|
825
974
|
},
|
|
826
975
|
description: asString(spec.description),
|
|
827
976
|
favicon: mintlifyFavicon(spec.favicon),
|
|
977
|
+
icons: mintlifyIcons(spec.icons),
|
|
828
978
|
logo: mintlifyLogo(spec.logo),
|
|
829
979
|
markdown: mintlifyMarkdown(spec.markdown, styling),
|
|
830
980
|
navigation: {
|
|
@@ -834,6 +984,7 @@ export const loadMintlifyConfig = async (
|
|
|
834
984
|
sidebarVariants: await mintlifySidebarVariants(spec),
|
|
835
985
|
tabs: mintlifyTabs(spec),
|
|
836
986
|
},
|
|
987
|
+
openapi: mintlifyOpenapi(spec),
|
|
837
988
|
redirects: mintlifyRedirects(spec),
|
|
838
989
|
search: {
|
|
839
990
|
indexing: {
|
|
@@ -851,6 +1002,7 @@ export const loadMintlifyConfig = async (
|
|
|
851
1002
|
backgroundDecoration: mintlifyBackgroundDecoration(spec.background),
|
|
852
1003
|
backgroundImage: backgroundImage.light,
|
|
853
1004
|
backgroundImageDark: backgroundImage.dark,
|
|
1005
|
+
fonts: mintlifyFonts(spec.fonts ?? spec.font),
|
|
854
1006
|
mode:
|
|
855
1007
|
appearance.default === "light" ||
|
|
856
1008
|
appearance.default === "dark" ||
|
|
@@ -88,8 +88,14 @@ export const rewriteSnippetImports = (
|
|
|
88
88
|
return { components, source: next };
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Component tags Blume has no equivalent for — reported for manual review.
|
|
93
|
+
* `<ParamField>`/`<ResponseField>`/`<RequestField>` are no longer here: Blume
|
|
94
|
+
* ships compat components for them, so migrated docs render as-is. Mintlify's
|
|
95
|
+
* `<Update>` changelog entry has no component form in Blume (changelog is
|
|
96
|
+
* frontmatter-driven via `type: changelog`), so it stays flagged.
|
|
97
|
+
*/
|
|
98
|
+
const UNSUPPORTED_COMPONENTS = ["Update"];
|
|
93
99
|
|
|
94
100
|
/** Names of Mintlify components in `source` that need manual attention. */
|
|
95
101
|
export const unsupportedMintlifyComponents = (source: string): string[] =>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
4
|
import { dirname, join } from "pathe";
|
|
5
5
|
import { glob } from "tinyglobby";
|
|
6
6
|
|
|
7
7
|
import type { BlumeConfig } from "../../core/schema.ts";
|
|
8
|
+
import { assetSegments } from "./assets.ts";
|
|
8
9
|
import { loadMintlifyConfig } from "./config.ts";
|
|
9
10
|
import { mintlifyI18n } from "./i18n.ts";
|
|
10
11
|
import { transformMintlifyContent } from "./transform.ts";
|
|
@@ -14,6 +15,56 @@ export interface MintlifyMigrationResult {
|
|
|
14
15
|
warnings: string[];
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
|
|
19
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
20
|
+
? (value as Record<string, unknown>)
|
|
21
|
+
: undefined;
|
|
22
|
+
|
|
23
|
+
const hasFontFamily = (value: unknown): boolean => {
|
|
24
|
+
const object = asRecord(value);
|
|
25
|
+
if (!object) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const named = (child: unknown): boolean =>
|
|
29
|
+
typeof asRecord(child)?.family === "string";
|
|
30
|
+
return (
|
|
31
|
+
typeof object.family === "string" ||
|
|
32
|
+
named(object.heading) ||
|
|
33
|
+
named(object.body)
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Warn about Mintlify site chrome that Blume's config doesn't model, so it isn't
|
|
39
|
+
* dropped silently: header links (`navbar.links`/`navbar.primary`), footer
|
|
40
|
+
* socials (`footer.socials`), and fonts outside Blume's curated Google set. The
|
|
41
|
+
* contextual page menu and last-updated timestamp are covered by Blume defaults
|
|
42
|
+
* (page actions, git-derived dates), so they need no warning.
|
|
43
|
+
*/
|
|
44
|
+
const droppedChromeWarnings = (
|
|
45
|
+
spec: Record<string, unknown>,
|
|
46
|
+
config: BlumeConfig
|
|
47
|
+
): string[] => {
|
|
48
|
+
const warnings: string[] = [];
|
|
49
|
+
const navbar = asRecord(spec.navbar);
|
|
50
|
+
if (navbar && (navbar.links || navbar.primary)) {
|
|
51
|
+
warnings.push(
|
|
52
|
+
"Header links (navbar.links/navbar.primary) have no blume.config equivalent and were dropped; re-add them with navigation.tabs or a Header layout override."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (asRecord(spec.footer)?.socials) {
|
|
56
|
+
warnings.push(
|
|
57
|
+
"Footer social links (footer.socials) have no blume.config equivalent and were dropped; add them with a Footer layout override."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (hasFontFamily(spec.fonts ?? spec.font) && !config.theme?.fonts) {
|
|
61
|
+
warnings.push(
|
|
62
|
+
"docs.json font family isn't in Blume's curated Google Fonts set; set theme.fonts to a supported slug or add @font-face rules in theme.css."
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return warnings;
|
|
66
|
+
};
|
|
67
|
+
|
|
17
68
|
/** Recursively drop `undefined`, empty arrays, and empty objects. */
|
|
18
69
|
const prune = (value: unknown): unknown => {
|
|
19
70
|
if (Array.isArray(value)) {
|
|
@@ -52,28 +103,37 @@ const writeBlumeConfig = async (
|
|
|
52
103
|
await writeFile(join(root, "blume.config.ts"), body, "utf-8");
|
|
53
104
|
};
|
|
54
105
|
|
|
55
|
-
|
|
106
|
+
interface RelocatedAssets {
|
|
107
|
+
/** Top-level dirs served in place via `content.assets` (no files moved). */
|
|
108
|
+
served: string[];
|
|
109
|
+
/** Top-level files moved under `public/`. */
|
|
110
|
+
moved: string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Make referenced top-level assets resolvable in Blume. Directories (e.g.
|
|
115
|
+
* Mintlify's `images/`) are left in place and served via `content.assets`, so
|
|
116
|
+
* the migration doesn't churn every file under them; loose top-level files
|
|
117
|
+
* (a root `favicon.png`, `logo.png`) are moved under `public/` since a mount
|
|
118
|
+
* points at a directory.
|
|
119
|
+
*/
|
|
56
120
|
const relocateAssets = async (
|
|
57
121
|
root: string,
|
|
58
|
-
|
|
59
|
-
): Promise<
|
|
60
|
-
const
|
|
61
|
-
for (const ref of refs) {
|
|
62
|
-
if (typeof ref !== "string" || !ref.startsWith("/")) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const [segment] = ref.replace(/^\/+/u, "").split("/");
|
|
66
|
-
if (segment) {
|
|
67
|
-
segments.add(segment);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
122
|
+
segments: string[]
|
|
123
|
+
): Promise<RelocatedAssets> => {
|
|
124
|
+
const served: string[] = [];
|
|
71
125
|
const moved: string[] = [];
|
|
72
126
|
for (const segment of segments) {
|
|
73
127
|
const source = join(root, segment);
|
|
74
128
|
if (!existsSync(source) || segment === "public") {
|
|
75
129
|
continue;
|
|
76
130
|
}
|
|
131
|
+
// oxlint-disable-next-line no-await-in-loop -- sequential fs stats
|
|
132
|
+
const stats = await stat(source);
|
|
133
|
+
if (stats.isDirectory()) {
|
|
134
|
+
served.push(segment);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
77
137
|
const dest = join(root, "public", segment);
|
|
78
138
|
if (existsSync(dest)) {
|
|
79
139
|
continue;
|
|
@@ -84,7 +144,32 @@ const relocateAssets = async (
|
|
|
84
144
|
await rename(source, dest);
|
|
85
145
|
moved.push(segment);
|
|
86
146
|
}
|
|
87
|
-
return moved;
|
|
147
|
+
return { moved, served };
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Fold relocated assets into the config (served dirs become `content.assets`)
|
|
152
|
+
* and record what happened. Served dirs stay in place; only loose files moved.
|
|
153
|
+
*/
|
|
154
|
+
const applyRelocatedAssets = (
|
|
155
|
+
config: BlumeConfig,
|
|
156
|
+
assets: RelocatedAssets,
|
|
157
|
+
warnings: string[]
|
|
158
|
+
): void => {
|
|
159
|
+
if (assets.served.length > 0) {
|
|
160
|
+
config.content = {
|
|
161
|
+
...config.content,
|
|
162
|
+
assets: [
|
|
163
|
+
...new Set([...(config.content?.assets ?? []), ...assets.served]),
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
warnings.push(
|
|
167
|
+
`Kept asset dir(s) in place, served via content.assets: ${assets.served.join(", ")}.`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (assets.moved.length > 0) {
|
|
171
|
+
warnings.push(`Moved assets into public/: ${assets.moved.join(", ")}.`);
|
|
172
|
+
}
|
|
88
173
|
};
|
|
89
174
|
|
|
90
175
|
/**
|
|
@@ -121,31 +206,6 @@ const cleanupSnippets = async (
|
|
|
121
206
|
}
|
|
122
207
|
};
|
|
123
208
|
|
|
124
|
-
/** Asset paths referenced by the resolved config (logo, favicon, backgrounds). */
|
|
125
|
-
const assetRefs = (config: BlumeConfig): unknown[] => {
|
|
126
|
-
const refs: unknown[] = ["/images"];
|
|
127
|
-
const logo = config.logo as
|
|
128
|
-
| string
|
|
129
|
-
| { dark?: string; light?: string }
|
|
130
|
-
| undefined;
|
|
131
|
-
if (typeof logo === "string") {
|
|
132
|
-
refs.push(logo);
|
|
133
|
-
} else if (logo) {
|
|
134
|
-
refs.push(logo.light, logo.dark);
|
|
135
|
-
}
|
|
136
|
-
const favicon = config.favicon as
|
|
137
|
-
| string
|
|
138
|
-
| { dark?: string; light?: string }
|
|
139
|
-
| undefined;
|
|
140
|
-
if (typeof favicon === "string") {
|
|
141
|
-
refs.push(favicon);
|
|
142
|
-
} else if (favicon) {
|
|
143
|
-
refs.push(favicon.light, favicon.dark);
|
|
144
|
-
}
|
|
145
|
-
refs.push(config.theme?.backgroundImage, config.theme?.backgroundImageDark);
|
|
146
|
-
return refs;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
209
|
/**
|
|
150
210
|
* Migrate a Mintlify project to Blume: translate `docs.json`/`mint.json` into
|
|
151
211
|
* `blume.config.ts`, rewrite every page to idiomatic Blume MDX in place, and
|
|
@@ -180,6 +240,13 @@ export const migrateMintlifyProject = async (
|
|
|
180
240
|
`Mapped ${i18n.locales.length} languages to i18n.locales (default: ${i18n.defaultLocale}); review the locale labels.`
|
|
181
241
|
);
|
|
182
242
|
}
|
|
243
|
+
const openapiSources = config.openapi?.sources ?? [];
|
|
244
|
+
if (openapiSources.length > 0) {
|
|
245
|
+
warnings.push(
|
|
246
|
+
`Mapped ${openapiSources.length} OpenAPI spec source(s) to openapi.sources (native reference renderer); verify each spec path or URL resolves.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
warnings.push(...droppedChromeWarnings(spec, config));
|
|
183
250
|
} else {
|
|
184
251
|
warnings.push("No docs.json or mint.json found; writing a default config.");
|
|
185
252
|
config = { content: { root: "." }, title: "Documentation" };
|
|
@@ -232,12 +299,13 @@ export const migrateMintlifyProject = async (
|
|
|
232
299
|
moved += 1;
|
|
233
300
|
}
|
|
234
301
|
|
|
235
|
-
const
|
|
302
|
+
const assets = await relocateAssets(root, assetSegments(config));
|
|
236
303
|
await cleanupSnippets(root, keptComponents, warnings);
|
|
237
304
|
|
|
238
305
|
if (config.content?.exclude) {
|
|
239
306
|
config.content.exclude = [...new Set(config.content.exclude)];
|
|
240
307
|
}
|
|
308
|
+
applyRelocatedAssets(config, assets, warnings);
|
|
241
309
|
await writeBlumeConfig(root, config);
|
|
242
310
|
|
|
243
311
|
if (Object.keys(variables).length > 0) {
|
|
@@ -245,9 +313,6 @@ export const migrateMintlifyProject = async (
|
|
|
245
313
|
`Inlined ${Object.keys(variables).length} docs.json variable(s) into content; Blume has no runtime variable substitution.`
|
|
246
314
|
);
|
|
247
315
|
}
|
|
248
|
-
if (movedAssets.length > 0) {
|
|
249
|
-
warnings.push(`Moved assets into public/: ${movedAssets.join(", ")}.`);
|
|
250
|
-
}
|
|
251
316
|
if (removedKeys.size > 0) {
|
|
252
317
|
warnings.push(
|
|
253
318
|
`Dropped unsupported page frontmatter keys: ${[...removedKeys].join(", ")}.`
|
|
@@ -255,7 +320,7 @@ export const migrateMintlifyProject = async (
|
|
|
255
320
|
}
|
|
256
321
|
if (unsupported.size > 0) {
|
|
257
322
|
warnings.push(
|
|
258
|
-
`Components without a Blume equivalent need manual review
|
|
323
|
+
`Components without a Blume equivalent need manual review: ${[...unsupported].join(", ")}.`
|
|
259
324
|
);
|
|
260
325
|
}
|
|
261
326
|
warnings.push(
|
package/src/migrate/shared.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
import { join } from "pathe";
|
|
4
|
+
import { isAbsolute, join, relative } from "pathe";
|
|
5
5
|
|
|
6
6
|
import type { BlumeConfig } from "../core/schema.ts";
|
|
7
7
|
import { pageMetaSchema } from "../core/schema.ts";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Whether `candidate` resolves to a path inside `root` (or is `root` itself).
|
|
11
|
+
* Guards migrators against `../` traversal in author-controlled source paths
|
|
12
|
+
* (`pages` entries, `<include>` targets) that would otherwise read or move
|
|
13
|
+
* files outside the docs tree.
|
|
14
|
+
*/
|
|
15
|
+
export const isInsideRoot = (root: string, candidate: string): boolean => {
|
|
16
|
+
const rel = relative(root, candidate);
|
|
17
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
18
|
+
};
|
|
19
|
+
|
|
9
20
|
/**
|
|
10
21
|
* Framework-agnostic helpers shared by more than one migrator. Each piece here
|
|
11
22
|
* was generalized from a migrator-specific implementation so Mintlify, Nextra,
|
|
@@ -86,32 +97,6 @@ export const rewriteFrameworkScripts = async (
|
|
|
86
97
|
return changed;
|
|
87
98
|
};
|
|
88
99
|
|
|
89
|
-
/** A `.gitignore` line, normalized for comparison (trailing slashes dropped). */
|
|
90
|
-
const gitignoreKey = (line: string): string => line.trim().replace(/\/+$/u, "");
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Ensure `.gitignore` ignores each of `entries`, appending any that are missing
|
|
94
|
-
* (creating the file when absent). Trailing-slash differences (`dist` vs
|
|
95
|
-
* `dist/`) count as already present. Returns the entries actually added.
|
|
96
|
-
*/
|
|
97
|
-
export const ensureGitignore = async (
|
|
98
|
-
root: string,
|
|
99
|
-
entries: string[]
|
|
100
|
-
): Promise<string[]> => {
|
|
101
|
-
const path = join(root, ".gitignore");
|
|
102
|
-
const existing = existsSync(path) ? await readFile(path, "utf-8") : "";
|
|
103
|
-
const present = new Set(
|
|
104
|
-
existing.split("\n").map(gitignoreKey).filter(Boolean)
|
|
105
|
-
);
|
|
106
|
-
const added = entries.filter((entry) => !present.has(gitignoreKey(entry)));
|
|
107
|
-
if (added.length === 0) {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
const gap = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
111
|
-
await writeFile(path, `${existing}${gap}${added.join("\n")}\n`, "utf-8");
|
|
112
|
-
return added;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
100
|
/** Of the candidate project-relative paths, the ones that still exist — the old
|
|
116
101
|
* framework files a migration leaves behind for the user to remove by hand. */
|
|
117
102
|
export const leftoverFiles = (root: string, candidates: string[]): string[] =>
|
package/src/og/card.ts
CHANGED
|
@@ -56,8 +56,20 @@ const MUTED = "#737373";
|
|
|
56
56
|
const FAINT = "#a3a3a3";
|
|
57
57
|
const BORDER = "#e5e5e5";
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Truncate to `max` code points with an ellipsis. Slices by code points, not
|
|
61
|
+
* UTF-16 units, so cutting mid-emoji doesn't leave a lone surrogate (a broken
|
|
62
|
+
* glyph) before the ellipsis.
|
|
63
|
+
*/
|
|
64
|
+
export const truncate = (value: string, max: number): string => {
|
|
65
|
+
const chars = [...value];
|
|
66
|
+
return chars.length > max
|
|
67
|
+
? `${chars
|
|
68
|
+
.slice(0, max - 1)
|
|
69
|
+
.join("")
|
|
70
|
+
.trimEnd()}…`
|
|
71
|
+
: value;
|
|
72
|
+
};
|
|
61
73
|
|
|
62
74
|
// Brand mark sizing: target this height, but scale down so a wide wordmark logo
|
|
63
75
|
// stays within the lockup.
|