@brandon_m_behring/book-scaffold-astro 3.3.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/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AstroUserConfig, AstroIntegration } from 'astro';
2
- import { b as BookConfigOptions, d as BookScaffoldIntegrationOptions, v as volatilityLevels } from './types-s8NxCLU2.js';
3
- export { A as AcademicChapter, B as BOOK_PROFILES, a as BookConfigError, c as BookProfile, e as BookSchemasOptions, C as ChapterFor, f as CourseNotesChapter, M as MinimalChapter, P as ProfileDefinition, R as RouteToggles, T as ToolsChapter, g as academicChapterSchema, h as academicParts, i as changeKinds, j as changelogSchema, k as chapterStatus, l as courseNotesChapterSchema, m as defineProfile, n as minimalChapterSchema, p as patternCategories, o as patternsSchema, r as resolveProfile, s as sourceTiers, q as sourcesSchema, t as toolSlugs, u as toolsChapterSchema } from './types-s8NxCLU2.js';
2
+ import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, y as volatilityLevels } from './types-Bb97Na9S.js';
3
+ export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, h as CourseNotesChapter, M as MinimalChapter, P as ProfileDefinition, R as RouteToggles, T as ToolsChapter, i as academicChapterSchema, j as academicParts, k as changeKinds, l as changelogSchema, m as chapterStatus, n as courseNotesChapterSchema, o as defineProfile, p as minimalChapterSchema, q as patternCategories, r as patternsSchema, s as resolvePreset, t as resolveProfile, u as sourceTiers, v as sourcesSchema, w as toolSlugs, x as toolsChapterSchema } from './types-Bb97Na9S.js';
4
4
  import 'astro/zod';
5
5
 
6
6
  declare function defineBookConfig(opts: BookConfigOptions): Promise<AstroUserConfig>;
