blume 0.4.0 → 0.5.1

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 (81) hide show
  1. package/dist/cli/index.js +1170 -820
  2. package/dist/cli/index.js.map +32 -27
  3. package/dist/types/core/data.d.ts +2 -0
  4. package/dist/types/core/project.d.ts +12 -2
  5. package/dist/types/core/schema.d.ts +154 -41
  6. package/dist/types/core/types.d.ts +7 -6
  7. package/dist/types/migrate/mintlify/config.d.ts +14 -0
  8. package/docs/01-quickstart.mdx +6 -2
  9. package/docs/02-deployment.mdx +3 -1
  10. package/docs/advanced/api-reference.mdx +37 -23
  11. package/docs/advanced/bridge.mdx +76 -0
  12. package/docs/advanced/custom-pages.mdx +3 -1
  13. package/docs/advanced/meta.ts +8 -1
  14. package/docs/advanced/migrate.mdx +123 -0
  15. package/docs/configuration/ai.mdx +3 -1
  16. package/docs/configuration/analytics.mdx +3 -1
  17. package/docs/configuration/export.mdx +6 -2
  18. package/docs/configuration/index.mdx +1 -1
  19. package/docs/configuration/seo.mdx +3 -1
  20. package/docs/content/components.mdx +55 -2
  21. package/docs/content/i18n.mdx +6 -2
  22. package/docs/content/islands.mdx +6 -2
  23. package/docs/content/meta.mdx +3 -1
  24. package/docs/content/syntax.mdx +40 -14
  25. package/docs/index.mdx +2 -2
  26. package/docs/reference/cli.mdx +29 -1
  27. package/docs/reference/frontmatter.mdx +5 -0
  28. package/package.json +11 -1
  29. package/src/astro/generate.ts +18 -9
  30. package/src/astro/templates.ts +28 -4
  31. package/src/cli/commands/build.ts +107 -63
  32. package/src/cli/commands/check.ts +20 -0
  33. package/src/cli/dev-lock.ts +13 -5
  34. package/src/cli/prepare.ts +3 -0
  35. package/src/components/BlumePage.astro +6 -0
  36. package/src/components/Icon.astro +13 -10
  37. package/src/components/content/ApiField.astro +75 -0
  38. package/src/components/content/ParamField.astro +39 -0
  39. package/src/components/content/RequestField.astro +23 -0
  40. package/src/components/content/ResponseField.astro +23 -0
  41. package/src/components/content/Step.astro +1 -1
  42. package/src/components/layout/Breadcrumbs.astro +7 -2
  43. package/src/components/layout/NavTree.astro +24 -8
  44. package/src/components/layout/RootLayout.astro +56 -34
  45. package/src/components/layout/Search.astro +1 -1
  46. package/src/components/openapi/ApiOverview.astro +84 -0
  47. package/src/components/openapi/MethodBadge.astro +28 -0
  48. package/src/components/openapi/Operation.astro +140 -0
  49. package/src/components/openapi/ParametersTable.astro +97 -0
  50. package/src/components/openapi/RequestBody.astro +58 -0
  51. package/src/components/openapi/RequestPanel.astro +169 -0
  52. package/src/components/openapi/Responses.astro +91 -0
  53. package/src/components/openapi/SchemaProperty.astro +118 -0
  54. package/src/components/openapi/SchemaTable.astro +86 -0
  55. package/src/components/openapi/helpers.ts +238 -0
  56. package/src/components/openapi/panel.ts +59 -0
  57. package/src/components/openapi/snippets.ts +201 -0
  58. package/src/core/builtin-tags.ts +5 -0
  59. package/src/core/data.ts +2 -0
  60. package/src/core/graph.ts +0 -3
  61. package/src/core/nav-diagnostics.ts +2 -12
  62. package/src/core/navigation.ts +0 -10
  63. package/src/core/project-graph.ts +5 -1
  64. package/src/core/project.ts +25 -3
  65. package/src/core/schema.ts +47 -14
  66. package/src/core/sources/mintlify.ts +1 -1
  67. package/src/core/sources/resolve.ts +28 -6
  68. package/src/core/types.ts +7 -7
  69. package/src/migrate/mintlify/config.ts +190 -97
  70. package/src/migrate/mintlify/content.ts +24 -2
  71. package/src/migrate/mintlify/index.ts +76 -2
  72. package/src/migrate/mintlify/transform.ts +2 -0
  73. package/src/openapi/model.ts +174 -0
  74. package/src/openapi/parse.ts +48 -0
  75. package/src/openapi/references.ts +164 -0
  76. package/src/openapi/render-mdx.ts +76 -0
  77. package/src/openapi/scalar.ts +15 -103
  78. package/src/openapi/source.ts +140 -0
  79. package/src/registry/eject.ts +15 -2
  80. package/src/theme/chrome-icons.ts +22 -0
  81. package/src/theme/icons.ts +151 -161
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -50,6 +50,7 @@
50
50
  "./components": "./src/components/index.ts",
