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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -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({ config, context, staged: hasStaged })
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"),
@@ -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 never walks into ignored trees. This matters when `content.root`
401
- // is the project root (Mintlify bridge mode, or a migrated `.`-rooted project):
402
- // without it the collection would scan `node_modules`, `snippets`, etc.
403
- const docsPattern = [
404
- ...config.content.include,
405
- ...(config.content.exclude ?? []).map((pattern) => `!${pattern}`),
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.
@@ -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 { getBlumeVersion } from "../../core/version.ts";
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
- packageTemplate(toPackageName(basename(root)), getBlumeVersion())
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
- const watcher = fsWatch(contentRoot, { recursive: true }, onChange);
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(
@@ -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
  // ---------------------------------------------------------------------------