blume 1.5.2 → 1.6.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.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/cli/index.js +3639 -1377
  3. package/dist/cli/index.js.map +103 -91
  4. package/dist/types/ai/component-markdown.d.ts +79 -0
  5. package/dist/types/components/layout/nav-utils.d.ts +60 -0
  6. package/dist/types/core/base-path.d.ts +9 -0
  7. package/dist/types/core/config-input.d.ts +206 -4
  8. package/dist/types/core/config.d.ts +6 -4
  9. package/dist/types/core/data.d.ts +23 -1
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +8 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +117 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +23 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +21 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/_snippets/include-demo.mdx +7 -0
  21. package/docs/advanced/api-reference.mdx +3 -3
  22. package/docs/advanced/custom-pages.mdx +1 -1
  23. package/docs/advanced/graphql.mdx +84 -0
  24. package/docs/advanced/meta.ts +8 -1
  25. package/docs/configuration/ai.mdx +21 -3
  26. package/docs/configuration/index.mdx +24 -0
  27. package/docs/configuration/search.mdx +13 -1
  28. package/docs/configuration/seo.mdx +27 -0
  29. package/docs/configuration/theming.mdx +17 -0
  30. package/docs/content/components.mdx +7 -0
  31. package/docs/content/includes.mdx +68 -0
  32. package/docs/content/meta.ts +1 -0
  33. package/docs/content/navigation.mdx +25 -0
  34. package/docs/content/sources.mdx +42 -1
  35. package/docs/content/syntax.mdx +69 -1
  36. package/docs/content/versioning.mdx +15 -9
  37. package/docs/reference/cli.mdx +2 -1
  38. package/package.json +23 -14
  39. package/skills/blume-migrate/SKILL.md +16 -7
  40. package/skills/blume-migrate/references/docusaurus.md +5 -3
  41. package/skills/blume-migrate/references/fumadocs.md +10 -2
  42. package/skills/blume-migrate/references/mintlify.md +3 -2
  43. package/skills/blume-migrate/references/nextra.md +2 -2
  44. package/skills/blume-migrate/references/starlight.md +1 -1
  45. package/src/ai/agent-readability.ts +2 -1
  46. package/src/ai/ask-data.ts +2 -1
  47. package/src/ai/component-markdown.ts +199 -36
  48. package/src/ai/llms.ts +93 -6
  49. package/src/ai/markdown.ts +2 -2
  50. package/src/ai/mcp/discovery.ts +10 -2
  51. package/src/ai/mcp/server.ts +74 -2
  52. package/src/astro/generate.ts +183 -116
  53. package/src/astro/include-hmr.ts +81 -0
  54. package/src/astro/include-refresh.ts +0 -0
  55. package/src/astro/index.ts +3 -5
  56. package/src/astro/templates.ts +125 -76
  57. package/src/cli/commands/build.ts +84 -15
  58. package/src/cli/init/questions.ts +1 -0
  59. package/src/cli/init/scaffold.ts +27 -4
  60. package/src/components/colors.ts +142 -0
  61. package/src/components/content/Badge.astro +5 -12
  62. package/src/components/content/Callout.astro +19 -36
  63. package/src/components/content/Card.astro +15 -21
  64. package/src/components/content/Component.astro +10 -1
  65. package/src/components/content/GithubInfo.astro +28 -9
  66. package/src/components/content/Tabs.astro +27 -5
  67. package/src/components/content/github-info.ts +20 -5
  68. package/src/components/dropdown-dismiss.ts +122 -0
  69. package/src/components/layout/Fonts.astro +15 -8
  70. package/src/components/layout/Header.astro +44 -0
  71. package/src/components/layout/LanguageSwitcher.astro +9 -1
  72. package/src/components/layout/NavSelector.astro +12 -3
  73. package/src/components/layout/NavTree.astro +6 -18
  74. package/src/components/layout/PageActions.astro +29 -8
  75. package/src/components/layout/PageLayout.astro +10 -1
  76. package/src/components/layout/ReferenceLayout.astro +6 -1
  77. package/src/components/layout/RootLayout.astro +46 -15
  78. package/src/components/layout/Search.astro +36 -4
  79. package/src/components/layout/TableOfContents.astro +8 -2
  80. package/src/components/layout/head-scripts.ts +53 -1
  81. package/src/components/openapi/ApiOverview.astro +13 -3
  82. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  83. package/src/components/openapi/GraphqlChip.astro +33 -0
  84. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  85. package/src/components/openapi/GraphqlOperation.astro +186 -0
  86. package/src/components/openapi/GraphqlType.astro +154 -0
  87. package/src/components/openapi/MethodBadge.astro +3 -14
  88. package/src/components/openapi/Operation.astro +12 -5
  89. package/src/components/openapi/OperationPanel.astro +43 -0
  90. package/src/components/openapi/RequestPanel.astro +5 -10
  91. package/src/components/openapi/Responses.astro +1 -16
  92. package/src/components/openapi/graphql-helpers.ts +466 -0
  93. package/src/components/openapi/playground-client.ts +15 -0
  94. package/src/components/openapi/sample-panels.ts +45 -0
  95. package/src/components/openapi/snippets.ts +13 -35
  96. package/src/core/base-path.ts +11 -0
  97. package/src/core/config-input.ts +209 -2
  98. package/src/core/config.ts +6 -4
  99. package/src/core/content-assets.ts +15 -4
  100. package/src/core/data.ts +18 -2
  101. package/src/core/diagnostics.ts +8 -0
  102. package/src/core/frontmatter.ts +20 -8
  103. package/src/core/github.ts +71 -0
  104. package/src/core/graph.ts +22 -8
  105. package/src/core/heading-markers.ts +96 -0
  106. package/src/core/i18n-ui.ts +11 -0
  107. package/src/core/includes.ts +632 -0
  108. package/src/core/last-modified.ts +36 -11
  109. package/src/core/links.ts +79 -13
  110. package/src/core/meta.ts +2 -1
  111. package/src/core/nav-diagnostics.ts +11 -2
  112. package/src/core/navigation.ts +27 -6
  113. package/src/core/project-graph.ts +61 -9
  114. package/src/core/schema.ts +226 -35
  115. package/src/core/server-features.ts +5 -9
  116. package/src/core/sources/github-releases.ts +2 -2
  117. package/src/core/sources/normalize.ts +502 -115
  118. package/src/core/sources/notion.ts +43 -8
  119. package/src/core/sources/obsidian.ts +1038 -0
  120. package/src/core/sources/read.ts +36 -1
  121. package/src/core/sources/resolve.ts +34 -1
  122. package/src/core/sources/types.ts +28 -6
  123. package/src/core/sources/watch.ts +12 -8
  124. package/src/core/tsconfig-aliases.ts +48 -35
  125. package/src/core/types.ts +25 -2
  126. package/src/core/ui-packs/ar.ts +1 -0
  127. package/src/core/ui-packs/bg.ts +2 -0
  128. package/src/core/ui-packs/bn.ts +1 -0
  129. package/src/core/ui-packs/ca.ts +2 -0
  130. package/src/core/ui-packs/cs.ts +1 -0
  131. package/src/core/ui-packs/da.ts +1 -0
  132. package/src/core/ui-packs/de.ts +2 -0
  133. package/src/core/ui-packs/el.ts +2 -0
  134. package/src/core/ui-packs/es.ts +2 -0
  135. package/src/core/ui-packs/fa.ts +1 -0
  136. package/src/core/ui-packs/fi.ts +1 -0
  137. package/src/core/ui-packs/fr.ts +2 -0
  138. package/src/core/ui-packs/he.ts +1 -0
  139. package/src/core/ui-packs/hi.ts +1 -0
  140. package/src/core/ui-packs/hr.ts +2 -0
  141. package/src/core/ui-packs/hu.ts +2 -0
  142. package/src/core/ui-packs/id.ts +2 -0
  143. package/src/core/ui-packs/it.ts +1 -0
  144. package/src/core/ui-packs/ja.ts +2 -0
  145. package/src/core/ui-packs/ko.ts +2 -0
  146. package/src/core/ui-packs/nl.ts +2 -0
  147. package/src/core/ui-packs/no.ts +2 -0
  148. package/src/core/ui-packs/pl.ts +2 -0
  149. package/src/core/ui-packs/pt-br.ts +2 -0
  150. package/src/core/ui-packs/pt.ts +2 -0
  151. package/src/core/ui-packs/ro.ts +2 -0
  152. package/src/core/ui-packs/ru.ts +2 -0
  153. package/src/core/ui-packs/sk.ts +1 -0
  154. package/src/core/ui-packs/sr.ts +1 -0
  155. package/src/core/ui-packs/sv.ts +2 -0
  156. package/src/core/ui-packs/th.ts +1 -0
  157. package/src/core/ui-packs/tr.ts +2 -0
  158. package/src/core/ui-packs/uk.ts +2 -0
  159. package/src/core/ui-packs/vi.ts +1 -0
  160. package/src/core/ui-packs/zh-tw.ts +1 -0
  161. package/src/core/ui-packs/zh.ts +1 -0
  162. package/src/core/version-cut.ts +21 -3
  163. package/src/core/yaml.ts +26 -0
  164. package/src/deploy/function-bundle.ts +251 -0
  165. package/src/eval/schema.ts +3 -1
  166. package/src/markdown/code-title.ts +22 -16
  167. package/src/markdown/features.ts +21 -0
  168. package/src/markdown/fence-meta.ts +50 -0
  169. package/src/markdown/heading-anchors.ts +198 -37
  170. package/src/markdown/include.ts +247 -0
  171. package/src/markdown/index.ts +43 -34
  172. package/src/markdown/language-icon.ts +2 -2
  173. package/src/markdown/mdast.ts +7 -3
  174. package/src/markdown/ts2js.ts +264 -0
  175. package/src/openapi/asyncapi.ts +4 -1
  176. package/src/openapi/graphql-build.ts +293 -0
  177. package/src/openapi/graphql.ts +212 -0
  178. package/src/openapi/model.ts +38 -5
  179. package/src/openapi/parse.ts +34 -0
  180. package/src/openapi/proxy.ts +30 -5
  181. package/src/openapi/references.ts +89 -13
  182. package/src/openapi/render-mdx.ts +48 -8
  183. package/src/openapi/scalar.ts +5 -12
  184. package/src/openapi/source.ts +91 -23
  185. package/src/registry/eject.ts +11 -0
  186. package/src/search/documents.ts +229 -37
  187. package/src/search/orama-index.ts +9 -5
  188. package/src/seo/jsonld.ts +293 -51
  189. package/src/theme/code-block-padding.ts +16 -0
  190. package/src/theme/entry.ts +65 -11
  191. package/src/theme/fonts.ts +189 -16
  192. package/src/translate/prompts.ts +2 -0
  193. package/src/translate/run.ts +7 -0
  194. package/src/translate/work-list.ts +0 -0
