@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 +1 -1
- package/recipes/09-validation.md +18 -1
- package/scripts/validate.mjs +11 -2
- package/scripts/walk-mdx.mjs +83 -0
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.
|
|
4
|
+
"version": "4.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
package/recipes/09-validation.md
CHANGED
|
@@ -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
|
|
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
|
|
package/scripts/validate.mjs
CHANGED
|
@@ -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 >
|
|
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;
|
package/scripts/walk-mdx.mjs
CHANGED
|
@@ -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;
|