@brandon_m_behring/book-scaffold-astro 3.2.0 → 3.4.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/LATEX_TO_MDX_MAPPING.md +130 -0
- package/dist/index.d.ts +70 -123
- package/dist/index.mjs +372 -138
- package/dist/schemas.d.ts +36 -2
- package/dist/schemas.mjs +183 -52
- package/package.json +3 -2
- package/pages/frontmatter/[...slug].astro +48 -0
- package/pages/print.astro +9 -1
- package/recipes/12-where-to-file-issues.md +58 -0
- package/scripts/build-bib.mjs +18 -0
- package/scripts/build-figures.mjs +19 -0
- package/scripts/build-labels.mjs +20 -0
- package/scripts/render-notebooks.mjs +19 -0
- package/scripts/validate.mjs +51 -37
- package/src/lib/freshness.ts +19 -4
package/dist/schemas.mjs
CHANGED
|
@@ -3,55 +3,9 @@ import { existsSync as existsSync2 } from "fs";
|
|
|
3
3
|
import { defineCollection } from "astro:content";
|
|
4
4
|
import { glob, file } from "astro/loaders";
|
|
5
5
|
|
|
6
|
-
// src/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var BookConfigError = class extends Error {
|
|
10
|
-
constructor(message) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = "BookConfigError";
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
function readEnvFile(path = ".env") {
|
|
16
|
-
try {
|
|
17
|
-
if (!existsSync(path)) return {};
|
|
18
|
-
const out = {};
|
|
19
|
-
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
20
|
-
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
21
|
-
if (!m) continue;
|
|
22
|
-
let val = m[2] ?? "";
|
|
23
|
-
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
24
|
-
val = val.slice(1, -1);
|
|
25
|
-
}
|
|
26
|
-
out[m[1]] = val;
|
|
27
|
-
}
|
|
28
|
-
return out;
|
|
29
|
-
} catch {
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function resolveProfile(explicit) {
|
|
34
|
-
let candidate = explicit ?? process.env.BOOK_PROFILE;
|
|
35
|
-
let source = "default";
|
|
36
|
-
if (explicit) source = "param";
|
|
37
|
-
else if (process.env.BOOK_PROFILE) source = "env";
|
|
38
|
-
if (!candidate) {
|
|
39
|
-
const fromFile = readEnvFile().BOOK_PROFILE;
|
|
40
|
-
if (fromFile) {
|
|
41
|
-
candidate = fromFile;
|
|
42
|
-
source = "dotenv";
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
candidate = candidate ?? "minimal";
|
|
46
|
-
if (!BOOK_PROFILES.includes(candidate)) {
|
|
47
|
-
throw new BookConfigError(
|
|
48
|
-
`profile must be one of ${BOOK_PROFILES.join(" | ")} (got ${JSON.stringify(candidate)})`
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
if (source === "default") {
|
|
52
|
-
console.warn("book-scaffold-astro: BOOK_PROFILE not set; falling back to 'minimal'.");
|
|
53
|
-
}
|
|
54
|
-
return candidate;
|
|
6
|
+
// src/profile-kit.ts
|
|
7
|
+
function defineProfile(p) {
|
|
8
|
+
return p;
|
|
55
9
|
}
|
|
56
10
|
|
|
57
11
|
// src/schemas.ts
|
|
@@ -122,6 +76,32 @@ var toolsChapterSchema = z.object({
|
|
|
122
76
|
draft: z.boolean().default(false),
|
|
123
77
|
updated: z.date().optional()
|
|
124
78
|
});
|
|
79
|
+
var minimalChapterSchema = toolsChapterSchema;
|
|
80
|
+
var courseNotesChapterSchema = z.object({
|
|
81
|
+
// Identity
|
|
82
|
+
title: z.string().min(1),
|
|
83
|
+
chapter: z.number().int().min(0).max(99),
|
|
84
|
+
part: z.number().int().min(0).max(20).default(1),
|
|
85
|
+
description: z.string().optional(),
|
|
86
|
+
// Source attribution
|
|
87
|
+
course: z.string().optional(),
|
|
88
|
+
instructor: z.string().optional(),
|
|
89
|
+
source_url: z.string().url().optional(),
|
|
90
|
+
// Pedagogy
|
|
91
|
+
learning_outcomes: z.array(
|
|
92
|
+
z.object({
|
|
93
|
+
id: z.string(),
|
|
94
|
+
verb: z.string(),
|
|
95
|
+
text: z.string()
|
|
96
|
+
})
|
|
97
|
+
).default([]),
|
|
98
|
+
tags: z.array(z.string()).default([]),
|
|
99
|
+
// Provenance + status (shared shape with tools profile)
|
|
100
|
+
last_verified: z.date(),
|
|
101
|
+
volatility: z.enum(volatilityLevels).default("architectural-pattern"),
|
|
102
|
+
sources: z.array(z.string()).default([]),
|
|
103
|
+
draft: z.boolean().default(false)
|
|
104
|
+
});
|
|
125
105
|
var sourcesSchema = z.object({
|
|
126
106
|
url: z.string().url(),
|
|
127
107
|
title: z.string().min(1),
|
|
@@ -158,17 +138,167 @@ var patternsSchema = z.object({
|
|
|
158
138
|
convergence_date: z.date().nullable().optional()
|
|
159
139
|
});
|
|
160
140
|
|
|
141
|
+
// src/profiles/academic.ts
|
|
142
|
+
var academicProfile = defineProfile({
|
|
143
|
+
name: "academic",
|
|
144
|
+
schema: academicChapterSchema,
|
|
145
|
+
routes: {
|
|
146
|
+
references: true,
|
|
147
|
+
search: true,
|
|
148
|
+
print: true,
|
|
149
|
+
chapters: false,
|
|
150
|
+
// academic consumers ship their own week-based /chapters listing
|
|
151
|
+
convergence: false,
|
|
152
|
+
// tools-profile-specific
|
|
153
|
+
frontmatter: false
|
|
154
|
+
// opt-in per book; see #7
|
|
155
|
+
},
|
|
156
|
+
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
157
|
+
katex: true
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// src/profiles/tools.ts
|
|
161
|
+
var toolsProfile = defineProfile({
|
|
162
|
+
name: "tools",
|
|
163
|
+
schema: toolsChapterSchema,
|
|
164
|
+
routes: {
|
|
165
|
+
references: true,
|
|
166
|
+
search: true,
|
|
167
|
+
print: true,
|
|
168
|
+
chapters: true,
|
|
169
|
+
// tools profile ships a flat chapter index
|
|
170
|
+
convergence: true,
|
|
171
|
+
// tools profile ships convergence dashboard
|
|
172
|
+
frontmatter: false
|
|
173
|
+
// opt-in per book; see #7
|
|
174
|
+
},
|
|
175
|
+
styles: [
|
|
176
|
+
"tokens.css",
|
|
177
|
+
"layout.css",
|
|
178
|
+
"callouts.css",
|
|
179
|
+
"chapter.css",
|
|
180
|
+
"typography.css",
|
|
181
|
+
"print.css",
|
|
182
|
+
"convergence.css",
|
|
183
|
+
"tool-filter.css"
|
|
184
|
+
]
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// src/profiles/minimal.ts
|
|
188
|
+
var minimalProfile = defineProfile({
|
|
189
|
+
name: "minimal",
|
|
190
|
+
schema: minimalChapterSchema,
|
|
191
|
+
routes: {
|
|
192
|
+
references: true,
|
|
193
|
+
search: true,
|
|
194
|
+
print: true,
|
|
195
|
+
chapters: false,
|
|
196
|
+
convergence: false,
|
|
197
|
+
frontmatter: false
|
|
198
|
+
// opt-in per book; see #7
|
|
199
|
+
},
|
|
200
|
+
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// src/profiles/course-notes.ts
|
|
204
|
+
var courseNotesProfile = defineProfile({
|
|
205
|
+
name: "course-notes",
|
|
206
|
+
schema: courseNotesChapterSchema,
|
|
207
|
+
routes: {
|
|
208
|
+
references: true,
|
|
209
|
+
search: true,
|
|
210
|
+
print: true,
|
|
211
|
+
chapters: false,
|
|
212
|
+
// multi-book consumers route via [book]/[slug] themselves
|
|
213
|
+
convergence: false,
|
|
214
|
+
frontmatter: false
|
|
215
|
+
// opt-in per book; see #7
|
|
216
|
+
},
|
|
217
|
+
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// src/profiles/index.ts
|
|
221
|
+
var PROFILES = {
|
|
222
|
+
academic: academicProfile,
|
|
223
|
+
tools: toolsProfile,
|
|
224
|
+
minimal: minimalProfile,
|
|
225
|
+
"course-notes": courseNotesProfile
|
|
226
|
+
};
|
|
227
|
+
var BOOK_PROFILES = Object.keys(PROFILES);
|
|
228
|
+
|
|
229
|
+
// src/types.ts
|
|
230
|
+
import { existsSync, readFileSync } from "fs";
|
|
231
|
+
var BOOK_PRESETS = BOOK_PROFILES;
|
|
232
|
+
var BookConfigError = class extends Error {
|
|
233
|
+
constructor(message) {
|
|
234
|
+
super(message);
|
|
235
|
+
this.name = "BookConfigError";
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
function readEnvFile(path = ".env") {
|
|
239
|
+
try {
|
|
240
|
+
if (!existsSync(path)) return {};
|
|
241
|
+
const out = {};
|
|
242
|
+
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
243
|
+
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
244
|
+
if (!m) continue;
|
|
245
|
+
let val = m[2] ?? "";
|
|
246
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
247
|
+
val = val.slice(1, -1);
|
|
248
|
+
}
|
|
249
|
+
out[m[1]] = val;
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
} catch {
|
|
253
|
+
return {};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function resolvePreset(explicitPreset, explicitProfile) {
|
|
257
|
+
let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
|
|
258
|
+
let source = "default";
|
|
259
|
+
if (explicitPreset || explicitProfile) source = "param";
|
|
260
|
+
else if (process.env.BOOK_PRESET || process.env.BOOK_PROFILE) source = "env";
|
|
261
|
+
if (!candidate) {
|
|
262
|
+
const env = readEnvFile();
|
|
263
|
+
const fromFile = env.BOOK_PRESET ?? env.BOOK_PROFILE;
|
|
264
|
+
if (fromFile) {
|
|
265
|
+
candidate = fromFile;
|
|
266
|
+
source = "dotenv";
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
candidate = candidate ?? "minimal";
|
|
270
|
+
if (!BOOK_PRESETS.includes(candidate)) {
|
|
271
|
+
throw new BookConfigError(
|
|
272
|
+
`preset must be one of ${BOOK_PRESETS.join(" | ")} (got ${JSON.stringify(candidate)})`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (source === "default") {
|
|
276
|
+
console.warn("book-scaffold-astro: BOOK_PRESET not set; falling back to 'minimal'.");
|
|
277
|
+
}
|
|
278
|
+
return candidate;
|
|
279
|
+
}
|
|
280
|
+
|
|
161
281
|
// src/schemas-entry.ts
|
|
282
|
+
function frontmatterCollection(schema, base = "./src/content/frontmatter") {
|
|
283
|
+
return defineCollection({
|
|
284
|
+
loader: glob({
|
|
285
|
+
pattern: ["**/*.{md,mdx}", "!**/_*"],
|
|
286
|
+
base
|
|
287
|
+
}),
|
|
288
|
+
schema
|
|
289
|
+
});
|
|
290
|
+
}
|
|
162
291
|
function defineBookSchemas(opts = {}) {
|
|
163
|
-
const profile =
|
|
292
|
+
const profile = resolvePreset(opts.preset, opts.profile);
|
|
164
293
|
const chaptersBase = opts.chaptersBase ?? "./src/content/chapters";
|
|
294
|
+
const schemaForProfile = profile === "academic" ? academicChapterSchema : profile === "course-notes" ? courseNotesChapterSchema : profile === "minimal" ? minimalChapterSchema : toolsChapterSchema;
|
|
165
295
|
const chapters = defineCollection({
|
|
166
296
|
loader: glob({
|
|
167
297
|
// Exclude underscore-prefixed files (standard "hidden" convention).
|
|
168
298
|
pattern: ["**/*.{md,mdx}", "!**/_*"],
|
|
169
299
|
base: chaptersBase
|
|
170
300
|
}),
|
|
171
|
-
schema:
|
|
301
|
+
schema: schemaForProfile
|
|
172
302
|
});
|
|
173
303
|
const collections = {
|
|
174
304
|
chapters
|
|
@@ -194,5 +324,6 @@ function defineBookSchemas(opts = {}) {
|
|
|
194
324
|
return { collections };
|
|
195
325
|
}
|
|
196
326
|
export {
|
|
197
|
-
defineBookSchemas
|
|
327
|
+
defineBookSchemas,
|
|
328
|
+
frontmatterCollection
|
|
198
329
|
};
|
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": "3.
|
|
4
|
+
"version": "3.4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -113,7 +113,8 @@
|
|
|
113
113
|
"pedagogy",
|
|
114
114
|
"examples",
|
|
115
115
|
"CLAUDE.md",
|
|
116
|
-
"README.md"
|
|
116
|
+
"README.md",
|
|
117
|
+
"LATEX_TO_MDX_MAPPING.md"
|
|
117
118
|
],
|
|
118
119
|
"scripts": {
|
|
119
120
|
"build": "tsup && rm -f dist/types-*.d.ts",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* pages/frontmatter/[...slug].astro — auto-injected route for the
|
|
4
|
+
* consumer-defined `frontmatter` content collection. v3.4.0 closes #7.
|
|
5
|
+
*
|
|
6
|
+
* Opt-in: consumer enables via defineBookConfig({ routes: { frontmatter: true } })
|
|
7
|
+
* AND defines the collection in src/content.config.ts via the
|
|
8
|
+
* `frontmatterCollection(schema)` helper. Drops MDX files under
|
|
9
|
+
* src/content/frontmatter/; each renders at /frontmatter/<slug>/.
|
|
10
|
+
*
|
|
11
|
+
* Why a single template route (not consumer-owned): the rendering shape
|
|
12
|
+
* is uniform (Base layout + prose + Content) — every consumer would write
|
|
13
|
+
* the same file. Centralizing it keeps the consumer-side surface to just
|
|
14
|
+
* the schema definition + the routes-toggle flip.
|
|
15
|
+
*
|
|
16
|
+
* mdx-components plumbing (issue #2): the consumer's src/mdx-components.ts
|
|
17
|
+
* components are imported via the virtual module and threaded through
|
|
18
|
+
* <Content components={mdxComponents} />, so custom MDX components render
|
|
19
|
+
* here exactly as they do on /print and chapter routes.
|
|
20
|
+
*/
|
|
21
|
+
import { getCollection, render } from 'astro:content';
|
|
22
|
+
import Base from '../../layouts/Base.astro';
|
|
23
|
+
import mdxComponents from 'virtual:book-scaffold/mdx-components';
|
|
24
|
+
|
|
25
|
+
export async function getStaticPaths() {
|
|
26
|
+
const entries = await getCollection('frontmatter');
|
|
27
|
+
return entries.map((entry) => ({
|
|
28
|
+
params: { slug: entry.id },
|
|
29
|
+
props: { entry },
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { entry } = Astro.props;
|
|
34
|
+
const { Content } = await render(entry);
|
|
35
|
+
|
|
36
|
+
// The schema is consumer-defined; we read defensively to avoid crashes
|
|
37
|
+
// if the consumer skipped title/description fields. The frontmatterCollection
|
|
38
|
+
// helper documents the recommended shape.
|
|
39
|
+
const data = entry.data as Record<string, unknown>;
|
|
40
|
+
const title = typeof data.title === 'string' ? data.title : entry.id;
|
|
41
|
+
const description = typeof data.description === 'string' ? data.description : undefined;
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
<Base title={title} description={description ?? ''}>
|
|
45
|
+
<article class="prose frontmatter-page">
|
|
46
|
+
<Content components={mdxComponents} />
|
|
47
|
+
</article>
|
|
48
|
+
</Base>
|
package/pages/print.astro
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
* body, and wraps in a <section.chapter-print> so print.css can force
|
|
8
8
|
* page breaks between chapters.
|
|
9
9
|
*
|
|
10
|
+
* v3.3.0 (closes #2): renders chapters with the consumer's MDX-components
|
|
11
|
+
* map. Consumer creates src/mdx-components.{ts,js,mjs} that default-exports
|
|
12
|
+
* a defineMdxComponents({...}) call; this route imports the map via the
|
|
13
|
+
* virtual:book-scaffold/mdx-components module exposed by the toolkit's
|
|
14
|
+
* Vite plugin. If the consumer has no mdx-components file, the virtual
|
|
15
|
+
* module exports {} so the import is harmless.
|
|
16
|
+
*
|
|
10
17
|
* Build pipeline:
|
|
11
18
|
* npm run build → Astro emits dist/print/index.html
|
|
12
19
|
* npm run pdf → pagedjs-cli fetches dist/print/ via preview
|
|
@@ -17,6 +24,7 @@ import Base from '../layouts/Base.astro';
|
|
|
17
24
|
import { render } from 'astro:content';
|
|
18
25
|
import { getAllChapters } from '../src/lib/chapters';
|
|
19
26
|
import ChapterHeader from '../components/ChapterHeader.astro';
|
|
27
|
+
import mdxComponents from 'virtual:book-scaffold/mdx-components';
|
|
20
28
|
|
|
21
29
|
const chapters = await getAllChapters();
|
|
22
30
|
const rendered = await Promise.all(
|
|
@@ -32,7 +40,7 @@ const rendered = await Promise.all(
|
|
|
32
40
|
{rendered.map(({ entry, Content }) => (
|
|
33
41
|
<section class="chapter-print">
|
|
34
42
|
<ChapterHeader data={entry.data} />
|
|
35
|
-
<Content />
|
|
43
|
+
<Content components={mdxComponents} />
|
|
36
44
|
</section>
|
|
37
45
|
))}
|
|
38
46
|
</main>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Recipe 12 — Where to file issues (consumer-driven evolution)
|
|
2
|
+
|
|
3
|
+
This toolkit grows through cross-consumer dogfooding. Each new book project you stand up — academic curriculum, AI-CLI comparison, course notes, research portfolio, or something new — is both content work *and* a structured test of the scaffold's abstraction.
|
|
4
|
+
|
|
5
|
+
## When to file an issue
|
|
6
|
+
|
|
7
|
+
File against [`brandon-behring/book-scaffold-astro/issues`](https://github.com/brandon-behring/book-scaffold-astro/issues) when:
|
|
8
|
+
|
|
9
|
+
- The scaffold's current schemas don't fit your book's content shape (e.g. course notes needing freeform `tags` instead of the `tools_compared` enum).
|
|
10
|
+
- An auto-injected route conflicts with your book's URL structure (e.g. multi-book corpus that routes via `[book]/[slug]/`).
|
|
11
|
+
- A scaffold-injected route can't render your custom MDX components (e.g. you have `<AnkiCard>` that needs to appear on `/print`).
|
|
12
|
+
- A CLI subcommand crashes or behaves unexpectedly (e.g. `validate` reports zero chapters).
|
|
13
|
+
- A scaffold component you rebuilt has an exact equivalent already shipped (waste signal — file as `docs: missing in LATEX_TO_MDX_MAPPING.md`).
|
|
14
|
+
- An API decision blocks one of your downstream projects.
|
|
15
|
+
|
|
16
|
+
## Issue shape
|
|
17
|
+
|
|
18
|
+
Mirror the pattern used by issues [#1–#14](https://github.com/brandon-behring/book-scaffold-astro/issues?q=is%3Aissue+sort%3Acreated-desc):
|
|
19
|
+
|
|
20
|
+
```markdown
|
|
21
|
+
## Problem
|
|
22
|
+
<observed behavior + repro steps + which consumer surfaced it>
|
|
23
|
+
|
|
24
|
+
## Evidence
|
|
25
|
+
<command output, file paths, version pin (`npm view @brandon_m_behring/book-scaffold-astro version`)>
|
|
26
|
+
|
|
27
|
+
## Suggested fix
|
|
28
|
+
<one or more concrete options; trade-offs noted>
|
|
29
|
+
|
|
30
|
+
## Acceptance criteria
|
|
31
|
+
<bulleted checklist a reviewer can verify>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Label with `bug` / `enhancement` / `documentation`. Reference the consumer repo + line where the friction was hit.
|
|
35
|
+
|
|
36
|
+
## Why this matters (the loop)
|
|
37
|
+
|
|
38
|
+
Each batch of cross-consumer issues drives a minor toolkit release:
|
|
39
|
+
|
|
40
|
+
- **v3.0–v3.2** absorbed Phase B/C/D feedback from `post_transformers` + `book-template-astro`.
|
|
41
|
+
- **v3.3.0** closed 5 issues surfaced from the DLAI knowledge-graphs-rag pilot (course-notes profile + defineMdxComponents + per-route override + LaTeX migration doc).
|
|
42
|
+
- **v3.4.0** closed 8 more (preset vocabulary + propagation + frontmatter helper + validate root fix + CI hygiene + docs).
|
|
43
|
+
- **v3.5.0** (future) is expected to add the `research-portfolio` preset per issue #6 once cross-repo coordination with `prompt-injection-portfolio` is ready.
|
|
44
|
+
|
|
45
|
+
Profile-by-profile growth is the explicit strategy: the toolkit gets a new profile when a real consumer needs one, not before.
|
|
46
|
+
|
|
47
|
+
## What NOT to file
|
|
48
|
+
|
|
49
|
+
- Bug reports from external users of a single book — file those against the book's repo, not the scaffold's.
|
|
50
|
+
- Style preferences that already have an escape hatch (e.g. `extraStyles` array, consumer-side `<style>` blocks).
|
|
51
|
+
- Speculative features ("we might one day want X"). Wait for the second consumer to actually need it.
|
|
52
|
+
|
|
53
|
+
## Where to find prior decisions
|
|
54
|
+
|
|
55
|
+
- [`CHANGELOG.md`](../../CHANGELOG.md) — release-by-release breakdown.
|
|
56
|
+
- [`PACKAGE_DESIGN.md`](../PACKAGE_DESIGN.md) §1 Q1–Q6 — original Phase A locked decisions.
|
|
57
|
+
- [`LATEX_TO_MDX_MAPPING.md`](../LATEX_TO_MDX_MAPPING.md) — 38-component reference.
|
|
58
|
+
- [Closed issues](https://github.com/brandon-behring/book-scaffold-astro/issues?q=is%3Aissue+is%3Aclosed) — many problems already have rejected-alternative discussion attached.
|
package/scripts/build-bib.mjs
CHANGED
|
@@ -33,6 +33,24 @@
|
|
|
33
33
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
34
34
|
import { dirname, resolve } from 'node:path';
|
|
35
35
|
import { fileURLToPath } from 'node:url';
|
|
36
|
+
|
|
37
|
+
// --help / -h: non-mutating (closes #14).
|
|
38
|
+
const USAGE = `Usage: book-scaffold build-bib
|
|
39
|
+
|
|
40
|
+
Bibliography pipeline (academic profile). Reads bibliography.bib (or
|
|
41
|
+
BOOK_BIB_PATH if set), parses via @citation-js, emits src/data/references.json.
|
|
42
|
+
|
|
43
|
+
Env:
|
|
44
|
+
BOOK_BIB_PATH Override path to .bib file (default: ./bibliography.bib).
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--help, -h Print this message and exit (non-mutating).
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
51
|
+
process.stdout.write(USAGE);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
36
54
|
import { Cite } from '@citation-js/core';
|
|
37
55
|
import '@citation-js/plugin-bibtex';
|
|
38
56
|
|
|
@@ -30,6 +30,25 @@ import { dirname, resolve, basename } from 'node:path';
|
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
31
|
import { spawnSync } from 'node:child_process';
|
|
32
32
|
|
|
33
|
+
// --help / -h: non-mutating (closes #14).
|
|
34
|
+
const USAGE = `Usage: book-scaffold build-figures
|
|
35
|
+
|
|
36
|
+
Figure pipeline. PDF -> SVG via pdftocairo (PNG fallback via pdftoppm at
|
|
37
|
+
200dpi). Walks figures/ (or BOOK_FIGURES_PATH), emits to public/figures/.
|
|
38
|
+
Graceful-skip if pdftocairo / pdftoppm not on PATH.
|
|
39
|
+
|
|
40
|
+
Env:
|
|
41
|
+
BOOK_FIGURES_PATH Override figures source (default: figures/).
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--help, -h Print this message and exit (non-mutating).
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
48
|
+
process.stdout.write(USAGE);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
53
|
const PROJECT_ROOT = process.cwd();
|
|
35
54
|
|
package/scripts/build-labels.mjs
CHANGED
|
@@ -36,6 +36,26 @@
|
|
|
36
36
|
import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
|
|
37
37
|
import { resolve, join, basename, dirname } from 'node:path';
|
|
38
38
|
|
|
39
|
+
// --help / -h: non-mutating (closes #14).
|
|
40
|
+
const USAGE = `Usage: book-scaffold build-labels
|
|
41
|
+
|
|
42
|
+
Emit src/data/labels.json for <XRef> resolution. Walks chapter MDX files,
|
|
43
|
+
extracts labelable components (Theorem, Figure, ...), assigns display strings
|
|
44
|
+
like "Theorem 4.2" matching LaTeX \\cref.
|
|
45
|
+
|
|
46
|
+
Env:
|
|
47
|
+
BOOK_CHAPTERS_DIR Override chapters dir (default: src/content/chapters).
|
|
48
|
+
BOOK_LABELS_OUT Override output path (default: src/data/labels.json).
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--help, -h Print this message and exit (non-mutating).
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
55
|
+
process.stdout.write(USAGE);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
const CHAPTERS_DIR = process.env.BOOK_CHAPTERS_DIR ?? 'src/content/chapters';
|
|
40
60
|
const OUTPUT_PATH = process.env.BOOK_LABELS_OUT ?? 'src/data/labels.json';
|
|
41
61
|
|
|
@@ -35,6 +35,25 @@ import { dirname, resolve, basename } from 'node:path';
|
|
|
35
35
|
import { fileURLToPath } from 'node:url';
|
|
36
36
|
import { spawnSync } from 'node:child_process';
|
|
37
37
|
|
|
38
|
+
// --help / -h: non-mutating (closes #14).
|
|
39
|
+
const USAGE = `Usage: book-scaffold render-notebooks
|
|
40
|
+
|
|
41
|
+
Notebook pipeline. .ipynb -> standalone HTML via Jupyter nbconvert (--basic).
|
|
42
|
+
Walks notebooks/ (or BOOK_NOTEBOOKS_PATH), emits to public/notebooks/.
|
|
43
|
+
Graceful-skip if uv not on PATH.
|
|
44
|
+
|
|
45
|
+
Env:
|
|
46
|
+
BOOK_NOTEBOOKS_PATH Override notebooks source (default: notebooks/).
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--help, -h Print this message and exit (non-mutating).
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
53
|
+
process.stdout.write(USAGE);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
39
58
|
const PROJECT_ROOT = process.cwd();
|
|
40
59
|
|
package/scripts/validate.mjs
CHANGED
|
@@ -7,56 +7,70 @@
|
|
|
7
7
|
* book so it's pre-commit-hook friendly.
|
|
8
8
|
*
|
|
9
9
|
* Checks performed (per Q14 in the v2.0 plan):
|
|
10
|
-
*
|
|
11
10
|
* 1. <Cite key="..." /> — key exists in src/data/references.json.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* typos ship to readers as "[?label]" placeholders.
|
|
18
|
-
*
|
|
19
|
-
* 3. <Figure src="/path/..." /> — referenced file exists under
|
|
20
|
-
* public/. Figure.astro renders a broken-image icon otherwise.
|
|
21
|
-
*
|
|
22
|
-
* 4. Internal markdown links [text](/foo) — target resolves to a
|
|
23
|
-
* known chapter slug or a known top-level route. External (http*)
|
|
24
|
-
* links are not checked (would need network IO).
|
|
11
|
+
* 2. <XRef id="..." /> — id exists in src/data/labels.json.
|
|
12
|
+
* 3. <Figure src="/path/..." /> — file exists under public/.
|
|
13
|
+
* 4. Internal markdown links [text](/foo) — target resolves.
|
|
14
|
+
* 5. <CodeRef path="..." line={N} /> — when BOOK_REPO_ROOT set,
|
|
15
|
+
* path exists + line in bounds.
|
|
25
16
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* isn't set (the scaffold default; only meaningful for academic
|
|
30
|
-
* books that paired with an experiments/ subtree).
|
|
31
|
-
*
|
|
32
|
-
* What this DOESN'T do (and why):
|
|
33
|
-
* - frontmatter Zod validation — already done by astro build's
|
|
34
|
-
* content-collection sync.
|
|
35
|
-
* - MDX renders — same; astro build will fail.
|
|
36
|
-
* - KaTeX strict-mode — covered by rehype-katex when academic
|
|
37
|
-
* profile is active; undefined macros become build errors.
|
|
17
|
+
* Run from the consumer's project root. Closes #8 (was resolving paths
|
|
18
|
+
* from the package's own directory inside node_modules — false negatives
|
|
19
|
+
* across all reference consumers).
|
|
38
20
|
*
|
|
39
21
|
* Usage:
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* Exit code = total failure count (0 = pass, ≥1 = errors).
|
|
22
|
+
* book-scaffold validate
|
|
23
|
+
* book-scaffold validate --preset academic
|
|
24
|
+
* BOOK_REPO_ROOT=/abs/path npx book-scaffold validate
|
|
44
25
|
*
|
|
45
|
-
*
|
|
46
|
-
* - package.json scripts: "validate": "node scripts/validate.mjs"
|
|
47
|
-
* - pre-commit hook: .pre-commit-config.yaml
|
|
48
|
-
* - CI build pipeline: run before `astro build`
|
|
26
|
+
* Exit code = total failure count (0 = pass, >=1 = errors).
|
|
49
27
|
*/
|
|
50
28
|
import { readFile, access } from 'node:fs/promises';
|
|
51
29
|
import { glob } from 'node:fs/promises';
|
|
52
30
|
import { resolve, dirname, join } from 'node:path';
|
|
53
|
-
import { fileURLToPath } from 'node:url';
|
|
54
31
|
|
|
55
|
-
|
|
32
|
+
// --help / -h: non-mutating (closes #14).
|
|
33
|
+
const USAGE = `Usage: book-scaffold validate [--preset <name>]
|
|
34
|
+
|
|
35
|
+
Pre-flight content validator. Checks Cite keys, XRef ids, Figure srcs,
|
|
36
|
+
internal markdown links, and (when BOOK_REPO_ROOT is set) CodeRef paths.
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--preset <name> academic | tools | minimal | course-notes
|
|
40
|
+
(overrides BOOK_PRESET / BOOK_PROFILE env)
|
|
41
|
+
--help, -h Print this message and exit (non-mutating).
|
|
42
|
+
|
|
43
|
+
Env:
|
|
44
|
+
BOOK_PRESET Preset name (preferred over BOOK_PROFILE).
|
|
45
|
+
BOOK_PROFILE Backward-compat alias for BOOK_PRESET.
|
|
46
|
+
BOOK_REPO_ROOT Absolute path to a sibling code repo for CodeRef checks.
|
|
47
|
+
|
|
48
|
+
Exit code = total failure count.
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
52
|
+
process.stdout.write(USAGE);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --preset <name> CLI flag (closes #9 — single source of truth across
|
|
57
|
+
// defineBookConfig + validate).
|
|
58
|
+
const argv = process.argv.slice(2);
|
|
59
|
+
const presetFlagIdx = argv.findIndex((a) => a === '--preset');
|
|
60
|
+
const presetFromFlag = presetFlagIdx >= 0 ? argv[presetFlagIdx + 1] : undefined;
|
|
61
|
+
|
|
62
|
+
// v3.4.0: ROOT is the consumer's CWD, not the package's own dir.
|
|
63
|
+
// Resolves issue #8 — three reference consumers reported "0 chapter(s) checked"
|
|
64
|
+
// because ROOT was the package directory inside node_modules.
|
|
65
|
+
const ROOT = process.cwd();
|
|
56
66
|
const CHAPTERS_DIR = resolve(ROOT, 'src/content/chapters');
|
|
57
67
|
const PUBLIC_DIR = resolve(ROOT, 'public');
|
|
58
68
|
const DATA_DIR = resolve(ROOT, 'src/data');
|
|
59
|
-
|
|
69
|
+
|
|
70
|
+
// Preset resolution: --preset flag > BOOK_PRESET env > BOOK_PROFILE env > 'minimal'.
|
|
71
|
+
const PRESET = presetFromFlag ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE ?? 'minimal';
|
|
72
|
+
// Alias kept for downstream message text only; the resolution above is canonical.
|
|
73
|
+
const PROFILE = PRESET;
|
|
60
74
|
const REPO_ROOT = process.env.BOOK_REPO_ROOT ?? null;
|
|
61
75
|
|
|
62
76
|
const errors = [];
|