@brandon_m_behring/book-scaffold-astro 4.31.0 → 5.0.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/CLAUDE.md +37 -2
- package/MIGRATION-v4-to-v5.md +183 -0
- package/README.md +30 -1
- package/components/AssessmentTest.astro +26 -5
- package/components/BookLink.astro +6 -3
- package/components/ChapterNav.astro +1 -1
- package/components/Cite.astro +37 -4
- package/components/ExerciseSolutions.astro +24 -4
- package/components/Flashcards.tsx +19 -11
- package/components/NavContent.astro +60 -11
- package/components/ObjectiveMap.astro +14 -2
- package/components/PartReview.astro +32 -4
- package/components/PatternTimeline.astro +6 -2
- package/components/Rationale.astro +20 -3
- package/components/Sidebar.astro +11 -3
- package/components/SourceArchive.astro +33 -3
- package/components/Term.astro +18 -1
- package/components/Theorem.astro +15 -5
- package/components/TipsCard.astro +40 -3
- package/components/XRef.astro +14 -2
- package/dist/components/ExamRunner.d.ts +1 -1
- package/dist/components/Flashcards.d.ts +6 -1
- package/dist/components/Flashcards.mjs +14 -11
- package/dist/{exam-manifest-X9IrX1G3.d.ts → exam-manifest-DttY7kyZ.d.ts} +1 -1
- package/dist/index.d.ts +76 -8
- package/dist/index.mjs +517 -43
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +311 -32
- package/dist/{types-DgSlAew3.d.ts → types-D1QZgKMO.d.ts} +73 -21
- package/layouts/Base.astro +38 -9
- package/layouts/Chapter.astro +10 -2
- package/package.json +4 -2
- package/pages/answers.astro +25 -6
- package/pages/book.astro +75 -0
- package/pages/chapters/[...slug].astro +39 -7
- package/pages/chapters-book.astro +17 -0
- package/pages/chapters.astro +87 -9
- package/pages/convergence.astro +40 -10
- package/pages/corpus-apparatus/answers.astro +15 -0
- package/pages/corpus-apparatus/convergence.astro +15 -0
- package/pages/corpus-apparatus/exercises.astro +15 -0
- package/pages/corpus-apparatus/flashcards.astro +15 -0
- package/pages/corpus-apparatus/glossary.astro +15 -0
- package/pages/corpus-apparatus/practice-exam.astro +15 -0
- package/pages/corpus-apparatus/print.astro +15 -0
- package/pages/corpus-apparatus/references.astro +15 -0
- package/pages/corpus-apparatus/tips.astro +15 -0
- package/pages/exercises.astro +18 -5
- package/pages/flashcards.astro +26 -5
- package/pages/glossary.astro +20 -3
- package/pages/index.astro +54 -1
- package/pages/practice-exam.astro +8 -2
- package/pages/print.astro +7 -2
- package/pages/references.astro +26 -6
- package/pages/search.astro +65 -4
- package/pages/tips.astro +17 -4
- package/recipes/09-validation.md +1 -1
- package/recipes/15-defining-styles.md +6 -6
- package/recipes/20-anki-export.md +15 -8
- package/recipes/21-multi-guide-single-app.md +293 -44
- package/recipes/22-responsive-nav-and-multibook-routing.md +7 -1
- package/recipes/README.md +2 -2
- package/scripts/build-bib.mjs +113 -35
- package/scripts/build-exercises.mjs +101 -24
- package/scripts/build-labels.mjs +199 -194
- package/scripts/build-tips.mjs +95 -23
- package/scripts/corpus-tooling.mjs +268 -0
- package/scripts/resolve-book-config.mjs +99 -1
- package/scripts/validate.mjs +676 -100
- package/scripts/walk-mdx.mjs +16 -4
- package/src/lib/book-link.ts +72 -0
- package/src/lib/chapters.ts +10 -4
- package/src/lib/corpus-collateral.ts +9 -0
- package/src/lib/corpus.ts +458 -0
- package/src/lib/define-style.ts +28 -15
- package/src/lib/exam-manifest.ts +4 -1
- package/src/lib/patterns.ts +15 -6
- package/src/lib/questions.ts +8 -3
- package/src/types.ts +603 -0
package/scripts/build-bib.mjs
CHANGED
|
@@ -40,6 +40,8 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
|
40
40
|
import { dirname, resolve } from 'node:path';
|
|
41
41
|
import { fileURLToPath } from 'node:url';
|
|
42
42
|
import { readEnvFile } from './read-env.mjs';
|
|
43
|
+
import { loadResolvedBookConfig } from './resolve-book-config.mjs';
|
|
44
|
+
import { mergeCorpusArtifact, resolveBookSelection } from './corpus-tooling.mjs';
|
|
43
45
|
|
|
44
46
|
// --help / -h: non-mutating (closes #14).
|
|
45
47
|
const USAGE = `Usage: book-scaffold build-bib
|
|
@@ -54,6 +56,7 @@ Env:
|
|
|
54
56
|
.env; default: ./bibliography.bib).
|
|
55
57
|
|
|
56
58
|
Options:
|
|
59
|
+
--book <id> In corpus mode, rebuild only one registered book.
|
|
57
60
|
--help, -h Print this message and exit (non-mutating).
|
|
58
61
|
`;
|
|
59
62
|
|
|
@@ -77,8 +80,13 @@ const BIB_PATH = configuredBibPath
|
|
|
77
80
|
const OUT_PATH = resolve(PROJECT_ROOT, 'src/data/references.json');
|
|
78
81
|
const SOURCES_PATH = resolve(PROJECT_ROOT, 'sources/manifest.yaml');
|
|
79
82
|
const SOURCES_OUT = resolve(PROJECT_ROOT, 'src/data/sources.json');
|
|
83
|
+
let DIAGNOSTIC_SCOPE = null;
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
function isRecord(value) {
|
|
86
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function buildReferences(selection) {
|
|
82
90
|
// Graceful skip when the .bib file is absent (minimal/tools profile, or
|
|
83
91
|
// an academic book that hasn't authored citations yet). Emits an empty
|
|
84
92
|
// references.json so consumers can still `import refs from '...'`.
|
|
@@ -87,15 +95,10 @@ async function buildReferences() {
|
|
|
87
95
|
bibText = await readFile(BIB_PATH, 'utf8');
|
|
88
96
|
} catch (err) {
|
|
89
97
|
if (err.code === 'ENOENT') {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
);
|
|
94
|
-
await mkdir(dirname(OUT_PATH), { recursive: true });
|
|
95
|
-
await writeFile(OUT_PATH, '{}\n', 'utf8');
|
|
96
|
-
return;
|
|
98
|
+
bibText = null;
|
|
99
|
+
} else {
|
|
100
|
+
throw err;
|
|
97
101
|
}
|
|
98
|
-
throw err;
|
|
99
102
|
}
|
|
100
103
|
// v4.0.0 (closes #54): strip `%`-comment lines before passing to citation-js.
|
|
101
104
|
// The plugin-bibtex lexer doesn't honor BibTeX's %-line-comment semantics —
|
|
@@ -107,13 +110,14 @@ async function buildReferences() {
|
|
|
107
110
|
// BibTeX's real comment grammar is "% at the start of a line (after optional
|
|
108
111
|
// whitespace) to end of line". Mid-line `%` (e.g., `note = {50% confidence}`)
|
|
109
112
|
// is NOT a comment and is preserved.
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
const data = bibText === null
|
|
114
|
+
? []
|
|
115
|
+
: new Cite(
|
|
116
|
+
bibText
|
|
117
|
+
.split(/\r?\n/)
|
|
118
|
+
.map((line) => (line.trimStart().startsWith('%') ? '' : line))
|
|
119
|
+
.join('\n'),
|
|
120
|
+
).data;
|
|
117
121
|
|
|
118
122
|
// Detect duplicates the way biber would (citation-js silently
|
|
119
123
|
// overwrites the earlier entry, which is the opposite of what we want).
|
|
@@ -124,19 +128,53 @@ async function buildReferences() {
|
|
|
124
128
|
seen.add(entry.id);
|
|
125
129
|
}
|
|
126
130
|
if (dupes.length > 0) {
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
const prefix = selection.corpus ? '[book:corpus] ' : '';
|
|
132
|
+
console.error(`${prefix}build-bib: ${dupes.length} duplicate bibkeys:`);
|
|
133
|
+
for (const id of dupes) console.error(`${prefix}build-bib: - ${id}`);
|
|
129
134
|
process.exit(1);
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
const byKey = Object.fromEntries(data.map((entry) => [entry.id, entry]));
|
|
133
138
|
|
|
139
|
+
if (selection.corpus) {
|
|
140
|
+
DIAGNOSTIC_SCOPE = 'corpus';
|
|
141
|
+
}
|
|
142
|
+
const output = selection.corpus
|
|
143
|
+
? await mergeCorpusArtifact({
|
|
144
|
+
path: OUT_PATH,
|
|
145
|
+
corpus: selection.corpus,
|
|
146
|
+
requestedBook: selection.requestedBook,
|
|
147
|
+
values: new Map(selection.books.map((book) => [book.id, byKey])),
|
|
148
|
+
emptyValue: () => ({}),
|
|
149
|
+
artifact: 'src/data/references.json',
|
|
150
|
+
validateValue: isRecord,
|
|
151
|
+
})
|
|
152
|
+
: byKey;
|
|
134
153
|
await mkdir(dirname(OUT_PATH), { recursive: true });
|
|
135
|
-
await writeFile(OUT_PATH, JSON.stringify(
|
|
154
|
+
await writeFile(OUT_PATH, JSON.stringify(output, null, 2) + '\n', 'utf8');
|
|
136
155
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
if (selection.corpus) {
|
|
157
|
+
for (const book of selection.books) {
|
|
158
|
+
console.log(
|
|
159
|
+
`[book:${book.id}] build-bib: ${data.length} entries -> ` +
|
|
160
|
+
OUT_PATH.replace(PROJECT_ROOT + '/', ''),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
console.log(
|
|
164
|
+
`[book:corpus] build-bib: ${data.length * selection.books.length} namespaced entries ` +
|
|
165
|
+
`across ${selection.books.length} book${selection.books.length === 1 ? '' : 's'} -> ` +
|
|
166
|
+
OUT_PATH.replace(PROJECT_ROOT + '/', ''),
|
|
167
|
+
);
|
|
168
|
+
} else if (bibText === null) {
|
|
169
|
+
console.log(
|
|
170
|
+
`build-bib: ${BIB_PATH.replace(PROJECT_ROOT + '/', '')} not found — ` +
|
|
171
|
+
`emitting empty references.json (no citations to process).`,
|
|
172
|
+
);
|
|
173
|
+
} else {
|
|
174
|
+
console.log(
|
|
175
|
+
`build-bib: ${data.length} entries -> ${OUT_PATH.replace(PROJECT_ROOT + '/', '')}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
140
178
|
}
|
|
141
179
|
|
|
142
180
|
// v4.10.0 (closes #85): tools-profile books keep their sources in
|
|
@@ -147,17 +185,24 @@ async function buildReferences() {
|
|
|
147
185
|
// references.json. Absent manifest -> no file written (academic/minimal books
|
|
148
186
|
// degrade to empty, exactly like a missing .bib). YAML is lazy-imported so the
|
|
149
187
|
// --help / no-manifest paths stay dependency-free.
|
|
150
|
-
async function buildSources() {
|
|
188
|
+
async function buildSources(selection) {
|
|
151
189
|
let yamlText;
|
|
152
190
|
try {
|
|
153
191
|
yamlText = await readFile(SOURCES_PATH, 'utf8');
|
|
154
192
|
} catch (err) {
|
|
155
|
-
if (err.code === 'ENOENT')
|
|
156
|
-
|
|
193
|
+
if (err.code === 'ENOENT') {
|
|
194
|
+
// A corpus artifact must be a complete, deterministic envelope. Clear
|
|
195
|
+
// stale source data when the shared manifest disappears; legacy
|
|
196
|
+
// single-book mode retains its historical no-file/no-write behavior.
|
|
197
|
+
if (!selection.corpus) return;
|
|
198
|
+
yamlText = null;
|
|
199
|
+
} else {
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
157
202
|
}
|
|
158
203
|
|
|
159
204
|
const { parse } = await import('yaml');
|
|
160
|
-
const parsed = parse(yamlText);
|
|
205
|
+
const parsed = yamlText === null ? null : parse(yamlText);
|
|
161
206
|
// The manifest is a YAML array of source objects. Keep only well-formed
|
|
162
207
|
// entries (a string `id` is the citation key + the /references anchor target).
|
|
163
208
|
// A blank or comments-only manifest parses to null/undefined/[].
|
|
@@ -165,21 +210,54 @@ async function buildSources() {
|
|
|
165
210
|
? parsed.filter((s) => s && typeof s.id === 'string')
|
|
166
211
|
: [];
|
|
167
212
|
|
|
213
|
+
const output = selection.corpus
|
|
214
|
+
? await mergeCorpusArtifact({
|
|
215
|
+
path: SOURCES_OUT,
|
|
216
|
+
corpus: selection.corpus,
|
|
217
|
+
requestedBook: selection.requestedBook,
|
|
218
|
+
values: new Map(selection.books.map((book) => [book.id, sources])),
|
|
219
|
+
emptyValue: () => [],
|
|
220
|
+
artifact: 'src/data/sources.json',
|
|
221
|
+
validateValue: Array.isArray,
|
|
222
|
+
})
|
|
223
|
+
: sources;
|
|
168
224
|
await mkdir(dirname(SOURCES_OUT), { recursive: true });
|
|
169
|
-
await writeFile(SOURCES_OUT, JSON.stringify(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
225
|
+
await writeFile(SOURCES_OUT, JSON.stringify(output, null, 2) + '\n', 'utf8');
|
|
226
|
+
if (selection.corpus) {
|
|
227
|
+
for (const book of selection.books) {
|
|
228
|
+
console.log(
|
|
229
|
+
`[book:${book.id}] build-bib: ${sources.length} ` +
|
|
230
|
+
`source${sources.length === 1 ? '' : 's'} -> ` +
|
|
231
|
+
SOURCES_OUT.replace(PROJECT_ROOT + '/', ''),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
console.log(
|
|
235
|
+
`[book:corpus] build-bib: ${sources.length * selection.books.length} namespaced ` +
|
|
236
|
+
`source${sources.length * selection.books.length === 1 ? '' : 's'} across ` +
|
|
237
|
+
`${selection.books.length} book${selection.books.length === 1 ? '' : 's'} -> ` +
|
|
238
|
+
SOURCES_OUT.replace(PROJECT_ROOT + '/', ''),
|
|
239
|
+
);
|
|
240
|
+
} else {
|
|
241
|
+
console.log(
|
|
242
|
+
`build-bib: ${sources.length} source${sources.length === 1 ? '' : 's'} -> ` +
|
|
243
|
+
`${SOURCES_OUT.replace(PROJECT_ROOT + '/', '')}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
174
246
|
}
|
|
175
247
|
|
|
176
248
|
async function main() {
|
|
177
|
-
await
|
|
178
|
-
|
|
249
|
+
const toolingConfig = await loadResolvedBookConfig(PROJECT_ROOT);
|
|
250
|
+
if (toolingConfig.corpus) DIAGNOSTIC_SCOPE = 'corpus';
|
|
251
|
+
const selection = resolveBookSelection(toolingConfig, process.argv.slice(2), 'build-bib');
|
|
252
|
+
await buildReferences(selection);
|
|
253
|
+
await buildSources(selection);
|
|
179
254
|
}
|
|
180
255
|
|
|
181
256
|
main().catch((err) => {
|
|
182
|
-
|
|
183
|
-
|
|
257
|
+
const message = String(err?.message ?? err);
|
|
258
|
+
const prefix = DIAGNOSTIC_SCOPE ? `[book:${DIAGNOSTIC_SCOPE}] ` : '';
|
|
259
|
+
console.error(
|
|
260
|
+
message.startsWith('[book:') ? message : `${prefix}build-bib: failed: ${message}`,
|
|
261
|
+
);
|
|
184
262
|
process.exit(1);
|
|
185
263
|
});
|
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
import { writeFile, mkdir, readFile } from 'node:fs/promises';
|
|
25
25
|
import { resolve, dirname, basename } from 'node:path';
|
|
26
26
|
import { walkMdx, readChaptersBase } from './walk-mdx.mjs';
|
|
27
|
+
import { loadResolvedBookConfig } from './resolve-book-config.mjs';
|
|
28
|
+
import {
|
|
29
|
+
assertLegacyBookMatches,
|
|
30
|
+
frontmatterSlug,
|
|
31
|
+
mergeCorpusArtifact,
|
|
32
|
+
resolveBookSelection,
|
|
33
|
+
} from './corpus-tooling.mjs';
|
|
27
34
|
|
|
28
35
|
const USAGE = `Usage: book-scaffold build-exercises
|
|
29
36
|
|
|
@@ -36,6 +43,7 @@ Env:
|
|
|
36
43
|
BOOK_EXERCISES_OUT Override output path (default: src/data/exercises.json).
|
|
37
44
|
|
|
38
45
|
Options:
|
|
46
|
+
--book <id> In corpus mode, rebuild only one registered book.
|
|
39
47
|
--help, -h Print this message and exit (non-mutating).
|
|
40
48
|
`;
|
|
41
49
|
|
|
@@ -45,8 +53,8 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
const CWD = process.cwd();
|
|
48
|
-
const CHAPTERS_DIR = await readChaptersBase(CWD);
|
|
49
56
|
const OUTPUT_PATH = process.env.BOOK_EXERCISES_OUT ?? 'src/data/exercises.json';
|
|
57
|
+
let DIAGNOSTIC_SCOPE = null;
|
|
50
58
|
|
|
51
59
|
/**
|
|
52
60
|
* Extract <Exercise id="..."> tags and their body content from MDX.
|
|
@@ -85,39 +93,108 @@ function extractExercises(source) {
|
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
async function main() {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
const toolingConfig = await loadResolvedBookConfig(CWD);
|
|
97
|
+
if (toolingConfig.corpus) DIAGNOSTIC_SCOPE = 'corpus';
|
|
98
|
+
const selection = resolveBookSelection(
|
|
99
|
+
toolingConfig,
|
|
100
|
+
process.argv.slice(2),
|
|
101
|
+
'build-exercises',
|
|
102
|
+
);
|
|
103
|
+
DIAGNOSTIC_SCOPE = selection.corpus ? 'corpus' : null;
|
|
104
|
+
const chaptersRoot = await readChaptersBase(CWD, { corpus: selection.corpus });
|
|
105
|
+
const runs = selection.corpus
|
|
106
|
+
? selection.books.map((book) => ({ book, dir: resolve(chaptersRoot, book.id) }))
|
|
107
|
+
: [{ book: null, dir: chaptersRoot }];
|
|
108
|
+
const values = new Map();
|
|
109
|
+
let corpusExercises = 0;
|
|
110
|
+
let corpusChapters = 0;
|
|
111
|
+
|
|
112
|
+
for (const run of runs) {
|
|
113
|
+
const byChapter = {};
|
|
114
|
+
let totalExercises = 0;
|
|
115
|
+
let chaptersWithExercises = 0;
|
|
91
116
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
for await (const rel of walkMdx(run.dir)) {
|
|
118
|
+
const chapterPath = resolve(run.dir, rel);
|
|
119
|
+
let source;
|
|
120
|
+
try {
|
|
121
|
+
source = await readFile(chapterPath, 'utf8');
|
|
122
|
+
} catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (run.book) {
|
|
126
|
+
assertLegacyBookMatches(
|
|
127
|
+
source,
|
|
128
|
+
run.book,
|
|
129
|
+
`[book:${run.book.id}] ${chapterPath.replace(`${CWD}/`, '')}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const fileLabel = run.book
|
|
133
|
+
? `[book:${run.book.id}] ${chapterPath.replace(`${CWD}/`, '')}`
|
|
134
|
+
: chapterPath.replace(`${CWD}/`, '');
|
|
135
|
+
const chapterSlug = run.book
|
|
136
|
+
? frontmatterSlug(source, fileLabel) ?? rel.replace(/\.mdx?$/, '')
|
|
137
|
+
: basename(rel).replace(/\.mdx?$/, '');
|
|
138
|
+
const exercises = extractExercises(source);
|
|
139
|
+
if (exercises.length > 0) {
|
|
140
|
+
byChapter[chapterSlug] = exercises;
|
|
141
|
+
totalExercises += exercises.length;
|
|
142
|
+
chaptersWithExercises += 1;
|
|
143
|
+
}
|
|
100
144
|
}
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
byChapter
|
|
104
|
-
|
|
105
|
-
|
|
145
|
+
|
|
146
|
+
if (run.book) {
|
|
147
|
+
values.set(run.book.id, byChapter);
|
|
148
|
+
corpusExercises += totalExercises;
|
|
149
|
+
corpusChapters += chaptersWithExercises;
|
|
150
|
+
process.stdout.write(
|
|
151
|
+
`[book:${run.book.id}] build-exercises: ${totalExercises} ` +
|
|
152
|
+
`exercise${totalExercises === 1 ? '' : 's'} across ${chaptersWithExercises} ` +
|
|
153
|
+
`chapter${chaptersWithExercises === 1 ? '' : 's'} → ${OUTPUT_PATH}\n`,
|
|
154
|
+
);
|
|
155
|
+
} else {
|
|
156
|
+
values.set('', byChapter);
|
|
157
|
+
corpusExercises = totalExercises;
|
|
158
|
+
corpusChapters = chaptersWithExercises;
|
|
106
159
|
}
|
|
107
160
|
}
|
|
108
161
|
|
|
109
162
|
const outPath = resolve(CWD, OUTPUT_PATH);
|
|
163
|
+
const output = selection.corpus
|
|
164
|
+
? await mergeCorpusArtifact({
|
|
165
|
+
path: outPath,
|
|
166
|
+
corpus: selection.corpus,
|
|
167
|
+
requestedBook: selection.requestedBook,
|
|
168
|
+
values,
|
|
169
|
+
emptyValue: () => ({}),
|
|
170
|
+
artifact: OUTPUT_PATH,
|
|
171
|
+
validateValue: (value) =>
|
|
172
|
+
value !== null && typeof value === 'object' && !Array.isArray(value),
|
|
173
|
+
})
|
|
174
|
+
: values.get('');
|
|
110
175
|
await mkdir(dirname(outPath), { recursive: true });
|
|
111
|
-
await writeFile(outPath, JSON.stringify(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
176
|
+
await writeFile(outPath, JSON.stringify(output, null, 2) + '\n');
|
|
177
|
+
if (selection.corpus) {
|
|
178
|
+
process.stdout.write(
|
|
179
|
+
`[book:corpus] build-exercises: ${corpusExercises} ` +
|
|
180
|
+
`exercise${corpusExercises === 1 ? '' : 's'} across ${corpusChapters} ` +
|
|
181
|
+
`chapter${corpusChapters === 1 ? '' : 's'} and ${selection.books.length} ` +
|
|
182
|
+
`book${selection.books.length === 1 ? '' : 's'} → ${OUTPUT_PATH}\n`,
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
process.stdout.write(
|
|
186
|
+
`build-exercises: ${corpusExercises} exercise${corpusExercises === 1 ? '' : 's'} across ` +
|
|
187
|
+
`${corpusChapters} chapter${corpusChapters === 1 ? '' : 's'} → ${OUTPUT_PATH}\n`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
116
190
|
}
|
|
117
191
|
|
|
118
192
|
main().catch((err) => {
|
|
119
|
-
|
|
120
|
-
|
|
193
|
+
const message = String(err?.message ?? err);
|
|
194
|
+
const prefix = DIAGNOSTIC_SCOPE ? `[book:${DIAGNOSTIC_SCOPE}] ` : '';
|
|
195
|
+
console.error(
|
|
196
|
+
message.startsWith('[book:') ? message : `${prefix}build-exercises: failed: ${message}`,
|
|
197
|
+
);
|
|
121
198
|
process.exit(1);
|
|
122
199
|
});
|
|
123
200
|
|