@@ -1,5 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
 
3
+ import { expandIncludes, hasIncludeStatements } from "../includes.ts";
3
4
  import type { PageRecord } from "../types.ts";
4
5
  import type { ContentSource } from "./types.ts";
5
6
 
@@ -12,7 +13,7 @@ export interface EntryReadContext {
12
13
  /**
13
14
  * Read an entry's raw body text without assuming a filesystem path. Prefers the
14
15
  * body captured at scan time (staged sources), then the owning source's lazy
15
- * `read()`, then the back-compat `sourcePath` (filesystem). Returns `""` when an
16
+ * `read()`, then the back-compat `sourcePath` (any local file). Returns `""` when an
16
17
  * entry can't be resolved, so callers degrade gracefully.
17
18
  */
18
19
  export const readEntryText = async (
@@ -34,3 +35,37 @@ export const readEntryText = async (
34
35
  }
35
36
  return "";
36
37
  };
38
+
39
+ /**
40
+ * Read an entry's raw body with `<include>` statements expanded — what search
41
+ * indexing, the `.md`/`.mdx` mirrors, and llms-full.txt should see, matching
42
+ * the spliced content the rendered page shows. Filesystem entries only
43
+ * (includes resolve as filesystem paths); everything else passes through
44
+ * verbatim. Expansion errors were already surfaced as scan diagnostics, so
45
+ * unresolvable statements simply stay verbatim here.
46
+ */
47
+ export const readExpandedEntryText = async (
48
+ ctx: EntryReadContext,
49
+ page: PageRecord
50
+ ): Promise<string> => {
51
+ const raw = await readEntryText(ctx, page);
52
+ if (!(page.sourcePath && hasIncludeStatements(raw))) {
53
+ return raw;
54
+ }
55
+ const source = ctx.sources?.find(
56
+ (candidate) => candidate.name === page.source.name
57
+ );
58
+ // Mirror the scan's expansion gate exactly (`!staged && contentRoot`): a
59
+ // staged/custom source's entries were never expanded at scan time, so no
60
+ // headings/links/diagnostics exist for the spliced content — expanding only
61
+ // here would splice text the rest of the pipeline never validated, with no
62
+ // content-root bound on resolution.
63
+ if (!source || source.staged || !source.contentRoot) {
64
+ return raw;
65
+ }
66
+ const expansion = await expandIncludes(raw, {
67
+ contentRoot: source.contentRoot,
68
+ sourcePath: page.sourcePath,
69
+ });
70
+ return expansion.text;
71
+ };
@@ -8,6 +8,7 @@ import { filesystemSource } from "./filesystem.ts";
8
8
  import { githubReleasesSource } from "./github-releases.ts";
9
9
  import { mdxRemoteSource } from "./mdx-remote.ts";
10
10
  import { notionSource } from "./notion.ts";
11
+ import { obsidianSource } from "./obsidian.ts";
11
12
  import { sanitySource } from "./sanity.ts";
12
13
  import type { ContentSource, SourceContext } from "./types.ts";
13
14
 
@@ -47,9 +48,25 @@ const sourceContext = (
47
48
  refresh: runtime.refresh ?? runtime.mode === "build",
48
49
  });
