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.
Files changed (108) hide show
  1. package/dist/cli/index.js +1179 -738
  2. package/dist/cli/index.js.map +52 -51
  3. package/dist/types/core/base-path.d.ts +38 -0
  4. package/dist/types/core/config-input.d.ts +74 -10
  5. package/dist/types/core/config.d.ts +3 -2
  6. package/dist/types/core/data.d.ts +2 -0
  7. package/dist/types/core/i18n-ui.d.ts +1 -3
  8. package/dist/types/core/schema.d.ts +95 -52
  9. package/dist/types/core/sources/types.d.ts +2 -0
  10. package/dist/types/core/types.d.ts +6 -1
  11. package/docs/02-deployment.mdx +16 -1
  12. package/docs/03-faq.mdx +8 -8
  13. package/docs/configuration/index.mdx +6 -0
  14. package/docs/content/components.mdx +29 -2
  15. package/docs/content/islands.mdx +8 -0
  16. package/docs/content/syntax.mdx +13 -0
  17. package/package.json +2 -1
  18. package/src/ai/agent-readability.ts +7 -2
  19. package/src/ai/ask.ts +12 -7
  20. package/src/ai/llms.ts +15 -4
  21. package/src/ai/mcp/data.ts +8 -4
  22. package/src/ai/mcp/server.ts +3 -0
  23. package/src/astro/component-slots.ts +5 -3
  24. package/src/astro/examples.ts +12 -7
  25. package/src/astro/generate.ts +317 -144
  26. package/src/astro/index.ts +5 -1
  27. package/src/astro/integration.ts +8 -4
  28. package/src/astro/islands.ts +11 -5
  29. package/src/astro/markdown-negotiation.ts +1 -1
  30. package/src/astro/pages.ts +8 -3
  31. package/src/astro/templates.ts +166 -19
  32. package/src/cli/commands/build.ts +32 -19
  33. package/src/cli/commands/dev.ts +48 -15
  34. package/src/cli/commands/doctor.ts +2 -2
  35. package/src/cli/commands/validate.ts +1 -0
  36. package/src/cli/dev-lock.ts +26 -15
  37. package/src/cli/required-secrets.ts +2 -1
  38. package/src/components/content/CodeBlock.astro +3 -0
  39. package/src/components/content/Component.astro +30 -16
  40. package/src/components/content/Diff.astro +3 -1
  41. package/src/components/content/auto-type-table.ts +18 -8
  42. package/src/components/content/diff.ts +12 -6
  43. package/src/components/content/mermaid-element.ts +3 -0
  44. package/src/components/index.ts +23 -1
  45. package/src/components/islands/ask-ai.tsx +12 -6
  46. package/src/components/islands/base-path.ts +28 -0
  47. package/src/components/islands/hooks.ts +16 -1
  48. package/src/components/layout/Banner.astro +2 -1
  49. package/src/components/layout/Breadcrumbs.astro +2 -1
  50. package/src/components/layout/Favicon.astro +3 -2
  51. package/src/components/layout/Header.astro +2 -1
  52. package/src/components/layout/LanguageSwitcher.astro +2 -1
  53. package/src/components/layout/Logo.astro +2 -1
  54. package/src/components/layout/NavSelector.astro +2 -1
  55. package/src/components/layout/NavTree.astro +5 -4
  56. package/src/components/layout/PageFeedback.astro +4 -1
  57. package/src/components/layout/PageLayout.astro +9 -4
  58. package/src/components/layout/Pagination.astro +3 -2
  59. package/src/components/layout/RootLayout.astro +7 -4
  60. package/src/components/layout/Search.astro +13 -5
  61. package/src/components/layout/nav-utils.ts +18 -10
  62. package/src/components/layout/search/pagefind.ts +3 -0
  63. package/src/components/layout/toc-element.ts +7 -1
  64. package/src/components/openapi/RequestPanel.astro +7 -1
  65. package/src/components/openapi/snippets.ts +25 -11
  66. package/src/core/base-path.ts +70 -0
  67. package/src/core/component-overrides.ts +103 -74
  68. package/src/core/config-input.ts +81 -15
  69. package/src/core/config.ts +5 -3
  70. package/src/core/content.ts +2 -0
  71. package/src/core/data.ts +2 -0
  72. package/src/core/diagnostics.ts +54 -34
  73. package/src/core/gitignore.ts +4 -1
  74. package/src/core/graph.ts +156 -88
  75. package/src/core/i18n-ui.ts +18 -3
  76. package/src/core/last-modified.ts +2 -0
  77. package/src/core/links.ts +38 -18
  78. package/src/core/manifest.ts +62 -45
  79. package/src/core/nav-diagnostics.ts +1 -1
  80. package/src/core/navigation.ts +116 -55
  81. package/src/core/project-graph.ts +10 -9
  82. package/src/core/schema.ts +572 -621
  83. package/src/core/sources/github-releases.ts +2 -1
  84. package/src/core/sources/mdx-remote.ts +58 -54
  85. package/src/core/sources/normalize.ts +116 -73
  86. package/src/core/sources/notion.ts +19 -10
  87. package/src/core/sources/types.ts +2 -0
  88. package/src/core/tsconfig-aliases.ts +59 -30
  89. package/src/core/types.ts +6 -1
  90. package/src/deploy/redirects.ts +18 -0
  91. package/src/deploy/robots.ts +6 -1
  92. package/src/deploy/rss.ts +10 -3
  93. package/src/deploy/sitemap.ts +14 -10
  94. package/src/markdown/base-links.ts +58 -0
  95. package/src/markdown/code-title.ts +11 -14
  96. package/src/markdown/index.ts +34 -9
  97. package/src/markdown/inline-code.ts +7 -2
  98. package/src/markdown/themes.ts +24 -0
  99. package/src/openapi/model.ts +3 -1
  100. package/src/openapi/references.ts +41 -17
  101. package/src/openapi/render-mdx.ts +11 -6
  102. package/src/openapi/scalar.ts +32 -16
  103. package/src/registry/eject.ts +64 -8
  104. package/src/search/build.ts +3 -0
  105. package/src/search/documents.ts +2 -2
  106. package/src/search/sync/typesense.ts +6 -4
  107. package/src/seo/jsonld.ts +16 -6
  108. package/src/theme/entry.ts +85 -20