51
51
  "./components/*": "./src/components/*",
52
52
  "./core/*": "./src/core/*",
53
+ "./openapi/*": "./src/openapi/*",
53
54
  "./sources/*": "./src/core/sources/*",
54
55
  "./theme/*": "./src/theme/*",
55
56
  "./package.json": "./package.json"
@@ -69,10 +70,19 @@
69
70
  "@astrojs/node": "^11.0.0",
70
71
  "@astrojs/react": "^6.0.0",
71
72
  "@astrojs/vercel": "^11.0.0",
73
+ "@iconify-json/fa6-brands": "^1.2.6",
74
+ "@iconify-json/fa6-regular": "^1.2.4",
75
+ "@iconify-json/fa6-solid": "^1.2.4",
76
+ "@iconify-json/lucide": "^1.2.115",
77
+ "@iconify-json/tabler": "^1.2.35",
78
+ "@iconify/types": "^2.0.0",
79
+ "@iconify/utils": "^3.1.3",
72
80
  "@modelcontextprotocol/sdk": "^1.29.0",
73
81
  "@orama/orama": "^3.1.18",
74
82
  "@pierre/diffs": "^1.2.11",
75
83
  "@scalar/astro": "^0.4.5",
84
+ "@scalar/openapi-parser": "^0.28.8",
85
+ "@scalar/openapi-types": "^0.9.1",
76
86
  "@shikijs/transformers": "^4.2.0",
77
87
  "@shikijs/twoslash": "^4.2.0",
78
88
  "@tailwindcss/typography": "^0.5.20",
@@ -36,11 +36,9 @@ import type { ResolvedConfig } from "../core/schema.ts";
36
36
  import { resolveTsconfigAliases } from "../core/tsconfig-aliases.ts";
37
37
  import type { Navigation } from "../core/types.ts";
38
38
  import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
39
- import {
40
- buildReferenceFiles,
41
- hasReferences,
42
- referenceTabs,
43
- } from "../openapi/scalar.ts";
39
+ import { hasScalarReferences, referenceTabs } from "../openapi/references.ts";
40
+ import { buildReferenceFiles } from "../openapi/scalar.ts";
41
+ import { isOpenApiSource } from "../openapi/source.ts";
44
42
  import { registry } from "../registry/registry.ts";
45
43
  import { buildSearchDocuments } from "../search/documents.ts";
46
44
  import { searchProviderMeta, servesStaticIndex } from "../search/providers.ts";
@@ -582,8 +580,9 @@ export const buildRuntimeData = (project: BlumeProject): string => {
582
580
 
583
581
  const { i18n } = config;
584
582
 
585
- // API reference routes (Scalar) surface as header tabs alongside the
586
- // content-derived ones, so the reference stays discoverable in every locale.
583
+ // API reference routes surface as header tabs alongside the content-derived
584
+ // ones (Blume-rendered references also own a tab-scoped sidebar of operations),
585
+ // so the reference stays discoverable in every locale.
587
586
  const withReferenceTabs = (nav: Navigation): Navigation => ({
588
587
  ...nav,
589
588
  repoUrl: config.navigation.repo && repoUrl ? repoUrl : null,
@@ -619,7 +618,6 @@ export const buildRuntimeData = (project: BlumeProject): string => {
619
618
  chromeVariants: [],
620
619
  selectors: [],
621
620
  sidebar: [],
622
- sidebarVariants: [],
623
621
  tabs: [],
624
622
  }
625
623
  ),
@@ -650,6 +648,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
650
648
  })),
651
649
  }
652
650
  : null,
651
+ icons: config.icons,
653
652
  imageZoom: config.markdown.imageZoom,
654
653
  logo: resolveLogo(project),
655
654
  mcp: config.mcp.enabled
