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.
- package/dist/cli/index.js +1170 -820
- package/dist/cli/index.js.map +32 -27
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/project.d.ts +12 -2
- package/dist/types/core/schema.d.ts +154 -41
- package/dist/types/core/types.d.ts +7 -6
- package/dist/types/migrate/mintlify/config.d.ts +14 -0
- package/docs/01-quickstart.mdx +6 -2
- package/docs/02-deployment.mdx +3 -1
- package/docs/advanced/api-reference.mdx +37 -23
- package/docs/advanced/bridge.mdx +76 -0
- package/docs/advanced/custom-pages.mdx +3 -1
- package/docs/advanced/meta.ts +8 -1
- package/docs/advanced/migrate.mdx +123 -0
- package/docs/configuration/ai.mdx +3 -1
- package/docs/configuration/analytics.mdx +3 -1
- package/docs/configuration/export.mdx +6 -2
- package/docs/configuration/index.mdx +1 -1
- package/docs/configuration/seo.mdx +3 -1
- package/docs/content/components.mdx +55 -2
- package/docs/content/i18n.mdx +6 -2
- package/docs/content/islands.mdx +6 -2
- package/docs/content/meta.mdx +3 -1
- package/docs/content/syntax.mdx +40 -14
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +29 -1
- package/docs/reference/frontmatter.mdx +5 -0
- package/package.json +11 -1
- package/src/astro/generate.ts +18 -9
- package/src/astro/templates.ts +28 -4
- package/src/cli/commands/build.ts +107 -63
- package/src/cli/commands/check.ts +20 -0
- package/src/cli/dev-lock.ts +13 -5
- package/src/cli/prepare.ts +3 -0
- package/src/components/BlumePage.astro +6 -0
- package/src/components/Icon.astro +13 -10
- package/src/components/content/ApiField.astro +75 -0
- package/src/components/content/ParamField.astro +39 -0
- package/src/components/content/RequestField.astro +23 -0
- package/src/components/content/ResponseField.astro +23 -0
- package/src/components/content/Step.astro +1 -1
- package/src/components/layout/Breadcrumbs.astro +7 -2
- package/src/components/layout/NavTree.astro +24 -8
- package/src/components/layout/RootLayout.astro +56 -34
- package/src/components/layout/Search.astro +1 -1
- package/src/components/openapi/ApiOverview.astro +84 -0
- package/src/components/openapi/MethodBadge.astro +28 -0
- package/src/components/openapi/Operation.astro +140 -0
- package/src/components/openapi/ParametersTable.astro +97 -0
- package/src/components/openapi/RequestBody.astro +58 -0
- package/src/components/openapi/RequestPanel.astro +169 -0
- package/src/components/openapi/Responses.astro +91 -0
- package/src/components/openapi/SchemaProperty.astro +118 -0
- package/src/components/openapi/SchemaTable.astro +86 -0
- package/src/components/openapi/helpers.ts +238 -0
- package/src/components/openapi/panel.ts +59 -0
- package/src/components/openapi/snippets.ts +201 -0
- package/src/core/builtin-tags.ts +5 -0
- package/src/core/data.ts +2 -0
- package/src/core/graph.ts +0 -3
- package/src/core/nav-diagnostics.ts +2 -12
- package/src/core/navigation.ts +0 -10
- package/src/core/project-graph.ts +5 -1
- package/src/core/project.ts +25 -3
- package/src/core/schema.ts +47 -14
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/resolve.ts +28 -6
- package/src/core/types.ts +7 -7
- package/src/migrate/mintlify/config.ts +190 -97
- package/src/migrate/mintlify/content.ts +24 -2
- package/src/migrate/mintlify/index.ts +76 -2
- package/src/migrate/mintlify/transform.ts +2 -0
- package/src/openapi/model.ts +174 -0
- package/src/openapi/parse.ts +48 -0
- package/src/openapi/references.ts +164 -0
- package/src/openapi/render-mdx.ts +76 -0
- package/src/openapi/scalar.ts +15 -103
- package/src/openapi/source.ts +140 -0
- package/src/registry/eject.ts +15 -2
- package/src/theme/chrome-icons.ts +22 -0
- package/src/theme/icons.ts +151 -161
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blume",
|
|
3
|
-
"version": "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",
|
package/src/astro/generate.ts
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
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
|
|
586
|
-
//
|
|
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 (
|
|
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`
|
package/src/astro/templates.ts
CHANGED
|
@@ -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
|
|
103
|
-
//
|
|
104
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
|
package/src/cli/dev-lock.ts
CHANGED
|
@@ -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
|
|
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 = (
|
|
78
|
-
|
|
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
|
|
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
|
}
|
package/src/cli/prepare.ts
CHANGED
|
@@ -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)
|
|
41
|
-
|
|
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
|
-
|
|
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={
|
|
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=
|
|
96
|
+
viewBox={resolvedIcon.viewBox}
|
|
94
97
|
width={size}
|
|
95
98
|
xmlns="http://www.w3.org/2000/svg"
|
|
96
99
|
/>
|