@@ -24,6 +24,7 @@ import {
24
24
  envTemplate,
25
25
  exampleMapTemplate,
26
26
  exampleWrapperTemplate,
27
+ examplesPageTemplate,
27
28
  exampleSlug,
28
29
  islandMapTemplate,
29
30
  islandWrapperTemplate,
@@ -45,11 +46,14 @@ import { buildReferenceFiles } from "../openapi/scalar.ts";
45
46
  import { isOpenApiSource } from "../openapi/source.ts";
46
47
  import { buildSearchDocuments } from "../search/documents.ts";
47
48
  import { servesStaticIndex } from "../search/providers.ts";
48
- import { tailwindEntryTemplate } from "../theme/entry.ts";
49
+ import {
50
+ examplesEntryTemplate,
51
+ tailwindEntryTemplate,
52
+ } from "../theme/entry.ts";
49
53
  import { buildThemeCss } from "../theme/palette.ts";
50
54
  import { twoslashCss } from "../theme/twoslash.ts";
51
55
 
52
- const POSIX = (path: string): string => path.split("\\").join("/");
56
+ const toPosix = (path: string): string => path.split("\\").join("/");
53
57
 
54
58
  /** The `blume:openapi` payload for the ejected app (`{}` when none). */
55
59
  const ejectOpenApiData = (project: BlumeProject): unknown => {
@@ -86,6 +90,40 @@ const askFiles = async (
86
90
  return files;
87
91
  };
88
92
 
93
+ /** Contents of the configured `examples.css`, or `""` when unset/absent. */
94
+ const readExamplesCss = (
95
+ root: string,
96
+ css: string | undefined
97
+ ): Promise<string> =>
98
+ css && existsSync(join(root, css))
99
+ ? readFile(join(root, css), "utf-8")
100
+ : Promise.resolve("");
101
+
102
+ /**
103
+ * The per-example preview route `<Component />` iframes embed, nested under
104
+ * `basePath` so it stays reachable behind a proxy that only forwards the
105
+ * base. Empty when the project has no examples.
106
+ */
107
+ const examplesPreviewFiles = (
108
+ srcDir: string,
109
+ basePath: string,
110
+ hasExamples: boolean
111
+ ): { content: string; path: string }[] =>
112
+ hasExamples
113
+ ? [
114
+ {
115
+ content: examplesPageTemplate(),
116
+ path: join(
117
+ srcDir,
118
+ "pages",
119
+ ...basePath.split("/").filter(Boolean),
120
+ "blume-examples",
121
+ "[...path].astro"
122
+ ),
123
+ },
124
+ ]
125
+ : [];
126
+
89
127
  /**
90
128
  * Promote the generated runtime into the project as an owned Astro app. After
91
129
  * eject the project has a normal `astro.config.mjs` and `src/`, the `blume` CLI
@@ -108,6 +146,7 @@ export const eject = async (root: string): Promise<string[]> => {
108
146
  needsReactRaw,
109
147
  usesMath,
110
148
  userTheme,
149
+ userExamplesCss,
111
150
  rawMarkdown,
112
151
  islands,
113
152
  examples,
@@ -118,9 +157,10 @@ export const eject = async (root: string): Promise<string[]> => {
118
157
  context.themeFile
119
158
  ? readFile(context.themeFile, "utf-8")
120
159
  : Promise.resolve(""),
160
+ readExamplesCss(root, config.examples.css),
121
161
  buildRawMarkdown(project),
122
162
  discoverIslands(root),
123
- discoverExamples(root, config.examples),
163
+ discoverExamples(root, config.examples.source),
124
164
  ]);
125
165
  // Island/example frameworks drive which Astro renderers the ejected config
126
166
  // wires in; React also switches on for project `.tsx`/`.jsx` and Ask AI.
@@ -135,16 +175,16 @@ export const eject = async (root: string): Promise<string[]> => {
135
175
  // A project-relative context so generated files use portable paths.
136
176
  const relContext: ProjectContext = {
137
177
  ...context,
138
- contentRoot: POSIX(relative(root, context.contentRoot)),
178
+ contentRoot: toPosix(relative(root, context.contentRoot)),
139
179
  outDir: ".",
140
180
  root: ".",
141
181
  };
142
182
 
143
183
  const componentsImport = context.componentsFile
144
- ? `../../${POSIX(relative(root, context.componentsFile))}`
184
+ ? `../../${toPosix(relative(root, context.componentsFile))}`
145
185
  : null;
146
186
  const relPages = pages.map((page) => ({
147
- entrypoint: POSIX(relative(root, page.entrypoint)),
187
+ entrypoint: toPosix(relative(root, page.entrypoint)),
148
188
  pattern: page.pattern,
149
189
  }));
150
190
 
@@ -168,6 +208,7 @@ export const eject = async (root: string): Promise<string[]> => {
168
208
  context: relContext,
169
209
  dataPath: "./src/generated/data.json",
170
210
  examplesPath: "./src/generated/examples.ts",
211
+ examplesThemePath: "./src/generated/examples.css",
171
212
  needsReact,
172
213
  needsSvelte,
173
214
  needsVue,
@@ -218,9 +259,19 @@ export const eject = async (root: string): Promise<string[]> => {
218
259
  path: join(genDir, "islands.ts"),
219
260
  },
220
261
  {
221
- content: exampleMapTemplate(examples.examples),
262
+ content: exampleMapTemplate(examples.examples, config.basePath),
222
263
  path: join(genDir, "examples.ts"),
223
264
  },
265
+ {
266
+ // The isolated Tailwind entry for `<Component />` preview frames.
267
+ // Relative sources keep the ejected app portable.
268
+ content: examplesEntryTemplate({
269
+ configTokens: buildThemeCss(config.theme),
270
+ sources: ["../../**/*.{astro,jsx,svelte,ts,tsx,vue}"],
271
+ userCss: userExamplesCss,
272
+ }),
273
+ path: join(genDir, "examples.css"),
274
+ },
224
275
  {
225
276
  content: tailwindEntryTemplate({
226
277
  configTokens: buildThemeCss(config.theme),
@@ -345,7 +396,12 @@ export const eject = async (root: string): Promise<string[]> => {
345
396
  ...examples.examples.map((example) => ({
346
397
  content: exampleWrapperTemplate(example),
347
398
  path: join(genDir, "examples", `${exampleSlug(example.path)}.astro`),
348
- }))
399
+ })),
400
+ ...examplesPreviewFiles(
401
+ srcDir,
402
+ config.basePath,
403
+ examples.examples.length > 0
404
+ )
349
405
  );