package/dist/index.mjs CHANGED
@@ -263,8 +263,10 @@ var academicProfile = defineProfile({
263
263
  print: true,
264
264
  chapters: false,
265
265
  // academic consumers ship their own week-based /chapters listing
266
- convergence: false
266
+ convergence: false,
267
267
  // tools-profile-specific
268
+ frontmatter: false
269
+ // opt-in per book; see #7
268
270
  },
269
271
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
270
272
  katex: true
@@ -280,8 +282,10 @@ var toolsProfile = defineProfile({
280
282
  print: true,
281
283
  chapters: true,
282
284
  // tools profile ships a flat chapter index
283
- convergence: true
285
+ convergence: true,
284
286
  // tools profile ships convergence dashboard
287
+ frontmatter: false
288
+ // opt-in per book; see #7
285
289
  },
286
290
  styles: [
287
291
  "tokens.css",
@@ -304,7 +308,9 @@ var minimalProfile = defineProfile({
304
308
  search: true,
305
309
  print: true,
306
310
  chapters: false,
307
- convergence: false
311
+ convergence: false,
312
+ frontmatter: false
313
+ // opt-in per book; see #7
308
314
  },
309
315
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
310
316
  });
@@ -319,7 +325,9 @@ var courseNotesProfile = defineProfile({
319
325
  print: true,
320
326
  chapters: false,
321
327
  // multi-book consumers route via [book]/[slug] themselves
322
- convergence: false
328
+ convergence: false,
329
+ frontmatter: false
330
+ // opt-in per book; see #7
323
331
  },
324
332
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
325
333
  });
@@ -335,6 +343,7 @@ var BOOK_PROFILES = Object.keys(PROFILES);
335
343
 
336
344
  // src/types.ts
337
345
  import { existsSync, readFileSync } from "fs";
346
+ var BOOK_PRESETS = BOOK_PROFILES;
338
347
  var BookConfigError = class extends Error {
339
348
  constructor(message) {
340
349
  super(message);
@@ -359,29 +368,33 @@ function readEnvFile(path = ".env") {
359
368
  return {};
360
369
  }
361
370
  }
362
- function resolveProfile(explicit) {
363
- let candidate = explicit ?? process.env.BOOK_PROFILE;
371
+ function resolvePreset(explicitPreset, explicitProfile) {
372
+ let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
364
373
  let source = "default";
365
- if (explicit) source = "param";
366
- else if (process.env.BOOK_PROFILE) source = "env";
374
+ if (explicitPreset || explicitProfile) source = "param";
375
+ else if (process.env.BOOK_PRESET || process.env.BOOK_PROFILE) source = "env";
367
376
  if (!candidate) {
368
- const fromFile = readEnvFile().BOOK_PROFILE;
377
+ const env = readEnvFile();
378
+ const fromFile = env.BOOK_PRESET ?? env.BOOK_PROFILE;
369
379
  if (fromFile) {
370
380
  candidate = fromFile;
371
381
  source = "dotenv";
372
382
  }
373
383
  }
374
384
  candidate = candidate ?? "minimal";
375
- if (!BOOK_PROFILES.includes(candidate)) {
385
+ if (!BOOK_PRESETS.includes(candidate)) {
376
386
  throw new BookConfigError(
377
- `profile must be one of ${BOOK_PROFILES.join(" | ")} (got ${JSON.stringify(candidate)})`
387
+ `preset must be one of ${BOOK_PRESETS.join(" | ")} (got ${JSON.stringify(candidate)})`
378
388
  );
379
389
  }
380
390
  if (source === "default") {
381
- console.warn("book-scaffold-astro: BOOK_PROFILE not set; falling back to 'minimal'.");
391
+ console.warn("book-scaffold-astro: BOOK_PRESET not set; falling back to 'minimal'.");
382
392
  }
383
393
  return candidate;
384
394
  }
395
+ function resolveProfile(explicit) {
396
+ return resolvePreset(void 0, explicit);
397
+ }
385
398
 
386
399
  // src/integration.ts
387
400
  import { fileURLToPath } from "url";
@@ -435,7 +448,11 @@ var ROUTE_REGISTRY = {
435
448
  search: { pattern: "/search", file: "search.astro" },
436
449
  print: { pattern: "/print", file: "print.astro" },
437
450
  chapters: { pattern: "/chapters", file: "chapters.astro" },
438
- convergence: { pattern: "/convergence", file: "convergence.astro" }
451
+ convergence: { pattern: "/convergence", file: "convergence.astro" },
452
+ // v3.4.0 (#7): consumer-collection-backed frontmatter route. Opt-in via
453
+ // routes: { frontmatter: true } AND content.config.ts defining the
454
+ // collection (use frontmatterCollection() helper from /schemas subpath).
455
+ frontmatter: { pattern: "/frontmatter/[slug]", file: "frontmatter/[...slug].astro" }
439
456
  };
440
457
  function resolvePage(file) {
441
458
  return fileURLToPath(new URL(`../pages/${file}`, import.meta.url));
@@ -466,9 +483,14 @@ function bookScaffoldIntegration(opts) {
466
483
  }
467
484
  const consumerRoot = fileURLToPath(config.root);
468
485
  const resolvedMdxPath = resolveMdxComponentsPath(consumerRoot, mdxComponentsModule);
486
+ const presetLiteral = JSON.stringify(profile);
469
487
  updateConfig({
470
488
  vite: {
471
- plugins: [makeMdxComponentsVitePlugin(resolvedMdxPath)]
489
+ plugins: [makeMdxComponentsVitePlugin(resolvedMdxPath)],
490
+ define: {
491
+ "import.meta.env.BOOK_PRESET": presetLiteral,
492
+ "import.meta.env.BOOK_PROFILE": presetLiteral
493
+ }
472
494
  }
473
495
  });
474
496
  }
@@ -478,7 +500,7 @@ function bookScaffoldIntegration(opts) {
478
500
 
479
501
  // src/config.ts
480
502
  async function defineBookConfig(opts) {
481
- const profile = resolveProfile(opts.profile);
503
+ const profile = resolvePreset(opts.preset, opts.profile);
482
504
  const remarkPlugins = [];
483
505
  const rehypePlugins = [];
484
506
  if (profile === "academic") {
@@ -531,6 +553,8 @@ async function defineBookConfig(opts) {
531
553
  ...userMarkdown
532
554
  };
533
555
  const {
556
+ preset: _preset,
557
+ // v3.4.0
534
558
  profile: _profile,
535
559
  routes: _routes,
536
560
  // v3.3.0
@@ -541,6 +565,7 @@ async function defineBookConfig(opts) {
541
565
  markdown: _markdown,
542
566
  ...rest
543
567
  } = opts;
568
+ void _preset;
544
569
  void _profile;
545
570
  void _routes;
546
571
  void _mdxComponentsModule;
@@ -598,6 +623,7 @@ function freshnessLabel(f) {
598
623
  }
599
624
  }
600
625
  export {
626
+ BOOK_PRESETS,
601
627
  BOOK_PROFILES,
602
628
  BookConfigError,
603
629
  academicChapterSchema,
@@ -615,6 +641,7 @@ export {
615
641
  minimalChapterSchema,
616
642
  patternCategories,
617
643
  patternsSchema,
644
+ resolvePreset,
618
645
  resolveProfile,
619
646
  sourceTiers,
620
647
  sourcesSchema,
package/dist/schemas.d.ts CHANGED
@@ -1,7 +1,40 @@
1
- import { e as BookSchemasOptions } from './types-s8NxCLU2.js';
1
+ import { defineCollection } from 'astro:content';
2
+ import { g as BookSchemasOptions } from './types-Bb97Na9S.js';
2
3
  import 'astro';
3
4
  import 'astro/zod';
4
5
 
6
+ /**
7
+ * v3.4.0 (closes #7): consumer-facing helper to define a `frontmatter`
8
+ * content collection that the scaffold's auto-injected
9
+ * `/frontmatter/[slug]` route can render.
10
+ *
11
+ * Usage in consumer's content.config.ts:
12
+ *
13
+ * import { defineBookSchemas, frontmatterCollection } from
14
+ * '@brandon_m_behring/book-scaffold-astro/schemas';
15
+ * import { z } from 'astro:content';
16
+ *
17
+ * export const { collections } = {
18
+ * collections: {
19
+ * ...defineBookSchemas().collections,
20
+ * frontmatter: frontmatterCollection(z.object({
21
+ * slug: z.string(),
22
+ * title: z.string(),
23
+ * order: z.number(),
24
+ * description: z.string().optional(),
25
+ * })),
26
+ * },
27
+ * };
28
+ *
29
+ * Then enable the route via `defineBookConfig({ routes: { frontmatter: true } })`
30
+ * and drop MDX files under `src/content/frontmatter/`. The scaffold-injected
31
+ * route renders each entry with the consumer's mdx-components in scope (issue #2
32
+ * plumbing applies).
33
+ *
34
+ * Default loader: `**\/*.{md,mdx}` under `./src/content/frontmatter` (excluding
35
+ * underscore-prefixed files). Override `base` via the second arg.
36
+ */
37
+ declare function frontmatterCollection(schema: Parameters<typeof defineCollection>[0]['schema'], base?: string): unknown;
5
38
  /**
6
39
  * Returns the package's default content collections. Closed shape per Q5;
7
40
  * consumer extends via object spread and Zod `.extend()` (see PACKAGE_DESIGN.md §5).
@@ -10,4 +43,4 @@ declare function defineBookSchemas(opts?: BookSchemasOptions): {
10
43
  collections: Record<string, unknown>;
11
44
  };
12
45
 
13
- export { defineBookSchemas };
46
+ export { defineBookSchemas, frontmatterCollection };
package/dist/schemas.mjs CHANGED
@@ -148,8 +148,10 @@ var academicProfile = defineProfile({
148
148
  print: true,
149
149
  chapters: false,
150
150
  // academic consumers ship their own week-based /chapters listing
151
- convergence: false
151
+ convergence: false,
152
152
  // tools-profile-specific
153
+ frontmatter: false
154
+ // opt-in per book; see #7
153
155
  },
154
156
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
155
157
  katex: true
@@ -165,8 +167,10 @@ var toolsProfile = defineProfile({
165
167
  print: true,
166
168
  chapters: true,
167
169
  // tools profile ships a flat chapter index
168
- convergence: true
170
+ convergence: true,
169
171
  // tools profile ships convergence dashboard
172
+ frontmatter: false
173
+ // opt-in per book; see #7
170
174
  },
171
175
  styles: [
172
176
  "tokens.css",
@@ -189,7 +193,9 @@ var minimalProfile = defineProfile({
189
193
  search: true,
190
194
  print: true,
191
195
  chapters: false,
192
- convergence: false
196
+ convergence: false,
197
+ frontmatter: false
198
+ // opt-in per book; see #7
193
199
  },
194
200
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
195
201
  });
@@ -204,7 +210,9 @@ var courseNotesProfile = defineProfile({
204
210
  print: true,
205
211
  chapters: false,
206
212
  // multi-book consumers route via [book]/[slug] themselves
207
- convergence: false
213
+ convergence: false,
214
+ frontmatter: false
215
+ // opt-in per book; see #7
208
216
  },
209
217
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
210
218
  });
@@ -220,6 +228,7 @@ var BOOK_PROFILES = Object.keys(PROFILES);
220
228
 
221
229
  // src/types.ts
222
230
  import { existsSync, readFileSync } from "fs";
231
+ var BOOK_PRESETS = BOOK_PROFILES;
223
232
  var BookConfigError = class extends Error {
224
233
  constructor(message) {
225
234
  super(message);
@@ -244,33 +253,43 @@ function readEnvFile(path = ".env") {
244
253
  return {};
245
254
  }
246
255
  }
247
- function resolveProfile(explicit) {
248
- let candidate = explicit ?? process.env.BOOK_PROFILE;
256
+ function resolvePreset(explicitPreset, explicitProfile) {
257
+ let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
249
258
  let source = "default";
250
- if (explicit) source = "param";
251
- else if (process.env.BOOK_PROFILE) source = "env";
259
+ if (explicitPreset || explicitProfile) source = "param";
260
+ else if (process.env.BOOK_PRESET || process.env.BOOK_PROFILE) source = "env";
252
261
  if (!candidate) {
253
- const fromFile = readEnvFile().BOOK_PROFILE;
262
+ const env = readEnvFile();
263
+ const fromFile = env.BOOK_PRESET ?? env.BOOK_PROFILE;
254
264
  if (fromFile) {
255
265
  candidate = fromFile;
256
266
  source = "dotenv";
257
267
  }
258
268
  }
259
269
  candidate = candidate ?? "minimal";
260
- if (!BOOK_PROFILES.includes(candidate)) {
270
+ if (!BOOK_PRESETS.includes(candidate)) {
261
271
  throw new BookConfigError(
262
- `profile must be one of ${BOOK_PROFILES.join(" | ")} (got ${JSON.stringify(candidate)})`
272
+ `preset must be one of ${BOOK_PRESETS.join(" | ")} (got ${JSON.stringify(candidate)})`
263
273
  );
264
274
  }
265
275
  if (source === "default") {
266
- console.warn("book-scaffold-astro: BOOK_PROFILE not set; falling back to 'minimal'.");
276
+ console.warn("book-scaffold-astro: BOOK_PRESET not set; falling back to 'minimal'.");
267
277
  }
268
278
  return candidate;
269
279
  }
270
280
 
271
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
+ }
272
291
  function defineBookSchemas(opts = {}) {
273
- const profile = resolveProfile(opts.profile);
292
+ const profile = resolvePreset(opts.preset, opts.profile);
274
293
  const chaptersBase = opts.chaptersBase ?? "./src/content/chapters";
275
294
  const schemaForProfile = profile === "academic" ? academicChapterSchema : profile === "course-notes" ? courseNotesChapterSchema : profile === "minimal" ? minimalChapterSchema : toolsChapterSchema;
276
295
  const chapters = defineCollection({
@@ -305,5 +324,6 @@ function defineBookSchemas(opts = {}) {
305
324
  return { collections };
306
325
  }
307
326
  export {
308
- defineBookSchemas
327
+ defineBookSchemas,
328
+ frontmatterCollection
309
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.3.0",
4
+ "version": "3.4.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Brandon Behring",
@@ -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>
@@ -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.
@@ -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
 
@@ -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
 
@@ -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
- * (Cite.astro already throws on unknown keys at build time; we
13
- * surface ALL bad keys at once instead of failing on the first.)
14
- *
15
- * 2. <XRef id="..." /> — id exists in src/data/labels.json. XRef
16
- * doesn't fail the build for unknown ids; without this check,
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
- * 5. <CodeRef path="..." line={N} /> when run inside a repo
27
- * whose root is BOOK_REPO_ROOT, the path exists and the line
28
- * number is within file bounds. Skipped when BOOK_REPO_ROOT
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
- * node scripts/validate.mjs
41
- * BOOK_REPO_ROOT=/abs/path/to/code/repo node scripts/validate.mjs
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
- * Wire into:
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
- const ROOT = resolve(fileURLToPath(import.meta.url), '../..');
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
- const PROFILE = process.env.BOOK_PROFILE ?? 'minimal';
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 = [];