@brandon_m_behring/book-scaffold-astro 4.6.1 → 4.7.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/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.6.1",
4
+ "version": "4.7.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Brandon Behring",
@@ -49,8 +49,25 @@ For pre-commit: add to `.pre-commit-config.yaml`:
49
49
 
50
50
  ## Environment variables
51
51
 
52
- - `BOOK_PROFILE` — `academic` enables Cite-key checking; otherwise skipped.
52
+ - `BOOK_PRESET` (canonical) / `BOOK_PROFILE` (alias) which preset to validate against. `academic` enables Cite-key checking.
53
53
  - `BOOK_REPO_ROOT` — absolute path to the paired code repo for CodeRef checks. Unset → skipped (the scaffold default; minimal/tools books rarely have a paired code repo).
54
+ - `BOOK_CHAPTERS_DIR` — override the chapters directory (default: read from `content.config.ts`, fallback `src/content/chapters`).
55
+
56
+ ## Preset / chaptersBase resolution (v4.7.0+, #75)
57
+
58
+ The validator resolves both `preset` and `chaptersBase` by consulting multiple sources in a documented order. Notable v4.7.0 addition: the v4.5+ canonical form
59
+
60
+ ```ts
61
+ // src/content.config.ts
62
+ export const { collections } = defineBookSchemas({
63
+ preset: 'research-portfolio',
64
+ chaptersBase: './src/content/textbook',
65
+ });
66
+ ```
67
+
68
+ is now read by the CLI (previously it was silently ignored — the CLI defaulted to `profile=minimal` and walked `./src/content/chapters/` while `astro build` applied the correct settings, masking real schema drift).
69
+
70
+ Full precedence chain documented in [`PACKAGE_DESIGN.md §8 — Preset + chaptersBase resolution`](../../PACKAGE_DESIGN.md#preset--chaptersbase-resolution-v470-closes-75).
54
71
 
55
72
  ## Output
56
73
 
@@ -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, readChaptersBase } from './walk-mdx.mjs';
31
+ import { walkMdx, readChaptersBase, readBookSchemaConfig } from './walk-mdx.mjs';
32
32
 
