@brandon_m_behring/book-scaffold-astro 4.1.0 → 4.1.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.
@@ -11,12 +11,7 @@
11
11
  * Closed `kind` union: discriminated literal type. To add a 6th kind in
12
12
  * a future release, expand the union + add a CSS block in poc-layouts.css.
13
13
  */
14
- export type PocLayoutKind =
15
- | 'tutorial'
16
- | 'how-to'
17
- | 'tldr'
18
- | 'part-summary'
19
- | 'cheat-sheet';
14
+ export type PocLayoutKind = 'tutorial' | 'how-to' | 'tldr' | 'part-summary' | 'cheat-sheet';
20
15
 
21
16
  interface Props {
22
17
  kind: PocLayoutKind;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@brandon_m_behring/book-scaffold-astro",
3
3
  "description": "Astro 6 + MDX toolkit for long-form technical books. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
4
- "version": "4.1.0",
4
+ "version": "4.1.2",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Brandon Behring",
@@ -34,7 +34,8 @@
34
34
  * Designed to run in <2 s on a medium book.
35
35
  */
36
36
  import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
37
- import { resolve, join, basename, dirname } from 'node:path';
37
+ import { resolve, relative, join, basename, dirname } from 'node:path';
38
+ import { readChaptersBase } from './walk-mdx.mjs';
38
39
 
39
40
  // --help / -h: non-mutating (closes #14).
40
41
  const USAGE = `Usage: book-scaffold build-labels
@@ -56,7 +57,15 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
56
57
  process.exit(0);
57
58
  }
58
59
 
59
- const CHAPTERS_DIR = process.env.BOOK_CHAPTERS_DIR ?? 'src/content/chapters';
60
+ // v4.1.1 (closes #63): readChaptersBase honors BOOK_CHAPTERS_DIR env (when set)
61
+ // then parses the consumer's content.config.{ts,mjs,js} for a `chapters`
62
+ // collection `loader.base` override. Multi-guide consumers use
63
+ // `src/content/<guide-slug>/` rather than the Astro 5 default.
64
+ const CHAPTERS_DIR_ABS = await readChaptersBase(process.cwd());
65
+ // build-labels uses CHAPTERS_DIR as a path relative to cwd elsewhere in the
66
+ // script (joined with `walkMdx`). Convert the absolute path back to relative
67
+ // for compatibility with the existing call sites.
68
+ const CHAPTERS_DIR = relative(process.cwd(), CHAPTERS_DIR_ABS) || 'src/content/chapters';
60
69
  const OUTPUT_PATH = process.env.BOOK_LABELS_OUT ?? 'src/data/labels.json';
61
70
 
62
71
  /** Component names that participate in cross-referencing. */
@@ -28,7 +28,7 @@
28
28
  import { readFile, access } from 'node:fs/promises';
29
29
  import { existsSync, readFileSync } from 'node:fs';
30
30
  import { resolve, dirname, join } from 'node:path';
31
- import { walkMdx } from './walk-mdx.mjs';
31
+ import { walkMdx, readChaptersBase } from './walk-mdx.mjs';
32
32
 
33
33
  /**
34
34
  * Best-effort .env reader. Mirrors `readEnvFile` in src/types.ts; kept inline
@@ -95,7 +95,11 @@ const presetFromFlag = presetFlagIdx >= 0 ? argv[presetFlagIdx + 1] : undefined;
95
95
  // Resolves issue #8 — three reference consumers reported "0 chapter(s) checked"
96
96
  // because ROOT was the package directory inside node_modules.
97
97
  const ROOT = process.cwd();
98
- const CHAPTERS_DIR = resolve(ROOT, 'src/content/chapters');
98
+ // v4.1.1 (closes #63): read the consumer's content.config.{ts,mjs,js} to
99
+ // honor `loader.base` overrides (multi-guide pattern uses
100
+ // `src/content/<guide-slug>/` instead of the Astro 5 default).
101
+ // Falls back to `src/content/chapters` when no override / no config file.
102
+ const CHAPTERS_DIR = await readChaptersBase(ROOT);
99
103
  const PUBLIC_DIR = resolve(ROOT, 'public');
100
104
  const DATA_DIR = resolve(ROOT, 'src/data');
101
105
 
@@ -12,8 +12,9 @@
12
12
  * Output: relative paths in POSIX form ("subdir/file.mdx"), matching what
13
13
  * the previous `glob('**\/*.{md,mdx}', { cwd })` produced.
14
14
  */
15
- import { readdir } from 'node:fs/promises';
16
- import { join, relative } from 'node:path';
15
+ import { readFile, readdir } from 'node:fs/promises';
16
+ import { existsSync } from 'node:fs';
17
+ import { join, relative, resolve } from 'node:path';
17
18
 
18
19
  export async function* walkMdx(dir, baseDir = dir) {
19
20
  let entries;
@@ -32,3 +33,68 @@ export async function* walkMdx(dir, baseDir = dir) {
32
33
  }
33
34
  }
34
35
  }
36
+
37
+ /**
38
+ * Read the consumer's `content.config.ts` (or `.mjs` / `.js`) and extract
39
+ * the `loader.base` path for the `chapters` content collection.
40
+ *
41
+ * v4.1.1 (closes #63): consumers in the multi-guide / multi-book pattern
42
+ * override the chapters dir to `src/content/<guide-slug>` rather than the
43
+ * Astro 5 default `src/content/chapters/`. Without this helper,
44
+ * `book-scaffold validate` + `book-scaffold build-labels` silently report
45
+ * 0 chapters because they walk the default path. This helper parses the
46
+ * consumer's config file and returns the actual base path so both scripts
47
+ * discover the consumer's chapter files.
48
+ *
49
+ * Strategy: regex-parse the source file (avoid runtime import; the file
50
+ * imports from `astro:content` / `astro/loaders` which don't resolve in
51
+ * plain Node). Matches both single- and double-quoted string literals;
52
+ * matches paths with or without the `./` prefix.
53
+ *
54
+ * Returns the resolved absolute path. Falls back to
55
+ * `${projectRoot}/src/content/chapters` when:
56
+ * - content.config.{ts,mjs,js} doesn't exist
57
+ * - the file exists but no `chapters` collection or `loader.base` found
58
+ * - the matched base path uses dynamic forms (variables, template literals)
59
+ * instead of a string literal
60
+ *
61
+ * Honors env override: BOOK_CHAPTERS_DIR (when set) wins over config parse.
62
+ */
63
+ export async function readChaptersBase(projectRoot) {
64
+ const envOverride = process.env.BOOK_CHAPTERS_DIR;
65
+ if (envOverride) {
66
+ return resolve(projectRoot, envOverride);
67
+ }
68
+ const DEFAULT_BASE = resolve(projectRoot, 'src/content/chapters');
69
+ for (const ext of ['ts', 'mjs', 'js']) {
70
+ const configPath = join(projectRoot, `src/content.config.${ext}`);
71
+ if (!existsSync(configPath)) continue;
72
+ let source;
73
+ try {
74
+ source = await readFile(configPath, 'utf8');
75
+ } catch {
76
+ return DEFAULT_BASE;
77
+ }
78
+ // Look for a `chapters` collection's `loader.base` string. Permissive
79
+ // form: match the `chapters` identifier, then within the next 500
80
+ // chars find `base: 'string'` or `base: "string"`. NOT template
81
+ // literals (which use backticks and may contain ${} interpolation —
82
+ // those fall back to the default since the value is dynamic).
83
+ //
84
+ // Forms matched:
85
+ // - `const chapters = defineCollection({ loader: glob({ base: './foo' }) })`
86
+ // - `export const collections = { chapters: defineCollection({ loader: glob({ base: './foo' }) }) }`
87
+ // - any indentation / line break style
88
+ const re = /\bchapters\b[\s\S]{0,500}?\bbase\s*:\s*'([^']+)'|\bchapters\b[\s\S]{0,500}?\bbase\s*:\s*"([^"]+)"/;
89
+ const m = source.match(re);
90
+ const captured = m && (m[1] || m[2]);
91
+ if (captured) {
92
+ return resolve(projectRoot, captured);
93
+ }
94
+ // File exists but no override found — assume the consumer uses the
95
+ // scaffold's defineBookSchemas() default.
96
+ return DEFAULT_BASE;
97
+ }
98
+ // No content.config.{ts,mjs,js} at all — return the Astro 5 default.
99
+ return DEFAULT_BASE;
100
+ }