49
50
 
51
+ /**
52
+ * The frontmatter keys each content type declares (`content.types[*].frontmatter`),
53
+ * keyed by type — so a source that drops unknown keys keeps, per note, exactly
54
+ * the ones `normalizeEntry` will accept for that note's type.
55
+ */
56
+ const typeFrontmatterKeys = (
57
+ config: ResolvedConfig
58
+ ): Record<string, string[]> =>
59
+ Object.fromEntries(
60
+ Object.entries(config.content.types).map(([type, def]) => [
61
+ type,
62
+ Object.keys(def.frontmatter),
63
+ ])
64
+ );
65
+
50
66
  const buildSource = (
51
67
  def: ContentSourceConfig,
52
68
  name: string,
69
+ config: ResolvedConfig,
53
70
  context: ProjectContext,
54
71
  runtime: SourceRuntime
55
72
  ): ContentSource => {
@@ -97,6 +114,22 @@ const buildSource = (
97
114
  sourceContext(context, name, runtime)
98
115
  );
99
116
  }
117
+ if (def.type === "obsidian") {
118
+ return obsidianSource(
119
+ {
120
+ defaultType: config.content.defaultType,
121
+ exclude: def.exclude,
122
+ frontmatterKeys: Object.keys(config.frontmatter.extend),
123
+ i18n: config.i18n,
124
+ name,
125
+ prefix: def.prefix,
126
+ typeFrontmatterKeys: typeFrontmatterKeys(config),
127
+ vault: def.vault,
128
+ versions: config.versions,
129
+ },
130
+ sourceContext(context, name, runtime)
131
+ );
132
+ }
100
133
  if (def.type === "github-releases") {
101
134
  return githubReleasesSource(
102
135
  {
@@ -198,7 +231,7 @@ const contentSources = (
198
231
 
199
232
  const nameFor = uniqueNamer();
200
233
  return defs.map((def) =>
201
- buildSource(def, nameFor(baseName(def)), context, runtime)
234
+ buildSource(def, nameFor(baseName(def)), config, context, runtime)
202
235
  );
203
236
  };
204
237
 
@@ -28,9 +28,15 @@ export interface SourceEntry {
28
28
  */
29
29
  raw?: string;
30
30
  /**
31
- * Absolute filesystem path when the entry originates from disk. Populated by
32
- * the filesystem adapter only; powers git last-modified and edit URLs and the
33
- * `sourcePath` back-compat window. Omitted by remote/CMS adapters.
31
+ * Absolute filesystem path when the entry originates from disk. Set by any
32
+ * local-file adapter — the filesystem source, and staged local sources such
33
+ * as Obsidian, whose bodies are rewritten but whose notes are still real
34
+ * files. It gives diagnostics a path the author recognizes and lets relative
35
+ * image checks resolve next to the file. Omitted by remote/CMS adapters.
36
+ *
37
+ * It does *not* imply the entry renders through the docs glob collection —
38
+ * read `source.staged` for that — and git last-modified additionally needs
39
+ * the owning source to expose a `contentRoot` to bound the log's pathspec.
34
40
  */
35
41
  sourcePath?: string;
36
42
  /** Optional provenance for "edit this page". */
@@ -39,6 +45,19 @@ export interface SourceEntry {
39
45
  lastModified?: string;
40
46
  /** Content hash for cache invalidation / HMR; adapter-computed when cheap. */
41
47
  hash?: string;
48
+ /**
49
+ * The body with `<include>` statements expanded, set by the scan's expansion
50
+ * pass (filesystem entries whose body contains a statement). `normalizeEntry`
51
+ * extracts headings/links/components from this text so a partial's content
52
+ * counts toward the including page, with `origins` mapping each expanded
53
+ * line back to the file and raw line it came from for diagnostics.
54
+ */
55
+ expanded?: {
56
+ text: string;
57
+ origins: { file: string; line: number }[];
58
+ /** Absolute paths of every file included, transitively. */
59
+ includes: string[];
60
+ };
42
61
  }
43
62
 
44
63
  /** The result of a single `ContentSource.load()` call. */
@@ -99,9 +118,12 @@ export interface ContentSource {
99
118
  /** Optional route prefix; the source's routes namespace under `/<prefix>/`. */
100
119
  readonly prefix?: string;
101
120
  /**
102
- * Resolved on-disk root, set by filesystem-backed sources only. Drives
103
- * folder-meta discovery (scan under this root) and the docs-collection base;
104
- * omitted by remote/CMS/staged sources that have no local tree.
121
+ * Resolved on-disk root, set by sources whose entries live on disk. For
122
+ * filesystem sources it drives folder-meta discovery (scan under this root)
123
+ * and the docs-collection base; for staged local sources (an Obsidian
124
+ * vault) it only bounds the git last-modified pathspec — folder-meta
125
+ * discovery is guarded on `staged`. Omitted by remote/CMS sources that have
126
+ * no local tree.
105
127
  */
106
128
  readonly contentRoot?: string;
107
129
  /** Pull every entry. Called once per scan. */
@@ -17,9 +17,8 @@ import type { WatchListener } from "node:fs";
17
17
  * render (e.g. `.blume/.astro/data-store.json`). Left unfiltered, each such write
18
18
  * re-triggers a rescan + runtime regeneration whose writes land back under
19
19
  * `.blume/` and fire the watcher again: a self-sustaining loop that stalls page
20
- * renders and floods the console (and, mid-render, corrupts Astro's dev module
21
- * graph so `astro:server-app.js` fails to load). `fs.watch` has no ignore
22
- * option, so we filter by the changed path in the callback.
20
+ * renders and floods the console. `fs.watch` has no ignore option, so we filter
21
+ * by the changed path in the callback.
23
22
  */
24
23
  export const BLUME_IGNORE_DIRS = [
25
24
  ".blume",
@@ -60,19 +59,24 @@ export const excludeDirSegments = (patterns: readonly string[]): string[] =>
60
59
 
61
60
  /**
62
61
  * Build a recursive-watch listener that fires `onChange` for content changes but
63
- * ignores events whose path crosses an ignored directory segment. A missing
64
- * `filename` — rare; the platform couldn't name the changed path — falls through
65
- * to `onChange` rather than silently dropping a real edit. Exported for testing.
62
+ * ignores events whose path crosses an ignored directory segment — one in
63
+ * `ignoreDirs`, or one `ignoreSegment` rejects, for a scan whose skip rule is
64
+ * a shape (every dot-name) rather than a list. A missing `filename` — rare;
65
+ * the platform couldn't name the changed path — falls through to `onChange`
66
+ * rather than silently dropping a real edit. Exported for testing.
66
67
  */
67
68
  export const ignoringWatchListener = (
68
69
  onChange: () => void,
69
- ignoreDirs: Iterable<string> = BLUME_WATCH_IGNORE_DIRS
70
+ ignoreDirs: Iterable<string> = BLUME_WATCH_IGNORE_DIRS,
71
+ ignoreSegment: (segment: string) => boolean = () => false
70
72
  ): WatchListener<string> => {
71
73
  const ignore = new Set(ignoreDirs);
72
74
  return (_event, filename) => {
73
75
  if (
74
76
  filename !== null &&
75
- filename.split(/[/\\]/u).some((segment) => ignore.has(segment))
77
+ filename
78
+ .split(/[/\\]/u)
79
+ .some((segment) => ignore.has(segment) || ignoreSegment(segment))
76
80
  ) {
77
81
  return;
78
82
  }
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
 
3
- import { parseTsconfig } from "get-tsconfig";
4
3
  import { dirname, join, resolve } from "pathe";
4
+ import ts from "typescript";
5
5
 
6
6
  /**
7
7
  * Read the project's TypeScript path aliases (`compilerOptions.paths`) and turn
@@ -14,21 +14,32 @@ import { dirname, join, resolve } from "pathe";
14
14
  * import would have to be rewritten to a relative path. Reading the aliases here
15
15
  * lets those components port over unchanged.
16
16
  *
17
- * Parsing is get-tsconfig's job — JSONC, the full `extends` chain (relative
18
- * paths, directories, package specifiers, TS 5.0 arrays), and the rebasing of
19
- * inherited relative paths all follow tsc's own semantics. Best-effort and
20
- * non-fatal: anything unparseable yields no aliases.
17
+ * Parsing is the TypeScript compiler's own job — JSONC, the full `extends`
18
+ * chain (relative paths, package specifiers, TS 5.0 arrays), `${configDir}`
19
+ * substitution, and where relative `paths` anchor all follow tsc exactly
20
+ * because tsc does the work. Best-effort and non-fatal: anything unreadable
21
+ * yields no aliases.
21
22
  */
22
23
 
23
- /** TS 5.5's config-relative template prefix, literal by design in tsconfig. */
24
- // oxlint-disable-next-line no-template-curly-in-string -- tsconfig's own syntax
25
- const CONFIG_DIR_TEMPLATE = "${configDir}";
24
+ /**
25
+ * tsc records the directory of the config that declared `paths` on the
26
+ * resolved options (that is what relative targets anchor to when there is no
27
+ * `baseUrl`), but leaves the field out of its public typings.
28
+ */
29
+ interface PathsBaseOptions {
30
+ pathsBasePath?: string;
31
+ }
26
32
 
27
- /** Substitute a leading `${configDir}` template with the config's directory. */
28
- const substituteConfigDir = (value: string, configDir: string): string =>
29
- value.startsWith(CONFIG_DIR_TEMPLATE)
30
- ? join(configDir, value.slice(CONFIG_DIR_TEMPLATE.length))
31
- : value;
33
+ /**
34
+ * Parse-only host: `include` globbing would walk the whole project for a file
35
+ * list nobody reads, so directories always enumerate as empty.
36
+ */
37
+ const PARSE_HOST: ts.ParseConfigHost = {
38
+ fileExists: ts.sys.fileExists,
39
+ readDirectory: () => [],
40
+ readFile: ts.sys.readFile,
41
+ useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
42
+ };
32
43
 
33
44
  /** Whether a `paths` fallback entry is a usable target (raw JSONC may lie). */
34
45
  const isPathTarget = (target: string | undefined): target is string =>
@@ -37,12 +48,11 @@ const isPathTarget = (target: string | undefined): target is string =>
37
48
  /** Convert one tsconfig `paths` mapping to a Vite alias, or null to skip. */
38
49
  const toAlias = (
39
50
  key: string,
40
- value: string | string[],
41
- baseDir: string,
42
- configDir: string
51
+ value: string[],
52
+ baseDir: string
43
53
  ): { find: string; replacement: string } | null => {
44
54
  // tsconfig allows a fallback array; Vite aliases are 1:1, so take the first.
45
- const first = Array.isArray(value) ? value[0] : value;
55
+ const [first] = value;
46
56
  if (!isPathTarget(first)) {
47
57
  return null;
48
58
  }
@@ -52,10 +62,7 @@ const toAlias = (
52
62
  if (find === "" || find === "*") {
53
63
  return null;
54
64
  }
55
- return {
56
- find,
57
- replacement: resolve(baseDir, substituteConfigDir(target, configDir)),
58
- };
65
+ return { find, replacement: resolve(baseDir, target) };
59
66
  };
60
67
 
61
68
  /**
@@ -72,27 +79,33 @@ export const resolveTsconfigAliases = (
72
79
  if (!entry) {
73
80
  return {};
74
81
  }
75
- let options: ReturnType<typeof parseTsconfig>["compilerOptions"];
76
- try {
77
- options = parseTsconfig(entry).compilerOptions;
78
- } catch {
79
- // Unparseable config or unresolvable extends: no aliases, as before.
80
- return {};
81
- }
82
- const paths = options?.paths;
83
- if (!paths) {
82
+ const { config, error } = ts.readConfigFile(entry, ts.sys.readFile);
83
+ if (error) {
84
+ // Unreadable or malformed JSONC: no aliases, as before.
84
85
  return {};
85
86
  }
86
87
  const configDir = dirname(entry);
87
- // get-tsconfig rebases inherited relative values onto the entry config, so
88
- // `baseUrl` (and bare `paths` entries) anchor here after substitution.
89
- const baseDir = resolve(
88
+ // Diagnostics (an unresolvable `extends`, no input files) are tsc's to
89
+ // report; like tsc, keep whatever options still resolved.
90
+ const { options } = ts.parseJsonConfigFileContent(
91
+ config,
92
+ PARSE_HOST,
90
93
  configDir,
91
- substituteConfigDir(options?.baseUrl ?? ".", configDir)
94
+ undefined,
95
+ entry
92
96
  );
97
+ const { paths } = options;
98
+ if (!paths) {
99
+ return {};
100
+ }
101
+ // SAFETY: the assertion only exposes `pathsBasePath`, which tsc populates on
102
+ // the resolved options but omits from `CompilerOptions`.
103
+ const { pathsBasePath } = options as PathsBaseOptions;
104
+ // tsc already made `baseUrl` absolute and expanded `${configDir}`.
105
+ const baseDir = options.baseUrl ?? pathsBasePath ?? configDir;
93
106
  const entries: [string, string][] = [];
94
107
  for (const [key, value] of Object.entries(paths)) {
95
- const alias = toAlias(key, value, baseDir, configDir);
108
+ const alias = toAlias(key, value, baseDir);
96
109
  if (alias) {
97
110
  entries.push([alias.find, alias.replacement]);
98
111
  }
package/src/core/types.ts CHANGED
@@ -66,6 +66,10 @@ export interface PageLink {
66
66
  line: number;
67
67
  /** 1-based column of the target within the line. */
68
68
  column: number;
69
+ /** Absolute path of the file the link was written in, when that isn't the
70
+ * page's own source — a link inside an included partial. Diagnostics point
71
+ * here so authors fix the partial, not the page that spliced it. */
72
+ file?: string;
69
73
  }
70
74
 
71
75
  /**
@@ -105,7 +109,7 @@ export interface PageRecord {
105
109
  id: string;
106
110
  /** Provenance: the owning source's name and its source-local ref. */
107
111
  source: { name: string; ref: string };
108
- /** Absolute source path. Populated by the filesystem adapter only (back-compat). */
112
+ /** Absolute source path. Populated by any local-file adapter (back-compat). */
109
113
  sourcePath?: string;
110
114
  /**
111
115
  * Renderable body captured at scan time. Set for staged (non-filesystem)
@@ -173,6 +177,12 @@ export interface PageRecord {
173
177
  // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- values are outputs of arbitrary user-supplied zod schemas (transforms included), so no narrower type exists
174
178
  custom?: Record<string, unknown>;
175
179
  headings: Heading[];
180
+ /**
181
+ * Raw HTML element ids (`<a id="…">`) outside headings, deduplicated —
182
+ * fragment-link targets `blume validate` accepts alongside heading slugs.
183
+ */
184
+ anchors: string[];
185
+
176
186
  /** Whether the file is `.md`/`.mdx`. */
177
187
  format: "md" | "mdx";
178
188
  /** Internal/asset links discovered in the page (for validation). */
@@ -181,6 +191,9 @@ export interface PageRecord {
181
191
  componentsUsed?: string[];
182
192
  /** Resolved "last updated" ISO date, when the feature is enabled. */
183
193
  lastModified?: string;
194
+ /** Absolute paths of files this page `<include>`s, transitively. Drives the
195
+ * dev-server invalidation edge from a partial to the pages that splice it. */
196
+ includes?: string[];
184
197
  }
185
198
 
186
199
  /** A node in the generated navigation tree. */
@@ -266,6 +279,12 @@ export interface FeaturedLink {
266
279
  icon?: string;
267
280
  }
268
281
 
282
+ /** A plain header link, or the header's single call to action. */
283
+ export interface HeaderAction {
284
+ href: string;
285
+ label: string;
286
+ }
287
+
269
288
  /** The complete navigation model derived from the content graph. */
270
289
  export interface Navigation {
271
290
  tabs: NavTab[];
@@ -281,6 +300,10 @@ export interface Navigation {
281
300
  * graphs; treat as `/`.
282
301
  */
283
302
  root?: string;
303
+ /** Plain links in the header, left of the icon buttons. */
304
+ actions?: HeaderAction[];
305
+ /** The single primary call to action in the header. */
306
+ cta?: HeaderAction | null;
284
307
  /** Pinned links shown above the sidebar sections, unscoped by tab. */
285
308
  featured: FeaturedLink[];
286
309
  /** Repo URL for the header link, or null when hidden (`navigation.repo`). */
@@ -340,7 +363,7 @@ export interface RouteManifestEntry {
340
363
  /** Astro collection-relative entry id, passed to `getEntry`. */
341
364
  entryId: string;
342
365
  path: string;
343
- /** Absolute source path; populated for filesystem entries only (back-compat). */
366
+ /** Absolute source path; populated for local-file entries only (back-compat). */
344
367
  sourcePath?: string;
345
368
  /** Adapter-supplied "edit this page" URL (non-filesystem sources). */
346
369
  editUrl?: string;
@@ -20,6 +20,7 @@ export const ar: UIStringsOverride = {
20
20
  generating: "جارٍ الإنشاء…",
21
21
  openIn: "فتح في {name}",
22
22
  openInChat: "فتح في المحادثة",
23
+ openInChatPrompt: "اقرأ {url} حتى أتمكن من طرح أسئلة حول هذه الصفحة.",
23
24
  scrollToTop: "العودة إلى الأعلى",
24
25
  },
25
26
  ask: {
@@ -20,6 +20,8 @@ export const bg: UIStringsOverride = {
20
20
  generating: "Генериране…",
21
21
  openIn: "Отвори в {name}",
22
22
  openInChat: "Отвори в чата",
23
+ openInChatPrompt:
24
+ "Прочетете {url}, за да мога да задавам въпроси за тази страница.",
23
25
  scrollToTop: "Нагоре",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,7 @@ export const bn: UIStringsOverride = {
20
20
  generating: "তৈরি হচ্ছে…",
21
21
  openIn: "{name}-এ খুলুন",
22
22
  openInChat: "চ্যাটে খুলুন",
23
+ openInChatPrompt: "{url} পড়ুন, যাতে আমি এই পৃষ্ঠা সম্পর্কে প্রশ্ন করতে পারি।",
23
24
  scrollToTop: "উপরে যান",
24
25
  },
25
26
  ask: {
@@ -20,6 +20,8 @@ export const ca: UIStringsOverride = {
20
20
  generating: "S'està generant…",
21
21
  openIn: "Obre a {name}",
22
22
  openInChat: "Obre al xat",
23
+ openInChatPrompt:
24
+ "Llegeix {url} perquè pugui fer-te preguntes sobre aquesta pàgina.",
23
25
  scrollToTop: "Torna a dalt",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,7 @@ export const cs: UIStringsOverride = {
20
20
  generating: "Generování…",
21
21
  openIn: "Otevřít v {name}",
22
22
  openInChat: "Otevřít v chatu",
23
+ openInChatPrompt: "Přečtěte si {url}, ať se mohu ptát na tuto stránku.",
23
24
  scrollToTop: "Zpět nahoru",
24
25
  },
25
26
  ask: {
@@ -20,6 +20,7 @@ export const da: UIStringsOverride = {
20
20
  generating: "Genererer…",
21
21
  openIn: "Åbn i {name}",
22
22
  openInChat: "Åbn i chat",
23
+ openInChatPrompt: "Læs {url}, så jeg kan stille spørgsmål om denne side.",
23
24
  scrollToTop: "Tilbage til toppen",
24
25
  },
25
26
  ask: {
@@ -20,6 +20,8 @@ export const de: UIStringsOverride = {
20
20
  generating: "Wird generiert…",
21
21
  openIn: "In {name} öffnen",
22
22
  openInChat: "Im Chat öffnen",
23
+ openInChatPrompt:
24
+ "Lies {url}, damit ich dir Fragen zu dieser Seite stellen kann.",
23
25
  scrollToTop: "Nach oben scrollen",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,8 @@ export const el: UIStringsOverride = {
20
20
  generating: "Δημιουργία…",
21
21
  openIn: "Άνοιγμα στο {name}",
22
22
  openInChat: "Άνοιγμα στη συνομιλία",
23
+ openInChatPrompt:
24
+ "Διαβάστε το {url} για να μπορώ να κάνω ερωτήσεις για αυτή τη σελίδα.",
23
25
  scrollToTop: "Επιστροφή στην κορυφή",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,8 @@ export const es: UIStringsOverride = {
20
20
  generating: "Generando…",
21
21
  openIn: "Abrir en {name}",
22
22
  openInChat: "Abrir en el chat",
23
+ openInChatPrompt:
24
+ "Lee {url} para que pueda hacerte preguntas sobre esta página.",
23
25
  scrollToTop: "Volver arriba",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,7 @@ export const fa: UIStringsOverride = {
20
20
  generating: "در حال تولید…",
21
21
  openIn: "باز کردن در {name}",
22
22
  openInChat: "باز کردن در گفتگو",
23
+ openInChatPrompt: "{url} را بخوانید تا بتوانم درباره این صفحه سؤال بپرسم.",
23
24
  scrollToTop: "بازگشت به بالا",
24
25
  },
25
26
  ask: {
@@ -20,6 +20,7 @@ export const fi: UIStringsOverride = {
20
20
  generating: "Luodaan…",
21
21
  openIn: "Avaa sovelluksessa {name}",
22
22
  openInChat: "Avaa keskustelussa",
23
+ openInChatPrompt: "Lue {url}, jotta voin kysyä kysymyksiä tästä sivusta.",
23
24
  scrollToTop: "Takaisin ylös",
24
25
  },
25
26
  ask: {
@@ -20,6 +20,8 @@ export const fr: UIStringsOverride = {
20
20
  generating: "Génération…",
21
21
  openIn: "Ouvrir dans {name}",
22
22
  openInChat: "Ouvrir dans le chat",
23
+ openInChatPrompt:
24
+ "Lisez {url} pour que je puisse vous poser des questions sur cette page.",
23
25
  scrollToTop: "Revenir en haut",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,7 @@ export const he: UIStringsOverride = {
20
20
  generating: "מייצר…",
21
21
  openIn: "פתח ב-{name}",
22
22
  openInChat: "פתח בצ'אט",
23
+ openInChatPrompt: "קרא את {url} כדי שאוכל לשאול שאלות על העמוד הזה.",
23
24
  scrollToTop: "חזרה למעלה",
24
25
  },
25
26
  ask: {
@@ -20,6 +20,7 @@ export const hi: UIStringsOverride = {
20
20
  generating: "जनरेट हो रहा है…",
21
21
  openIn: "{name} में खोलें",
22
22
  openInChat: "चैट में खोलें",
23
+ openInChatPrompt: "{url} पढ़ें ताकि मैं इस पेज के बारे में प्रश्न पूछ सकूँ।",
23
24
  scrollToTop: "ऊपर जाएँ",
24
25
  },
25
26
  ask: {
@@ -20,6 +20,8 @@ export const hr: UIStringsOverride = {
20
20
  generating: "Generiranje…",
21
21
  openIn: "Otvori u {name}",
22
22
  openInChat: "Otvori u chatu",
23
+ openInChatPrompt:
24
+ "Pročitajte {url} — postavljat ću pitanja o ovoj stranici.",
23
25
  scrollToTop: "Natrag na vrh",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,8 @@ export const hu: UIStringsOverride = {
20
20
  generating: "Generálás…",
21
21
  openIn: "Megnyitás itt: {name}",
22
22
  openInChat: "Megnyitás a csevegésben",
23
+ openInChatPrompt:
24
+ "Olvasd el a következőt: {url}, hogy kérdéseket tehessek fel erről az oldalról.",
23
25
  scrollToTop: "Vissza a tetejére",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,8 @@ export const id: UIStringsOverride = {
20
20
  generating: "Menghasilkan…",
21
21
  openIn: "Buka di {name}",
22
22
  openInChat: "Buka di obrolan",
23
+ openInChatPrompt:
24
+ "Baca {url} agar saya bisa mengajukan pertanyaan tentang halaman ini.",
23
25
  scrollToTop: "Kembali ke atas",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,7 @@ export const it: UIStringsOverride = {
20
20
  generating: "Generazione in corso…",
21
21
  openIn: "Apri in {name}",
22
22
  openInChat: "Apri nella chat",
23
+ openInChatPrompt: "Leggi {url} così posso farti domande su questa pagina.",
23
24
  scrollToTop: "Torna su",
24
25
  },
25
26
  ask: {
@@ -20,6 +20,8 @@ export const ja: UIStringsOverride = {
20
20
  generating: "生成中…",
21
21
  openIn: "{name} で開く",
22
22
  openInChat: "チャットで開く",
23
+ openInChatPrompt:
24
+ "{url} を読んで、このページについての質問に答えられるようにしてください。",
23
25
  scrollToTop: "トップに戻る",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,8 @@ export const ko: UIStringsOverride = {
20
20
  generating: "생성 중…",
21
21
  openIn: "{name}에서 열기",
22
22
  openInChat: "채팅에서 열기",
23
+ openInChatPrompt:
24
+ "{url} 페이지를 읽어 주세요. 이 페이지에 대해 질문하겠습니다.",
23
25
  scrollToTop: "맨 위로",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,8 @@ export const nl: UIStringsOverride = {
20
20
  generating: "Genereren…",
21
21
  openIn: "Openen in {name}",
22
22
  openInChat: "Openen in chat",
23
+ openInChatPrompt:
24
+ "Lees {url}, zodat ik je vragen kan stellen over deze pagina.",
23
25
  scrollToTop: "Naar boven scrollen",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,8 @@ export const no: UIStringsOverride = {
20
20
  generating: "Genererer…",
21
21
  openIn: "Åpne i {name}",
22
22
  openInChat: "Åpne i chat",
23
+ openInChatPrompt:
24
+ "Les {url}, slik at jeg kan stille spørsmål om denne siden.",
23
25
  scrollToTop: "Til toppen",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,8 @@ export const pl: UIStringsOverride = {
20
20
  generating: "Generowanie…",
21
21
  openIn: "Otwórz w {name}",
22
22
  openInChat: "Otwórz w czacie",
23
+ openInChatPrompt:
24
+ "Przeczytaj {url}, aby móc odpowiadać na pytania dotyczące tej strony.",
23
25
  scrollToTop: "Wróć na górę",
24
26
  },
25
27
  ask: {
@@ -20,6 +20,8 @@ export const ptBR: UIStringsOverride = {
20
20
  generating: "Gerando…",
21
21
  openIn: "Abrir em {name}",
22
22
  openInChat: "Abrir no chat",
23
+ openInChatPrompt:
24
+ "Leia {url} para que eu possa fazer perguntas sobre esta página.",
23
25
  scrollToTop: "Voltar ao topo",
24
26
  },
25
27
  ask: {