350
406
 
351
407
  // Materialize staged source bodies under `<root>/blume-staged/<source>/<ref>`,
@@ -15,6 +15,9 @@ export const buildSearchIndex = async (outDir: string): Promise<number> => {
15
15
  throw new Error("Failed to create Pagefind index.");
16
16
  }
17
17
 
18
+ // These awaits are strictly ordered, not independent: the directory must be
19
+ // indexed before its files are written, and the index closed only after.
20
+ // oxlint-disable-next-line react-doctor/async-parallel
18
21
  const result = await index.addDirectory({ path: outDir });
19
22
  await index.writeFiles({ outputPath: join(outDir, "pagefind") });
20
23
  await pagefind.close();
@@ -65,9 +65,9 @@ const toPlainText = (markdown: string): string => {
65
65
  for (const match of withoutBlocks.matchAll(INLINE_CODE)) {
66
66
  const start = match.index ?? 0;
67
67
  pieces.push(
68
- withoutBlocks.slice(cursor, start).replaceAll(HTML_OR_JSX, " ")
68
+ withoutBlocks.slice(cursor, start).replaceAll(HTML_OR_JSX, " "),
69
+ match.groups?.code ?? ""
69
70
  );
70
- pieces.push(match.groups?.code ?? "");
71
71
  cursor = start + match[0].length;
72
72
  }
73
73
  pieces.push(withoutBlocks.slice(cursor).replaceAll(HTML_OR_JSX, " "));
@@ -40,10 +40,12 @@ export const syncTypesense = async (
40
40
  });
