blume 0.5.1 → 0.5.2
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 +121 -78
- package/dist/cli/index.js.map +10 -9
- package/package.json +1 -1
- package/src/astro/generate.ts +11 -1
- package/src/astro/templates.ts +34 -8
- package/src/cli/commands/init.ts +2 -27
- package/src/core/package-json.ts +32 -0
- package/src/core/sources/mintlify.ts +44 -1
- package/src/migrate/mintlify/index.ts +24 -0
- package/src/migrate/shared.ts +24 -1
package/package.json
CHANGED
package/src/astro/generate.ts
CHANGED
|
@@ -973,6 +973,11 @@ export const generateRuntime = async (
|
|
|
973
973
|
// entryId so i18n duplicates of one entry write a single file.
|
|
974
974
|
const staged = collectStaged(project);
|
|
975
975
|
const hasStaged = staged.size > 0;
|
|
976
|
+
// Only emit a project-scanning `docs` collection when a filesystem source
|
|
977
|
+
// actually feeds it. Bridge mode has just the staged Mintlify source, so the
|
|
978
|
+
// `docs` glob would otherwise scan (and watch) the whole project root for
|
|
979
|
+
// nothing — see contentConfigTemplate.
|
|
980
|
+
const hasFilesystemSource = project.sources.some((source) => !source.staged);
|
|
976
981
|
|
|
977
982
|
const structural = await Promise.all([
|
|
978
983
|
write(
|
|
@@ -1003,7 +1008,12 @@ export const generateRuntime = async (
|
|
|
1003
1008
|
write(join(srcDir, "env.d.ts"), envTemplate()),
|
|
1004
1009
|
write(
|
|
1005
1010
|
join(srcDir, "content.config.ts"),
|
|
1006
|
-
contentConfigTemplate({
|
|
1011
|
+
contentConfigTemplate({
|
|
1012
|
+
config,
|
|
1013
|
+
context,
|
|
1014
|
+
filesystem: hasFilesystemSource,
|
|
1015
|
+
staged: hasStaged,
|
|
1016
|
+
})
|
|
1007
1017
|
),
|
|
1008
1018
|
write(
|
|
1009
1019
|
join(srcDir, "pages", "[...slug].astro"),
|
package/src/astro/templates.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import { dirname, join } from "pathe";
|
|
3
|
+
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";
|
|
@@ -392,18 +392,44 @@ export const contentConfigTemplate = (options: {
|
|
|
392
392
|
staged?: boolean;
|
|
393
393
|
/** Base dir for the staged collection; defaults to `<outDir>/content`. */
|
|
394
394
|
stagedBase?: string;
|
|
395
|
+
/**
|
|
396
|
+
* Whether any filesystem (non-staged) source feeds the `docs` collection.
|
|
397
|
+
* When false (e.g. Mintlify bridge mode, where every page is staged), the
|
|
398
|
+
* collection globs nothing — see below.
|
|
399
|
+
*/
|
|
400
|
+
filesystem?: boolean;
|
|
395
401
|
}): string => {
|
|
396
402
|
const { context, config } = options;
|
|
397
403
|
const stagedBase = options.stagedBase ?? stagedContentDir(context.outDir);
|
|
398
404
|
|
|
399
405
|
// Fold the content excludes into the glob as negative patterns so the `docs`
|
|
400
|
-
// collection
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
406
|
+
// collection doesn't ingest ignored trees (`node_modules`, `snippets`, the
|
|
407
|
+
// staged bodies under `.blume/content`, …) as entries. This matters when
|
|
408
|
+
// `content.root` is the project root (a migrated `.`-rooted project).
|
|
409
|
+
const outDirRel = relative(context.contentRoot, context.outDir);
|
|
410
|
+
const outDirIgnore =
|
|
411
|
+
outDirRel && !outDirRel.startsWith("..") && !isAbsolute(outDirRel)
|
|
412
|
+
? [`!${outDirRel}/**`]
|
|
413
|
+
: [];
|
|
414
|
+
|
|
415
|
+
// With no filesystem source, no route renders through `docs`, so glob nothing.
|
|
416
|
+
// Beyond skipping wasted work, this is the only thing that keeps Astro's
|
|
417
|
+
// content-layer *watcher* out of `.blume/`: bridge mode roots the collection
|
|
418
|
+
// at the project dir (which contains `.blume/.astro/fonts`, rewritten on every
|
|
419
|
+
// request), and the watcher's match test is `picomatch.isMatch(path, pattern)`
|
|
420
|
+
// — with array-OR semantics, any `!ignored/**` negation *matches* unrelated
|
|
421
|
+
// files, so negative patterns can't exclude a subtree there. An empty pattern
|
|
422
|
+
// matches nothing, so the watcher stays silent. The collection is still
|
|
423
|
+
// declared below so `getCollection("docs")` / `getEntry` resolve (to empty).
|
|
424
|
+
const filesystem = options.filesystem ?? true;
|
|
425
|
+
const docsPattern = filesystem
|
|
426
|
+
? [
|
|
427
|
+
...config.content.include,
|
|
428
|
+
...(config.content.exclude ?? []).map((pattern) => `!${pattern}`),
|
|
429
|
+
"!**/node_modules/**",
|
|
430
|
+
...outDirIgnore,
|
|
431
|
+
]
|
|
432
|
+
: [];
|
|
407
433
|
|
|
408
434
|
// Non-filesystem sources render through a parallel `staged` collection backed
|
|
409
435
|
// by materialized MDX, so the filesystem `docs` collection stays untouched.
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -5,35 +5,10 @@ import { defineCommand } from "citty";
|
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative } from "pathe";
|
|
6
6
|
|
|
7
7
|
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { blumePackageJson, toPackageName } from "../../core/package-json.ts";
|
|
9
9
|
import { eject } from "../../registry/eject.ts";
|
|
10
10
|
import { logger } from "../log.ts";
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Derive a valid npm package name from a directory name, falling back to
|
|
14
|
-
* `docs` when nothing usable remains.
|
|
15
|
-
*/
|
|
16
|
-
const toPackageName = (raw: string): string =>
|
|
17
|
-
raw
|
|
18
|
-
.toLowerCase()
|
|
19
|
-
.replaceAll(/[^a-z0-9._-]+/gu, "-")
|
|
20
|
-
.replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
|
|
21
|
-
|
|
22
|
-
const packageTemplate = (name: string, version: string): string => `{
|
|
23
|
-
"name": ${JSON.stringify(name)},
|
|
24
|
-
"private": true,
|
|
25
|
-
"type": "module",
|
|
26
|
-
"scripts": {
|
|
27
|
-
"dev": "blume dev",
|
|
28
|
-
"build": "blume build",
|
|
29
|
-
"doctor": "blume doctor"
|
|
30
|
-
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"blume": "^${version}"
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
12
|
const TEMPLATES = ["docs", "api", "sdk", "changelog"] as const;
|
|
38
13
|
type Template = (typeof TEMPLATES)[number];
|
|
39
14
|
|
|
@@ -221,7 +196,7 @@ export const initCommand = defineCommand({
|
|
|
221
196
|
const starter = STARTERS[template];
|
|
222
197
|
const createdPackage = await writeFileSafe(
|
|
223
198
|
join(root, "package.json"),
|
|
224
|
-
|
|
199
|
+
blumePackageJson(toPackageName(basename(root)))
|
|
225
200
|
);
|
|
226
201
|
await writeFileSafe(join(root, "blume.config.ts"), starter.config);
|
|
227
202
|
await Promise.all(
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getBlumeVersion } from "./version.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derive a valid npm package name from a directory name, falling back to
|
|
5
|
+
* `docs` when nothing usable remains.
|
|
6
|
+
*/
|
|
7
|
+
export const toPackageName = (raw: string): string =>
|
|
8
|
+
raw
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replaceAll(/[^a-z0-9._-]+/gu, "-")
|
|
11
|
+
.replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A minimal, runnable `package.json` body for a Blume project: the `blume`
|
|
15
|
+
* dependency pinned to the installed version plus `dev`/`build`/`doctor`
|
|
16
|
+
* scripts, so `npm install && npm run dev` works immediately. Shared by
|
|
17
|
+
* `blume init` and the migrators, which scaffold one when a project has none.
|
|
18
|
+
*/
|
|
19
|
+
export const blumePackageJson = (name: string): string => `{
|
|
20
|
+
"name": ${JSON.stringify(name)},
|
|
21
|
+
"private": true,
|
|
22
|
+
"type": "module",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "blume dev",
|
|
25
|
+
"build": "blume build",
|
|
26
|
+
"doctor": "blume doctor"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"blume": "^${getBlumeVersion()}"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, watch as fsWatch } from "node:fs";
|
|
2
|
+
import type { WatchListener } from "node:fs";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
|
|
4
5
|
import { isAbsolute, join, relative, resolve } from "pathe";
|
|
@@ -42,6 +43,41 @@ const MINTLIFY_SOURCE_IGNORES = [
|
|
|
42
43
|
"snippets/**",
|
|
43
44
|
];
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Directory names the recursive dev watcher must ignore. In bridge mode the
|
|
48
|
+
* content root is the project root, so a naive recursive `fs.watch` also sees
|
|
49
|
+
* Blume's own `.blume/` output — which the dev server rewrites on every request
|
|
50
|
+
* (`.blume/.astro/data-store.json`). Left unfiltered, each such write re-triggers
|
|
51
|
+
* a full rescan + runtime regeneration, whose writes land back under `.blume/`
|
|
52
|
+
* and fire the watcher again: a self-sustaining storm that stalls page renders
|
|
53
|
+
* and floods the console. `fs.watch` has no ignore option, so we filter by the
|
|
54
|
+
* changed path in the callback. Derived from {@link MINTLIFY_SOURCE_IGNORES}
|
|
55
|
+
* (dir prefixes) plus VCS metadata.
|
|
56
|
+
*/
|
|
57
|
+
const WATCH_IGNORE_DIRS = new Set([
|
|
58
|
+
...MINTLIFY_SOURCE_IGNORES.map((pattern) => pattern.replace(/\/\*\*$/u, "")),
|
|
59
|
+
".git",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the recursive-watch listener: fire `onChange` for content changes but
|
|
64
|
+
* ignore events whose path crosses a {@link WATCH_IGNORE_DIRS} segment (Blume's
|
|
65
|
+
* own `.blume/` output, `node_modules`, VCS metadata, …). A missing `filename`
|
|
66
|
+
* — rare; the platform couldn't name the changed path — falls through to
|
|
67
|
+
* regenerate rather than silently dropping a real edit. Exported for testing.
|
|
68
|
+
*/
|
|
69
|
+
export const mintlifyWatchListener =
|
|
70
|
+
(onChange: () => void): WatchListener<string> =>
|
|
71
|
+
(_event, filename) => {
|
|
72
|
+
if (
|
|
73
|
+
typeof filename === "string" &&
|
|
74
|
+
filename.split(/[/\\]/u).some((segment) => WATCH_IGNORE_DIRS.has(segment))
|
|
75
|
+
) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
onChange();
|
|
79
|
+
};
|
|
80
|
+
|
|
45
81
|
/**
|
|
46
82
|
* The Mintlify bridge content source. Reads an unconverted Mintlify project in
|
|
47
83
|
* place and transforms each page to Blume MDX at scan time (callouts → `:::`
|
|
@@ -126,7 +162,14 @@ export const mintlifySource = (
|
|
|
126
162
|
const watch = (onChange: () => void): (() => void) => {
|
|
127
163
|
const disposers: (() => void)[] = [];
|
|
128
164
|
if (existsSync(contentRoot)) {
|
|
129
|
-
|
|
165
|
+
// Recursively watch the content root, but skip Blume's own output and
|
|
166
|
+
// other non-content trees so the dev server's `.blume/` writes don't feed
|
|
167
|
+
// a regeneration loop (`fs.watch` has no ignore option, so filter here).
|
|
168
|
+
const watcher = fsWatch(
|
|
169
|
+
contentRoot,
|
|
170
|
+
{ recursive: true },
|
|
171
|
+
mintlifyWatchListener(onChange)
|
|
172
|
+
);
|
|
130
173
|
disposers.push(() => watcher.close());
|
|
131
174
|
}
|
|
132
175
|
// Watch docs.json directly: it lives at the content root but a non-recursive
|
|
@@ -4,7 +4,9 @@ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, join } from "pathe";
|
|
5
5
|
import { glob } from "tinyglobby";
|
|
6
6
|
|
|
7
|
+
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
7
8
|
import type { BlumeConfig } from "../../core/schema.ts";
|
|
9
|
+
import { ensurePackageJson } from "../shared.ts";
|
|
8
10
|
import { assetSegments } from "./assets.ts";
|
|
9
11
|
import { loadMintlifyConfig, partitionMintlifyRedirects } from "./config.ts";
|
|
10
12
|
import { mintlifyI18n } from "./i18n.ts";
|
|
@@ -188,6 +190,27 @@ const applyRelocatedAssets = (
|
|
|
188
190
|
}
|
|
189
191
|
};
|
|
190
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Scaffold the project files a config-only Mintlify repo lacks: a runnable
|
|
195
|
+
* `package.json` (it ships no npm manifest) and a `.gitignore` for Blume's
|
|
196
|
+
* generated `.blume/` runtime and `dist/` build output. Both are idempotent —
|
|
197
|
+
* an existing file is extended, not overwritten — and noted in the warnings.
|
|
198
|
+
*/
|
|
199
|
+
const scaffoldProjectFiles = async (
|
|
200
|
+
root: string,
|
|
201
|
+
warnings: string[]
|
|
202
|
+
): Promise<void> => {
|
|
203
|
+
if (await ensurePackageJson(root)) {
|
|
204
|
+
warnings.push(
|
|
205
|
+
"Created a package.json with blume as a dependency; run `npm install`, then `npm run dev`."
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const ignored = await ensureGitignore(root, [".blume/", "dist/"]);
|
|
209
|
+
if (ignored.length > 0) {
|
|
210
|
+
warnings.push(`Added ${ignored.join(", ")} to .gitignore.`);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
191
214
|
/**
|
|
192
215
|
* Delete the inlined markdown snippets. Component files (e.g. `.jsx`) are kept
|
|
193
216
|
* because their imports were rewritten to resolve against `/snippets`.
|
|
@@ -324,6 +347,7 @@ export const migrateMintlifyProject = async (
|
|
|
324
347
|
}
|
|
325
348
|
applyRelocatedAssets(config, assets, warnings);
|
|
326
349
|
await writeBlumeConfig(root, config);
|
|
350
|
+
await scaffoldProjectFiles(root, warnings);
|
|
327
351
|
|
|
328
352
|
if (Object.keys(variables).length > 0) {
|
|
329
353
|
warnings.push(
|
package/src/migrate/shared.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
import { isAbsolute, join, relative } from "pathe";
|
|
4
|
+
import { basename, isAbsolute, join, relative } from "pathe";
|
|
5
5
|
|
|
6
|
+
import { blumePackageJson, toPackageName } from "../core/package-json.ts";
|
|
6
7
|
import type { BlumeConfig } from "../core/schema.ts";
|
|
7
8
|
import { pageMetaSchema } from "../core/schema.ts";
|
|
8
9
|
|
|
@@ -102,6 +103,28 @@ export const rewriteFrameworkScripts = async (
|
|
|
102
103
|
export const leftoverFiles = (root: string, candidates: string[]): string[] =>
|
|
103
104
|
candidates.filter((candidate) => existsSync(join(root, candidate)));
|
|
104
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Scaffold a minimal, runnable `package.json` when the migrated project has
|
|
108
|
+
* none. Config-only sources (e.g. a Mintlify `docs.json`) ship no npm manifest,
|
|
109
|
+
* so a fresh migration has nothing to run `blume dev` with; this writes a stub
|
|
110
|
+
* with `blume` as a dependency and `dev`/`build`/`doctor` scripts, making
|
|
111
|
+
* `npm install && npm run dev` work immediately. A pre-existing `package.json`
|
|
112
|
+
* is left untouched — {@link rewriteFrameworkScripts} repoints those instead.
|
|
113
|
+
* Returns true when a file was created.
|
|
114
|
+
*/
|
|
115
|
+
export const ensurePackageJson = async (root: string): Promise<boolean> => {
|
|
116
|
+
const pkgPath = join(root, "package.json");
|
|
117
|
+
if (existsSync(pkgPath)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
await writeFile(
|
|
121
|
+
pkgPath,
|
|
122
|
+
blumePackageJson(toPackageName(basename(root))),
|
|
123
|
+
"utf-8"
|
|
124
|
+
);
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
|
|
105
128
|
// ---------------------------------------------------------------------------
|
|
106
129
|
// Callout components -> Blume `:::` directives
|
|
107
130
|
// ---------------------------------------------------------------------------
|