blume 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +3349 -7024
- package/dist/cli/index.js.map +39 -69
- package/dist/types/core/config.d.ts +0 -8
- package/dist/types/core/data.d.ts +6 -2
- package/dist/types/core/i18n-ui.d.ts +50 -0
- package/dist/types/core/schema.d.ts +379 -485
- package/dist/types/core/types.d.ts +8 -6
- package/docs/advanced/meta.ts +1 -8
- package/docs/advanced/skills.mdx +28 -0
- package/docs/configuration/ai.mdx +58 -0
- package/docs/configuration/index.mdx +13 -17
- package/docs/configuration/seo.mdx +59 -1
- package/docs/configuration/theming.mdx +15 -18
- package/docs/content/components.mdx +2 -53
- package/docs/content/i18n.mdx +0 -4
- package/docs/content/meta.mdx +3 -17
- package/docs/content/navigation.mdx +41 -4
- package/docs/content/syntax.mdx +1 -1
- package/docs/index.mdx +0 -1
- package/docs/reference/cli.mdx +12 -13
- package/package.json +6 -6
- package/skills/blume/SKILL.md +71 -0
- package/skills/blume-update-docs/SKILL.md +52 -0
- package/skills/blume-update-docs/references/audit-checklist.md +46 -0
- package/src/ai/agent-readability.ts +97 -0
- package/src/ai/ask-context.ts +131 -8
- package/src/ai/ask-data.ts +4 -1
- package/src/astro/generate.ts +19 -12
- package/src/astro/integration.ts +0 -21
- package/src/astro/templates.ts +33 -21
- package/src/cli/commands/build.ts +15 -0
- package/src/cli/commands/dev.ts +31 -20
- package/src/cli/commands/validate.ts +0 -2
- package/src/cli/dev-lock.ts +94 -21
- package/src/cli/index.ts +0 -2
- package/src/components/BlumePage.astro +0 -6
- package/src/components/Icon.astro +1 -12
- package/src/components/content/AccordionItem.astro +3 -6
- package/src/components/content/Badge.astro +1 -3
- package/src/components/content/Callout.astro +3 -9
- package/src/components/content/Card.astro +2 -3
- package/src/components/content/ColorItem.astro +2 -2
- package/src/components/content/Column.astro +1 -1
- package/src/components/content/GithubInfo.astro +11 -10
- package/src/components/content/Prompt.astro +1 -1
- package/src/components/content/Step.astro +3 -4
- package/src/components/content/Tab.astro +2 -3
- package/src/components/content/TypeTable.astro +13 -8
- package/src/components/content/Update.astro +1 -1
- package/src/components/islands/AskAI.astro +66 -2
- package/src/components/islands/ask-ai.tsx +289 -53
- package/src/components/layout/Header.astro +27 -4
- package/src/components/layout/Logo.astro +5 -1
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/NavTree.astro +15 -15
- package/src/components/layout/PageActions.astro +73 -30
- package/src/components/layout/PageLayout.astro +42 -0
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +79 -4
- package/src/components/layout/Search.astro +5 -5
- package/src/components/layout/nav-utils.ts +9 -4
- package/src/components/openapi/ApiOverview.astro +4 -50
- package/src/components/openapi/ApiTagOperations.astro +42 -0
- package/src/core/builtin-tags.ts +1 -3
- package/src/core/config.ts +5 -28
- package/src/core/data.ts +6 -2
- package/src/core/graph.ts +8 -6
- package/src/core/i18n-ui.ts +5 -0
- package/src/core/links.ts +5 -19
- package/src/core/meta.ts +1 -1
- package/src/core/nav-diagnostics.ts +7 -0
- package/src/core/navigation.ts +38 -17
- package/src/core/project-graph.ts +0 -5
- package/src/core/schema.ts +133 -95
- package/src/core/sources/filesystem.ts +5 -1
- package/src/core/sources/resolve.ts +0 -13
- package/src/core/sources/watch.ts +43 -11
- package/src/core/types.ts +8 -6
- package/src/deploy/robots.ts +37 -4
- package/src/openapi/parse.ts +197 -14
- package/src/openapi/render-mdx.ts +44 -10
- package/src/openapi/scalar.ts +1 -1
- package/src/openapi/source.ts +19 -2
- package/src/search/documents.ts +9 -2
- package/src/theme/entry.ts +45 -17
- package/src/theme/icons.ts +18 -109
- package/src/theme/palette.ts +25 -51
- package/src/theme/twoslash.ts +6 -1
- package/dist/types/core/bridge.d.ts +0 -24
- package/dist/types/core/package-json.d.ts +0 -12
- package/dist/types/migrate/mintlify/assets.d.ts +0 -8
- package/dist/types/migrate/mintlify/config.d.ts +0 -16
- package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
- package/dist/types/migrate/shared.d.ts +0 -153
- package/docs/advanced/bridge.mdx +0 -76
- package/docs/advanced/migrate.mdx +0 -124
- package/src/astro/static-assets.ts +0 -124
- package/src/cli/commands/migrate.ts +0 -39
- package/src/components/content/ApiField.astro +0 -75
- package/src/components/content/ParamField.astro +0 -39
- package/src/components/content/RequestField.astro +0 -23
- package/src/components/content/ResponseField.astro +0 -23
- package/src/components/content/Warning.astro +0 -9
- package/src/core/assets.ts +0 -31
- package/src/core/bridge.ts +0 -102
- package/src/core/sources/mintlify.ts +0 -190
- package/src/migrate/fumadocs/config.ts +0 -155
- package/src/migrate/fumadocs/content.ts +0 -376
- package/src/migrate/fumadocs/frontmatter.ts +0 -18
- package/src/migrate/fumadocs/groups.ts +0 -237
- package/src/migrate/fumadocs/index.ts +0 -355
- package/src/migrate/fumadocs/meta.ts +0 -244
- package/src/migrate/migrate.ts +0 -53
- package/src/migrate/mintlify/assets.ts +0 -46
- package/src/migrate/mintlify/config.ts +0 -954
- package/src/migrate/mintlify/content.ts +0 -120
- package/src/migrate/mintlify/frontmatter.ts +0 -126
- package/src/migrate/mintlify/i18n.ts +0 -51
- package/src/migrate/mintlify/icons.ts +0 -128
- package/src/migrate/mintlify/index.ts +0 -459
- package/src/migrate/mintlify/snippets.ts +0 -315
- package/src/migrate/mintlify/transform.ts +0 -82
- package/src/migrate/nextra/content.ts +0 -46
- package/src/migrate/nextra/frontmatter.ts +0 -40
- package/src/migrate/nextra/index.ts +0 -389
- package/src/migrate/nextra/meta.ts +0 -266
- package/src/migrate/shared.ts +0 -801
- package/src/migrate/starlight/config.ts +0 -455
- package/src/migrate/starlight/content.ts +0 -75
- package/src/migrate/starlight/frontmatter.ts +0 -111
- package/src/migrate/starlight/i18n.ts +0 -54
- package/src/migrate/starlight/index.ts +0 -131
package/src/astro/templates.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { dirname, isAbsolute, join, relative } from "pathe";
|
|
|
4
4
|
|
|
5
5
|
import { askBackendRuntimeDep } from "../ai/ask.ts";
|
|
6
6
|
import type { AskBackend } from "../ai/ask.ts";
|
|
7
|
-
import { resolveAssetMounts } from "../core/assets.ts";
|
|
8
7
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
8
|
+
import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
|
|
9
9
|
import type { ProjectContext } from "../core/types.ts";
|
|
10
10
|
import { hasScalarReferences } from "../openapi/references.ts";
|
|
11
11
|
import { searchProviderMeta } from "../search/providers.ts";
|
|
@@ -313,12 +313,10 @@ export const astroConfigTemplate = (options: {
|
|
|
313
313
|
if (needsSvelte) {
|
|
314
314
|
integrations.push("svelte()");
|
|
315
315
|
}
|
|
316
|
-
// Always mounted: injects user pages (a no-op when there are none)
|
|
317
|
-
//
|
|
318
|
-
// negotiation over the content routes.
|
|
319
|
-
const assets = resolveAssetMounts(context.root, config.content.assets);
|
|
316
|
+
// Always mounted: injects user pages (a no-op when there are none) and wires
|
|
317
|
+
// up dev-server `Accept: text/markdown` negotiation over the content routes.
|
|
320
318
|
integrations.push(
|
|
321
|
-
`blumeIntegration(${JSON.stringify({
|
|
319
|
+
`blumeIntegration(${JSON.stringify({ base: deployment.base, contentRoutes, pages })})`
|
|
322
320
|
);
|
|
323
321
|
|
|
324
322
|
return `// Generated by Blume. Do not edit; this file is recreated on each run.
|
|
@@ -357,9 +355,17 @@ export default defineConfig({
|
|
|
357
355
|
// native bindings resolve at runtime and isolated linkers don't bundle
|
|
358
356
|
// symlinked store copies (which would surface their children as unresolvable
|
|
359
357
|
// imports). See RENDER_EXTERNAL_DEPS / prerenderDepsPlugin.
|
|
358
|
+
//
|
|
359
|
+
// The SSR externals go through the legacy \`ssr.external\` key rather than
|
|
360
|
+
// \`environments.ssr\`: defining a user-owned \`environments.ssr\` block
|
|
361
|
+
// collides with the internal environment Astro 7 builds the server under and
|
|
362
|
+
// detaches the adapter's server entrypoint from the rolldown input, so the
|
|
363
|
+
// SSR entry is emitted as \`index.mjs\` instead of the \`entry.mjs\` the
|
|
364
|
+
// Vercel adapter's \`astro:build:done\` hook then fails to find. \`prerender\`
|
|
365
|
+
// is Astro-only and has no legacy equivalent, so it stays under \`environments\`.
|
|
366
|
+
ssr: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} },
|
|
360
367
|
environments: {
|
|
361
368
|
prerender: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
|
|
362
|
-
ssr: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
|
|
363
369
|
},
|
|
364
370
|
resolve: {
|
|
365
371
|
alias: {
|
|
@@ -403,8 +409,8 @@ export const contentConfigTemplate = (options: {
|
|
|
403
409
|
stagedBase?: string;
|
|
404
410
|
/**
|
|
405
411
|
* Whether any filesystem (non-staged) source feeds the `docs` collection.
|
|
406
|
-
* When false (e.g.
|
|
407
|
-
* collection globs nothing — see below.
|
|
412
|
+
* When false (e.g. an all-staged project where every page is materialized by
|
|
413
|
+
* a non-filesystem source), the collection globs nothing — see below.
|
|
408
414
|
*/
|
|
409
415
|
filesystem?: boolean;
|
|
410
416
|
}): string => {
|
|
@@ -423,8 +429,8 @@ export const contentConfigTemplate = (options: {
|
|
|
423
429
|
|
|
424
430
|
// With no filesystem source, no route renders through `docs`, so glob nothing.
|
|
425
431
|
// Beyond skipping wasted work, this is the only thing that keeps Astro's
|
|
426
|
-
// content-layer *watcher* out of `.blume/`:
|
|
427
|
-
// at the project dir (which contains `.blume/.astro/fonts`, rewritten on every
|
|
432
|
+
// content-layer *watcher* out of `.blume/`: an all-staged project roots the
|
|
433
|
+
// collection at the project dir (which contains `.blume/.astro/fonts`, rewritten on every
|
|
428
434
|
// request), and the watcher's match test is `picomatch.isMatch(path, pattern)`
|
|
429
435
|
// — with array-OR semantics, any `!ignored/**` negation *matches* unrelated
|
|
430
436
|
// files, so negative patterns can't exclude a subtree there. An empty pattern
|
|
@@ -435,7 +441,15 @@ export const contentConfigTemplate = (options: {
|
|
|
435
441
|
? [
|
|
436
442
|
...config.content.include,
|
|
437
443
|
...(config.content.exclude ?? []).map((pattern) => `!${pattern}`),
|
|
438
|
-
|
|
444
|
+
// Mirror the filesystem scan's baseline ignores (see BLUME_IGNORE_DIRS):
|
|
445
|
+
// Astro's content layer roots at the project dir, so a `.`-wide content
|
|
446
|
+
// root would otherwise re-ingest dependency trees and build output —
|
|
447
|
+
// e.g. a prior `dist/*.mdx` render — and crash the content-module graph.
|
|
448
|
+
// The runtime dir (`.blume`, or a custom distDir) is excluded precisely
|
|
449
|
+
// by `outDirIgnore` instead, so it's left out of this baseline.
|
|
450
|
+
...BLUME_IGNORE_DIRS.filter((dir) => dir !== ".blume").map(
|
|
451
|
+
(dir) => `!**/${dir}/**`
|
|
452
|
+
),
|
|
439
453
|
...outDirIgnore,
|
|
440
454
|
]
|
|
441
455
|
: [];
|
|
@@ -869,7 +883,7 @@ const siteHost = (() => {
|
|
|
869
883
|
|
|
870
884
|
export async function GET({ props }) {
|
|
871
885
|
const png = await renderOgImage({
|
|
872
|
-
accent: data.config.theme.accent,
|
|
886
|
+
accent: data.config.theme.accent.light,
|
|
873
887
|
brand: data.config.title,
|
|
874
888
|
description: data.config.description,
|
|
875
889
|
logo: data.config.logo?.svg,
|
|
@@ -942,7 +956,7 @@ export const catchAllPageTemplate = (options: {
|
|
|
942
956
|
? 'import AskAI from "blume/components/islands/AskAI.astro";\n'
|
|
943
957
|
: "";
|
|
944
958
|
const askSlot = options.askEnabled
|
|
945
|
-
? '\n <AskAI slot="ask" strings={ui.ask} />'
|
|
959
|
+
? '\n <AskAI slot="ask" strings={ui.ask} suggestions={data.config.ask?.suggestions ?? []} />'
|
|
946
960
|
: "";
|
|
947
961
|
const mathImport = options.mathEnabled
|
|
948
962
|
? 'import Math from "blume/components/content/Math.astro";\n'
|
|
@@ -980,10 +994,7 @@ import FileTree from "blume/components/content/FileTree.astro";
|
|
|
980
994
|
import Frame from "blume/components/content/Frame.astro";
|
|
981
995
|
import GithubInfo from "blume/components/content/GithubInfo.astro";
|
|
982
996
|
import Panel from "blume/components/content/Panel.astro";
|
|
983
|
-
import ParamField from "blume/components/content/ParamField.astro";
|
|
984
997
|
import Prompt from "blume/components/content/Prompt.astro";
|
|
985
|
-
import RequestField from "blume/components/content/RequestField.astro";
|
|
986
|
-
import ResponseField from "blume/components/content/ResponseField.astro";
|
|
987
998
|
import Step from "blume/components/content/Step.astro";
|
|
988
999
|
import Steps from "blume/components/content/Steps.astro";
|
|
989
1000
|
import Tab from "blume/components/content/Tab.astro";
|
|
@@ -998,6 +1009,7 @@ import Visibility from "blume/components/content/Visibility.astro";
|
|
|
998
1009
|
import YouTube from "blume/components/content/YouTube.astro";
|
|
999
1010
|
import Icon from "blume/components/Icon.astro";
|
|
1000
1011
|
import ApiOverview from "blume/components/openapi/ApiOverview.astro";
|
|
1012
|
+
import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
|
|
1001
1013
|
import Operation from "blume/components/openapi/Operation.astro";
|
|
1002
1014
|
${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
|
|
1003
1015
|
import { islandComponents } from "../generated/islands.ts";
|
|
@@ -1016,6 +1028,7 @@ const components = {
|
|
|
1016
1028
|
Accordion,
|
|
1017
1029
|
AccordionItem,
|
|
1018
1030
|
ApiOverview,
|
|
1031
|
+
ApiTagOperations,
|
|
1019
1032
|
AutoTypeTable,
|
|
1020
1033
|
Badge,
|
|
1021
1034
|
Callout,
|
|
@@ -1035,10 +1048,7 @@ const components = {
|
|
|
1035
1048
|
Icon,
|
|
1036
1049
|
Operation,
|
|
1037
1050
|
Panel,
|
|
1038
|
-
ParamField,
|
|
1039
1051
|
Prompt,
|
|
1040
|
-
RequestField,
|
|
1041
|
-
ResponseField,
|
|
1042
1052
|
Step,
|
|
1043
1053
|
Steps,
|
|
1044
1054
|
Tab,
|
|
@@ -1217,7 +1227,9 @@ export const changelogIndexTemplate = (options: {
|
|
|
1217
1227
|
const askImport = options.askEnabled
|
|
1218
1228
|
? 'import AskAI from "blume/components/islands/AskAI.astro";\n'
|
|
1219
1229
|
: "";
|
|
1220
|
-
const askSlot = options.askEnabled
|
|
1230
|
+
const askSlot = options.askEnabled
|
|
1231
|
+
? '\n <AskAI slot="ask" strings={data.ui.ask} suggestions={data.config.ask?.suggestions ?? []} />'
|
|
1232
|
+
: "";
|
|
1221
1233
|
const clientData = options.needsReact
|
|
1222
1234
|
? '\n clientData={{ config: data.config, navigation: data.navigation, page: { route: "/changelog", title: data.config.title + " changelog" } }}'
|
|
1223
1235
|
: "";
|
|
@@ -5,6 +5,7 @@ import { build } from "astro";
|
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { join } from "pathe";
|
|
7
7
|
|
|
8
|
+
import { buildAgentReadability } from "../../ai/agent-readability.ts";
|
|
8
9
|
import { buildLlmsFiles } from "../../ai/llms.ts";
|
|
9
10
|
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
10
11
|
import type { BlumeProject } from "../../core/project-graph.ts";
|
|
@@ -218,6 +219,19 @@ const publishBuildArtifacts = async (
|
|
|
218
219
|
logger.success("Generated robots.txt");
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
const agentReadability = buildAgentReadability(project);
|
|
223
|
+
if (
|
|
224
|
+
agentReadability &&
|
|
225
|
+
!existsSync(join(distDir, "agent-readability.json"))
|
|
226
|
+
) {
|
|
227
|
+
await writeFile(
|
|
228
|
+
join(distDir, "agent-readability.json"),
|
|
229
|
+
`${JSON.stringify(agentReadability, null, 2)}\n`,
|
|
230
|
+
"utf-8"
|
|
231
|
+
);
|
|
232
|
+
logger.success("Generated agent-readability.json");
|
|
233
|
+
}
|
|
234
|
+
|
|
221
235
|
await emitRedirectFiles(project.config, distDir);
|
|
222
236
|
|
|
223
237
|
const { config } = project;
|
|
@@ -231,6 +245,7 @@ const publishBuildArtifacts = async (
|
|
|
231
245
|
`Redirects ${config.redirects.length}`,
|
|
232
246
|
`Sitemap ${sitemap ? "yes" : "no (set deployment.site)"}`,
|
|
233
247
|
`Robots ${robots ? "yes" : "no"}`,
|
|
248
|
+
`Agent JSON ${agentReadability ? "yes" : "no"}`,
|
|
234
249
|
`LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
|
|
235
250
|
`Server features ${features.length > 0 ? features.join(", ") : "none"}`,
|
|
236
251
|
].join("\n")
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -6,9 +6,15 @@ import { defineCommand } from "citty";
|
|
|
6
6
|
import { generateRuntime } from "../../astro/generate.ts";
|
|
7
7
|
import { showBlumeErrorOverlay } from "../../astro/integration.ts";
|
|
8
8
|
import { scanProject } from "../../core/project-graph.ts";
|
|
9
|
+
import { resolveRuntimeDir } from "../../core/project.ts";
|
|
9
10
|
import { parsePort } from "../args.ts";
|
|
10
11
|
import { coalescedRunner } from "../coalesce.ts";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
acquireDevLock,
|
|
14
|
+
describeDevLock,
|
|
15
|
+
readDevLock,
|
|
16
|
+
updateDevLockPort,
|
|
17
|
+
} from "../dev-lock.ts";
|
|
12
18
|
import { logger } from "../log.ts";
|
|
13
19
|
import { prepareProject } from "../prepare.ts";
|
|
14
20
|
|
|
@@ -47,6 +53,23 @@ export const devCommand = defineCommand({
|
|
|
47
53
|
const explicitPort = parsePort(args.port);
|
|
48
54
|
const port = explicitPort ?? 4321;
|
|
49
55
|
const devServerUrl = `http://localhost:${port}`;
|
|
56
|
+
|
|
57
|
+
// Claim the shared `.blume` dir BEFORE preparing: `prepareProject`
|
|
58
|
+
// regenerates the runtime, so even a refused second dev server would
|
|
59
|
+
// otherwise clobber the running one's generated tree (with this
|
|
60
|
+
// invocation's port baked in) on its way out. Dev never relocates the
|
|
61
|
+
// runtime dir, so the lock always lives at `<root>/.blume`.
|
|
62
|
+
const outDir = resolveRuntimeDir(root);
|
|
63
|
+
const running = readDevLock(outDir);
|
|
64
|
+
if (running) {
|
|
65
|
+
logger.error(
|
|
66
|
+
`A \`blume dev\` server is already running${describeDevLock(running)} in this project. Reuse that server instead of starting a second one — two dev servers would corrupt the shared .blume dir. If it crashed, delete .blume/dev.lock.`
|
|
67
|
+
);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const releaseLock = acquireDevLock(outDir, port);
|
|
71
|
+
process.on("exit", releaseLock);
|
|
72
|
+
|
|
50
73
|
const project = await prepareProject({
|
|
51
74
|
devServerUrl,
|
|
52
75
|
mode: "dev",
|
|
@@ -56,25 +79,6 @@ export const devCommand = defineCommand({
|
|
|
56
79
|
strict: args.strict,
|
|
57
80
|
});
|
|
58
81
|
|
|
59
|
-
if (project.bridge) {
|
|
60
|
-
logger.info(
|
|
61
|
-
'Detected docs.json — running in Mintlify bridge mode (no migration). Run "blume migrate mintlify" to convert permanently.'
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Claim the shared `.blume` dir so a concurrent build/eject/sync refuses
|
|
66
|
-
// rather than regenerating or deleting it out from under this server. A
|
|
67
|
-
// second dev server would fight over the same generated tree the same
|
|
68
|
-
// way, so it must refuse too instead of silently clobbering the lock.
|
|
69
|
-
if (isDevLocked(project.context.outDir)) {
|
|
70
|
-
logger.error(
|
|
71
|
-
"Another `blume dev` is already running in this project; two dev servers would corrupt the shared .blume dir. Stop the other one first (or delete .blume/dev.lock if it crashed)."
|
|
72
|
-
);
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
const releaseLock = acquireDevLock(project.context.outDir);
|
|
76
|
-
process.on("exit", releaseLock);
|
|
77
|
-
|
|
78
82
|
const server = await dev({
|
|
79
83
|
logLevel: args.debug ? "debug" : "info",
|
|
80
84
|
root: project.context.outDir,
|
|
@@ -85,6 +89,13 @@ export const devCommand = defineCommand({
|
|
|
85
89
|
},
|
|
86
90
|
});
|
|
87
91
|
|
|
92
|
+
// Vite bumps to the next free port when the default is taken, so record
|
|
93
|
+
// the port the server actually bound — the lock's URL is what a refused
|
|
94
|
+
// second invocation tells its caller to reuse.
|
|
95
|
+
if (server.address.port !== port) {
|
|
96
|
+
updateDevLockPort(outDir, server.address.port);
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
// Mirror any initial diagnostics into the browser overlay now the server
|
|
89
100
|
// (and its HMR channel) is up.
|
|
90
101
|
showBlumeErrorOverlay(project.diagnostics);
|
|
@@ -3,7 +3,6 @@ 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";
|
|
7
6
|
import { BlumeError } from "../../core/diagnostics.ts";
|
|
8
7
|
import { validateLinks } from "../../core/links.ts";
|
|
9
8
|
import { scanProject } from "../../core/project-graph.ts";
|
|
@@ -48,7 +47,6 @@ export const validateCommand = defineCommand({
|
|
|
48
47
|
const publicDir = join(root, "public");
|
|
49
48
|
diagnostics.push(
|
|
50
49
|
...(await validateLinks(project.graph, {
|
|
51
|
-
assetMounts: resolveAssetMounts(root, project.config.content.assets),
|
|
52
50
|
checkExternal: Boolean(args.external),
|
|
53
51
|
publicDir: existsSync(publicDir) ? publicDir : null,
|
|
54
52
|
redirects: project.config.redirects,
|
package/src/cli/dev-lock.ts
CHANGED
|
@@ -14,26 +14,48 @@ import { logger } from "./log.ts";
|
|
|
14
14
|
/**
|
|
15
15
|
* A best-effort PID lock in the shared `.blume/` runtime dir. `blume dev`
|
|
16
16
|
* regenerates and serves `.blume` continuously, so a concurrent `build`,
|
|
17
|
-
* `eject`, or
|
|
17
|
+
* `eject`, or second `dev` that regenerates or deletes it out from under the
|
|
18
18
|
* running Vite server corrupts the dev session. The lock lets those commands
|
|
19
|
-
* detect a live dev server and refuse
|
|
19
|
+
* detect a live dev server and refuse — and, because it records the server's
|
|
20
|
+
* port, point the caller (often an agent that just tried to start its own
|
|
21
|
+
* server) at the URL to reuse instead.
|
|
20
22
|
*/
|
|
21
23
|
|
|
24
|
+
export interface DevLockInfo {
|
|
25
|
+
pid: number;
|
|
26
|
+
/** Port the dev server is bound to, when known. */
|
|
27
|
+
port?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
const lockPath = (outDir: string): string => join(outDir, "dev.lock");
|
|
23
31
|
|
|
32
|
+
const isValidPid = (pid: unknown): pid is number =>
|
|
33
|
+
typeof pid === "number" && Number.isInteger(pid) && pid > 0;
|
|
34
|
+
|
|
24
35
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
36
|
+
* Parse a lock file body. Current locks are JSON (`{"pid":123,"port":3001}`);
|
|
37
|
+
* a bare integer (the pre-port format) still parses as a pid-only lock.
|
|
27
38
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
const parseLock = (raw: string): DevLockInfo | null => {
|
|
40
|
+
let data: unknown;
|
|
41
|
+
try {
|
|
42
|
+
data = JSON.parse(raw.trim());
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
32
45
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return false;
|
|
46
|
+
if (isValidPid(data)) {
|
|
47
|
+
return { pid: data };
|
|
36
48
|
}
|
|
49
|
+
if (typeof data === "object" && data !== null) {
|
|
50
|
+
const { pid, port } = data as { pid?: unknown; port?: unknown };
|
|
51
|
+
if (isValidPid(pid)) {
|
|
52
|
+
return typeof port === "number" ? { pid, port } : { pid };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isProcessAlive = (pid: number): boolean => {
|
|
37
59
|
try {
|
|
38
60
|
// Signal 0 probes liveness without actually signaling the process.
|
|
39
61
|
process.kill(pid, 0);
|
|
@@ -45,15 +67,53 @@ export const isDevLocked = (outDir: string): boolean => {
|
|
|
45
67
|
}
|
|
46
68
|
};
|
|
47
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Read the lock on `outDir` held by a live `blume dev`, or null. A lock left
|
|
72
|
+
* by a process that has since exited (stale) is treated as absent.
|
|
73
|
+
*/
|
|
74
|
+
export const readDevLock = (outDir: string): DevLockInfo | null => {
|
|
75
|
+
const path = lockPath(outDir);
|
|
76
|
+
if (!existsSync(path)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const lock = parseLock(readFileSync(path, "utf-8"));
|
|
80
|
+
return lock && isProcessAlive(lock.pid) ? lock : null;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Whether another live `blume dev` holds the lock on `outDir`. */
|
|
84
|
+
export const isDevLocked = (outDir: string): boolean =>
|
|
85
|
+
readDevLock(outDir) !== null;
|
|
86
|
+
|
|
87
|
+
const writeLock = (outDir: string, port?: number): void => {
|
|
88
|
+
writeFileSync(
|
|
89
|
+
lockPath(outDir),
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
pid: process.pid,
|
|
92
|
+
...(port === undefined ? {} : { port }),
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const ownsLock = (outDir: string): boolean => {
|
|
98
|
+
const path = lockPath(outDir);
|
|
99
|
+
if (!existsSync(path)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return parseLock(readFileSync(path, "utf-8"))?.pid === process.pid;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
48
109
|
/**
|
|
49
110
|
* Write the current process's dev lock into `outDir` and return a release
|
|
50
111
|
* function. The release only removes the file if it's still ours, so a newer
|
|
51
112
|
* dev server's lock is never clobbered.
|
|
52
113
|
*/
|
|
53
|
-
export const acquireDevLock = (outDir: string): (() => void) => {
|
|
54
|
-
const path = lockPath(outDir);
|
|
114
|
+
export const acquireDevLock = (outDir: string, port?: number): (() => void) => {
|
|
55
115
|
mkdirSync(outDir, { recursive: true });
|
|
56
|
-
|
|
116
|
+
writeLock(outDir, port);
|
|
57
117
|
let released = false;
|
|
58
118
|
return () => {
|
|
59
119
|
if (released) {
|
|
@@ -61,11 +121,8 @@ export const acquireDevLock = (outDir: string): (() => void) => {
|
|
|
61
121
|
}
|
|
62
122
|
released = true;
|
|
63
123
|
try {
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
readFileSync(path, "utf-8").trim() === String(process.pid)
|
|
67
|
-
) {
|
|
68
|
-
rmSync(path, { force: true });
|
|
124
|
+
if (ownsLock(outDir)) {
|
|
125
|
+
rmSync(lockPath(outDir), { force: true });
|
|
69
126
|
}
|
|
70
127
|
} catch {
|
|
71
128
|
// Best-effort cleanup; a stale lock is handled by the liveness check.
|
|
@@ -73,6 +130,21 @@ export const acquireDevLock = (outDir: string): (() => void) => {
|
|
|
73
130
|
};
|
|
74
131
|
};
|
|
75
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Rewrite this process's lock with the port the server actually bound (the
|
|
135
|
+
* lock is acquired before the server starts, and Vite may bump a busy port).
|
|
136
|
+
* A lock owned by another process is left alone.
|
|
137
|
+
*/
|
|
138
|
+
export const updateDevLockPort = (outDir: string, port: number): void => {
|
|
139
|
+
if (ownsLock(outDir)) {
|
|
140
|
+
writeLock(outDir, port);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Human-readable location of a locked dev server, e.g. " at http://localhost:3001". */
|
|
145
|
+
export const describeDevLock = (lock: DevLockInfo): string =>
|
|
146
|
+
lock.port === undefined ? "" : ` at http://localhost:${lock.port}`;
|
|
147
|
+
|
|
76
148
|
/**
|
|
77
149
|
* Exit with an error when a live `blume dev` owns the runtime dir under `root`.
|
|
78
150
|
* `action` names the operation being refused (e.g. "building"). `runtimeDir`
|
|
@@ -85,9 +157,10 @@ export const refuseIfDevRunning = (
|
|
|
85
157
|
action: string,
|
|
86
158
|
runtimeDir?: string
|
|
87
159
|
): void => {
|
|
88
|
-
|
|
160
|
+
const lock = readDevLock(resolveRuntimeDir(root, runtimeDir));
|
|
161
|
+
if (lock) {
|
|
89
162
|
logger.error(
|
|
90
|
-
`A \`blume dev\` server is running
|
|
163
|
+
`A \`blume dev\` server is running${describeDevLock(lock)}; ${action} would corrupt its .blume runtime. Reuse that server, stop it first, or re-run with --isolated to build/verify against .blume-verify without touching it.`
|
|
91
164
|
);
|
|
92
165
|
process.exit(1);
|
|
93
166
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { devCommand } from "./commands/dev.ts";
|
|
|
8
8
|
import { doctorCommand } from "./commands/doctor.ts";
|
|
9
9
|
import { ejectCommand } from "./commands/eject.ts";
|
|
10
10
|
import { initCommand } from "./commands/init.ts";
|
|
11
|
-
import { migrateCommand } from "./commands/migrate.ts";
|
|
12
11
|
import { previewCommand } from "./commands/preview.ts";
|
|
13
12
|
import { syncCommand } from "./commands/sync.ts";
|
|
14
13
|
import { validateCommand } from "./commands/validate.ts";
|
|
@@ -29,7 +28,6 @@ const main = defineCommand({
|
|
|
29
28
|
doctor: doctorCommand,
|
|
30
29
|
eject: ejectCommand,
|
|
31
30
|
init: initCommand,
|
|
32
|
-
migrate: migrateCommand,
|
|
33
31
|
preview: previewCommand,
|
|
34
32
|
sync: syncCommand,
|
|
35
33
|
validate: validateCommand,
|
|
@@ -38,10 +38,7 @@ 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";
|
|
42
41
|
import Prompt from "./content/Prompt.astro";
|
|
43
|
-
import RequestField from "./content/RequestField.astro";
|
|
44
|
-
import ResponseField from "./content/ResponseField.astro";
|
|
45
42
|
import Step from "./content/Step.astro";
|
|
46
43
|
import Steps from "./content/Steps.astro";
|
|
47
44
|
import Tab from "./content/Tab.astro";
|
|
@@ -90,10 +87,7 @@ const components = {
|
|
|
90
87
|
GithubInfo,
|
|
91
88
|
Icon,
|
|
92
89
|
Panel,
|
|
93
|
-
ParamField,
|
|
94
90
|
Prompt,
|
|
95
|
-
RequestField,
|
|
96
|
-
ResponseField,
|
|
97
91
|
Step,
|
|
98
92
|
Steps,
|
|
99
93
|
Tab,
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
import data from "blume:data";
|
|
3
2
|
import { resolveIcon } from "../theme/icons.ts";
|
|
4
3
|
|
|
5
4
|
interface Props {
|
|
6
5
|
name?: unknown;
|
|
7
6
|
icon?: unknown;
|
|
8
|
-
iconType?: string;
|
|
9
|
-
/** Icon library for a bare name; defaults to the project's `icons.library`. */
|
|
10
|
-
library?: string;
|
|
11
7
|
size?: number;
|
|
12
8
|
class?: string;
|
|
13
9
|
className?: string;
|
|
@@ -18,8 +14,6 @@ interface Props {
|
|
|
18
14
|
const {
|
|
19
15
|
color,
|
|
20
16
|
icon,
|
|
21
|
-
iconType,
|
|
22
|
-
library,
|
|
23
17
|
name,
|
|
24
18
|
size = 16,
|
|
25
19
|
class: astroClass,
|
|
@@ -41,12 +35,7 @@ const isImageIcon = (value: string): boolean =>
|
|
|
41
35
|
/\.(?:avif|gif|jpe?g|png|svg|webp)$/iu.test(value);
|
|
42
36
|
const imageSrc = iconName && !rawSvg && isImageIcon(iconName) ? iconName : null;
|
|
43
37
|
const resolvedIcon =
|
|
44
|
-
iconName && !(imageSrc || rawSvg)
|
|
45
|
-
? resolveIcon(iconName, {
|
|
46
|
-
iconType,
|
|
47
|
-
library: library ?? data.config.icons.library,
|
|
48
|
-
})
|
|
49
|
-
: null;
|
|
38
|
+
iconName && !(imageSrc || rawSvg) ? resolveIcon(iconName) : null;
|
|
50
39
|
const resolvedClass = astroClass ?? className;
|
|
51
40
|
const customStyle = `display:inline-flex;width:${size}px;height:${size}px;${
|
|
52
41
|
color ? `color:${color}` : ""
|
|
@@ -7,11 +7,9 @@ interface Props {
|
|
|
7
7
|
defaultOpen?: boolean;
|
|
8
8
|
id?: string;
|
|
9
9
|
icon?: string;
|
|
10
|
-
iconType?: string;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
const { defaultOpen = false, description, icon,
|
|
14
|
-
Astro.props;
|
|
12
|
+
const { defaultOpen = false, description, icon, id, title } = Astro.props;
|
|
15
13
|
const slugify = (value: string): string =>
|
|
16
14
|
value
|
|
17
15
|
.toLowerCase()
|
|
@@ -32,18 +30,17 @@ const accordionId = id ?? slugify(title);
|
|
|
32
30
|
<summary
|
|
33
31
|
class="flex cursor-pointer list-none items-center justify-between p-4 font-medium [&::-webkit-details-marker]:hidden"
|
|
34
32
|
>
|
|
35
|
-
<span class="flex
|
|
33
|
+
<span class="flex items-start gap-2">
|
|
36
34
|
{
|
|
37
35
|
icon && (
|
|
38
36
|
<Icon
|
|
39
37
|
class="mt-0.5 shrink-0 text-muted-foreground"
|
|
40
|
-
iconType={iconType}
|
|
41
38
|
name={icon}
|
|
42
39
|
size={16}
|
|
43
40
|
/>
|
|
44
41
|
)
|
|
45
42
|
}
|
|
46
|
-
<span
|
|
43
|
+
<span>
|
|
47
44
|
<span class="block">{title}</span>
|
|
48
45
|
{
|
|
49
46
|
description && (
|
|
@@ -10,7 +10,6 @@ interface Props {
|
|
|
10
10
|
color?: string;
|
|
11
11
|
disabled?: boolean | string;
|
|
12
12
|
icon?: unknown;
|
|
13
|
-
iconType?: string;
|
|
14
13
|
shape?: BadgeShape;
|
|
15
14
|
size?: BadgeSize;
|
|
16
15
|
stroke?: boolean | string;
|
|
@@ -23,7 +22,6 @@ const {
|
|
|
23
22
|
color,
|
|
24
23
|
disabled = false,
|
|
25
24
|
icon,
|
|
26
|
-
iconType,
|
|
27
25
|
shape = "rounded",
|
|
28
26
|
size = "md",
|
|
29
27
|
stroke = false,
|
|
@@ -119,6 +117,6 @@ const customStyle = customColor
|
|
|
119
117
|
style={customStyle}
|
|
120
118
|
title={tooltip}
|
|
121
119
|
>
|
|
122
|
-
{icon && <Icon icon={icon}
|
|
120
|
+
{icon && <Icon icon={icon} size={iconSize[size]} />}
|
|
123
121
|
<slot />
|
|
124
122
|
</span>
|
|
@@ -14,11 +14,10 @@ interface Props {
|
|
|
14
14
|
type?: CalloutType;
|
|
15
15
|
title?: string;
|
|
16
16
|
icon?: unknown;
|
|
17
|
-
iconType?: string;
|
|
18
17
|
color?: string;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
const { color, icon,
|
|
20
|
+
const { color, icon, type = "info", title } = Astro.props;
|
|
22
21
|
|
|
23
22
|
const iconByType: Record<CalloutType, string> = {
|
|
24
23
|
check: "circle-check",
|
|
@@ -59,14 +58,9 @@ const iconClass: Record<CalloutType, string> = {
|
|
|
59
58
|
style={color ? `border:1px solid ${color};color:${color}` : undefined}
|
|
60
59
|
>
|
|
61
60
|
<span class:list={["mt-0.5 shrink-0", color ? "" : iconClass[type]]}>
|
|
62
|
-
<Icon
|
|
63
|
-
color={color}
|
|
64
|
-
icon={icon ?? iconByType[type]}
|
|
65
|
-
iconType={iconType}
|
|
66
|
-
size={16}
|
|
67
|
-
/>
|
|
61
|
+
<Icon color={color} icon={icon ?? iconByType[type]} size={16} />
|
|
68
62
|
</span>
|
|
69
|
-
<div class="
|
|
63
|
+
<div class="flex-1 [&>:first-child]:mt-0! [&>:last-child]:mb-0!">
|
|
70
64
|
{title && <p class="mb-1 font-semibold text-foreground">{title}</p>}
|
|
71
65
|
<slot />
|
|
72
66
|
</div>
|
|
@@ -5,7 +5,6 @@ interface Props {
|
|
|
5
5
|
title?: string;
|
|
6
6
|
href?: string;
|
|
7
7
|
icon?: string;
|
|
8
|
-
iconType?: string;
|
|
9
8
|
img?: string;
|
|
10
9
|
horizontal?: boolean | string;
|
|
11
10
|
cta?: string;
|
|
@@ -14,7 +13,7 @@ interface Props {
|
|
|
14
13
|
color?: string;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
const { arrow, color, cta, horizontal, href, icon,
|
|
16
|
+
const { arrow, color, cta, horizontal, href, icon, img, title, type } =
|
|
18
17
|
Astro.props;
|
|
19
18
|
const Tag = href ? "a" : "div";
|
|
20
19
|
const external = href?.startsWith("http");
|
|
@@ -75,7 +74,7 @@ const variantClass = {
|
|
|
75
74
|
{
|
|
76
75
|
iconName && (
|
|
77
76
|
<div class:list={["mb-2.5", color ? "" : type ? iconClass[type] : "text-accent"]}>
|
|
78
|
-
<Icon color={color}
|
|
77
|
+
<Icon color={color} name={iconName} size={20} />
|
|
79
78
|
</div>
|
|
80
79
|
)
|
|
81
80
|
}
|
|
@@ -21,7 +21,7 @@ const displayValue =
|
|
|
21
21
|
|
|
22
22
|
<button
|
|
23
23
|
aria-label={`Copy ${name} color ${displayValue}`}
|
|
24
|
-
class="group flex w-full
|
|
24
|
+
class="group flex w-full cursor-pointer flex-col gap-2.5 text-center focus:outline-none"
|
|
25
25
|
data-blume-color-copy={displayValue}
|
|
26
26
|
style={`--blume-color-light:${lightValue};--blume-color-dark:${darkValue};`}
|
|
27
27
|
type="button"
|
|
@@ -32,7 +32,7 @@ const displayValue =
|
|
|
32
32
|
style="background:var(--blume-color-light)"
|
|
33
33
|
>
|
|
34
34
|
</span>
|
|
35
|
-
<span class="block
|
|
35
|
+
<span class="block leading-tight">
|
|
36
36
|
<span class="block truncate font-semibold text-foreground text-sm">
|
|
37
37
|
{name}
|
|
38
38
|
</span>
|