41
41
 
42
42
  const collection = client.collections(config.collection);
43
- const exists = await collection
44
- .retrieve()
45
- .then(() => true)
46
- .catch(() => false);
43
+ let exists = true;
44
+ try {
45
+ await collection.retrieve();
46
+ } catch {
47
+ exists = false;
48
+ }
47
49
  if (exists) {
48
50
  await collection.delete();
49
51
  }
package/src/seo/jsonld.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import type { Crumb } from "../components/layout/nav-utils.ts";
2
+ import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
3
+
4
+ /** A date-ish value carried through frontmatter (string, YAML Date, or unset). */
5
+ type DateInput = string | Date | null;
2
6
 
3
7
  /** Inputs for a page's JSON-LD, all known at render time in RootLayout. */
4
8
  export interface StructuredDataInput {
@@ -9,12 +13,14 @@ export interface StructuredDataInput {
9
13
  description?: string;
10
14
  /** Page route, e.g. `/blog/post`. */
11
15
  route: string;
16
+ /** Deployment base (`import.meta.env.BASE_URL`); prefixed onto absolute URLs. */
17
+ base?: string;
12
18
  /** Content type — `blog` and `changelog` map to richer article types. */
13
19
  pageType?: string;
14
20
  /** Publish date (string or YAML Date); emitted as ISO `datePublished`. */
15
- published?: string | Date | null;
21
+ published?: DateInput;
16
22
  /** Last-modified date; emitted as ISO `dateModified`. */
17
- modified?: string | Date | null;
23
+ modified?: DateInput;
18
24
  /** BCP-47 language tag for `inLanguage`; defaults to `en`. */
19
25
  locale?: string;
20
26
  breadcrumbs: Crumb[];
@@ -31,7 +37,7 @@ const trimSlash = (value: string): string => value.replace(/\/$/u, "");
31
37
  const absolute = (base: string | null, path: string): string =>
32
38
  base ? `${base}${path}` : path;
33
39
 
34
- const toIso = (value: string | Date | null | undefined): string | undefined => {
40
+ const toIso = (value: DateInput | undefined): string | undefined => {
35
41
  if (!value) {
36
42
  return;
37
43
  }
@@ -49,7 +55,11 @@ export const buildStructuredData = (
49
55
  input: StructuredDataInput
50
56
  ): Record<string, unknown> | null => {
51
57
  const base = input.siteUrl ? trimSlash(input.siteUrl) : null;
52
- const pageUrl = absolute(base, input.route);
58
+ // Routes carry `basePath`; a `deployment.base` subdirectory is layered on top
59
+ // so JSON-LD URLs match the served location.
60
+ const deployBase = normalizeBasePath(input.base);
61
+ const pageUrl = absolute(base, withBasePath(deployBase, input.route));
62
+ const rootUrl = absolute(base, deployBase);
53
63
  const graph: Record<string, unknown>[] = [];
54
64
 
55
65
  if (base) {
@@ -57,7 +67,7 @@ export const buildStructuredData = (
57
67
  "@id": `${base}#website`,
58
68
  "@type": "WebSite",
59
69
  name: input.siteName,
60
- url: base,
70
+ url: rootUrl,
61
71
  });
62
72
  }
63
73
 
@@ -98,7 +108,7 @@ export const buildStructuredData = (
98
108
  position: index + 1,
99
109
  };
100
110
  if (crumb.route) {
101
- item.item = absolute(base, crumb.route);
111
+ item.item = absolute(base, withBasePath(deployBase, crumb.route));
102
112
  }
103
113
  return item;
104
114
  }),
@@ -12,25 +12,16 @@ interface TailwindEntryOptions {
12
12
  twoslashCss?: string;
13
13
  }
14
14
 
15
+ /** Dark mode is driven by `data-theme` on the root element (both sheets). */
16
+ const DARK_VARIANT = `/* Dark mode is driven by data-theme on the <html> element. */
17
+ @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));`;
18
+
15
19
  /**
16
- * Build the single Tailwind v4 entry stylesheet for the generated runtime.
17
- *
18
- * Everything flows through one Tailwind-processed file so utilities are
19
- * generated and design tokens cascade deterministically:
20
- * base defaults -> config tokens -> user theme.css.
20
+ * The default `--blume-*` design tokens (light + dark), shared by the app
21
+ * sheet and the isolated example-preview sheet so previews inherit the site's
22
+ * palette by default.
21
23
  */
22
- export const tailwindEntryTemplate = (options: TailwindEntryOptions): string =>
23
- `/* Generated by Blume. Do not edit. */
24
- @import "tailwindcss";
25
- @plugin "@tailwindcss/typography";
26
-
27
- /* Scan Blume's components and the user's project for utility classes. */
28
- ${options.sources.map((source) => `@source "${source}";`).join("\n")}
29
-
30
- /* Dark mode is driven by data-theme on the <html> element. */
31
- @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
32
-
33
- :root {
24
+ const TOKEN_DEFAULTS = `:root {
34
25
  --blume-background: oklch(1 0 0);
35
26
  --blume-background-image: none;
36
27
  --blume-background-image-repeat: no-repeat;
@@ -108,9 +99,15 @@ ${options.sources.map((source) => `@source "${source}";`).join("\n")}
108
99
  --blume-code-remove-border: oklch(0.7 0.2 22 / 0.7);
109
100
  --blume-code-word: oklch(0.7 0.14 255 / 0.22);
110
101
  --blume-code-word-border: oklch(0.7 0.14 255 / 0.55);
111
- }
102
+ }`;
112
103
 
113
- @theme inline {
104
+ /**
105
+ * Tailwind theme mapping from `--blume-*` tokens to utility-facing names
106
+ * (`bg-background`, `border-border`, `rounded-blume`, the font stacks).
107
+ * Shared by both sheets so example code written against Blume's utility
108
+ * vocabulary renders identically inside the isolated preview frame.
109
+ */
110
+ const THEME_MAPPING = `@theme inline {
114
111
  --color-background: var(--blume-background);
115
112
  --color-foreground: var(--blume-foreground);
116
113
  --color-muted: var(--blume-muted);
@@ -125,7 +122,28 @@ ${options.sources.map((source) => `@source "${source}";`).join("\n")}
125
122
  --font-sans: var(--blume-font-body);
126
123
  --font-mono: var(--blume-font-mono);
127
124
  --font-display: var(--blume-font-display);
128
- }
125
+ }`;
126
+
127
+ /**
128
+ * Build the single Tailwind v4 entry stylesheet for the generated runtime.
129
+ *
130
+ * Everything flows through one Tailwind-processed file so utilities are
131
+ * generated and design tokens cascade deterministically:
132
+ * base defaults -> config tokens -> user theme.css.
133
+ */
134
+ export const tailwindEntryTemplate = (options: TailwindEntryOptions): string =>
135
+ `/* Generated by Blume. Do not edit. */
136
+ @import "tailwindcss";
137
+ @plugin "@tailwindcss/typography";
138
+
139
+ /* Scan Blume's components and the user's project for utility classes. */
140
+ ${options.sources.map((source) => `@source "${source}";`).join("\n")}
141
+
142
+ ${DARK_VARIANT}
143
+
144
+ ${TOKEN_DEFAULTS}
145
+
146
+ ${THEME_MAPPING}
129
147
 
