blume 0.7.0 → 0.8.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/CHANGELOG.md +618 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/cli/index.js +1487 -360
- package/dist/cli/index.js.map +91 -85
- package/dist/types/ai/component-markdown.d.ts +34 -0
- package/dist/types/components/content/youtube.d.ts +18 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +36 -2
- 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 +476 -132
- package/dist/types/core/schema.d.ts +216 -145
- package/dist/types/index.d.ts +1 -0
- package/dist/types/openapi/references.d.ts +60 -0
- package/docs/01-quickstart.mdx +5 -2
- package/docs/02-deployment.mdx +8 -8
- package/docs/03-faq.mdx +46 -16
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/configuration/ai.mdx +49 -10
- package/docs/configuration/customization.mdx +11 -0
- package/docs/configuration/index.mdx +27 -3
- package/docs/configuration/seo.mdx +2 -2
- package/docs/content/components.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/navigation.mdx +3 -3
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +4 -2
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +8 -6
- package/package.json +14 -4
- package/skills/blume/SKILL.md +5 -3
- package/skills/blume-update-docs/SKILL.md +3 -2
- package/src/ai/agent-readability.ts +9 -8
- package/src/ai/ask-context.ts +7 -2
- package/src/ai/ask-data.ts +3 -0
- package/src/ai/component-markdown.ts +461 -0
- package/src/ai/llms.ts +135 -26
- package/src/ai/markdown.ts +35 -6
- package/src/ai/mcp/data.ts +25 -4
- package/src/ai/mcp/discovery.ts +10 -3
- package/src/ai/mcp/server.ts +21 -7
- package/src/ai/visibility.ts +74 -0
- package/src/astro/component-slots.ts +11 -1
- package/src/astro/generate.ts +76 -45
- package/src/astro/integration.ts +1 -1
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/pages.ts +81 -19
- package/src/astro/templates.ts +99 -12
- package/src/blume-modules.d.ts +8 -0
- package/src/cli/commands/build.ts +99 -19
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/dev.ts +26 -5
- package/src/cli/commands/eject.ts +47 -19
- package/src/cli/commands/init.ts +120 -180
- package/src/cli/commands/preview.ts +4 -1
- package/src/cli/commands/validate.ts +43 -2
- package/src/cli/dev-lock.ts +8 -4
- package/src/cli/eject-scripts.ts +72 -0
- package/src/cli/env.ts +15 -5
- package/src/cli/init/questions.ts +158 -0
- package/src/cli/init/scaffold.ts +380 -0
- package/src/components/content/AccordionItem.astro +23 -4
- package/src/components/content/Badge.astro +3 -1
- package/src/components/content/Card.astro +4 -2
- package/src/components/content/Step.astro +10 -1
- package/src/components/content/Tabs.astro +15 -3
- package/src/components/content/Tile.astro +2 -1
- package/src/components/content/Tooltip.astro +3 -1
- package/src/components/content/Update.astro +9 -2
- package/src/components/content/auto-type-table.ts +7 -1
- package/src/components/content/base-href.ts +33 -0
- package/src/components/content/changelog-element.ts +9 -2
- package/src/components/content/mermaid-element.ts +7 -2
- package/src/components/islands/AskAI.astro +5 -2
- package/src/components/islands/ask-ai.tsx +56 -6
- package/src/components/islands/hooks.ts +28 -8
- package/src/components/layout/Banner.astro +10 -2
- package/src/components/layout/Header.astro +13 -4
- package/src/components/layout/Logo.astro +11 -3
- package/src/components/layout/NavTree.astro +17 -3
- package/src/components/layout/PageActions.astro +25 -10
- package/src/components/layout/PageLayout.astro +45 -8
- package/src/components/layout/ReferenceLayout.astro +8 -1
- package/src/components/layout/RootLayout.astro +67 -9
- package/src/components/layout/Search.astro +94 -22
- package/src/components/layout/search/algolia.ts +11 -2
- package/src/components/layout/search/endpoint.ts +11 -5
- package/src/components/layout/search/orama-cloud.ts +8 -2
- package/src/components/layout/search/types.ts +5 -1
- package/src/components/layout/search/typesense.ts +4 -1
- package/src/components/layout/toc-element.ts +1 -1
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/Operation.astro +47 -40
- package/src/components/openapi/RequestPanel.astro +1 -1
- package/src/components/openapi/helpers.ts +71 -3
- package/src/components/openapi/panel.ts +1 -1
- package/src/core/base-path.ts +24 -0
- package/src/core/builtin-tags.ts +2 -0
- package/src/core/config-input.ts +37 -2
- package/src/core/config.ts +3 -2
- package/src/core/data.ts +2 -0
- package/src/core/graph.ts +15 -5
- package/src/core/i18n-ui.ts +45 -0
- package/src/core/last-modified.ts +13 -6
- package/src/core/links.ts +32 -8
- package/src/core/navigation.ts +29 -4
- package/src/core/package-json.ts +17 -2
- package/src/core/project-graph.ts +15 -6
- package/src/core/schema.ts +36 -2
- package/src/core/sources/assets.ts +6 -1
- package/src/core/sources/filesystem.ts +4 -0
- package/src/core/sources/mdx-remote.ts +23 -14
- package/src/core/sources/normalize.ts +152 -50
- package/src/core/sources/notion.ts +8 -8
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -1
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/deploy/adapter-output.ts +18 -8
- package/src/deploy/redirects.ts +7 -2
- package/src/deploy/sitemap.ts +53 -11
- package/src/index.ts +5 -0
- package/src/markdown/base-links.ts +10 -8
- package/src/markdown/index.ts +15 -3
- package/src/markdown/inline-code.ts +7 -2
- package/src/markdown/package-commands.ts +10 -4
- package/src/openapi/model.ts +12 -4
- package/src/openapi/parse.ts +21 -0
- package/src/openapi/references.ts +38 -8
- package/src/openapi/source.ts +59 -10
- package/src/registry/eject.ts +184 -12
- package/src/registry/registry.ts +0 -3
- package/src/search/documents.ts +34 -2
- package/src/seo/jsonld.ts +13 -12
|
@@ -42,16 +42,23 @@ export const parseGitLog = (output: string): Map<string, string> => {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Resolve each source file's last-modified date from git history, keyed by
|
|
45
|
-
* absolute source path. Runs a single `git log` over the content
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* absolute source path. Runs a single `git log` over the given content roots
|
|
46
|
+
* (each filesystem source's own root, which may diverge from `content.root`)
|
|
47
|
+
* and maps repo-root-relative paths back to the given absolute paths
|
|
48
|
+
* (monorepo-safe via `rev-parse --show-toplevel`). Returns an empty map if git
|
|
49
|
+
* is unavailable or the project isn't a repo — the feature then simply shows
|
|
50
|
+
* no dates.
|
|
49
51
|
*/
|
|
50
52
|
export const gitLastModifiedTimes = (
|
|
51
53
|
root: string,
|
|
52
|
-
|
|
54
|
+
contentRoots: string[],
|
|
53
55
|
sourcePaths: string[]
|
|
54
56
|
): Map<string, string> => {
|
|
57
|
+
// Nothing to date — don't pay for a git scan (an empty pathspec list would
|
|
58
|
+
// log the entire repository).
|
|
59
|
+
if (sourcePaths.length === 0) {
|
|
60
|
+
return new Map();
|
|
61
|
+
}
|
|
55
62
|
try {
|
|
56
63
|
const gitRoot = execFileSync(
|
|
57
64
|
// oxlint-disable-next-line sonarjs/no-os-command-from-path -- git is a required dev-tool dependency resolved from PATH
|
|
@@ -71,7 +78,7 @@ export const gitLastModifiedTimes = (
|
|
|
71
78
|
"--format=%x00%cI",
|
|
72
79
|
"--name-only",
|
|
73
80
|
"--",
|
|
74
|
-
|
|
81
|
+
...contentRoots,
|
|
75
82
|
],
|
|
76
83
|
{ encoding: "utf-8", maxBuffer: 256 * 1024 * 1024 }
|
|
77
84
|
);
|
package/src/core/links.ts
CHANGED
|
@@ -49,6 +49,9 @@ interface LinkContext {
|
|
|
49
49
|
anchors: Map<string, Set<string>>;
|
|
50
50
|
/** Site-wide route mount point (`""` or `/seg`); routes carry it, assets don't. */
|
|
51
51
|
basePath: string;
|
|
52
|
+
/** Servable routes outside the graph (custom pages, generated routes); their
|
|
53
|
+
* headings are unknown, so anchors there are accepted unchecked. */
|
|
54
|
+
extraRoutes: Set<string>;
|
|
52
55
|
publicDir: string | null;
|
|
53
56
|
/** Normalized `redirect.from` paths — valid targets that resolve at runtime. */
|
|
54
57
|
redirects: Set<string>;
|
|
@@ -62,16 +65,24 @@ const assetIsPresent = (resolved: string, ctx: LinkContext): boolean =>
|
|
|
62
65
|
/** Outcome of classifying one link target. */
|
|
63
66
|
type LinkResult = Diagnostic | "asset-unchecked" | null;
|
|
64
67
|
|
|
68
|
+
// Mirrors the ordering-prefix strip in `sources/normalize.ts`: route mapping
|
|
69
|
+
// drops the prefix before recognizing `index`, so `01-index.mdx` is an index.
|
|
70
|
+
const NUMERIC_PREFIX = /^\d+[-_.]/u;
|
|
71
|
+
|
|
65
72
|
/**
|
|
66
|
-
* Whether a page is a directory index (`…/index.md(x)
|
|
67
|
-
* its directory, so a relative link must
|
|
68
|
-
* its parent — otherwise `./sibling`
|
|
69
|
-
* would resolve to `/sibling` and be
|
|
73
|
+
* Whether a page is a directory index (`…/index.md(x)`, ordering prefix
|
|
74
|
+
* ignored). Its route already *is* its directory, so a relative link must
|
|
75
|
+
* resolve against the route itself, not its parent — otherwise `./sibling`
|
|
76
|
+
* from `guides/index.mdx` (route `/guides`) would resolve to `/sibling` and be
|
|
77
|
+
* falsely flagged as broken. Tested against `navPath` — the locale-stripped
|
|
78
|
+
* path — so a dot-parser localized index (`index.fr.mdx`) and a shared
|
|
79
|
+
* locale-agnostic one (`index.$.mdx`) count too, matching how route mapping
|
|
80
|
+
* recognizes them.
|
|
70
81
|
*/
|
|
71
|
-
const isIndexPage = (page: PageRecord): boolean =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
const isIndexPage = (page: PageRecord): boolean =>
|
|
83
|
+
/^index\.(?:md|mdx)$/iu.test(
|
|
84
|
+
basename(page.navPath).replace(NUMERIC_PREFIX, "")
|
|
85
|
+
);
|
|
75
86
|
|
|
76
87
|
/** Apply one relative-path segment to the accumulated route segments. */
|
|
77
88
|
const applyRelativePart = (segments: string[], part: string): void => {
|
|
@@ -162,6 +173,11 @@ const checkPathLink = (
|
|
|
162
173
|
if (ctx.routes.has(route)) {
|
|
163
174
|
return fragment ? checkAnchor(route, fragment, site, ctx) : null;
|
|
164
175
|
}
|
|
176
|
+
// A custom `.astro` page or generated route serves this path, but its
|
|
177
|
+
// headings aren't indexed — accept any fragment rather than false-flag it.
|
|
178
|
+
if (ctx.extraRoutes.has(route)) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
165
181
|
// A configured `redirect.from` resolves at runtime, so it's a valid target.
|
|
166
182
|
// Its destination (and any anchor there) is validated on its own page, so we
|
|
167
183
|
// don't follow the redirect to check the fragment here.
|
|
@@ -365,6 +381,13 @@ export const validateLinks = async (
|
|
|
365
381
|
options: {
|
|
366
382
|
/** Site-wide route mount point (`""` or `/seg`); routes and redirects carry it. */
|
|
367
383
|
basePath?: string;
|
|
384
|
+
/**
|
|
385
|
+
* Servable routes the graph can't know: custom `.astro` pages and generated
|
|
386
|
+
* routes (e.g. the `/changelog` index). Mounted outside `basePath` (they're
|
|
387
|
+
* injected at their pattern), so they are *not* based here — mirroring the
|
|
388
|
+
* full-route-set resolution in `nav-diagnostics.ts`/`generateRuntime`.
|
|
389
|
+
*/
|
|
390
|
+
extraRoutes?: string[];
|
|
368
391
|
publicDir: string | null;
|
|
369
392
|
checkExternal?: boolean;
|
|
370
393
|
/** Configured redirects; their `from` paths count as valid link targets. */
|
|
@@ -375,6 +398,7 @@ export const validateLinks = async (
|
|
|
375
398
|
const ctx: LinkContext = {
|
|
376
399
|
anchors: buildAnchorIndex(graph.pages),
|
|
377
400
|
basePath,
|
|
401
|
+
extraRoutes: new Set((options.extraRoutes ?? []).map(toRoute)),
|
|
378
402
|
publicDir: options.publicDir,
|
|
379
403
|
redirects: new Set(
|
|
380
404
|
(options.redirects ?? []).map((redirect) =>
|
package/src/core/navigation.ts
CHANGED
|
@@ -38,6 +38,14 @@ const segmentKey = (raw: string): string => {
|
|
|
38
38
|
return (group ?? raw).replace(NUMERIC_PREFIX, "");
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Whether a filename stem is a directory index, ignoring an ordering prefix:
|
|
43
|
+
* route mapping strips the prefix before dropping `index`, so `01-index` routes
|
|
44
|
+
* exactly like `index` and must be treated as one here too.
|
|
45
|
+
*/
|
|
46
|
+
const isIndexStem = (stem: string): boolean =>
|
|
47
|
+
stem.replace(NUMERIC_PREFIX, "") === "index";
|
|
48
|
+
|
|
41
49
|
interface MutablePage {
|
|
42
50
|
kind: "page";
|
|
43
51
|
key: string;
|
|
@@ -106,7 +114,7 @@ const pageOrder = (page: PageRecord, filename: string): number => {
|
|
|
106
114
|
if (page.meta.sidebar.order !== undefined) {
|
|
107
115
|
return page.meta.sidebar.order;
|
|
108
116
|
}
|
|
109
|
-
if (filename.replace(extname(filename), "")
|
|
117
|
+
if (isIndexStem(filename.replace(extname(filename), ""))) {
|
|
110
118
|
return Number.NEGATIVE_INFINITY;
|
|
111
119
|
}
|
|
112
120
|
// Changelog entries read newest-first, matching the generated timeline. Sort
|
|
@@ -277,8 +285,9 @@ const buildFileSystemSidebar = (
|
|
|
277
285
|
// An index page's route IS its folder's route (no page segment to drop),
|
|
278
286
|
// and `(group)` folders contribute no route segment at all.
|
|
279
287
|
const routeSegments = page.route.split("/").filter(Boolean);
|
|
280
|
-
const folderParts =
|
|
281
|
-
|
|
288
|
+
const folderParts = isIndexStem(stem)
|
|
289
|
+
? routeSegments
|
|
290
|
+
: routeSegments.slice(0, -1);
|
|
282
291
|
const routeDirCount = dirs.filter((dir) => !GROUP_FOLDER.test(dir)).length;
|
|
283
292
|
const offset = Math.max(0, folderParts.length - routeDirCount);
|
|
284
293
|
|
|
@@ -447,6 +456,13 @@ export const buildNavigation = (
|
|
|
447
456
|
refByLogical?: boolean;
|
|
448
457
|
/** Shared `meta.$.*` meta, keyed by locale-stripped dir path. */
|
|
449
458
|
sharedFolderMeta?: Map<string, FolderMeta>;
|
|
459
|
+
/**
|
|
460
|
+
* The tree's root route before `basePath` (`"/"`, or the locale prefix
|
|
461
|
+
* under i18n, e.g. `/fr` — tab paths arrive already localized). The tab
|
|
462
|
+
* pointing here spans the whole tree, so it is excluded from tab-section
|
|
463
|
+
* scoping.
|
|
464
|
+
*/
|
|
465
|
+
localizedRoot?: string;
|
|
450
466
|
}
|
|
451
467
|
): Navigation => {
|
|
452
468
|
const basePath = options.basePath ?? "";
|
|
@@ -511,6 +527,13 @@ export const buildNavigation = (
|
|
|
511
527
|
};
|
|
512
528
|
}
|
|
513
529
|
|
|
530
|
+
// A tab pointing at the tree root spans the whole sidebar rather than one
|
|
531
|
+
// section, so it must not feed tab-section hoisting. `tabs` carries final
|
|
532
|
+
// paths (localized, then based), so the root is compared in the same space —
|
|
533
|
+
// a root-level `(group)` folder's routePath is exactly the based/localized
|
|
534
|
+
// prefix (`/docs`, `/fr`) and a bare `"/"` check would miss the match (or,
|
|
535
|
+
// under a base, falsely scope a group named like the prefix).
|
|
536
|
+
const rootTabPath = withBasePath(basePath, options.localizedRoot ?? "/");
|
|
514
537
|
return {
|
|
515
538
|
featured,
|
|
516
539
|
selectors,
|
|
@@ -520,7 +543,9 @@ export const buildNavigation = (
|
|
|
520
543
|
sharedFolderMeta,
|
|
521
544
|
metaPrefix,
|
|
522
545
|
display,
|
|
523
|
-
new Set(
|
|
546
|
+
new Set(
|
|
547
|
+
tabs.flatMap((tab) => (tab.path === rootTabPath ? [] : [tab.path]))
|
|
548
|
+
)
|
|
524
549
|
),
|
|
525
550
|
tabs,
|
|
526
551
|
};
|
package/src/core/package-json.ts
CHANGED
|
@@ -15,8 +15,22 @@ export const toPackageName = (raw: string): string =>
|
|
|
15
15
|
* dependency pinned to the installed version plus `dev`/`build`/`doctor`
|
|
16
16
|
* scripts, so `npm install && npm run dev` works immediately. Shared by
|
|
17
17
|
* `blume init` and the migrators, which scaffold one when a project has none.
|
|
18
|
+
* `extraDeps` adds source SDKs (e.g. `@notionhq/client`) beside `blume`.
|
|
18
19
|
*/
|
|
19
|
-
export const blumePackageJson = (
|
|
20
|
+
export const blumePackageJson = (
|
|
21
|
+
name: string,
|
|
22
|
+
extraDeps: Record<string, string> = {}
|
|
23
|
+
): string => {
|
|
24
|
+
const dependencies = Object.entries({
|
|
25
|
+
blume: `^${getBlumeVersion()}`,
|
|
26
|
+
...extraDeps,
|
|
27
|
+
})
|
|
28
|
+
.toSorted(([a], [b]) => (a < b ? -1 : 1))
|
|
29
|
+
.map(
|
|
30
|
+
([dep, range]) => ` ${JSON.stringify(dep)}: ${JSON.stringify(range)}`
|
|
31
|
+
)
|
|
32
|
+
.join(",\n");
|
|
33
|
+
return `{
|
|
20
34
|
"name": ${JSON.stringify(name)},
|
|
21
35
|
"private": true,
|
|
22
36
|
"type": "module",
|
|
@@ -26,7 +40,8 @@ export const blumePackageJson = (name: string): string => `{
|
|
|
26
40
|
"doctor": "blume doctor"
|
|
27
41
|
},
|
|
28
42
|
"dependencies": {
|
|
29
|
-
|
|
43
|
+
${dependencies}
|
|
30
44
|
}
|
|
31
45
|
}
|
|
32
46
|
`;
|
|
47
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { relative } from "pathe";
|
|
2
2
|
|
|
3
3
|
import { loadConfig } from "./config.ts";
|
|
4
|
+
import { applyDeploymentEnv } from "./deployment-env.ts";
|
|
4
5
|
import { buildContentGraph } from "./graph.ts";
|
|
5
6
|
import { i18nDiagnostics } from "./i18n.ts";
|
|
6
7
|
import {
|
|
@@ -108,7 +109,7 @@ const entryIdDiagnostics = (
|
|
|
108
109
|
message: `Content source "${page.source.name}" is rooted outside the docs collection base, so ${page.route} resolves entry id "${entryId}" but the collection would generate "${expected}" — the page would 404 at runtime.`,
|
|
109
110
|
severity: "error",
|
|
110
111
|
suggestion:
|
|
111
|
-
"
|
|
112
|
+
"Use a single filesystem source (the docs collection roots at it), or root every filesystem source at content.root and partition them with include globs — a root at a subdirectory of content.root still mismatches.",
|
|
112
113
|
});
|
|
113
114
|
}
|
|
114
115
|
}
|
|
@@ -139,7 +140,13 @@ export const scanProject = async (
|
|
|
139
140
|
const configResult = await loadConfig(root, {
|
|
140
141
|
devServerUrl: options.devServerUrl,
|
|
141
142
|
});
|
|
142
|
-
|
|
143
|
+
// Re-run platform detection after CLI overrides: `loadConfig` already ran it,
|
|
144
|
+
// but adapter inference keys off `deployment.output`, which `--output server`
|
|
145
|
+
// only sets here. Idempotent for already-resolved fields — `deployment.site`
|
|
146
|
+
// keeps loadConfig's explicit > platform env > devServerUrl precedence.
|
|
147
|
+
const config = applyDeploymentEnv(
|
|
148
|
+
applyConfigOverrides(configResult.config, options.overrides)
|
|
149
|
+
);
|
|
143
150
|
const context = resolveProjectContext(root, config, {
|
|
144
151
|
runtimeDir: options.runtimeDir,
|
|
145
152
|
});
|
|
@@ -217,11 +224,13 @@ export const scanProject = async (
|
|
|
217
224
|
const fsPaths = pages
|
|
218
225
|
.map((page) => page.sourcePath)
|
|
219
226
|
.filter((path): path is string => path !== undefined);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
// The git pathspecs must cover where the pages actually live: each
|
|
228
|
+
// filesystem source's own root, which diverges from the global
|
|
229
|
+
// `content.root` when a source configures a non-default `root`.
|
|
230
|
+
const contentRoots = sources.flatMap((source) =>
|
|
231
|
+
source.staged || !source.contentRoot ? [] : [source.contentRoot]
|
|
224
232
|
);
|
|
233
|
+
const gitTimes = gitLastModifiedTimes(context.root, contentRoots, fsPaths);
|
|
225
234
|
for (const page of pages) {
|
|
226
235
|
if (!page.lastModified && page.sourcePath) {
|
|
227
236
|
page.lastModified = gitTimes.get(page.sourcePath);
|
package/src/core/schema.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
import type { ComponentMarkdown } from "../ai/component-markdown.ts";
|
|
4
|
+
import { normalizeRoute } from "../openapi/references.ts";
|
|
3
5
|
import { FONT_SLUGS, isFontSlug } from "../theme/fonts.ts";
|
|
4
6
|
import { normalizeBasePath } from "./base-path.ts";
|
|
5
7
|
import { uiLocaleOverridesSchema } from "./i18n-ui.ts";
|
|
@@ -575,7 +577,35 @@ const aiConfigSchema = z.strictObject({
|
|
|
575
577
|
}
|
|
576
578
|
})
|
|
577
579
|
.optional(),
|
|
578
|
-
|
|
580
|
+
/**
|
|
581
|
+
* `llms.txt`/`llms-full.txt` emission. A bare boolean toggles it; the object
|
|
582
|
+
* form adds `openapi: false` to keep generated API reference pages out of
|
|
583
|
+
* both files (e.g. when the configured spec is example content).
|
|
584
|
+
*/
|
|
585
|
+
llmsTxt: z
|
|
586
|
+
.union([
|
|
587
|
+
z.boolean(),
|
|
588
|
+
z.strictObject({
|
|
589
|
+
enabled: z.boolean().default(true),
|
|
590
|
+
openapi: z.boolean().default(true),
|
|
591
|
+
}),
|
|
592
|
+
])
|
|
593
|
+
.default(true)
|
|
594
|
+
.transform((value) =>
|
|
595
|
+
typeof value === "boolean" ? { enabled: value, openapi: true } : value
|
|
596
|
+
),
|
|
597
|
+
// Serializers for the agent-facing Markdown downlevel (the `.md` mirror,
|
|
598
|
+
// llms-full.txt, MCP get_page), keyed by JSX name. Functions live here —
|
|
599
|
+
// not in components.tsx — because the config file is executed at build
|
|
600
|
+
// time while the components file is only statically analyzed. A same-name
|
|
601
|
+
// entry replaces the built-in serializer.
|
|
602
|
+
markdownComponents: z
|
|
603
|
+
.record(
|
|
604
|
+
z.custom<ComponentMarkdown>((value) => typeof value === "function", {
|
|
605
|
+
message: "Expected a serializer function.",
|
|
606
|
+
})
|
|
607
|
+
)
|
|
608
|
+
.default({}),
|
|
579
609
|
});
|
|
580
610
|
|
|
581
611
|
/**
|
|
@@ -641,7 +671,11 @@ const mcpConfigSchema = z.strictObject({
|
|
|
641
671
|
instructions: z.string().optional(),
|
|
642
672
|
/** Server name shown to clients; defaults to the site title. */
|
|
643
673
|
name: z.string().optional(),
|
|
644
|
-
|
|
674
|
+
/**
|
|
675
|
+
* Normalized like `openapi.route`: a slash-less value would otherwise be
|
|
676
|
+
* string-concatenated onto the site origin (`https://acme.comdocs-mcp`).
|
|
677
|
+
*/
|
|
678
|
+
route: z.string().default("/mcp").transform(normalizeRoute),
|
|
645
679
|
});
|
|
646
680
|
|
|
647
681
|
/** A configured locale: ISO-ish code plus display metadata for the switcher. */
|
|
@@ -68,7 +68,12 @@ export const materializeAssets = async (
|
|
|
68
68
|
throw new Error(`${res.status}`);
|
|
69
69
|
}
|
|
70
70
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
71
|
-
|
|
71
|
+
// Hash the query-less URL (as `extFor` does): CMS asset URLs are
|
|
72
|
+
// pre-signed, so the query changes on every fetch of the same image —
|
|
73
|
+
// hashing it would mint a new file each refresh and re-dirty the
|
|
74
|
+
// content digest. Two real assets sharing scheme+host+path and
|
|
75
|
+
// differing only in query are rare enough to accept colliding.
|
|
76
|
+
const file = `${hashText(url.split("?")[0] ?? url)}${extFor(url)}`;
|
|
72
77
|
await mkdir(ctx.assetsDir, { recursive: true });
|
|
73
78
|
await writeFile(join(ctx.assetsDir, file), bytes);
|
|
74
79
|
rewrites.set(url, `${ctx.assetsBaseUrl}/${file}`);
|
|
@@ -63,6 +63,10 @@ export const filesystemSource = (
|
|
|
63
63
|
return {
|
|
64
64
|
body: { format, text: parsed.content },
|
|
65
65
|
data: parsed.data,
|
|
66
|
+
// The unstripped text: lets `normalizeEntry` offset link line numbers
|
|
67
|
+
// by the frontmatter block's height, so diagnostics point at the real
|
|
68
|
+
// file line (and spares a re-read on the frontmatter-error path).
|
|
69
|
+
raw: source,
|
|
66
70
|
ref: relative(contentRoot, file),
|
|
67
71
|
sourcePath: file,
|
|
68
72
|
};
|
|
@@ -170,6 +170,21 @@ export const mdxRemoteSource = (
|
|
|
170
170
|
const cache = snapshotCache(ctx.cacheDir);
|
|
171
171
|
let snapshot = new Map<string, SourceEntry>();
|
|
172
172
|
|
|
173
|
+
// Validated up front in `load`, *before* the cached-fetch path: thrown from
|
|
174
|
+
// inside `loadWithCache`'s fetch callback, a misconfiguration would be masked
|
|
175
|
+
// as BLUME_SOURCE_FETCH_FAILED (no cache) or downgraded to a stale-cache
|
|
176
|
+
// BLUME_SOURCE_OFFLINE warning (cache present).
|
|
177
|
+
const assertConfigured = (): void => {
|
|
178
|
+
if (options.github || (options.files && options.url)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
throw new BlumeError({
|
|
182
|
+
code: "BLUME_SOURCE_MISCONFIGURED",
|
|
183
|
+
message: `Source "${options.name}" needs either { github } or { url, files }.`,
|
|
184
|
+
severity: "error",
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
173
188
|
const enumerate = async (): Promise<{
|
|
174
189
|
refs: RemoteRef[];
|
|
175
190
|
truncated: boolean;
|
|
@@ -177,20 +192,13 @@ export const mdxRemoteSource = (
|
|
|
177
192
|
if (options.github) {
|
|
178
193
|
return await enumerateGithub(options.github, options.include, doFetch);
|
|
179
194
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return { refs, truncated: false };
|
|
188
|
-
}
|
|
189
|
-
throw new BlumeError({
|
|
190
|
-
code: "BLUME_SOURCE_MISCONFIGURED",
|
|
191
|
-
message: `Source "${options.name}" needs either { github } or { url, files }.`,
|
|
192
|
-
severity: "error",
|
|
193
|
-
});
|
|
195
|
+
const base = (options.url ?? "").replace(/\/$/u, "");
|
|
196
|
+
const refs = (options.files ?? []).flatMap((ref) =>
|
|
197
|
+
matchesInclude(ref, options.include)
|
|
198
|
+
? [{ editUrl: `${base}/${ref}`, fetchUrl: `${base}/${ref}`, ref }]
|
|
199
|
+
: []
|
|
200
|
+
);
|
|
201
|
+
return { refs, truncated: false };
|
|
194
202
|
};
|
|
195
203
|
|
|
196
204
|
const fetchEntry = async (item: RemoteRef): Promise<SourceEntry> => {
|
|
@@ -216,6 +224,7 @@ export const mdxRemoteSource = (
|
|
|
216
224
|
const load = async (
|
|
217
225
|
refresh = ctx.refresh ?? true
|
|
218
226
|
): Promise<SourceLoadResult> => {
|
|
227
|
+
assertConfigured();
|
|
219
228
|
const skipped: Diagnostic[] = [];
|
|
220
229
|
const result = await loadWithCache(
|
|
221
230
|
options.name,
|