blume 0.6.7 → 0.7.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 +1179 -738
- package/dist/cli/index.js.map +52 -51
- package/dist/types/core/base-path.d.ts +38 -0
- package/dist/types/core/config-input.d.ts +74 -10
- package/dist/types/core/config.d.ts +3 -2
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/i18n-ui.d.ts +1 -3
- package/dist/types/core/schema.d.ts +95 -52
- package/dist/types/core/sources/types.d.ts +2 -0
- package/dist/types/core/types.d.ts +6 -1
- package/docs/02-deployment.mdx +16 -1
- package/docs/03-faq.mdx +8 -8
- package/docs/configuration/index.mdx +6 -0
- package/docs/content/components.mdx +29 -2
- package/docs/content/islands.mdx +8 -0
- package/docs/content/syntax.mdx +13 -0
- package/package.json +2 -1
- package/src/ai/agent-readability.ts +7 -2
- package/src/ai/ask.ts +12 -7
- package/src/ai/llms.ts +15 -4
- package/src/ai/mcp/data.ts +8 -4
- package/src/ai/mcp/server.ts +3 -0
- package/src/astro/component-slots.ts +5 -3
- package/src/astro/examples.ts +12 -7
- package/src/astro/generate.ts +317 -144
- package/src/astro/index.ts +5 -1
- package/src/astro/integration.ts +8 -4
- package/src/astro/islands.ts +11 -5
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/pages.ts +8 -3
- package/src/astro/templates.ts +166 -19
- package/src/cli/commands/build.ts +32 -19
- package/src/cli/commands/dev.ts +48 -15
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/validate.ts +1 -0
- package/src/cli/dev-lock.ts +26 -15
- package/src/cli/required-secrets.ts +2 -1
- package/src/components/content/CodeBlock.astro +3 -0
- package/src/components/content/Component.astro +30 -16
- package/src/components/content/Diff.astro +3 -1
- package/src/components/content/auto-type-table.ts +18 -8
- package/src/components/content/diff.ts +12 -6
- package/src/components/content/mermaid-element.ts +3 -0
- package/src/components/index.ts +23 -1
- package/src/components/islands/ask-ai.tsx +12 -6
- package/src/components/islands/base-path.ts +28 -0
- package/src/components/islands/hooks.ts +16 -1
- package/src/components/layout/Banner.astro +2 -1
- package/src/components/layout/Breadcrumbs.astro +2 -1
- package/src/components/layout/Favicon.astro +3 -2
- package/src/components/layout/Header.astro +2 -1
- package/src/components/layout/LanguageSwitcher.astro +2 -1
- package/src/components/layout/Logo.astro +2 -1
- package/src/components/layout/NavSelector.astro +2 -1
- package/src/components/layout/NavTree.astro +5 -4
- package/src/components/layout/PageFeedback.astro +4 -1
- package/src/components/layout/PageLayout.astro +9 -4
- package/src/components/layout/Pagination.astro +3 -2
- package/src/components/layout/RootLayout.astro +7 -4
- package/src/components/layout/Search.astro +13 -5
- package/src/components/layout/nav-utils.ts +18 -10
- package/src/components/layout/search/pagefind.ts +3 -0
- package/src/components/layout/toc-element.ts +7 -1
- package/src/components/openapi/RequestPanel.astro +7 -1
- package/src/components/openapi/snippets.ts +25 -11
- package/src/core/base-path.ts +70 -0
- package/src/core/component-overrides.ts +103 -74
- package/src/core/config-input.ts +81 -15
- package/src/core/config.ts +5 -3
- package/src/core/content.ts +2 -0
- package/src/core/data.ts +2 -0
- package/src/core/diagnostics.ts +54 -34
- package/src/core/gitignore.ts +4 -1
- package/src/core/graph.ts +156 -88
- package/src/core/i18n-ui.ts +18 -3
- package/src/core/last-modified.ts +2 -0
- package/src/core/links.ts +38 -18
- package/src/core/manifest.ts +62 -45
- package/src/core/nav-diagnostics.ts +1 -1
- package/src/core/navigation.ts +116 -55
- package/src/core/project-graph.ts +10 -9
- package/src/core/schema.ts +572 -621
- package/src/core/sources/github-releases.ts +2 -1
- package/src/core/sources/mdx-remote.ts +58 -54
- package/src/core/sources/normalize.ts +116 -73
- package/src/core/sources/notion.ts +19 -10
- package/src/core/sources/types.ts +2 -0
- package/src/core/tsconfig-aliases.ts +59 -30
- package/src/core/types.ts +6 -1
- package/src/deploy/redirects.ts +18 -0
- package/src/deploy/robots.ts +6 -1
- package/src/deploy/rss.ts +10 -3
- package/src/deploy/sitemap.ts +14 -10
- package/src/markdown/base-links.ts +58 -0
- package/src/markdown/code-title.ts +11 -14
- package/src/markdown/index.ts +34 -9
- package/src/markdown/inline-code.ts +7 -2
- package/src/markdown/themes.ts +24 -0
- package/src/openapi/model.ts +3 -1
- package/src/openapi/references.ts +41 -17
- package/src/openapi/render-mdx.ts +11 -6
- package/src/openapi/scalar.ts +32 -16
- package/src/registry/eject.ts +64 -8
- package/src/search/build.ts +3 -0
- package/src/search/documents.ts +2 -2
- package/src/search/sync/typesense.ts +6 -4
- package/src/seo/jsonld.ts +16 -6
- package/src/theme/entry.ts +85 -20
package/src/core/config-input.ts
CHANGED
|
@@ -315,11 +315,16 @@ export interface NavSelectorItem {
|
|
|
315
315
|
* A header dropdown for switching context — versions, languages, products, or a
|
|
316
316
|
* generic dropdown. `kind` drives the icon and a11y labeling.
|
|
317
317
|
*/
|
|
318
|
+
/** Context-partition selector kinds (a versioned/localized/multi-product site). */
|
|
319
|
+
type NavSelectorContextKind = "product" | "version";
|
|
320
|
+
/** What a header selector switches between. */
|
|
321
|
+
type NavSelectorKind = "dropdown" | "language" | NavSelectorContextKind;
|
|
322
|
+
|
|
318
323
|
export interface NavSelector {
|
|
319
324
|
/** The options shown in the dropdown. */
|
|
320
325
|
items?: NavSelectorItem[];
|
|
321
326
|
/** What the selector switches between. */
|
|
322
|
-
kind:
|
|
327
|
+
kind: NavSelectorKind;
|
|
323
328
|
/** Selector label / current value. */
|
|
324
329
|
label: string;
|
|
325
330
|
}
|
|
@@ -385,6 +390,11 @@ export interface FontsConfig {
|
|
|
385
390
|
}
|
|
386
391
|
|
|
387
392
|
/** Colors, fonts, radius, and color-mode behavior. */
|
|
393
|
+
/** Corner radius scale (`none`/`sm` tighter, `md`/`lg` rounder). */
|
|
394
|
+
type RadiusScaleTight = "none" | "sm";
|
|
395
|
+
type RadiusScaleRound = "md" | "lg";
|
|
396
|
+
type RadiusScale = RadiusScaleTight | RadiusScaleRound;
|
|
397
|
+
|
|
388
398
|
export interface ThemeConfig {
|
|
389
399
|
/**
|
|
390
400
|
* Accent color. A palette name (`blue`, `violet`, `green`, …) or any CSS
|
|
@@ -405,7 +415,7 @@ export interface ThemeConfig {
|
|
|
405
415
|
/** Initial color mode. Defaults to `system`. */
|
|
406
416
|
mode?: "system" | "light" | "dark";
|
|
407
417
|
/** Corner radius scale. Defaults to `md`. */
|
|
408
|
-
radius?:
|
|
418
|
+
radius?: RadiusScale;
|
|
409
419
|
}
|
|
410
420
|
|
|
411
421
|
// ---------------------------------------------------------------------------
|
|
@@ -477,6 +487,10 @@ export interface AskSuggestion {
|
|
|
477
487
|
}
|
|
478
488
|
|
|
479
489
|
/** The Ask AI chat assistant. */
|
|
490
|
+
/** Backends that can route an Ask AI request. */
|
|
491
|
+
type AskProviderGateway = "gateway" | "openrouter" | "llmgateway";
|
|
492
|
+
type AskProvider = AskProviderGateway | "inkeep" | "openai-compatible";
|
|
493
|
+
|
|
480
494
|
export interface AskConfig {
|
|
481
495
|
/**
|
|
482
496
|
* Name of the env var holding the provider API key. Each provider has a
|
|
@@ -493,12 +507,7 @@ export interface AskConfig {
|
|
|
493
507
|
/** Model id to use. Defaults to `openai/gpt-5.5`. */
|
|
494
508
|
model?: string;
|
|
495
509
|
/** Which backend routes the request. Defaults to `gateway`. */
|
|
496
|
-
provider?:
|
|
497
|
-
| "gateway"
|
|
498
|
-
| "openrouter"
|
|
499
|
-
| "llmgateway"
|
|
500
|
-
| "inkeep"
|
|
501
|
-
| "openai-compatible";
|
|
510
|
+
provider?: AskProvider;
|
|
502
511
|
/** Starter prompts shown before the first question. */
|
|
503
512
|
suggestions?: AskSuggestion[];
|
|
504
513
|
}
|
|
@@ -603,9 +612,13 @@ export interface I18nConfig {
|
|
|
603
612
|
* Where and how the site deploys. `site` (and `adapter`) are auto-detected from
|
|
604
613
|
* the platform env on Vercel, Netlify, and Cloudflare.
|
|
605
614
|
*/
|
|
615
|
+
/** Astro server-output adapters, by hosting platform. */
|
|
616
|
+
type CloudDeploymentAdapter = "netlify" | "cloudflare";
|
|
617
|
+
type DeploymentAdapter = "vercel" | "node" | CloudDeploymentAdapter;
|
|
618
|
+
|
|
606
619
|
export interface DeploymentConfig {
|
|
607
620
|
/** Astro adapter for server output. `null` (default) keeps a static build. */
|
|
608
|
-
adapter?:
|
|
621
|
+
adapter?: DeploymentAdapter | null;
|
|
609
622
|
/** Base path when the site is served from a subdirectory. */
|
|
610
623
|
base?: string;
|
|
611
624
|
/** Build output mode. Defaults to `static`. */
|
|
@@ -617,12 +630,17 @@ export interface DeploymentConfig {
|
|
|
617
630
|
site?: string;
|
|
618
631
|
}
|
|
619
632
|
|
|
633
|
+
/** HTTP redirect status codes: permanent (301/308) and temporary (302/307). */
|
|
634
|
+
type RedirectStatusPermanent = 301 | 308;
|
|
635
|
+
type RedirectStatusTemporary = 302 | 307;
|
|
636
|
+
type RedirectStatus = RedirectStatusPermanent | RedirectStatusTemporary;
|
|
637
|
+
|
|
620
638
|
/** A URL redirect rule. */
|
|
621
639
|
export interface RedirectConfig {
|
|
622
640
|
/** Path to redirect from. */
|
|
623
641
|
from: string;
|
|
624
642
|
/** HTTP status. Defaults to `301`. */
|
|
625
|
-
status?:
|
|
643
|
+
status?: RedirectStatus;
|
|
626
644
|
/** Path or URL to redirect to. */
|
|
627
645
|
to: string;
|
|
628
646
|
}
|
|
@@ -717,7 +735,10 @@ export interface CodeConfig {
|
|
|
717
735
|
export interface MarkdownConfig {
|
|
718
736
|
/** Code-block rendering: language icons, line wrap. */
|
|
719
737
|
code?: CodeConfig;
|
|
720
|
-
/**
|
|
738
|
+
/**
|
|
739
|
+
* Syntax-highlighting themes for every code surface — fenced blocks, inline
|
|
740
|
+
* `` `code`{:lang} ``, `<CodeBlock>`, and `<Diff>`.
|
|
741
|
+
*/
|
|
721
742
|
codeBlocks?: {
|
|
722
743
|
/** Shiki theme names per color mode. */
|
|
723
744
|
theme?: {
|
|
@@ -736,6 +757,16 @@ export interface MarkdownConfig {
|
|
|
736
757
|
imageZoom?: boolean;
|
|
737
758
|
}
|
|
738
759
|
|
|
760
|
+
/** React island behavior. */
|
|
761
|
+
export interface ReactConfig {
|
|
762
|
+
/**
|
|
763
|
+
* Auto-memoize React components/hooks with the React Compiler
|
|
764
|
+
* (`babel-plugin-react-compiler`). On by default whenever React is enabled;
|
|
765
|
+
* set to `false` to skip the compiler's babel pass. Defaults to `true`.
|
|
766
|
+
*/
|
|
767
|
+
compiler?: boolean;
|
|
768
|
+
}
|
|
769
|
+
|
|
739
770
|
// ---------------------------------------------------------------------------
|
|
740
771
|
// OpenAPI / AsyncAPI
|
|
741
772
|
// ---------------------------------------------------------------------------
|
|
@@ -787,6 +818,25 @@ export interface AsyncApiConfig {
|
|
|
787
818
|
// Misc top-level unions
|
|
788
819
|
// ---------------------------------------------------------------------------
|
|
789
820
|
|
|
821
|
+
/** `<Component />` example previews (the object form of `examples`). */
|
|
822
|
+
export interface ExamplesConfig {
|
|
823
|
+
/**
|
|
824
|
+
* A stylesheet, relative to the project root, injected into every preview
|
|
825
|
+
* frame after Blume's default tokens. Previews render inside an isolated
|
|
826
|
+
* iframe the docs styles never reach, so design tokens for the previewed
|
|
827
|
+
* components — shadcn variables, `@theme` mappings, custom fonts — live
|
|
828
|
+
* here. Tailwind is already provided in the frame; the file should hold
|
|
829
|
+
* tokens and styles, not another `@import "tailwindcss"`.
|
|
830
|
+
*/
|
|
831
|
+
css?: string;
|
|
832
|
+
/**
|
|
833
|
+
* Where example files live, relative to the project root. Defaults to
|
|
834
|
+
* `examples`; may be a glob to target a registry that colocates component
|
|
835
|
+
* sources with their examples (e.g. `registry/<pkg>/**\/examples/*`).
|
|
836
|
+
*/
|
|
837
|
+
source?: string;
|
|
838
|
+
}
|
|
839
|
+
|
|
790
840
|
/**
|
|
791
841
|
* Reader-facing "Export" page actions. A boolean toggles both formats; the
|
|
792
842
|
* object form enables each individually. Defaults to `false`.
|
|
@@ -843,6 +893,16 @@ export interface BlumeConfig {
|
|
|
843
893
|
asyncapi?: AsyncApiConfig;
|
|
844
894
|
/** Site-wide announcement banner shown above the header. */
|
|
845
895
|
banner?: BannerConfig;
|
|
896
|
+
/**
|
|
897
|
+
* Site-wide mount point prepended to every generated route (e.g. `/docs`) —
|
|
898
|
+
* pages, links, redirects, sitemap, OG images, `llms.txt`, and the search
|
|
899
|
+
* index — while staying invisible to the sidebar/nav tree (no wrapper group).
|
|
900
|
+
* Distinct from a per-source `prefix` (which namespaces one source *and*
|
|
901
|
+
* creates a group) and from `deployment.base` (Astro's host-subdirectory
|
|
902
|
+
* base, for serving the whole site — root included — from a subpath). The two
|
|
903
|
+
* compose: with both set, a page lands at `{deployment.base}/{basePath}/page`.
|
|
904
|
+
*/
|
|
905
|
+
basePath?: string;
|
|
846
906
|
/** Where content lives and how it's discovered. */
|
|
847
907
|
content?: ContentConfig;
|
|
848
908
|
/** Where and how the site deploys (site URL, adapter, output mode). */
|
|
@@ -850,11 +910,15 @@ export interface BlumeConfig {
|
|
|
850
910
|
/** Default meta description, used where a page sets none. */
|
|
851
911
|
description?: string;
|
|
852
912
|
/**
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
* target a registry that colocates
|
|
913
|
+
* `<Component path>` example previews. A string is shorthand for
|
|
914
|
+
* `{ source }`: where examples live, relative to the project root (defaults
|
|
915
|
+
* to `examples`; may be a glob to target a registry that colocates
|
|
916
|
+
* component sources with their examples). The object form adds `css` — a
|
|
917
|
+
* stylesheet injected into every preview frame (previews render in an
|
|
918
|
+
* iframe the docs theme never reaches), for the previewed components'
|
|
919
|
+
* design tokens, e.g. shadcn variables.
|
|
856
920
|
*/
|
|
857
|
-
examples?: string;
|
|
921
|
+
examples?: string | ExamplesConfig;
|
|
858
922
|
/** Reader-facing PDF/EPUB export actions. Defaults to `false`. */
|
|
859
923
|
export?: ExportConfig;
|
|
860
924
|
/** Show the per-page "Was this helpful?" widget. Defaults to `true`. */
|
|
@@ -875,6 +939,8 @@ export interface BlumeConfig {
|
|
|
875
939
|
navigation?: NavigationConfig;
|
|
876
940
|
/** Native OpenAPI reference. */
|
|
877
941
|
openapi?: OpenApiConfig;
|
|
942
|
+
/** React island behavior (compiler auto-memoization). */
|
|
943
|
+
react?: ReactConfig;
|
|
878
944
|
/** URL redirect rules. */
|
|
879
945
|
redirects?: RedirectConfig[];
|
|
880
946
|
/** Search backend and credentials. */
|
package/src/core/config.ts
CHANGED
|
@@ -91,8 +91,9 @@ import type { Diagnostic } from "./types.ts";
|
|
|
91
91
|
* - `i18n` — opt-in multi-locale: `locales`, `defaultLocale`, `parser`
|
|
92
92
|
* (`dir` vs filename `dot` suffix), and per-locale UI overrides.
|
|
93
93
|
*
|
|
94
|
-
* - `examples` —
|
|
95
|
-
*
|
|
94
|
+
* - `examples` — `<Component path>` previews: `source` (default `examples/`;
|
|
95
|
+
* supports a glob for colocated registries) and `css`, a stylesheet injected
|
|
96
|
+
* into the isolated preview frames (e.g. shadcn variables).
|
|
96
97
|
*
|
|
97
98
|
* @example Zero-config — just render the Markdown under `docs/`.
|
|
98
99
|
* ```ts
|
|
@@ -197,11 +198,12 @@ export const loadConfig = async (
|
|
|
197
198
|
};
|
|
198
199
|
// Surface every issue in one failing run — reporting only the first turns
|
|
199
200
|
// a three-mistake config into three fix-rerun-fail loops.
|
|
201
|
+
const moreIssues = rest.map((d) => ` - ${d.message}`).join("\n");
|
|
200
202
|
throw new BlumeError(
|
|
201
203
|
rest.length > 0
|
|
202
204
|
? {
|
|
203
205
|
...primary,
|
|
204
|
-
message: `${primary.message}\n${rest.length} more config issue(s):\n${
|
|
206
|
+
message: `${primary.message}\n${rest.length} more config issue(s):\n${moreIssues}`,
|
|
205
207
|
}
|
|
206
208
|
: primary
|
|
207
209
|
);
|
package/src/core/content.ts
CHANGED
|
@@ -22,6 +22,7 @@ export const discoverContent = async (options: {
|
|
|
22
22
|
include: string[];
|
|
23
23
|
exclude: string[];
|
|
24
24
|
defaultType: string;
|
|
25
|
+
basePath?: string;
|
|
25
26
|
i18n?: ResolvedI18nConfig;
|
|
26
27
|
}): Promise<{ pages: PageRecord[]; diagnostics: Diagnostic[] }> => {
|
|
27
28
|
const source = filesystemSource({
|
|
@@ -38,6 +39,7 @@ export const discoverContent = async (options: {
|
|
|
38
39
|
|
|
39
40
|
for (const entry of entries) {
|
|
40
41
|
const normalized = normalizeEntry(entry, {
|
|
42
|
+
basePath: options.basePath ?? "",
|
|
41
43
|
defaultType: options.defaultType,
|
|
42
44
|
i18n: options.i18n,
|
|
43
45
|
source: { name: source.name, prefix: source.prefix, staged: false },
|
package/src/core/data.ts
CHANGED
|
@@ -93,6 +93,8 @@ export interface BlumeDataConfig {
|
|
|
93
93
|
suggestions: NonNullable<ResolvedConfig["ai"]["ask"]>["suggestions"];
|
|
94
94
|
} | null;
|
|
95
95
|
banner: BlumeBanner | null;
|
|
96
|
+
/** `markdown.codeBlocks.theme`: light/dark Shiki themes for code surfaces. */
|
|
97
|
+
codeThemes: ResolvedConfig["markdown"]["codeBlocks"]["theme"];
|
|
96
98
|
/** `markdown.code.wrap`: wrap long code lines instead of scrolling. */
|
|
97
99
|
codeWrap: boolean;
|
|
98
100
|
description: string | undefined;
|
package/src/core/diagnostics.ts
CHANGED
|
@@ -20,35 +20,40 @@ export const createDiagnostic = (diagnostic: Diagnostic): Diagnostic =>
|
|
|
20
20
|
/** Docs site base; diagnostic help links resolve against it. */
|
|
21
21
|
const DOCS_BASE = "https://useblume.dev";
|
|
22
22
|
|
|
23
|
+
const DOCS_DEPLOYMENT = "/docs/deployment";
|
|
24
|
+
const DOCS_REFERENCE_CLI = "/docs/reference/cli";
|
|
25
|
+
const DOCS_CONTENT_SOURCES = "/docs/content/sources";
|
|
26
|
+
const DOCS_CONTENT_NAVIGATION = "/docs/content/navigation";
|
|
27
|
+
|
|
23
28
|
/** Diagnostic code → the docs page that explains it. */
|
|
24
29
|
const DOCS_PATHS: Record<string, string> = {
|
|
25
|
-
BLUME_ADAPTER_REQUIRED:
|
|
26
|
-
BLUME_ASSETS_UNCHECKED:
|
|
27
|
-
BLUME_ASSET_FETCH_FAILED:
|
|
28
|
-
BLUME_BROKEN_ANCHOR:
|
|
29
|
-
BLUME_BROKEN_ASSET:
|
|
30
|
-
BLUME_BROKEN_LINK:
|
|
30
|
+
BLUME_ADAPTER_REQUIRED: DOCS_DEPLOYMENT,
|
|
31
|
+
BLUME_ASSETS_UNCHECKED: DOCS_REFERENCE_CLI,
|
|
32
|
+
BLUME_ASSET_FETCH_FAILED: DOCS_CONTENT_SOURCES,
|
|
33
|
+
BLUME_BROKEN_ANCHOR: DOCS_REFERENCE_CLI,
|
|
34
|
+
BLUME_BROKEN_ASSET: DOCS_REFERENCE_CLI,
|
|
35
|
+
BLUME_BROKEN_LINK: DOCS_REFERENCE_CLI,
|
|
31
36
|
BLUME_CONFIG_INVALID: "/docs/configuration",
|
|
32
37
|
BLUME_CONFIG_LOAD_FAILED: "/docs/configuration",
|
|
33
|
-
BLUME_CONTENT_ROOT_MISSING:
|
|
34
|
-
BLUME_DEAD_LINK:
|
|
35
|
-
BLUME_DUPLICATE_ROUTE:
|
|
38
|
+
BLUME_CONTENT_ROOT_MISSING: DOCS_CONTENT_SOURCES,
|
|
39
|
+
BLUME_DEAD_LINK: DOCS_REFERENCE_CLI,
|
|
40
|
+
BLUME_DUPLICATE_ROUTE: DOCS_CONTENT_NAVIGATION,
|
|
36
41
|
BLUME_FRONTMATTER_INVALID: "/docs/reference/frontmatter",
|
|
37
42
|
BLUME_META_INVALID: "/docs/content/meta",
|
|
38
43
|
BLUME_META_LOAD_FAILED: "/docs/content/meta",
|
|
39
|
-
BLUME_MISSING_SECRET:
|
|
40
|
-
BLUME_NAV_DUPLICATE_LABEL:
|
|
41
|
-
BLUME_NAV_HIDDEN_IN_SIDEBAR:
|
|
42
|
-
BLUME_NAV_MISSING_PAGE:
|
|
44
|
+
BLUME_MISSING_SECRET: DOCS_DEPLOYMENT,
|
|
45
|
+
BLUME_NAV_DUPLICATE_LABEL: DOCS_CONTENT_NAVIGATION,
|
|
46
|
+
BLUME_NAV_HIDDEN_IN_SIDEBAR: DOCS_CONTENT_NAVIGATION,
|
|
47
|
+
BLUME_NAV_MISSING_PAGE: DOCS_CONTENT_NAVIGATION,
|
|
43
48
|
BLUME_NODE_VERSION: "/docs/quickstart",
|
|
44
|
-
BLUME_SERVER_FEATURE_REQUIRED:
|
|
45
|
-
BLUME_SOURCE_FETCH_FAILED:
|
|
46
|
-
BLUME_SOURCE_MISCONFIGURED:
|
|
47
|
-
BLUME_SOURCE_OFFLINE:
|
|
48
|
-
BLUME_SOURCE_SDK_MISSING:
|
|
49
|
-
BLUME_SOURCE_UNAVAILABLE:
|
|
49
|
+
BLUME_SERVER_FEATURE_REQUIRED: DOCS_DEPLOYMENT,
|
|
50
|
+
BLUME_SOURCE_FETCH_FAILED: DOCS_CONTENT_SOURCES,
|
|
51
|
+
BLUME_SOURCE_MISCONFIGURED: DOCS_CONTENT_SOURCES,
|
|
52
|
+
BLUME_SOURCE_OFFLINE: DOCS_CONTENT_SOURCES,
|
|
53
|
+
BLUME_SOURCE_SDK_MISSING: DOCS_CONTENT_SOURCES,
|
|
54
|
+
BLUME_SOURCE_UNAVAILABLE: DOCS_CONTENT_SOURCES,
|
|
50
55
|
BLUME_UNKNOWN_COMPONENT: "/docs/configuration/customization",
|
|
51
|
-
BLUME_UNKNOWN_ICON:
|
|
56
|
+
BLUME_UNKNOWN_ICON: DOCS_CONTENT_NAVIGATION,
|
|
52
57
|
};
|
|
53
58
|
|
|
54
59
|
/** The docs URL that explains a diagnostic code, if one is mapped. */
|
|
@@ -74,6 +79,29 @@ const escapeRegExp = (value: string): string =>
|
|
|
74
79
|
* lands under its parent. Array indices are skipped. Returns 1-based line/column,
|
|
75
80
|
* or undefined when nothing matches.
|
|
76
81
|
*/
|
|
82
|
+
const stepSegment = (
|
|
83
|
+
source: string,
|
|
84
|
+
segment: string | number,
|
|
85
|
+
cursor: number
|
|
86
|
+
): { index: number; next: number; stop: boolean } => {
|
|
87
|
+
// A non-string path segment (array index) is skipped without moving on.
|
|
88
|
+
if (typeof segment !== "string") {
|
|
89
|
+
return { index: -1, next: cursor, stop: false };
|
|
90
|
+
}
|
|
91
|
+
// The negative lookbehind keeps a segment like `title` from matching the
|
|
92
|
+
// tail of an unrelated key such as `subtitle:`.
|
|
93
|
+
const matcher = new RegExp(
|
|
94
|
+
`(?<![\\w$])${escapeRegExp(segment)}\\s*[:=]`,
|
|
95
|
+
"gu"
|
|
96
|
+
);
|
|
97
|
+
matcher.lastIndex = cursor;
|
|
98
|
+
const match = matcher.exec(source);
|
|
99
|
+
if (!match) {
|
|
100
|
+
return { index: -1, next: cursor, stop: true };
|
|
101
|
+
}
|
|
102
|
+
return { index: match.index, next: matcher.lastIndex, stop: false };
|
|
103
|
+
};
|
|
104
|
+
|
|
77
105
|
const locatePath = (
|
|
78
106
|
source: string,
|
|
79
107
|
path: readonly (string | number)[]
|
|
@@ -81,22 +109,14 @@ const locatePath = (
|
|
|
81
109
|
let cursor = 0;
|
|
82
110
|
let found = -1;
|
|
83
111
|
for (const segment of path) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
// The negative lookbehind keeps a segment like `title` from matching the
|
|
88
|
-
// tail of an unrelated key such as `subtitle:`.
|
|
89
|
-
const matcher = new RegExp(
|
|
90
|
-
`(?<![\\w$])${escapeRegExp(segment)}\\s*[:=]`,
|
|
91
|
-
"gu"
|
|
92
|
-
);
|
|
93
|
-
matcher.lastIndex = cursor;
|
|
94
|
-
const match = matcher.exec(source);
|
|
95
|
-
if (!match) {
|
|
112
|
+
const step = stepSegment(source, segment, cursor);
|
|
113
|
+
if (step.stop) {
|
|
96
114
|
break;
|
|
97
115
|
}
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
cursor = step.next;
|
|
117
|
+
if (step.index >= 0) {
|
|
118
|
+
found = step.index;
|
|
119
|
+
}
|
|
100
120
|
}
|
|
101
121
|
if (found < 0) {
|
|
102
122
|
return;
|
package/src/core/gitignore.ts
CHANGED
|
@@ -18,7 +18,10 @@ export const ensureGitignore = async (
|
|
|
18
18
|
const path = join(root, ".gitignore");
|
|
19
19
|
const existing = existsSync(path) ? await readFile(path, "utf-8") : "";
|
|
20
20
|
const present = new Set(
|
|
21
|
-
existing.split("\n").
|
|
21
|
+
existing.split("\n").flatMap((line) => {
|
|
22
|
+
const key = gitignoreKey(line);
|
|
23
|
+
return key ? [key] : [];
|
|
24
|
+
})
|
|
22
25
|
);
|
|
23
26
|
const added = entries.filter((entry) => !present.has(gitignoreKey(entry)));
|
|
24
27
|
if (added.length === 0) {
|
package/src/core/graph.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withBasePath } from "./base-path.ts";
|
|
1
2
|
import { localizeRoute, resolveFallbackLocale } from "./i18n.ts";
|
|
2
3
|
import { validateNavIcons, validateNavStructure } from "./nav-diagnostics.ts";
|
|
3
4
|
import { buildNavigation } from "./navigation.ts";
|
|
@@ -13,19 +14,23 @@ import type {
|
|
|
13
14
|
PageRecord,
|
|
14
15
|
} from "./types.ts";
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
interface BuildContentGraphOptions {
|
|
18
|
+
/** Site-wide route mount point (`""` or `/seg`); invisible to the nav tree. */
|
|
19
|
+
basePath?: string;
|
|
20
|
+
folderMeta: Map<string, FolderMeta>;
|
|
21
|
+
sharedFolderMeta?: Map<string, FolderMeta>;
|
|
22
|
+
navigation: ResolvedConfig["navigation"];
|
|
23
|
+
i18n?: ResolvedI18nConfig;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type FallbackLocale = ReturnType<typeof resolveFallbackLocale>;
|
|
27
|
+
|
|
28
|
+
/** Build the route → page-id map, flagging any duplicate-route collisions. */
|
|
29
|
+
const collectRoutes = (
|
|
30
|
+
pages: PageRecord[]
|
|
31
|
+
): { diagnostics: Diagnostic[]; routes: Map<string, string> } => {
|
|
26
32
|
const routes = new Map<string, string>();
|
|
27
33
|
const diagnostics: Diagnostic[] = [];
|
|
28
|
-
|
|
29
34
|
for (const page of pages) {
|
|
30
35
|
const existing = routes.get(page.route);
|
|
31
36
|
if (existing) {
|
|
@@ -40,94 +45,157 @@ export const buildContentGraph = (
|
|
|
40
45
|
}
|
|
41
46
|
routes.set(page.route, page.id);
|
|
42
47
|
}
|
|
48
|
+
return { diagnostics, routes };
|
|
49
|
+
};
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
/**
|
|
52
|
+
* A locale's pages, padded with fallback-locale entries for any translation it
|
|
53
|
+
* hasn't authored yet, so navigation mirrors the default structure instead of
|
|
54
|
+
* showing an empty (or partial) tree.
|
|
55
|
+
*/
|
|
56
|
+
const localePagesFor = (
|
|
57
|
+
code: string,
|
|
58
|
+
real: PageRecord[],
|
|
59
|
+
fallback: FallbackLocale,
|
|
60
|
+
fallbackByKey: Map<string, PageRecord>,
|
|
61
|
+
i18n: ResolvedI18nConfig,
|
|
62
|
+
basePath: string
|
|
63
|
+
): PageRecord[] => {
|
|
64
|
+
if (!(fallback && code !== fallback)) {
|
|
65
|
+
return real;
|
|
66
|
+
}
|
|
67
|
+
const present = new Set(real.map((page) => page.translationKey));
|
|
68
|
+
const filled: PageRecord[] = [];
|
|
69
|
+
for (const [key, source] of fallbackByKey) {
|
|
70
|
+
if (!present.has(key)) {
|
|
71
|
+
filled.push({
|
|
72
|
+
...source,
|
|
73
|
+
locale: code,
|
|
74
|
+
route: withBasePath(basePath, localizeRoute(key, code, i18n)),
|
|
75
|
+
});
|
|
60
76
|
}
|
|
77
|
+
}
|
|
78
|
+
return [...real, ...filled];
|
|
79
|
+
};
|
|
61
80
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
/** Build one locale's navigation tree from its own pages and folder meta. */
|
|
82
|
+
const buildLocaleNavigation = (
|
|
83
|
+
code: string,
|
|
84
|
+
pages: PageRecord[],
|
|
85
|
+
fallback: FallbackLocale,
|
|
86
|
+
fallbackByKey: Map<string, PageRecord>,
|
|
87
|
+
options: BuildContentGraphOptions,
|
|
88
|
+
i18n: ResolvedI18nConfig
|
|
89
|
+
): Navigation => {
|
|
90
|
+
// Localize internal tab paths so a header tab points to its in-locale route
|
|
91
|
+
// (e.g. `/docs` -> `/fr/docs`); external paths pass through.
|
|
92
|
+
const tabs = options.navigation.tabs?.map((tab) => ({
|
|
93
|
+
...tab,
|
|
94
|
+
path: tab.path.startsWith("/")
|
|
95
|
+
? localizeRoute(tab.path, code, i18n)
|
|
96
|
+
: tab.path,
|
|
97
|
+
}));
|
|
98
|
+
const real = pages.filter((page) => page.locale === code);
|
|
99
|
+
const localePages = localePagesFor(
|
|
100
|
+
code,
|
|
101
|
+
real,
|
|
102
|
+
fallback,
|
|
103
|
+
fallbackByKey,
|
|
104
|
+
i18n,
|
|
105
|
+
options.basePath ?? ""
|
|
106
|
+
);
|
|
107
|
+
return buildNavigation(localePages, {
|
|
108
|
+
basePath: options.basePath ?? "",
|
|
109
|
+
display: options.navigation.sidebar.display,
|
|
110
|
+
featured: options.navigation.featured,
|
|
111
|
+
folderMeta: options.folderMeta,
|
|
112
|
+
// Meta files live in locale directories only under the `dir` parser
|
|
113
|
+
// (`fr/guides/meta.ts` -> key `fr/guides`). Under `dot`, translations sit
|
|
114
|
+
// next to the originals and `guides/meta.ts` applies to every locale —
|
|
115
|
+
// prefixing would look up keys that can never exist.
|
|
116
|
+
metaPrefix:
|
|
117
|
+
i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "",
|
|
118
|
+
refByLogical: true,
|
|
119
|
+
selectors: options.navigation.selectors,
|
|
120
|
+
sharedFolderMeta: options.sharedFolderMeta,
|
|
121
|
+
sidebar: options.navigation.sidebar.items,
|
|
122
|
+
tabs,
|
|
123
|
+
});
|
|
124
|
+
};
|
|
73
125
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
126
|
+
/** Per-locale navigation trees plus the default-locale tree for i18n sites. */
|
|
127
|
+
const buildI18nNavigation = (
|
|
128
|
+
pages: PageRecord[],
|
|
129
|
+
options: BuildContentGraphOptions,
|
|
130
|
+
i18n: ResolvedI18nConfig
|
|
131
|
+
): {
|
|
132
|
+
navigation: Navigation;
|
|
133
|
+
navigationByLocale: Record<string, Navigation>;
|
|
134
|
+
} => {
|
|
135
|
+
// Pages of the fallback locale, by translation key — used to fill in a
|
|
136
|
+
// locale's sidebar for pages it hasn't translated yet.
|
|
137
|
+
const fallback = resolveFallbackLocale(i18n);
|
|
138
|
+
const fallbackByKey = new Map<string, PageRecord>();
|
|
139
|
+
if (fallback) {
|
|
140
|
+
for (const page of pages) {
|
|
141
|
+
if (page.locale === fallback) {
|
|
142
|
+
fallbackByKey.set(page.translationKey, page);
|
|
89
143
|
}
|
|
90
|
-
|
|
91
|
-
navigationByLocale[code] = buildNavigation(localePages, {
|
|
92
|
-
display: options.navigation.sidebar.display,
|
|
93
|
-
featured: options.navigation.featured,
|
|
94
|
-
folderMeta: options.folderMeta,
|
|
95
|
-
// Meta files live in locale directories only under the `dir` parser
|
|
96
|
-
// (`fr/guides/meta.ts` -> key `fr/guides`). Under `dot`, translations
|
|
97
|
-
// sit next to the originals and `guides/meta.ts` applies to every
|
|
98
|
-
// locale — prefixing would look up keys that can never exist.
|
|
99
|
-
metaPrefix:
|
|
100
|
-
i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "",
|
|
101
|
-
refByLogical: true,
|
|
102
|
-
selectors: options.navigation.selectors,
|
|
103
|
-
sharedFolderMeta: options.sharedFolderMeta,
|
|
104
|
-
sidebar: options.navigation.sidebar.items,
|
|
105
|
-
tabs,
|
|
106
|
-
});
|
|
107
144
|
}
|
|
108
|
-
navigation = navigationByLocale[i18n.defaultLocale] ?? {
|
|
109
|
-
featured: [],
|
|
110
|
-
selectors: [],
|
|
111
|
-
sidebar: [],
|
|
112
|
-
tabs: [],
|
|
113
|
-
};
|
|
114
|
-
} else {
|
|
115
|
-
navigation = buildNavigation(pages, {
|
|
116
|
-
display: options.navigation.sidebar.display,
|
|
117
|
-
featured: options.navigation.featured,
|
|
118
|
-
folderMeta: options.folderMeta,
|
|
119
|
-
selectors: options.navigation.selectors,
|
|
120
|
-
sharedFolderMeta: options.sharedFolderMeta,
|
|
121
|
-
sidebar: options.navigation.sidebar.items,
|
|
122
|
-
tabs: options.navigation.tabs,
|
|
123
|
-
});
|
|
124
145
|
}
|
|
125
146
|
|
|
147
|
+
// Each locale gets an independent tree, so navigation may diverge per language.
|
|
148
|
+
const navigationByLocale: Record<string, Navigation> = {};
|
|
149
|
+
for (const { code } of i18n.locales) {
|
|
150
|
+
navigationByLocale[code] = buildLocaleNavigation(
|
|
151
|
+
code,
|
|
152
|
+
pages,
|
|
153
|
+
fallback,
|
|
154
|
+
fallbackByKey,
|
|
155
|
+
options,
|
|
156
|
+
i18n
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const navigation = navigationByLocale[i18n.defaultLocale] ?? {
|
|
160
|
+
featured: [],
|
|
161
|
+
selectors: [],
|
|
162
|
+
sidebar: [],
|
|
163
|
+
tabs: [],
|
|
164
|
+
};
|
|
165
|
+
return { navigation, navigationByLocale };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** Assemble the content graph: routes map, nav, and duplicate diagnostics. */
|
|
169
|
+
export const buildContentGraph = (
|
|
170
|
+
pages: PageRecord[],
|
|
171
|
+
options: BuildContentGraphOptions
|
|
172
|
+
): ContentGraph => {
|
|
173
|
+
const { diagnostics, routes } = collectRoutes(pages);
|
|
174
|
+
const { i18n } = options;
|
|
175
|
+
|
|
176
|
+
const { navigation, navigationByLocale } = i18n
|
|
177
|
+
? buildI18nNavigation(pages, options, i18n)
|
|
178
|
+
: {
|
|
179
|
+
navigation: buildNavigation(pages, {
|
|
180
|
+
basePath: options.basePath ?? "",
|
|
181
|
+
display: options.navigation.sidebar.display,
|
|
182
|
+
featured: options.navigation.featured,
|
|
183
|
+
folderMeta: options.folderMeta,
|
|
184
|
+
selectors: options.navigation.selectors,
|
|
185
|
+
sharedFolderMeta: options.sharedFolderMeta,
|
|
186
|
+
sidebar: options.navigation.sidebar.items,
|
|
187
|
+
tabs: options.navigation.tabs,
|
|
188
|
+
}),
|
|
189
|
+
navigationByLocale: {} as Record<string, Navigation>,
|
|
190
|
+
};
|
|
191
|
+
|
|
126
192
|
// Icon typos, duplicate labels, and hidden-page-in-sidebar are validated on
|
|
127
193
|
// the built navigation. Missing-target detection needs the full route set
|
|
128
194
|
// (incl. custom + generated pages), so it runs later in generateRuntime.
|
|
129
|
-
diagnostics.push(
|
|
130
|
-
|
|
195
|
+
diagnostics.push(
|
|
196
|
+
...validateNavIcons(navigation),
|
|
197
|
+
...validateNavStructure(navigation, pages)
|
|
198
|
+
);
|
|
131
199
|
|
|
132
200
|
return {
|
|
133
201
|
diagnostics,
|