33
33
  /**
34
34
  * Best-effort .env reader. Mirrors `readEnvFile` in src/types.ts; kept inline
@@ -105,17 +105,26 @@ const DATA_DIR = resolve(ROOT, 'src/data');
105
105
 
106
106
  // Preset resolution (matches resolvePreset in src/types.ts):
107
107
  // --preset flag > BOOK_PRESET env > BOOK_PROFILE env >
108
- // .env BOOK_PRESET > .env BOOK_PROFILE > 'minimal'.
108
+ // .env BOOK_PRESET > .env BOOK_PROFILE >
109
+ // defineBookSchemas({ preset }) in content.config.ts >
110
+ // defineBookSchemas({ profile }) in content.config.ts (alias) >
111
+ // 'minimal'.
109
112
  // .env fallback closes #20 — without it, consumers who set BOOK_PROFILE in
110
113
  // .env (the documented convenience in SKILL.md + create-book defaults) saw
111
114
  // the CLI silently default to minimal, hiding academic-profile errors.
115
+ // content.config.ts fallback closes #75 — without it, consumers using the
116
+ // canonical v4.5+ defineBookSchemas({ preset, chaptersBase }) form had the
117
+ // CLI silently default to minimal, hiding research-portfolio (and any
118
+ // non-env-set) profile errors while astro build applied the correct settings.
112
119
  const dotenv = readEnvFile(resolve(ROOT, '.env'));
120
+ const schemaConfig = await readBookSchemaConfig(ROOT);
113
121
  const PRESET =
114
122
  presetFromFlag ??
115
123
  process.env.BOOK_PRESET ??
116
124
  process.env.BOOK_PROFILE ??
117
125
  dotenv.BOOK_PRESET ??
118
126
  dotenv.BOOK_PROFILE ??
127
+ schemaConfig.preset ??
119
128
  'minimal';
120
129
  // Alias kept for downstream message text only; the resolution above is canonical.
121
130
  const PROFILE = PRESET;
@@ -34,6 +34,79 @@ export async function* walkMdx(dir, baseDir = dir) {
34
34
  }
35
35
  }
36
36
 
37
+ /**
38
+ * Read the consumer's `content.config.ts` (or `.mjs` / `.js`) and extract
39
+ * `defineBookSchemas({ preset?, profile?, chaptersBase? })` options.
40
+ *
41
+ * v4.7.0 (closes #75): consumers using the v4.5+ canonical form
42
+ *
43
+ * export const { collections } = defineBookSchemas({
44
+ * preset: 'research-portfolio',
45
+ * chaptersBase: './src/content/textbook',
46
+ * });
47
+ *
48
+ * previously had BOTH options ignored by the CLI scripts. `validate` and
49
+ * `build-labels` resolved preset only from env vars and walked the default
50
+ * chapters dir, silently checking the wrong directory under the wrong
51
+ * profile while astro build applied the correct settings — a divergence
52
+ * masking real schema drift.
53
+ *
54
+ * Strategy: regex-parse the source file (avoid runtime import; the file
55
+ * imports from `astro:content` / `astro/loaders` which don't resolve in
56
+ * plain Node). Captures the entire `defineBookSchemas({ ... })` options
57
+ * object, then field-by-field regex within that scope for `preset:`,
58
+ * `profile:` (alias), and `chaptersBase:`.
59
+ *
60
+ * Returns `{ preset, chaptersBase }` — both nullable. Returns `null` for
61
+ * either field when:
62
+ * - content.config.{ts,mjs,js} doesn't exist
63
+ * - no `defineBookSchemas(...)` call found
64
+ * - the field is absent or uses a dynamic form (variable, template literal)
65
+ *
66
+ * `preset` and `profile` are aliases (canonical name flipped in v3.7+);
67
+ * `preset` wins when both are present.
68
+ */
69
+ export async function readBookSchemaConfig(projectRoot) {
70
+ const result = { preset: null, chaptersBase: null };
71
+ for (const ext of ['ts', 'mjs', 'js']) {
72
+ const configPath = join(projectRoot, `src/content.config.${ext}`);
73
+ if (!existsSync(configPath)) continue;
74
+ let source;
75
+ try {
76
+ source = await readFile(configPath, 'utf8');
77
+ } catch {
78
+ return result;
79
+ }
80
+ // Match `defineBookSchemas({ ... })` — capture the options object body.
81
+ // Non-greedy `[\s\S]*?` matches the smallest balanced-enough scope; for
82
+ // typical configs the options object is small (≤200 chars) and any
83
+ // nested braces (uncommon in this API) would terminate the match early.
84
+ // Acceptable trade-off: simple regex over a real parser.
85
+ const callMatch = source.match(
86
+ /\bdefineBookSchemas\s*\(\s*\{([\s\S]*?)\}\s*\)/,
87
+ );
88
+ if (callMatch) {
89
+ const optsBody = callMatch[1];
90
+ // preset is canonical (v3.7+); profile is backward-compat alias.
91
+ const presetMatch =
92
+ optsBody.match(/\bpreset\s*:\s*'([^']+)'/) ||
93
+ optsBody.match(/\bpreset\s*:\s*"([^"]+)"/);
94
+ const profileMatch =
95
+ optsBody.match(/\bprofile\s*:\s*'([^']+)'/) ||
96
+ optsBody.match(/\bprofile\s*:\s*"([^"]+)"/);
97
+ result.preset = presetMatch?.[1] ?? profileMatch?.[1] ?? null;
98
+ const chaptersBaseMatch =
99
+ optsBody.match(/\bchaptersBase\s*:\s*'([^']+)'/) ||
100
+ optsBody.match(/\bchaptersBase\s*:\s*"([^"]+)"/);
101
+ result.chaptersBase = chaptersBaseMatch?.[1] ?? null;
102
+ }
103
+ // First existing file wins (priority: .ts > .mjs > .js).
104
+ return result;
105
+ }
106
+ // No content.config.{ts,mjs,js} at all.
107
+ return result;
108
+ }
109
+
37
110
  /**
38
111
  * Read the consumer's `content.config.ts` (or `.mjs` / `.js`) and extract
39
112
  * the `loader.base` path for the `chapters` content collection.
@@ -46,6 +119,9 @@ export async function* walkMdx(dir, baseDir = dir) {
46
119
  * consumer's config file and returns the actual base path so both scripts
47
120
  * discover the consumer's chapter files.
48
121
  *
122
+ * v4.7.0 (closes #75): when the raw Astro form isn't present, also consult
123
+ * `readBookSchemaConfig()` for `defineBookSchemas({ chaptersBase })`.
124
+ *
49
125
  * Strategy: regex-parse the source file (avoid runtime import; the file
50
126
  * imports from `astro:content` / `astro/loaders` which don't resolve in
51
127
  * plain Node). Matches both single- and double-quoted string literals;
@@ -55,6 +131,7 @@ export async function* walkMdx(dir, baseDir = dir) {
55
131
  * `${projectRoot}/src/content/chapters` when:
56
132
  * - content.config.{ts,mjs,js} doesn't exist
57
133
  * - the file exists but no `chapters` collection or `loader.base` found
134
+ * AND no `defineBookSchemas({ chaptersBase: ... })` form found
58
135
  * - the matched base path uses dynamic forms (variables, template literals)
59
136
  * instead of a string literal
60
137
  *
@@ -91,6 +168,12 @@ export async function readChaptersBase(projectRoot) {
91
168
  if (captured) {
92
169
  return resolve(projectRoot, captured);
93
170
  }
171
+ // v4.7.0 (closes #75): no raw Astro form match — try the v4.5+ form
172
+ // `defineBookSchemas({ chaptersBase: '...' })`.
173
+ const schemaConfig = await readBookSchemaConfig(projectRoot);
174
+ if (schemaConfig.chaptersBase) {
175
+ return resolve(projectRoot, schemaConfig.chaptersBase);
176
+ }
94
177
  // File exists but no override found — assume the consumer uses the
95
178
  // scaffold's defineBookSchemas() default.
96
179
  return DEFAULT_BASE;