130
148
  @layer base {
131
149
  /* Nothing refuses to shrink below its intrinsic content width. This global
@@ -670,3 +688,50 @@ ${options.twoslashCss ?? ""}
670
688
  ${options.configTokens}
671
689
  ${options.userTheme}
672
690
  `;
691
+
692
+ interface ExamplesEntryOptions {
693
+ /** Config-derived token overrides (`:root { --blume-accent: ... }`). */
694
+ configTokens: string;
695
+ /** Globs to scan for utility classes (example files and their imports). */
696
+ sources: string[];
697
+ /** Raw contents of the configured `examples.css`, if any. */
698
+ userCss: string;
699
+ }
700
+
701
+ /**
702
+ * Build the Tailwind entry for `<Component />` preview frames. Each example
703
+ * renders in its own iframe so none of the app sheet above — prose typography,
704
+ * component chrome, base overrides — can reach it. This sheet provides only
705
+ * what an example needs to look like it does in an app: Tailwind (preflight +
706
+ * utilities scanned from the example sources), the `--blume-*` tokens and
707
+ * their utility mapping (so `bg-background`-style classes keep working and
708
+ * previews follow the site palette by default), and the user's example CSS
709
+ * (shadcn variables, `@theme` mappings, custom styles) last so it wins.
710
+ */
711
+ export const examplesEntryTemplate = (options: ExamplesEntryOptions): string =>
712
+ `/* Generated by Blume. Do not edit. */
713
+ @import "tailwindcss";
714
+
715
+ /* Scan the example files and the project sources they import. */
716
+ ${options.sources.map((source) => `@source "${source}";`).join("\n")}
717
+
718
+ ${DARK_VARIANT}
719
+
720
+ ${TOKEN_DEFAULTS}
721
+
722
+ ${THEME_MAPPING}
723
+
724
+ /* Frame defaults: readable text in both modes, transparent so the docs pane's
725
+ surface shows through. Base layer, so any user/example CSS wins. */
726
+ @layer base {
727
+ body {
728
+ background: transparent;
729
+ color: var(--blume-foreground);
730
+ }
731
+ }
732
+
733
+ /* Token overrides: config first, then the configured examples css (highest
734
+ priority) — the place for shadcn variables and other component tokens. */
735
+ ${options.configTokens}
736
+ ${options.userCss}
737
+ `;