@@ -912,6 +911,7 @@ export const generateRuntime = async (
912
911
  const themePath = join(srcDir, "generated", "app.css");
913
912
  const searchClientPath = join(srcDir, "generated", "search-client.ts");
914
913
  const examplesPath = join(srcDir, "generated", "examples.ts");
914
+ const openapiPath = join(srcDir, "generated", "openapi.json");
915
915
 
916
916
  // Record every file this pass writes so orphans (from a now-disabled feature)
917
917
  // can be pruned afterwards. `write` wraps the atomic writer and tracks paths.
@@ -987,6 +987,7 @@ export const generateRuntime = async (
987
987
  needsReact,
988
988
  needsSvelte,
989
989
  needsVue,
990
+ openapiPath,
990
991
  pages,
991
992
  searchClientPath,
992
993
  themePath,
@@ -1235,7 +1236,7 @@ export const generateRuntime = async (
1235
1236
  // React ships with Blume; Vue/Svelte islands need their Astro integration
1236
1237
  // installed by the project. Warn early rather than let Vite fail to resolve it.
1237
1238
  warnings.push(...islandFrameworkWarnings(frameworks, context.root));
1238
- if (hasReferences(config)) {
1239
+ if (hasScalarReferences(config)) {
1239
1240
  const references = await buildReferenceFiles({
1240
1241
  config,
1241
1242
  contentRoutes: new Set(project.graph.pages.map((page) => page.route)),
@@ -1254,6 +1255,14 @@ export const generateRuntime = async (
1254
1255
  join(srcDir, "generated", "data.json"),
1255
1256
  buildRuntimeData(project)
1256
1257
  );
1258
+ // The parsed OpenAPI specs behind the `blume:openapi` alias. Always written
1259
+ // (even as `{}`) so the alias resolves whether or not a reference is enabled;
1260
+ // the source parsed the specs during the scan, so this is just serialization.
1261
+ const openApiSource = project.sources.find(isOpenApiSource);
1262
+ await write(
1263
+ openapiPath,
1264
+ `${JSON.stringify(openApiSource ? openApiSource.openApiData() : {})}\n`
1265
+ );
1257
1266
  await write(
1258
1267
  join(out, "blume.manifest.json"),
1259
1268
  `${JSON.stringify(project.manifest, null, 2)}\n`
@@ -7,6 +7,7 @@ import type { AskBackend } from "../ai/ask.ts";
7
7
  import { resolveAssetMounts } from "../core/assets.ts";
8
8
  import type { ResolvedConfig } from "../core/schema.ts";
9
9
  import type { ProjectContext } from "../core/types.ts";
10
+ import { hasScalarReferences } from "../openapi/references.ts";
10
11
  import { searchProviderMeta } from "../search/providers.ts";
11
12
  import { buildFontEntries } from "../theme/fonts.ts";
12
13
  import type { ExampleSpec } from "./examples.ts";
@@ -99,9 +100,10 @@ export const runtimeDependencies = (options: {
99
100
  if (needsSvelte) {
100
101
  deps.push("@astrojs/svelte");
101
102
  }
102
- // The Scalar integration is only declared when an API reference is configured,
103
- // so projects that don't use it never pull it into the runtime.
104
- if (config.openapi.enabled || config.asyncapi.enabled) {
103
+ // The Scalar integration is only declared for a Scalar-rendered reference
104
+ // (the `renderer: "scalar"` fallback, or AsyncAPI). Blume-rendered OpenAPI
105
+ // parses at generate time and needs no runtime Scalar dependency.
106
+ if (hasScalarReferences(config)) {
105
107
  deps.push("@scalar/astro");
106
108
  }
107
109
  // Only the configured search provider's SDK is declared, so a project pulls in
@@ -178,6 +180,10 @@ const renderUserAliases = (
178
180
  )
179
181
  .join("");
180
182
 
183
+ /** Astro's build output dir: the runtime's own `distDir`, else `<root>/dist`. */
184
+ const astroOutDir = (context: ProjectContext): string =>
185
+ context.distDir ?? `${context.root}/dist`;
186
+
181
187
  export const astroConfigTemplate = (options: {
182
188
  context: ProjectContext;
183
189
  config: ResolvedConfig;
@@ -190,6 +196,7 @@ export const astroConfigTemplate = (options: {
190
196
  examplesPath: string;
191
197
  themePath: string;
192
198
  searchClientPath: string;
199
+ openapiPath: string;
193
200
  /** Project tsconfig path aliases (`find` -> absolute dir), e.g. `@` -> src. */
194
201
  aliases?: Record<string, string>;
195
202
  }): string => {
@@ -199,6 +206,7 @@ export const astroConfigTemplate = (options: {
199
206
  examplesPath,
200
207
  needsSvelte,
201
208
  needsVue,
209
+ openapiPath,
202
210
  searchClientPath,
203
211
  } = options;
204
212
  const { deployment } = config;
@@ -322,7 +330,7 @@ ${twoslashImport}${reactImport}${vueImport}${svelteImport}${blumeImport}${adapte
322
330
  export default defineConfig({
323
331
  root: ${JSON.stringify(context.outDir)},
324
332
  srcDir: ${JSON.stringify(`${context.outDir}/src`)},
325
- outDir: ${JSON.stringify(`${context.root}/dist`)},
333
+ outDir: ${JSON.stringify(astroOutDir(context))},
326
334
  publicDir: ${JSON.stringify(`${context.root}/public`)},
327
335
  output: ${JSON.stringify(deployment.output)},${adapterOption}${siteOption}${baseOption}${redirectsOption}${i18nOption}${fontsOption}
328
336
  integrations: [${integrations.join(", ")}],
@@ -357,6 +365,7 @@ export default defineConfig({
357
365
  alias: {
358
366
  "blume:data": ${JSON.stringify(dataPath)},
359
367
  "blume:examples": ${JSON.stringify(examplesPath)},
368
+ "blume:openapi": ${JSON.stringify(openapiPath)},
360
369
  "blume:search-client": ${JSON.stringify(searchClientPath)},
361
370
  "blume:theme": ${JSON.stringify(themePath)},${userAliasLines}
362
371
  },
@@ -913,7 +922,10 @@ import FileTree from "blume/components/content/FileTree.astro";
913
922
  import Frame from "blume/components/content/Frame.astro";
914
923
  import GithubInfo from "blume/components/content/GithubInfo.astro";
915
924
  import Panel from "blume/components/content/Panel.astro";
925
+ import ParamField from "blume/components/content/ParamField.astro";
916
926
  import Prompt from "blume/components/content/Prompt.astro";
927
+ import RequestField from "blume/components/content/RequestField.astro";
928
+ import ResponseField from "blume/components/content/ResponseField.astro";
917
929
  import Step from "blume/components/content/Step.astro";
918
930
  import Steps from "blume/components/content/Steps.astro";
919
931
  import Tab from "blume/components/content/Tab.astro";
@@ -927,6 +939,8 @@ import TypeTable from "blume/components/content/TypeTable.astro";
927
939
  import Visibility from "blume/components/content/Visibility.astro";
928
940
  import YouTube from "blume/components/content/YouTube.astro";
929
941
  import Icon from "blume/components/Icon.astro";
942
+ import ApiOverview from "blume/components/openapi/ApiOverview.astro";
943
+ import Operation from "blume/components/openapi/Operation.astro";
930
944
  ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
931
945
  import { islandComponents } from "../generated/islands.ts";
932
946
  import data from "../generated/data.json";
@@ -943,6 +957,7 @@ export const prerender = true;
943
957
  const components = {
944
958
  Accordion,
945
959
  AccordionItem,
960
+ ApiOverview,
946
961
  AutoTypeTable,
947
962
  Badge,
948
963
  Callout,
@@ -960,8 +975,12 @@ const components = {
960
975
  Frame,
961
976
  GithubInfo,
962
977
  Icon,
978
+ Operation,
963
979
  Panel,
980
+ ParamField,
964
981
  Prompt,
982
+ RequestField,
983
+ ResponseField,
965
984
  Step,
966
985
  Steps,
967
986
  Tab,
@@ -1448,6 +1467,11 @@ declare module "blume:data" {
1448
1467
  export default data;
1449
1468
  }
1450
1469
 
1470
+ declare module "blume:openapi" {
1471
+ const specs: import("blume/openapi/model.ts").OpenApiData;
1472
+ export default specs;
1473
+ }
1474
+
1451
1475
  declare module "blume:search-client" {
1452
1476
  export const createSearch: () =>
1453
1477
  | import("blume/components/layout/search/types.ts").SearchFn
@@ -6,6 +6,8 @@ import { defineCommand } from "citty";
6
6
  import { join } from "pathe";
7
7
 
8
8
  import { buildLlmsFiles } from "../../ai/llms.ts";
9
+ import { ensureGitignore } from "../../core/gitignore.ts";
10
+ import type { BlumeProject } from "../../core/project-graph.ts";
9
11
  import type { ResolvedConfig } from "../../core/schema.ts";
10
12
  import { serverFeatures } from "../../core/server-features.ts";
11
13
  import {
@@ -169,6 +171,82 @@ const enforceBudget = async (
169
171
  return passed ? "pass" : "fail";
170
172
  };
171
173
 
174
+ /**
175
+ * Run every deploy post-step of a real (non-isolated) build: the search index +
176
+ * hosted-provider sync, llms.txt, sitemap/robots, redirect files, the summary
177
+ * box, and the optional bundle report / budget gate. Exits non-zero if a budget
178
+ * is exceeded. Isolated verify builds skip all of this.
179
+ */
180
+ const publishBuildArtifacts = async (
181
+ project: BlumeProject,
182
+ distDir: string,
183
+ args: { analyze?: boolean; "budget-css"?: string; "budget-js"?: string }
184
+ ): Promise<void> => {
185
+ if (project.config.search.provider === "pagefind") {
186
+ logger.start("Building search index");
187
+ const indexed = await buildSearchIndex(distDir);
188
+ logger.success(`Indexed ${indexed} page(s) for search`);
189
+ }
190
+
191
+ // Upload the index to a hosted provider (Algolia, Orama Cloud, Typesense).
192
+ // Skipped with a warning when its admin key isn't configured.
193
+ await syncSearchProvider(project, {
194
+ start: (message) => logger.start(message),
195
+ success: (message) => logger.success(message),
196
+ warn: (message) => logger.warn(message),
197
+ });
198
+
199
+ if (project.config.ai.llmsTxt) {
200
+ const { index, full } = await buildLlmsFiles(project);
201
+ await Promise.all([
202
+ writeFile(join(distDir, "llms.txt"), index, "utf-8"),
203
+ writeFile(join(distDir, "llms-full.txt"), full, "utf-8"),
204
+ ]);
205
+ logger.success("Generated llms.txt and llms-full.txt");
206
+ }
207
+
208
+ // A user's own public/ file (copied into dist by Astro) always wins.
209
+ const sitemap = buildSitemap(project);
210
+ if (sitemap && !existsSync(join(distDir, "sitemap.xml"))) {
211
+ await writeFile(join(distDir, "sitemap.xml"), sitemap, "utf-8");
212
+ logger.success("Generated sitemap.xml");
213
+ }
214
+
215
+ const robots = buildRobots(project);
216
+ if (robots && !existsSync(join(distDir, "robots.txt"))) {
217
+ await writeFile(join(distDir, "robots.txt"), robots, "utf-8");
218
+ logger.success("Generated robots.txt");
219
+ }
220
+
221
+ await emitRedirectFiles(project.config, distDir);
222
+
223
+ const { config } = project;
224
+ const features = serverFeatures(config);
225
+ logger.box(
226
+ [
227
+ `Output ${config.deployment.output}`,
228
+ `Adapter ${config.deployment.adapter ?? "none"}`,
229
+ `Site ${config.deployment.site ?? "not set"}`,
230
+ `Search ${config.search.provider}`,
231
+ `Redirects ${config.redirects.length}`,
232
+ `Sitemap ${sitemap ? "yes" : "no (set deployment.site)"}`,
233
+ `Robots ${robots ? "yes" : "no"}`,
234
+ `LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
235
+ `Server features ${features.length > 0 ? features.join(", ") : "none"}`,
236
+ ].join("\n")
237
+ );
238
+
239
+ if (args.analyze) {
240
+ await reportBundleSizes(distDir);
241
+ }
242
+
243
+ if ((await enforceBudget(distDir, args)) === "fail") {
244
+ process.exit(1);
245
+ }
246
+
247
+ logger.success(`Built to ${distDir}`);
248
+ };
249
+
172
250
  export const buildCommand = defineCommand({
173
251
  args: {
174
252
  adapter: {
@@ -191,6 +269,11 @@ export const buildCommand = defineCommand({
191
269
  description: "Fail if total client JavaScript exceeds this many kB.",
192
270
  type: "string",
193
271
  },
272
+ isolated: {
273
+ description:
274
+ "Build into an isolated .blume-verify runtime (and its own dist) so a running dev server and the real dist/ are untouched. For verifying changes while `blume dev` runs.",
275
+ type: "boolean",
276
+ },
194
277
  output: {
195
278
  description: "Output mode: static | server.",
196
279
  type: "string",
@@ -207,7 +290,18 @@ export const buildCommand = defineCommand({
207
290
  },
208
291
  async run({ args }) {
209
292
  const root = process.cwd();
210
- refuseIfDevRunning(root, "building");
293
+
294
+ // `--isolated` (or BLUME_RUNTIME_DIR) relocates the whole runtime to a
295
+ // sibling dir so this build never touches a live dev server's `.blume/` or
296
+ // the user's real `dist/`. A non-default runtime dir has no dev lock, so the
297
+ // refusal below lets it proceed; a plain build still refuses.
298
+ const runtimeDir = args.isolated
299
+ ? ".blume-verify"
300
+ : process.env.BLUME_RUNTIME_DIR;
301
+ refuseIfDevRunning(root, "building", runtimeDir);
302
+ if (args.isolated) {
303
+ await ensureGitignore(root, [".blume-verify/"]);
304
+ }
211
305
 
212
306
  if (args.output && args.output !== "static" && args.output !== "server") {
213
307
  logger.error(`Invalid --output "${args.output}" (use static | server).`);
@@ -230,6 +324,7 @@ export const buildCommand = defineCommand({
230
324
  },
231
325
  preview: args.preview,
232
326
  root,
327
+ runtimeDir,
233
328
  strict: args.strict,
234
329
  });
235
330
 
@@ -242,70 +337,19 @@ export const buildCommand = defineCommand({
242
337
  root: project.context.outDir,
243
338
  });
244
339
 
245
- const distDir = join(root, "dist");
246
-
247
- if (project.config.search.provider === "pagefind") {
248
- logger.start("Building search index");
249
- const indexed = await buildSearchIndex(distDir);
250
- logger.success(`Indexed ${indexed} page(s) for search`);
251
- }
252
-
253
- // Upload the index to a hosted provider (Algolia, Orama Cloud, Typesense).
254
- // Skipped with a warning when its admin key isn't configured.
255
- await syncSearchProvider(project, {
256
- start: (message) => logger.start(message),
257
- success: (message) => logger.success(message),
258
- warn: (message) => logger.warn(message),
259
- });
260
-
261
- if (project.config.ai.llmsTxt) {
262
- const { index, full } = await buildLlmsFiles(project);
263
- await Promise.all([
264
- writeFile(join(distDir, "llms.txt"), index, "utf-8"),
265
- writeFile(join(distDir, "llms-full.txt"), full, "utf-8"),
266
- ]);
267
- logger.success("Generated llms.txt and llms-full.txt");
268
- }
340
+ const distDir = project.context.distDir ?? join(root, "dist");
269
341
 
270
- // A user's own public/ file (copied into dist by Astro) always wins.
271
- const sitemap = buildSitemap(project);
272
- if (sitemap && !existsSync(join(distDir, "sitemap.xml"))) {
273
- await writeFile(join(distDir, "sitemap.xml"), sitemap, "utf-8");
274
- logger.success("Generated sitemap.xml");
275
- }
276
-
277
- const robots = buildRobots(project);
278
- if (robots && !existsSync(join(distDir, "robots.txt"))) {
279
- await writeFile(join(distDir, "robots.txt"), robots, "utf-8");
280
- logger.success("Generated robots.txt");
281
- }
282
-
283
- await emitRedirectFiles(project.config, distDir);
284
-
285
- const { config } = project;
286
- const features = serverFeatures(config);
287
- logger.box(
288
- [
289
- `Output ${config.deployment.output}`,
290
- `Adapter ${config.deployment.adapter ?? "none"}`,
291
- `Site ${config.deployment.site ?? "not set"}`,
292
- `Search ${config.search.provider}`,
293
- `Redirects ${config.redirects.length}`,
294
- `Sitemap ${sitemap ? "yes" : "no (set deployment.site)"}`,
295
- `Robots ${robots ? "yes" : "no"}`,
296
- `LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
297
- `Server features ${features.length > 0 ? features.join(", ") : "none"}`,
298
- ].join("\n")
299
- );
300
-
301
- if (args.analyze) {
302
- await reportBundleSizes(distDir);
303
- }
304
-
305
- if ((await enforceBudget(distDir, args)) === "fail") {
306
- process.exit(1);
342
+ // An isolated build is a throwaway verify: it only needs to confirm the site
343
+ // compiles and renders. Skip the network post-steps (search sync) and
344
+ // deploy artifacts (index/llms/sitemap/robots/redirects) that only matter
345
+ // for a real publish and would push to hosted providers.
346
+ if (runtimeDir) {
347
+ logger.success(
348
+ `Isolated build OK — output at ${distDir} (not published).`
349
+ );
350
+ return;
307
351
  }
308
352
 
309
- logger.success(`Built to ${distDir}`);
353
+ await publishBuildArtifacts(project, distDir, args);
310
354
  },
311
355
  });
@@ -5,11 +5,18 @@ import { sync } from "astro";
5
5
  import { defineCommand } from "citty";
6
6
  import { join } from "pathe";
7
7
 
8
+ import { ensureGitignore } from "../../core/gitignore.ts";
9
+ import { refuseIfDevRunning } from "../dev-lock.ts";
8
10
  import { logger } from "../log.ts";
9
11
  import { prepareProject } from "../prepare.ts";
10
12
 
11
13
  export const checkCommand = defineCommand({
12
14
  args: {
15
+ isolated: {
16
+ description:
17
+ "Type-check in an isolated .blume-verify runtime so a running dev server is untouched. For verifying changes while `blume dev` runs.",
18
+ type: "boolean",
19
+ },
13
20
  preview: {
14
21
  description: "Include drafts and unpublished CMS content.",
15
22
  type: "boolean",
@@ -25,10 +32,23 @@ export const checkCommand = defineCommand({
25
32
  },
26
33
  async run({ args }) {
27
34
  const root = process.cwd();
35
+
36
+ // `blume check` regenerates `.blume` just like `build`, so it must refuse a
37
+ // live dev server unless isolated. `--isolated` (or BLUME_RUNTIME_DIR)
38
+ // relocates the runtime to `.blume-verify`, which dev never locks.
39
+ const runtimeDir = args.isolated
40
+ ? ".blume-verify"
41
+ : process.env.BLUME_RUNTIME_DIR;
42
+ refuseIfDevRunning(root, "checking", runtimeDir);
43
+ if (args.isolated) {
44
+ await ensureGitignore(root, [".blume-verify/"]);
45
+ }
46
+
28
47
  const project = await prepareProject({
29
48
  mode: "build",
30
49
  preview: args.preview,
31
50
  root,
51
+ runtimeDir,
32
52
  strict: args.strict,
33
53
  });
34
54
 
@@ -8,6 +8,7 @@ import {
8
8
 
9
9
  import { join } from "pathe";
10
10
 
11
+ import { resolveRuntimeDir } from "../core/project.ts";
11
12
  import { logger } from "./log.ts";
12
13
 
13
14
  /**
@@ -71,13 +72,20 @@ export const acquireDevLock = (outDir: string): (() => void) => {
71
72
  };
72
73
 
73
74
  /**
74
- * Exit with an error when a live `blume dev` owns the `.blume` dir under `root`.
75
- * `action` names the operation being refused (e.g. "building").
75
+ * Exit with an error when a live `blume dev` owns the runtime dir under `root`.
76
+ * `action` names the operation being refused (e.g. "building"). `runtimeDir`
77
+ * relocates the checked dir: an isolated verify (`.blume-verify`) targets a dir
78
+ * dev never locks, so it proceeds; a default or `--runtime-dir .blume` run still
79
+ * refuses.
76
80
  */
77
- export const refuseIfDevRunning = (root: string, action: string): void => {
78
- if (isDevLocked(join(root, ".blume"))) {
81
+ export const refuseIfDevRunning = (
82
+ root: string,
83
+ action: string,
84
+ runtimeDir?: string
85
+ ): void => {
86
+ if (isDevLocked(resolveRuntimeDir(root, runtimeDir))) {
79
87
  logger.error(
80
- `A \`blume dev\` server is running against .blume; ${action} would corrupt it. Stop the dev server first.`
88
+ `A \`blume dev\` server is running against .blume; ${action} would corrupt it. Stop the dev server, or re-run with --isolated to build/verify against .blume-verify without touching it.`
81
89
  );
82
90
  process.exit(1);
83
91
  }
@@ -24,6 +24,8 @@ export interface PrepareOptions {
24
24
  refresh?: boolean;
25
25
  /** CLI config overrides (e.g. `--output`, `--content-dir`). */
26
26
  overrides?: ConfigOverrides;
27
+ /** Relocate the generated runtime (e.g. `.blume-verify` for `--isolated`). */
28
+ runtimeDir?: string;
27
29
  }
28
30
 
29
31
  /**
@@ -45,6 +47,7 @@ export const prepareProject = async (
45
47
  overrides: options.overrides,
46
48
  preview: options.preview,
47
49
  refresh: options.refresh,
50
+ runtimeDir: options.runtimeDir,
48
51
  });
49
52
  } catch (error) {
50
53
  if (error instanceof BlumeError) {
@@ -38,7 +38,10 @@ import FileTree from "./content/FileTree.astro";
38
38
  import Frame from "./content/Frame.astro";
39
39
  import GithubInfo from "./content/GithubInfo.astro";
40
40
  import Panel from "./content/Panel.astro";
41
+ import ParamField from "./content/ParamField.astro";
41
42
  import Prompt from "./content/Prompt.astro";
43
+ import RequestField from "./content/RequestField.astro";
44
+ import ResponseField from "./content/ResponseField.astro";
42
45
  import Step from "./content/Step.astro";
43
46
  import Steps from "./content/Steps.astro";
44
47
  import Tab from "./content/Tab.astro";
@@ -87,7 +90,10 @@ const components = {
87
90
  GithubInfo,
88
91
  Icon,
89
92
  Panel,
93
+ ParamField,
90
94
  Prompt,
95
+ RequestField,
96
+ ResponseField,
91
97
  Step,
92
98
  Steps,
93
99
  Tab,
@@ -1,10 +1,13 @@
1
1
  ---
2
+ import data from "blume:data";
2
3
  import { resolveIcon } from "../theme/icons.ts";
3
4
 
4
5
  interface Props {
5
6
  name?: unknown;
6
7
  icon?: unknown;
7
8
  iconType?: string;
9
+ /** Icon library for a bare name; defaults to the project's `icons.library`. */
10
+ library?: string;
8
11
  size?: number;
9
12
  class?: string;
10
13
  className?: string;
@@ -16,6 +19,7 @@ const {
16
19
  color,
17
20
  icon,
18
21
  iconType,
22
+ library,
19
23
  name,
20
24
  size = 16,
21
25
  class: astroClass,
@@ -37,8 +41,12 @@ const isImageIcon = (value: string): boolean =>
37
41
  /\.(?:avif|gif|jpe?g|png|svg|webp)$/iu.test(value);
38
42
  const imageSrc = iconName && !rawSvg && isImageIcon(iconName) ? iconName : null;
39
43
  const resolvedIcon =
40
- iconName && !(imageSrc || rawSvg) ? resolveIcon(iconName, iconType) : null;
41
- const markup = resolvedIcon?.markup;
44
+ iconName && !(imageSrc || rawSvg)
45
+ ? resolveIcon(iconName, {
46
+ iconType,
47
+ library: library ?? data.config.icons.library,
48
+ })
49
+ : null;
42
50
  const resolvedClass = astroClass ?? className;
43
51
  const customStyle = `display:inline-flex;width:${size}px;height:${size}px;${
44
52
  color ? `color:${color}` : ""
@@ -76,21 +84,16 @@ const style = color ? `color:${color}` : undefined;
76
84
  width={size}
77
85
  />
78
86
  ) : (
79
- markup && (
87
+ resolvedIcon && (
80
88
  <svg
81
89
  aria-hidden={label ? undefined : "true"}
82
90
  aria-label={label}
83
91
  class={resolvedClass}
84
- fill="none"
85
92
  height={size}
86
93
  role={label ? "img" : undefined}
87
- set:html={markup}
88
- stroke="currentColor"
89
- stroke-linecap="round"
90
- stroke-linejoin="round"
91
- stroke-width="2"
94
+ set:html={resolvedIcon.body}
92
95
  style={style}
93
- viewBox="0 0 24 24"
96
+ viewBox={resolvedIcon.viewBox}
94
97
  width={size}
95
98
  xmlns="http://www.w3.org/2000/svg"
96
99
  />