blume 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2429 -792
- package/dist/cli/index.js.map +63 -44
- package/dist/types/core/data.d.ts +16 -0
- package/dist/types/core/define-components.d.ts +9 -2
- package/dist/types/core/diagnostics.d.ts +5 -0
- package/dist/types/core/schema.d.ts +313 -778
- package/dist/types/core/types.d.ts +2 -2
- package/dist/types/migrate/mintlify/assets.d.ts +8 -0
- package/docs/01-quickstart.mdx +5 -16
- package/docs/02-deployment.mdx +26 -40
- package/docs/advanced/api-reference.mdx +10 -37
- package/docs/advanced/blog.mdx +9 -25
- package/docs/advanced/changelog.mdx +10 -33
- package/docs/advanced/custom-pages.mdx +66 -61
- package/docs/configuration/ai.mdx +47 -91
- package/docs/configuration/analytics.mdx +20 -38
- package/docs/configuration/customization.mdx +92 -27
- package/docs/configuration/export.mdx +9 -34
- package/docs/configuration/index.mdx +78 -85
- package/docs/configuration/search.mdx +17 -54
- package/docs/configuration/seo.mdx +18 -44
- package/docs/configuration/theming.mdx +20 -42
- package/docs/content/components.mdx +42 -101
- package/docs/content/i18n.mdx +21 -72
- package/docs/content/index.mdx +18 -48
- package/docs/content/islands.mdx +79 -33
- package/docs/content/meta.mdx +23 -50
- package/docs/content/navigation.mdx +42 -56
- package/docs/content/sources.mdx +20 -83
- package/docs/content/syntax.mdx +37 -105
- package/docs/index.mdx +13 -51
- package/docs/reference/cli.mdx +49 -18
- package/docs/reference/frontmatter.mdx +2 -5
- package/package.json +3 -1
- package/src/ai/ask-context.ts +131 -0
- package/src/ai/ask-data.ts +25 -0
- package/src/astro/component-slots.ts +165 -0
- package/src/astro/generate.ts +132 -13
- package/src/astro/integration.ts +85 -3
- package/src/astro/islands.ts +6 -2
- package/src/astro/markdown-negotiation.ts +17 -3
- package/src/astro/pages.ts +11 -13
- package/src/astro/static-assets.ts +117 -0
- package/src/astro/templates.ts +120 -50
- package/src/blume-modules.d.ts +25 -0
- package/src/cli/args.ts +23 -0
- package/src/cli/commands/build.ts +209 -1
- package/src/cli/commands/check.ts +62 -0
- package/src/cli/commands/dev.ts +32 -3
- package/src/cli/commands/doctor.ts +32 -6
- package/src/cli/commands/eject.ts +3 -1
- package/src/cli/commands/init.ts +184 -16
- package/src/cli/commands/preview.ts +2 -1
- package/src/cli/commands/validate.ts +27 -2
- package/src/cli/dev-lock.ts +84 -0
- package/src/cli/index.ts +15 -0
- package/src/cli/internal-error.ts +63 -0
- package/src/cli/log.ts +41 -1
- package/src/cli/prepare.ts +17 -3
- package/src/cli/required-secrets.ts +44 -0
- package/src/components/BlumePage.astro +109 -0
- package/src/components/content/YouTube.astro +35 -0
- package/src/components/content/youtube.ts +46 -0
- package/src/components/index.ts +3 -3
- package/src/components/islands/ask-ai.tsx +29 -15
- package/src/components/islands/hooks.ts +188 -0
- package/src/components/layout/Empty.astro +6 -0
- package/src/components/layout/Header.astro +24 -39
- package/src/components/layout/Logo.astro +50 -0
- package/src/components/layout/NavSelector.astro +75 -0
- package/src/components/layout/PageLayout.astro +38 -2
- package/src/components/layout/RootLayout.astro +70 -4
- package/src/components/layout/hydration-hint.ts +30 -0
- package/src/components/layout/overrides.ts +6 -4
- package/src/components/props.ts +71 -0
- package/src/core/assets.ts +31 -0
- package/src/core/bridge.ts +10 -0
- package/src/core/builtin-tags.ts +40 -0
- package/src/core/component-diagnostics.ts +44 -0
- package/src/core/component-overrides.ts +478 -0
- package/src/core/config.ts +8 -0
- package/src/core/data.ts +14 -0
- package/src/core/define-components.ts +9 -2
- package/src/core/diagnostics.ts +95 -1
- package/src/core/gitignore.ts +30 -0
- package/src/core/graph.ts +7 -0
- package/src/core/links.ts +60 -19
- package/src/core/nav-diagnostics.ts +205 -0
- package/src/core/project-graph.ts +40 -1
- package/src/core/schema.ts +35 -96
- package/src/core/sources/mdx-remote.ts +54 -8
- package/src/core/sources/normalize.ts +57 -1
- package/src/core/sources/notion.ts +49 -5
- package/src/core/sources/sanity.ts +5 -1
- package/src/core/types.ts +2 -2
- package/src/deploy/redirects.ts +43 -0
- package/src/deploy/rss.ts +1 -8
- package/src/deploy/sitemap.ts +20 -1
- package/src/deploy/xml.ts +8 -0
- package/src/markdown/directives.ts +15 -7
- package/src/markdown/package-commands.ts +26 -4
- package/src/migrate/fumadocs/content.ts +14 -1
- package/src/migrate/fumadocs/groups.ts +7 -0
- package/src/migrate/fumadocs/index.ts +5 -2
- package/src/migrate/mintlify/assets.ts +46 -0
- package/src/migrate/mintlify/config.ts +1 -176
- package/src/migrate/mintlify/index.ts +53 -45
- package/src/migrate/shared.ts +12 -27
- package/src/migrate/starlight/config.ts +0 -4
- package/src/og/card.ts +175 -38
- package/src/registry/eject.ts +52 -12
- package/src/registry/registry.ts +172 -0
- package/src/registry/rewrite-imports.ts +31 -19
- package/src/runtime/index.ts +61 -0
- package/src/search/documents.ts +23 -5
- package/src/search/sync/algolia.ts +5 -1
- package/src/search/sync/typesense.ts +24 -16
- package/src/theme/palette.ts +26 -7
- package/src/vite-env.d.ts +14 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { check } from "@astrojs/check";
|
|
4
|
+
import { sync } from "astro";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
import { join } from "pathe";
|
|
7
|
+
|
|
8
|
+
import { logger } from "../log.ts";
|
|
9
|
+
import { prepareProject } from "../prepare.ts";
|
|
10
|
+
|
|
11
|
+
export const checkCommand = defineCommand({
|
|
12
|
+
args: {
|
|
13
|
+
preview: {
|
|
14
|
+
description: "Include drafts and unpublished CMS content.",
|
|
15
|
+
type: "boolean",
|
|
16
|
+
},
|
|
17
|
+
strict: {
|
|
18
|
+
description: "Fail on content diagnostics as well as type errors.",
|
|
19
|
+
type: "boolean",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
meta: {
|
|
23
|
+
description: "Type-check the docs site with astro check.",
|
|
24
|
+
name: "check",
|
|
25
|
+
},
|
|
26
|
+
async run({ args }) {
|
|
27
|
+
const root = process.cwd();
|
|
28
|
+
const project = await prepareProject({
|
|
29
|
+
mode: "build",
|
|
30
|
+
preview: args.preview,
|
|
31
|
+
root,
|
|
32
|
+
strict: args.strict,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { outDir } = project.context;
|
|
36
|
+
|
|
37
|
+
// Generate Astro's content/collection and font types into `.blume/.astro`
|
|
38
|
+
// so `astro:*` virtual modules resolve during the check.
|
|
39
|
+
await sync({ logLevel: "warn", root: outDir });
|
|
40
|
+
|
|
41
|
+
// The project-root tsconfig is what covers the authored `pages/` and config;
|
|
42
|
+
// without it astro check only sees the generated `.blume` project. Falls back
|
|
43
|
+
// to the generated project's own tsconfig when the project has none.
|
|
44
|
+
const tsconfig = join(root, "tsconfig.json");
|
|
45
|
+
|
|
46
|
+
logger.start(`Type-checking ${project.graph.pages.length} page(s)`);
|
|
47
|
+
const failed = await check({
|
|
48
|
+
minimumFailingSeverity: "error",
|
|
49
|
+
minimumSeverity: "hint",
|
|
50
|
+
root: outDir,
|
|
51
|
+
tsconfig: existsSync(tsconfig) ? tsconfig : undefined,
|
|
52
|
+
watch: false,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (failed) {
|
|
56
|
+
logger.error("Type check failed.");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
logger.success("No type errors.");
|
|
61
|
+
},
|
|
62
|
+
});
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -4,12 +4,23 @@ import { dev } from "astro";
|
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
|
|
6
6
|
import { generateRuntime } from "../../astro/generate.ts";
|
|
7
|
+
import { showBlumeErrorOverlay } from "../../astro/integration.ts";
|
|
7
8
|
import { scanProject } from "../../core/project-graph.ts";
|
|
9
|
+
import { parsePort } from "../args.ts";
|
|
10
|
+
import { acquireDevLock } from "../dev-lock.ts";
|
|
8
11
|
import { logger } from "../log.ts";
|
|
9
12
|
import { prepareProject } from "../prepare.ts";
|
|
10
13
|
|
|
11
14
|
export const devCommand = defineCommand({
|
|
12
15
|
args: {
|
|
16
|
+
"content-dir": {
|
|
17
|
+
description: "Content folder to scan, overriding config (content.root).",
|
|
18
|
+
type: "string",
|
|
19
|
+
},
|
|
20
|
+
debug: {
|
|
21
|
+
description: "Verbose Astro/Vite logging for troubleshooting.",
|
|
22
|
+
type: "boolean",
|
|
23
|
+
},
|
|
13
24
|
host: { description: "Network host to bind.", type: "string" },
|
|
14
25
|
open: { description: "Open the browser on start.", type: "boolean" },
|
|
15
26
|
port: { description: "Port to listen on.", type: "string" },
|
|
@@ -26,14 +37,19 @@ export const devCommand = defineCommand({
|
|
|
26
37
|
async run({ args }) {
|
|
27
38
|
const root = process.cwd();
|
|
28
39
|
const preview = args.preview ?? false;
|
|
40
|
+
const overrides = args["content-dir"]
|
|
41
|
+
? { contentRoot: args["content-dir"] }
|
|
42
|
+
: undefined;
|
|
29
43
|
// Astro's dev server defaults to 4321 when no port is passed. Feeding the
|
|
30
44
|
// resolved URL in as the `deployment.site` fallback lets site-gated features
|
|
31
45
|
// (OG images, canonicals, sitemap) work locally without configuring a site.
|
|
32
|
-
const
|
|
46
|
+
const explicitPort = parsePort(args.port);
|
|
47
|
+
const port = explicitPort ?? 4321;
|
|
33
48
|
const devServerUrl = `http://localhost:${port}`;
|
|
34
49
|
const project = await prepareProject({
|
|
35
50
|
devServerUrl,
|
|
36
51
|
mode: "dev",
|
|
52
|
+
overrides,
|
|
37
53
|
preview,
|
|
38
54
|
root,
|
|
39
55
|
strict: args.strict,
|
|
@@ -45,16 +61,25 @@ export const devCommand = defineCommand({
|
|
|
45
61
|
);
|
|
46
62
|
}
|
|
47
63
|
|
|
64
|
+
// Claim the shared `.blume` dir so a concurrent build/eject/sync refuses
|
|
65
|
+
// rather than regenerating or deleting it out from under this server.
|
|
66
|
+
const releaseLock = acquireDevLock(project.context.outDir);
|
|
67
|
+
process.on("exit", releaseLock);
|
|
68
|
+
|
|
48
69
|
const server = await dev({
|
|
49
|
-
logLevel: "info",
|
|
70
|
+
logLevel: args.debug ? "debug" : "info",
|
|
50
71
|
root: project.context.outDir,
|
|
51
72
|
server: {
|
|
52
73
|
host: args.host ?? false,
|
|
53
74
|
open: args.open ?? false,
|
|
54
|
-
port:
|
|
75
|
+
port: explicitPort,
|
|
55
76
|
},
|
|
56
77
|
});
|
|
57
78
|
|
|
79
|
+
// Mirror any initial diagnostics into the browser overlay now the server
|
|
80
|
+
// (and its HMR channel) is up.
|
|
81
|
+
showBlumeErrorOverlay(project.diagnostics);
|
|
82
|
+
|
|
58
83
|
// Watch user inputs and regenerate the runtime data on change. Astro/Vite
|
|
59
84
|
// hot-reloads the generated data module so nav and routes stay in sync.
|
|
60
85
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -67,9 +92,12 @@ export const devCommand = defineCommand({
|
|
|
67
92
|
const next = await scanProject(root, {
|
|
68
93
|
devServerUrl,
|
|
69
94
|
mode: "dev",
|
|
95
|
+
overrides,
|
|
70
96
|
preview,
|
|
71
97
|
});
|
|
72
98
|
await generateRuntime(next);
|
|
99
|
+
// Surface any content/config errors in the browser overlay too.
|
|
100
|
+
showBlumeErrorOverlay(next.diagnostics);
|
|
73
101
|
} catch (error) {
|
|
74
102
|
logger.error(`Regeneration failed: ${(error as Error).message}`);
|
|
75
103
|
}
|
|
@@ -98,6 +126,7 @@ export const devCommand = defineCommand({
|
|
|
98
126
|
for (const dispose of disposers) {
|
|
99
127
|
dispose();
|
|
100
128
|
}
|
|
129
|
+
releaseLock();
|
|
101
130
|
await server.stop();
|
|
102
131
|
process.exit(0);
|
|
103
132
|
};
|
|
@@ -4,16 +4,28 @@ import { BlumeError } from "../../core/diagnostics.ts";
|
|
|
4
4
|
import { scanProject } from "../../core/project-graph.ts";
|
|
5
5
|
import { serverFeatures } from "../../core/server-features.ts";
|
|
6
6
|
import type { Diagnostic } from "../../core/types.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { reportInternalError } from "../internal-error.ts";
|
|
8
|
+
import {
|
|
9
|
+
flushStdout,
|
|
10
|
+
logger,
|
|
11
|
+
reportDiagnostics,
|
|
12
|
+
reportDiagnosticsJson,
|
|
13
|
+
} from "../log.ts";
|
|
8
14
|
|
|
9
15
|
const MIN_NODE_MAJOR = 20;
|
|
10
16
|
|
|
11
17
|
export const doctorCommand = defineCommand({
|
|
18
|
+
args: {
|
|
19
|
+
json: {
|
|
20
|
+
description: "Emit diagnostics as JSON on stdout (for CI/editors).",
|
|
21
|
+
type: "boolean",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
12
24
|
meta: {
|
|
13
25
|
description: "Diagnose common configuration and content problems.",
|
|
14
26
|
name: "doctor",
|
|
15
27
|
},
|
|
16
|
-
async run() {
|
|
28
|
+
async run({ args }) {
|
|
17
29
|
const root = process.cwd();
|
|
18
30
|
const diagnostics: Diagnostic[] = [];
|
|
19
31
|
|
|
@@ -52,15 +64,29 @@ export const doctorCommand = defineCommand({
|
|
|
52
64
|
});
|
|
53
65
|
}
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
if (!args.json) {
|
|
68
|
+
logger.info(`Pages: ${project.graph.pages.length}`);
|
|
69
|
+
logger.info(`Output: ${config.deployment.output}`);
|
|
70
|
+
logger.info(`Search: ${config.search.provider}`);
|
|
71
|
+
}
|
|
58
72
|
} catch (error) {
|
|
59
73
|
if (error instanceof BlumeError) {
|
|
60
74
|
diagnostics.push(error.diagnostic);
|
|
61
75
|
} else {
|
|
62
|
-
|
|
76
|
+
reportInternalError(error);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (args.json) {
|
|
82
|
+
// Drain stdout before exiting non-zero: `process.exit` would otherwise
|
|
83
|
+
// truncate the JSON payload mid-write when stdout is a pipe — exactly how
|
|
84
|
+
// `--json` is consumed in CI/editors.
|
|
85
|
+
if (reportDiagnosticsJson(diagnostics, root)) {
|
|
86
|
+
await flushStdout();
|
|
87
|
+
process.exit(1);
|
|
63
88
|
}
|
|
89
|
+
return;
|
|
64
90
|
}
|
|
65
91
|
|
|
66
92
|
const hadErrors = reportDiagnostics(diagnostics, root);
|
|
@@ -4,6 +4,7 @@ import { defineCommand } from "citty";
|
|
|
4
4
|
import { join, relative } from "pathe";
|
|
5
5
|
|
|
6
6
|
import { eject } from "../../registry/eject.ts";
|
|
7
|
+
import { refuseIfDevRunning } from "../dev-lock.ts";
|
|
7
8
|
import { logger } from "../log.ts";
|
|
8
9
|
|
|
9
10
|
const updatePackageScripts = async (root: string): Promise<void> => {
|
|
@@ -34,10 +35,11 @@ export const ejectCommand = defineCommand({
|
|
|
34
35
|
},
|
|
35
36
|
async run({ args }) {
|
|
36
37
|
const root = process.cwd();
|
|
38
|
+
refuseIfDevRunning(root, "ejecting");
|
|
37
39
|
|
|
38
40
|
if (!args.yes) {
|
|
39
41
|
logger.warn(
|
|
40
|
-
"Eject is one-way: it writes astro.config.mjs and
|
|
42
|
+
"Eject is one-way: it writes astro.config.mjs, src/, and (if absent) tsconfig.json, rewrites your package.json scripts, and removes .blume. An existing tsconfig.json is left untouched."
|
|
41
43
|
);
|
|
42
44
|
logger.info("Re-run with --yes to proceed.");
|
|
43
45
|
return;
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
|
-
import { basename, dirname, join } from "pathe";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative } from "pathe";
|
|
6
6
|
|
|
7
|
+
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
7
8
|
import { getBlumeVersion } from "../../core/version.ts";
|
|
9
|
+
import { eject } from "../../registry/eject.ts";
|
|
8
10
|
import { logger } from "../log.ts";
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -32,25 +34,119 @@ const packageTemplate = (name: string, version: string): string => `{
|
|
|
32
34
|
}
|
|
33
35
|
`;
|
|
34
36
|
|
|
35
|
-
const
|
|
37
|
+
const TEMPLATES = ["docs", "api", "sdk", "changelog"] as const;
|
|
38
|
+
type Template = (typeof TEMPLATES)[number];
|
|
39
|
+
|
|
40
|
+
const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"] as const;
|
|
41
|
+
type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
42
|
+
|
|
43
|
+
/** A starter: the config plus seed content files (path is relative to root). */
|
|
44
|
+
interface Starter {
|
|
45
|
+
config: string;
|
|
46
|
+
files: (contentDir: string) => { content: string; path: string }[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const configFor = (
|
|
50
|
+
extra: string
|
|
51
|
+
): string => `import { defineConfig } from "blume";
|
|
36
52
|
|
|
37
53
|
export default defineConfig({
|
|
38
54
|
title: "My Docs",
|
|
39
|
-
description: "Documentation powered by Blume."
|
|
55
|
+
description: "Documentation powered by Blume.",${extra}
|
|
40
56
|
});
|
|
41
57
|
`;
|
|
42
58
|
|
|
43
|
-
const
|
|
44
|
-
title:
|
|
45
|
-
description: Welcome to your new Blume docs.
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
# Introduction
|
|
59
|
+
const page = (title: string, description: string, body: string): string =>
|
|
60
|
+
`---\ntitle: ${title}\ndescription: ${description}\n---\n\n${body}\n`;
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
const STARTERS: Record<Template, Starter> = {
|
|
63
|
+
api: {
|
|
64
|
+
config: configFor(`
|
|
65
|
+
openapi: {
|
|
66
|
+
enabled: true,
|
|
67
|
+
route: "/api",
|
|
68
|
+
sources: [
|
|
69
|
+
{
|
|
70
|
+
label: "Petstore",
|
|
71
|
+
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},`),
|
|
75
|
+
files: (dir) => [
|
|
76
|
+
{
|
|
77
|
+
content: page(
|
|
78
|
+
"API Reference",
|
|
79
|
+
"Explore the API.",
|
|
80
|
+
"# API Reference\n\nYour OpenAPI spec renders at [`/api`](/api). Point `openapi.sources` at your own spec in `blume.config.ts`."
|
|
81
|
+
),
|
|
82
|
+
path: join(dir, "index.mdx"),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
changelog: {
|
|
87
|
+
config: configFor(`
|
|
88
|
+
navigation: {
|
|
89
|
+
tabs: [
|
|
90
|
+
{ label: "Docs", path: "/" },
|
|
91
|
+
{ label: "Changelog", path: "/changelog" },
|
|
92
|
+
],
|
|
93
|
+
},`),
|
|
94
|
+
files: (dir) => [
|
|
95
|
+
{
|
|
96
|
+
content: page(
|
|
97
|
+
"Introduction",
|
|
98
|
+
"Welcome to your new Blume docs.",
|
|
99
|
+
"# Introduction\n\nWrite your docs here, and log releases under `changelog/`."
|
|
100
|
+
),
|
|
101
|
+
path: join(dir, "index.mdx"),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
content: `---\ntitle: v1.0.0\ntype: changelog\ndate: 2026-01-01\n---\n\nThe first release. Edit \`${dir}/changelog/v1-0-0.mdx\` or add new entries beside it.\n`,
|
|
105
|
+
path: join(dir, "changelog", "v1-0-0.mdx"),
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
docs: {
|
|
110
|
+
config: configFor(""),
|
|
111
|
+
files: (dir) => [
|
|
112
|
+
{
|
|
113
|
+
content: page(
|
|
114
|
+
"Introduction",
|
|
115
|
+
"Welcome to your new Blume docs.",
|
|
116
|
+
`# Introduction\n\nWelcome to **Blume** — markdown-first docs powered by Astro and Vite.\n\nEdit \`${dir}/index.mdx\` to get started, then run \`blume dev\`.`
|
|
117
|
+
),
|
|
118
|
+
path: join(dir, "index.mdx"),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
sdk: {
|
|
123
|
+
config: configFor(""),
|
|
124
|
+
files: (dir) => [
|
|
125
|
+
{
|
|
126
|
+
content: page(
|
|
127
|
+
"Introduction",
|
|
128
|
+
"Get started with the SDK.",
|
|
129
|
+
"# Introduction\n\nInstall the SDK and make your first call. See [Installation](/installation)."
|
|
130
|
+
),
|
|
131
|
+
path: join(dir, "index.mdx"),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
content: page(
|
|
135
|
+
"Installation",
|
|
136
|
+
"Install the SDK.",
|
|
137
|
+
"# Installation\n\n```package-install\nyour-sdk\n```"
|
|
138
|
+
),
|
|
139
|
+
path: join(dir, "installation.mdx"),
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
};
|
|
51
144
|
|
|
52
|
-
|
|
53
|
-
|
|
145
|
+
/** Install + dev commands to print for the chosen package manager. */
|
|
146
|
+
const commandsFor = (pm: PackageManager): { dev: string; install: string } => ({
|
|
147
|
+
dev: pm === "npm" ? "npm run dev" : `${pm} dev`,
|
|
148
|
+
install: `${pm} install`,
|
|
149
|
+
});
|
|
54
150
|
|
|
55
151
|
const writeFileSafe = async (
|
|
56
152
|
path: string,
|
|
@@ -73,6 +169,19 @@ export const initCommand = defineCommand({
|
|
|
73
169
|
description: "Content directory.",
|
|
74
170
|
type: "string",
|
|
75
171
|
},
|
|
172
|
+
eject: {
|
|
173
|
+
description: "Eject to a standalone Astro project after scaffolding.",
|
|
174
|
+
type: "boolean",
|
|
175
|
+
},
|
|
176
|
+
"package-manager": {
|
|
177
|
+
description:
|
|
178
|
+
"Package manager for the next-steps hint (npm|pnpm|yarn|bun).",
|
|
179
|
+
type: "string",
|
|
180
|
+
},
|
|
181
|
+
template: {
|
|
182
|
+
description: "Starter template: docs | api | sdk | changelog.",
|
|
183
|
+
type: "string",
|
|
184
|
+
},
|
|
76
185
|
yes: { description: "Skip prompts.", type: "boolean" },
|
|
77
186
|
},
|
|
78
187
|
meta: {
|
|
@@ -82,17 +191,76 @@ export const initCommand = defineCommand({
|
|
|
82
191
|
async run({ args }) {
|
|
83
192
|
const root = process.cwd();
|
|
84
193
|
const contentDir = args["content-dir"] ?? "docs";
|
|
194
|
+
// The content dir is joined into every scaffolded file path, so an absolute
|
|
195
|
+
// or `../`-escaping value would write outside the project. Reject it.
|
|
196
|
+
if (
|
|
197
|
+
isAbsolute(contentDir) ||
|
|
198
|
+
relative(root, join(root, contentDir)).startsWith("..")
|
|
199
|
+
) {
|
|
200
|
+
logger.error(
|
|
201
|
+
`Invalid --content-dir "${contentDir}" (must be a path inside the project).`
|
|
202
|
+
);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
85
205
|
|
|
206
|
+
const template = (args.template ?? "docs") as Template;
|
|
207
|
+
if (!TEMPLATES.includes(template)) {
|
|
208
|
+
logger.error(
|
|
209
|
+
`Unknown template "${args.template}" (use ${TEMPLATES.join(" | ")}).`
|
|
210
|
+
);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
const pm = (args["package-manager"] ?? "npm") as PackageManager;
|
|
214
|
+
if (!PACKAGE_MANAGERS.includes(pm)) {
|
|
215
|
+
logger.error(
|
|
216
|
+
`Unknown package manager "${args["package-manager"]}" (use ${PACKAGE_MANAGERS.join(" | ")}).`
|
|
217
|
+
);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const starter = STARTERS[template];
|
|
86
222
|
const createdPackage = await writeFileSafe(
|
|
87
223
|
join(root, "package.json"),
|
|
88
224
|
packageTemplate(toPackageName(basename(root)), getBlumeVersion())
|
|
89
225
|
);
|
|
90
|
-
await writeFileSafe(join(root, "blume.config.ts"),
|
|
91
|
-
await
|
|
226
|
+
await writeFileSafe(join(root, "blume.config.ts"), starter.config);
|
|
227
|
+
await Promise.all(
|
|
228
|
+
starter
|
|
229
|
+
.files(contentDir)
|
|
230
|
+
.map((file) => writeFileSafe(join(root, file.path), file.content))
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Keep Blume's generated runtime (`.blume/`) and build output (`dist/`) out
|
|
234
|
+
// of version control. Idempotent: creates `.gitignore` when absent and skips
|
|
235
|
+
// entries already present (trailing-slash agnostic).
|
|
236
|
+
const ignored = await ensureGitignore(root, [".blume/", "dist/"]);
|
|
237
|
+
if (ignored.length > 0) {
|
|
238
|
+
logger.success(`Added ${ignored.join(", ")} to .gitignore`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const commands = commandsFor(pm);
|
|
242
|
+
|
|
243
|
+
if (args.eject) {
|
|
244
|
+
// Eject only generates files (no Astro runtime), so it works right after
|
|
245
|
+
// scaffolding. The standalone app then runs with Astro directly.
|
|
246
|
+
try {
|
|
247
|
+
await eject(root);
|
|
248
|
+
logger.success("Ejected to a standalone Astro project.");
|
|
249
|
+
logger.box(`Next steps:\n\n ${commands.install}\n npx astro dev\n`);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
logger.warn(
|
|
252
|
+
`Scaffolded, but eject failed: ${(error as Error).message}`
|
|
253
|
+
);
|
|
254
|
+
logger.box(
|
|
255
|
+
`Next steps:\n\n ${commands.install}\n blume eject --yes\n`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
92
260
|
|
|
93
261
|
const nextSteps = createdPackage
|
|
94
|
-
?
|
|
95
|
-
:
|
|
262
|
+
? `Next steps:\n\n ${commands.install}\n ${commands.dev}\n`
|
|
263
|
+
: `Next steps:\n\n ${commands.dev}\n`;
|
|
96
264
|
logger.box(nextSteps);
|
|
97
265
|
},
|
|
98
266
|
});
|
|
@@ -6,6 +6,7 @@ import { join } from "pathe";
|
|
|
6
6
|
|
|
7
7
|
import { loadConfig } from "../../core/config.ts";
|
|
8
8
|
import { resolveProjectContext } from "../../core/project.ts";
|
|
9
|
+
import { parsePort } from "../args.ts";
|
|
9
10
|
import { logger } from "../log.ts";
|
|
10
11
|
|
|
11
12
|
export const previewCommand = defineCommand({
|
|
@@ -32,7 +33,7 @@ export const previewCommand = defineCommand({
|
|
|
32
33
|
root: context.outDir,
|
|
33
34
|
server: {
|
|
34
35
|
host: args.host ?? false,
|
|
35
|
-
port:
|
|
36
|
+
port: parsePort(args.port),
|
|
36
37
|
},
|
|
37
38
|
});
|
|
38
39
|
},
|
|
@@ -3,11 +3,18 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { join } from "pathe";
|
|
5
5
|
|
|
6
|
+
import { resolveAssetMounts } from "../../core/assets.ts";
|
|
6
7
|
import { BlumeError } from "../../core/diagnostics.ts";
|
|
7
8
|
import { validateLinks } from "../../core/links.ts";
|
|
8
9
|
import { scanProject } from "../../core/project-graph.ts";
|
|
9
10
|
import type { Diagnostic } from "../../core/types.ts";
|
|
10
|
-
import {
|
|
11
|
+
import { reportInternalError } from "../internal-error.ts";
|
|
12
|
+
import {
|
|
13
|
+
flushStdout,
|
|
14
|
+
logger,
|
|
15
|
+
reportDiagnostics,
|
|
16
|
+
reportDiagnosticsJson,
|
|
17
|
+
} from "../log.ts";
|
|
11
18
|
|
|
12
19
|
export const validateCommand = defineCommand({
|
|
13
20
|
args: {
|
|
@@ -15,6 +22,10 @@ export const validateCommand = defineCommand({
|
|
|
15
22
|
description: "Check external (HTTP) links over the network.",
|
|
16
23
|
type: "boolean",
|
|
17
24
|
},
|
|
25
|
+
json: {
|
|
26
|
+
description: "Emit diagnostics as JSON on stdout (for CI/editors).",
|
|
27
|
+
type: "boolean",
|
|
28
|
+
},
|
|
18
29
|
strict: {
|
|
19
30
|
description: "Treat warnings as errors.",
|
|
20
31
|
type: "boolean",
|
|
@@ -37,6 +48,7 @@ export const validateCommand = defineCommand({
|
|
|
37
48
|
const publicDir = join(root, "public");
|
|
38
49
|
diagnostics.push(
|
|
39
50
|
...(await validateLinks(project.graph, {
|
|
51
|
+
assetMounts: resolveAssetMounts(root, project.config.content.assets),
|
|
40
52
|
checkExternal: Boolean(args.external),
|
|
41
53
|
publicDir: existsSync(publicDir) ? publicDir : null,
|
|
42
54
|
redirects: project.config.redirects,
|
|
@@ -46,8 +58,21 @@ export const validateCommand = defineCommand({
|
|
|
46
58
|
if (error instanceof BlumeError) {
|
|
47
59
|
diagnostics.push(error.diagnostic);
|
|
48
60
|
} else {
|
|
49
|
-
|
|
61
|
+
reportInternalError(error);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (args.json) {
|
|
67
|
+
// Drain stdout before exiting non-zero: `process.exit` would otherwise
|
|
68
|
+
// truncate the JSON payload mid-write when stdout is a pipe — exactly how
|
|
69
|
+
// `--json` is consumed in CI/editors.
|
|
70
|
+
const hadErrors = reportDiagnosticsJson(diagnostics, root);
|
|
71
|
+
if (hadErrors || (Boolean(args.strict) && diagnostics.length > 0)) {
|
|
72
|
+
await flushStdout();
|
|
73
|
+
process.exit(1);
|
|
50
74
|
}
|
|
75
|
+
return;
|
|
51
76
|
}
|
|
52
77
|
|
|
53
78
|
const hadErrors = reportDiagnostics(diagnostics, root);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
|
|
9
|
+
import { join } from "pathe";
|
|
10
|
+
|
|
11
|
+
import { logger } from "./log.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A best-effort PID lock in the shared `.blume/` runtime dir. `blume dev`
|
|
15
|
+
* regenerates and serves `.blume` continuously, so a concurrent `build`,
|
|
16
|
+
* `eject`, or `sync --force` that regenerates or deletes it out from under the
|
|
17
|
+
* running Vite server corrupts the dev session. The lock lets those commands
|
|
18
|
+
* detect a live dev server and refuse.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const lockPath = (outDir: string): string => join(outDir, "dev.lock");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether another live `blume dev` holds the lock on `outDir`. A lock left by a
|
|
25
|
+
* process that has since exited (stale) is treated as absent.
|
|
26
|
+
*/
|
|
27
|
+
export const isDevLocked = (outDir: string): boolean => {
|
|
28
|
+
const path = lockPath(outDir);
|
|
29
|
+
if (!existsSync(path)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const pid = Number.parseInt(readFileSync(path, "utf-8").trim(), 10);
|
|
33
|
+
if (!(Number.isInteger(pid) && pid > 0)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
// Signal 0 probes liveness without actually signaling the process.
|
|
38
|
+
process.kill(pid, 0);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write the current process's dev lock into `outDir` and return a release
|
|
47
|
+
* function. The release only removes the file if it's still ours, so a newer
|
|
48
|
+
* dev server's lock is never clobbered.
|
|
49
|
+
*/
|
|
50
|
+
export const acquireDevLock = (outDir: string): (() => void) => {
|
|
51
|
+
const path = lockPath(outDir);
|
|
52
|
+
mkdirSync(outDir, { recursive: true });
|
|
53
|
+
writeFileSync(path, String(process.pid));
|
|
54
|
+
let released = false;
|
|
55
|
+
return () => {
|
|
56
|
+
if (released) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
released = true;
|
|
60
|
+
try {
|
|
61
|
+
if (
|
|
62
|
+
existsSync(path) &&
|
|
63
|
+
readFileSync(path, "utf-8").trim() === String(process.pid)
|
|
64
|
+
) {
|
|
65
|
+
rmSync(path, { force: true });
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Best-effort cleanup; a stale lock is handled by the liveness check.
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
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").
|
|
76
|
+
*/
|
|
77
|
+
export const refuseIfDevRunning = (root: string, action: string): void => {
|
|
78
|
+
if (isDevLocked(join(root, ".blume"))) {
|
|
79
|
+
logger.error(
|
|
80
|
+
`A \`blume dev\` server is running against .blume; ${action} would corrupt it. Stop the dev server first.`
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
};
|
package/src/cli/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { defineCommand, runMain } from "citty";
|
|
|
3
3
|
import { getBlumeVersion } from "../core/version.ts";
|
|
4
4
|
import { addCommand } from "./commands/add.ts";
|
|
5
5
|
import { buildCommand } from "./commands/build.ts";
|
|
6
|
+
import { checkCommand } from "./commands/check.ts";
|
|
6
7
|
import { devCommand } from "./commands/dev.ts";
|
|
7
8
|
import { doctorCommand } from "./commands/doctor.ts";
|
|
8
9
|
import { ejectCommand } from "./commands/eject.ts";
|
|
@@ -12,6 +13,7 @@ import { previewCommand } from "./commands/preview.ts";
|
|
|
12
13
|
import { syncCommand } from "./commands/sync.ts";
|
|
13
14
|
import { validateCommand } from "./commands/validate.ts";
|
|
14
15
|
import { loadEnvFiles } from "./env.ts";
|
|
16
|
+
import { reportInternalError } from "./internal-error.ts";
|
|
15
17
|
|
|
16
18
|
const main = defineCommand({
|
|
17
19
|
meta: {
|
|
@@ -22,6 +24,7 @@ const main = defineCommand({
|
|
|
22
24
|
subCommands: {
|
|
23
25
|
add: addCommand,
|
|
24
26
|
build: buildCommand,
|
|
27
|
+
check: checkCommand,
|
|
25
28
|
dev: devCommand,
|
|
26
29
|
doctor: doctorCommand,
|
|
27
30
|
eject: ejectCommand,
|
|
@@ -37,4 +40,16 @@ const main = defineCommand({
|
|
|
37
40
|
// can read their tokens (e.g. `GITHUB_TOKEN`) during the content scan.
|
|
38
41
|
loadEnvFiles(process.cwd());
|
|
39
42
|
|
|
43
|
+
// Backstop for unexpected async failures that escape a command's own handling
|
|
44
|
+
// (e.g. a rejected timer/watcher in `blume dev`), so even those report through
|
|
45
|
+
// the stable internal-error contract rather than a bare stack trace.
|
|
46
|
+
process.on("uncaughtException", (error) => {
|
|
47
|
+
reportInternalError(error);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
|
50
|
+
process.on("unhandledRejection", (error) => {
|
|
51
|
+
reportInternalError(error);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
40
55
|
runMain(main);
|