@heroiclands/package-build 23.9.0 → 23.11.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -0
  3. package/bin/content-commands.mjs +197 -68
  4. package/docs/api.md +365 -70
  5. package/docs/authoring/first-note.md +10 -0
  6. package/docs/authoring/frontmatter.md +1 -1
  7. package/docs/authoring/links-and-markup.md +186 -49
  8. package/docs/commands.md +92 -3
  9. package/docs/content-format.md +10 -3
  10. package/docs/design.md +18 -0
  11. package/docs/guides/book.md +7 -0
  12. package/docs/guides/site.md +7 -0
  13. package/docs/reference/format-details.md +186 -15
  14. package/docs/reference/note-types.md +4 -4
  15. package/engine/anchors.mjs +43 -9
  16. package/engine/base-compiler.mjs +6 -4
  17. package/engine/changelog-group.mjs +58 -34
  18. package/engine/changelog-lint.mjs +6 -2
  19. package/engine/compendiums.mjs +15 -1
  20. package/engine/compile-corpus.mjs +14 -1
  21. package/engine/content-charset.mjs +13 -1
  22. package/engine/content-html.mjs +18 -4
  23. package/engine/content-icons.mjs +15 -1
  24. package/engine/content-images.mjs +28 -9
  25. package/engine/content-index.mjs +102 -23
  26. package/engine/content-links.mjs +68 -30
  27. package/engine/content-session.mjs +247 -0
  28. package/engine/content-sources.mjs +110 -0
  29. package/engine/content-spans.mjs +118 -14
  30. package/engine/content-tables.mjs +1 -0
  31. package/engine/content-tasklists.mjs +20 -4
  32. package/engine/diagnostics.mjs +17 -6
  33. package/engine/extension-attributes.mjs +28 -5
  34. package/engine/first-use.mjs +371 -0
  35. package/engine/foundry-entries.mjs +12 -9
  36. package/engine/generate.mjs +13 -4
  37. package/engine/helpers.mjs +47 -16
  38. package/engine/index-records.mjs +1 -0
  39. package/engine/index.mjs +3 -0
  40. package/engine/journals.mjs +9 -5
  41. package/engine/language-data.mjs +2 -2
  42. package/engine/language-rows.mjs +4 -4
  43. package/engine/language-terms.mjs +4 -4
  44. package/engine/lexical-sections.mjs +5 -5
  45. package/engine/markup-validation.mjs +7 -2
  46. package/engine/metadata-index.mjs +1 -0
  47. package/engine/name-markup.mjs +114 -0
  48. package/engine/name-rows.mjs +70 -0
  49. package/engine/names.mjs +460 -0
  50. package/engine/note-addresses.mjs +25 -4
  51. package/engine/path-charset.mjs +24 -1
  52. package/engine/pdf-render.mjs +15 -1
  53. package/engine/site-index.mjs +25 -2
  54. package/engine/sql-tables.mjs +76 -15
  55. package/engine/stub-lint.mjs +4 -3
  56. package/engine/web-wikilinks.mjs +33 -12
  57. package/engine/wikilink-syntax.mjs +65 -8
  58. package/engine/wikilinks.mjs +29 -11
  59. package/engine/yaml-lint.mjs +3 -5
  60. package/hugo-theme/layouts/partials/footer.html +2 -0
  61. package/hugo-theme/static/css/style.css +20 -0
  62. package/hugo-theme/static/js/inline-spoilers.js +23 -0
  63. package/package.json +1 -1
  64. package/types/engine/anchors.d.mts +6 -3
  65. package/types/engine/compendiums.d.mts +5 -1
  66. package/types/engine/compile-corpus.d.mts +5 -2
  67. package/types/engine/content-charset.d.mts +5 -1
  68. package/types/engine/content-html.d.mts +5 -1
  69. package/types/engine/content-icons.d.mts +5 -1
  70. package/types/engine/content-images.d.mts +5 -1
  71. package/types/engine/content-index.d.mts +39 -19
  72. package/types/engine/content-links.d.mts +22 -8
  73. package/types/engine/content-session.d.mts +108 -0
  74. package/types/engine/content-sources.d.mts +32 -0
  75. package/types/engine/content-spans.d.mts +8 -6
  76. package/types/engine/content-tasklists.d.mts +5 -1
  77. package/types/engine/diagnostics.d.mts +10 -0
  78. package/types/engine/extension-attributes.d.mts +16 -2
  79. package/types/engine/first-use.d.mts +31 -0
  80. package/types/engine/foundry-entries.d.mts +11 -4
  81. package/types/engine/generate.d.mts +5 -1
  82. package/types/engine/helpers.d.mts +30 -7
  83. package/types/engine/index.d.mts +1 -0
  84. package/types/engine/journals.d.mts +5 -2
  85. package/types/engine/lexical-sections.d.mts +3 -2
  86. package/types/engine/markup-validation.d.mts +2 -1
  87. package/types/engine/name-markup.d.mts +176 -0
  88. package/types/engine/name-rows.d.mts +35 -0
  89. package/types/engine/names.d.mts +356 -0
  90. package/types/engine/note-addresses.d.mts +3 -3
  91. package/types/engine/path-charset.d.mts +16 -1
  92. package/types/engine/site-build.d.mts +2 -2
  93. package/types/engine/site-index.d.mts +3 -1
  94. package/types/engine/sql-tables.d.mts +7 -2
  95. package/types/engine/stub-lint.d.mts +3 -1
  96. package/types/engine/wikilink-syntax.d.mts +40 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 23.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - References to private notes and secret sections now conceal their labels on the web; authors can keep selected references visible.
8
+ - Inline spoilers preserve formatted text across websites, Foundry journals and books.
9
+
10
+ ## 23.10.0
11
+
12
+ ### Minor Changes
13
+
14
+ **Notes**
15
+
16
+ - Notes with CRLF frontmatter retain their identity, body and links in compiled content and published indexes.
17
+ - Correct malformed YAML in CRLF frontmatter before building; YAML errors in these notes prevent publication.
18
+
19
+ **Language notes**
20
+
21
+ - Language notes write a name element's joining form as `joining`; `genitive` is refused.
22
+ - Compiled-skill references in culture, literature and affiliation language fields are refused instead of silently dropping glossary entries; write `skill-provnzlng` or `<pkg>-note-skill-provnzlng`.
23
+
24
+ ### Patch Changes
25
+
26
+ **Names**
27
+
28
+ - Mark names without notes as `[Name]{.name}` and link indexed names; lint checks introductions and repeats, and marked names appear bold on sites and in books.
29
+ - Mark unwritten names with `[Name]{.name data-type="place"}`; query indexed uses and review grouped lint advice about unwritten names and conflicting types.
30
+
31
+ **Commands and release notes**
32
+
33
+ - Selected content checks and compendium builds can run together in one command.
34
+ - Release notes retain every summary when paragraphs and bullets are combined.
35
+
3
36
  ## 23.9.0
4
37
 
5
38
  ### Minor Changes
package/README.md CHANGED
@@ -16,6 +16,8 @@ npm install -D @heroiclands/package-build
16
16
 
17
17
  The generated project includes a homepage, website and book targets, npm scripts, and a local `.gitignore`. [Getting started](docs/getting-started.md) routes you to a [system](docs/guides/system-project.md), [module](docs/guides/module-project.md), or [content authoring](docs/authoring/first-note.md) path. The [content-language-server](https://github.com/HeroicLands/content-language-server) provides editor navigation and completion.
18
18
 
19
+ Run selected content operations together with `package-build content run index lint links compile`. The [shared content command](docs/commands.md#package-build-content-run-targets) captures one source tree for the selected checks and outputs; standalone commands remain available.
20
+
19
21
  ## Working on this repository
20
22
 
21
23
  Run `npm ci` in each fresh clone or worktree. Its `prepare` step installs the
@@ -118,6 +118,9 @@ import {
118
118
  } from "../engine/site-build.mjs";
119
119
  import { auditLinks, buildLinkIndex, walkReachability } from "../engine/content-links.mjs";
120
120
  import { prepareLinkSqlTables, prepareTreeSqlTables } from "../engine/sql-tables.mjs";
121
+ import { createContentSession, CONTENT_TARGETS } from "../engine/content-session.mjs";
122
+ import { createFirstUseChecker } from "../engine/first-use.mjs";
123
+ import { createNameCatalog, createNamesReport, namesReportFinding } from "../engine/names.mjs";
121
124
  // The one place a link finding is worded, shared with both builds.
122
125
  import { linkFindingMessage } from "../engine/wikilink-syntax.mjs";
123
126
  import {
@@ -183,6 +186,11 @@ prefix.apply(log, {
183
186
  },
184
187
  });
185
188
 
189
+ const CONTENT_SESSION = Symbol("content session");
190
+ const CONTENT_CONFIG = Symbol("content configuration");
191
+ const PREPARATION_REPORTED = Symbol("preparation reported");
192
+ const REPORTED_FAILURES = Symbol("reported preparation failures");
193
+
186
194
  /**
187
195
  * Report a command's failure.
188
196
  *
@@ -203,6 +211,29 @@ function reportFailure(err) {
203
211
  else log.error(message);
204
212
  }
205
213
 
214
+ /** Report one shared preparation failure once in a combined invocation. */
215
+ function reportContentFailure(err, argv) {
216
+ const reported = argv[REPORTED_FAILURES];
217
+ if (reported?.has(err)) return;
218
+ reported?.add(err);
219
+ reportFailure(err);
220
+ }
221
+
222
+ /** Apply the package command's existing eligibility rule before a selected writer.
223
+ * @param {object} config - Resolved configuration.
224
+ * @param {string} action - The requested package action.
225
+ */
226
+ function assertCompendiumOperation(config, action) {
227
+ if (!compilesFoundryDocuments(config)) {
228
+ throw new Error(
229
+ `\`packageKind: ${config.packageKind}\` compiles no ` +
230
+ `compendium, so there is nothing to ${action}. Build ` +
231
+ `its site with \`package-build site\` and its book ` +
232
+ `with \`package-build pdf\`.`,
233
+ );
234
+ }
235
+ }
236
+
206
237
  /**
207
238
  * A note's source text, or `""` when it cannot be read.
208
239
  *
@@ -235,7 +266,12 @@ export function registerContentCommands(cli) {
235
266
  return cli
236
267
  .command(
237
268
  withIndexPreflight(
238
- withBodyPreflight(packageCommand(), (_config, argv) => argv.action === "compile"),
269
+ withBodyPreflight(
270
+ packageCommand(),
271
+ (_config, argv) => argv.action === "compile",
272
+ undefined,
273
+ true,
274
+ ),
239
275
  (_config, argv) => argv.action === "compile",
240
276
  ),
241
277
  )
@@ -245,6 +281,7 @@ export function registerContentCommands(cli) {
245
281
  .command(proseCommand())
246
282
  .command(contentFormatCommand())
247
283
  .command(withIndexPreflight(linksCommand()))
284
+ .command(withIndexPreflight(contentCommand()))
248
285
  .command(formatCommand())
249
286
  .command(markdownCommand())
250
287
  .command(
@@ -252,6 +289,7 @@ export function registerContentCommands(cli) {
252
289
  contentIndexCommand(),
253
290
  () => true,
254
291
  (config, argv) => argv.root || config.paths.content,
292
+ true,
255
293
  ),
256
294
  )
257
295
  .command(withIndexPreflight(withBodyPreflight(siteCommand()), publishesContentPages))
@@ -404,19 +442,33 @@ function withBodyPreflight(
404
442
  command,
405
443
  shouldCheck = () => true,
406
444
  contentRoot = (config) => config.paths.content,
445
+ shareCapture = false,
407
446
  ) {
408
447
  const handler = command.handler;
409
448
  return {
410
449
  ...command,
411
450
  handler: async (argv) => {
451
+ let owned;
412
452
  try {
413
- const config = loadPackConfig();
453
+ const config = argv[CONTENT_CONFIG] ?? loadPackConfig();
454
+ let next = argv;
414
455
  if (shouldCheck(config, argv)) {
415
456
  const contentBase = contentRoot(config, argv);
416
457
  if (!fs.existsSync(contentBase)) return handler(argv);
417
- const records = indexRecordsFor({ config, contentBase });
458
+ const session =
459
+ shareCapture ?
460
+ (argv[CONTENT_SESSION] ??
461
+ (owned = await createContentSession({ config, contentBase })))
462
+ : undefined;
463
+ const records = session?.records ?? indexRecordsFor({ config, contentBase });
464
+ if (session?.problems.length) {
465
+ emitDiagnostic(session.problems[0]);
466
+ process.exitCode = 1;
467
+ return;
468
+ }
418
469
  const { findings } = lintNoteStates(contentBase, {
419
470
  records,
471
+ parsed: session?.parsed,
420
472
  contentPackage: config.contentPackage,
421
473
  });
422
474
  const errors = findings.filter((finding) => finding.severity === "error");
@@ -425,13 +477,16 @@ function withBodyPreflight(
425
477
  process.exitCode = 1;
426
478
  return;
427
479
  }
480
+ if (session) next = { ...argv, [CONTENT_SESSION]: session };
428
481
  }
482
+ return await handler(next);
429
483
  } catch (err) {
430
484
  reportFailure(err);
431
485
  process.exitCode = 1;
432
486
  return;
487
+ } finally {
488
+ owned?.dispose();
433
489
  }
434
- return handler(argv);
435
490
  },
436
491
  };
437
492
  }
@@ -457,7 +512,7 @@ function withIndexPreflight(command, shouldCheck = () => true) {
457
512
  process.exitCode = 1;
458
513
  return;
459
514
  }
460
- return handler(argv);
515
+ return handler({ ...argv, [CONTENT_CONFIG]: config });
461
516
  },
462
517
  };
463
518
  }
@@ -906,8 +961,10 @@ function lintCommand() {
906
961
  });
907
962
  },
908
963
  handler: async (argv) => {
964
+ let session;
909
965
  try {
910
- const config = loadPackConfig();
966
+ const config =
967
+ argv[CONTENT_SESSION]?.config ?? argv[CONTENT_CONFIG] ?? loadPackConfig();
911
968
  const root = argv.root ?? config.paths.content;
912
969
 
913
970
  // The corpus, enumerated once for this command and handed to
@@ -921,30 +978,22 @@ function lintCommand() {
921
978
  // finding rather than thrown: one malformed note must
922
979
  // not take every other finding in the tree with it, and the
923
980
  // reader needs a line to open, not a stack.
924
- const corpusProblems = [];
925
- const parsed = new Map();
926
981
  const indexStarted = performance.now();
927
- const records = indexRecordsFor({
928
- contentBase: root,
929
- config,
930
- skipDirectories: config.skipDirectories,
931
- problems: corpusProblems,
932
- parsed,
933
- });
982
+ session =
983
+ argv[CONTENT_SESSION] ??
984
+ (await createContentSession({ config, contentBase: root }));
985
+ session.assertContext({ config, contentBase: root });
986
+ const { problems: corpusProblems, parsed, records } = session;
934
987
  const indexMs = performance.now() - indexStarted;
935
- for (const problem of corpusProblems) emitDiagnostic(problem);
988
+ if (!argv[PREPARATION_REPORTED])
989
+ for (const problem of corpusProblems) emitDiagnostic(problem);
936
990
  // An error whatever the command's own strictness: the note is
937
991
  // absent from every answer below, so reporting it and exiting 0
938
992
  // would call the tree clean while silently omitting a note.
939
993
  if (corpusProblems.length) process.exitCode = 1;
940
994
 
941
995
  const sqlStarted = performance.now();
942
- const sqlTables = await prepareTreeSqlTables(root, {
943
- config,
944
- records,
945
- parsed,
946
- skipDirectories: config.skipDirectories,
947
- });
996
+ const sqlTables = await session.sqlTables("all");
948
997
  const sqlMs = performance.now() - sqlStarted;
949
998
  const validationStarted = performance.now();
950
999
 
@@ -959,12 +1008,19 @@ function lintCommand() {
959
1008
  // One index, built once, for the reference check. It is the
960
1009
  // same resolver the wikilink audit uses, so a frontmatter
961
1010
  // reference and a body link answer the same way.
962
- const index = buildLinkIndex(root, {
963
- config,
1011
+ const index = await session.audit("lint");
1012
+ const catalog = createNameCatalog({ records, index });
1013
+ const firstUseChecker = createFirstUseChecker({
964
1014
  records,
965
- skipDirectories: config.skipDirectories,
966
- sqlTables,
1015
+ index,
1016
+ parsed: session.parsed,
1017
+ syntax: session.syntax,
1018
+ catalog,
967
1019
  });
1020
+ const names = namesReportFinding(
1021
+ createNamesReport({ package: config.contentPackage, records, catalog }),
1022
+ { contentBase: root },
1023
+ );
968
1024
  // Which system blocks this tree carries, derived from what it
969
1025
  // declares it ships for. Read before the lint so the
970
1026
  // systems it will *not* check can be said out loud below.
@@ -1104,6 +1160,7 @@ function lintCommand() {
1104
1160
  // the subject is every character authored, including the ones
1105
1161
  // that stopped a note parsing at all.
1106
1162
  const charset = lintContentCharset(root, {
1163
+ sources: session.sources,
1107
1164
  skipDirectories: config.skipDirectories,
1108
1165
  });
1109
1166
 
@@ -1112,6 +1169,7 @@ function lintCommand() {
1112
1169
  // than the parsed notes, so a picture, which has no note,
1113
1170
  // is covered by the same rule.
1114
1171
  const pathCharset = lintPathCharset({
1172
+ inventory: session.inventory,
1115
1173
  contentBase: root,
1116
1174
  assetsBase: config.paths.assets,
1117
1175
  skipDirectories: config.skipDirectories,
@@ -1126,6 +1184,7 @@ function lintCommand() {
1126
1184
  // package's own entries merged over it, which is where a
1127
1185
  // Game-Icons glyph becomes a name a note may write.
1128
1186
  const icons = lintContentIcons(root, {
1187
+ sources: session.sources,
1129
1188
  skipDirectories: config.skipDirectories,
1130
1189
  registry: config.icons,
1131
1190
  });
@@ -1135,6 +1194,7 @@ function lintCommand() {
1135
1194
  // still unrenderable in a book — the character rule one level
1136
1195
  // up.
1137
1196
  const html = lintContentHtml(root, {
1197
+ sources: session.sources,
1138
1198
  skipDirectories: config.skipDirectories,
1139
1199
  });
1140
1200
 
@@ -1143,6 +1203,7 @@ function lintCommand() {
1143
1203
  // ordinary width looks exactly like a directive that worked —
1144
1204
  // so these are errors and they fail the run.
1145
1205
  const images = lintContentImages(root, {
1206
+ sources: session.sources,
1146
1207
  skipDirectories: config.skipDirectories,
1147
1208
  // Which packages this build can reach is configuration, and
1148
1209
  // an address naming one it cannot reaches a journal as a
@@ -1154,6 +1215,7 @@ function lintCommand() {
1154
1215
  // miscompiled, since every surface sets the marker as the
1155
1216
  // item's own text and all three agree, so this is advisory.
1156
1217
  const taskLists = lintContentTaskLists(root, {
1218
+ sources: session.sources,
1157
1219
  skipDirectories: config.skipDirectories,
1158
1220
  });
1159
1221
 
@@ -1163,6 +1225,7 @@ function lintCommand() {
1163
1225
  // wrong, and 464 warnings a reader cannot clear is how a report
1164
1226
  // teaches its reader to stop looking at it.
1165
1227
  const states = lintNoteStates(root, {
1228
+ parsed,
1166
1229
  records,
1167
1230
  contentPackage: config.contentPackage,
1168
1231
  });
@@ -1195,7 +1258,11 @@ function lintCommand() {
1195
1258
  });
1196
1259
 
1197
1260
  const findings = [
1198
- ...index.notes.flatMap((note) => markupFindings(note.raw, note.file)),
1261
+ ...index.notes.flatMap((note) =>
1262
+ markupFindings(note.raw, note.file, session.syntax),
1263
+ ),
1264
+ ...index.notes.flatMap(firstUseChecker),
1265
+ ...(names ? [names] : []),
1199
1266
  ...addresses.findings,
1200
1267
  ...frontmatter.findings,
1201
1268
  ...schemaFindings,
@@ -1234,8 +1301,10 @@ function lintCommand() {
1234
1301
  `SQL ${sqlMs.toFixed(1)} ms; validation ${validationMs.toFixed(1)} ms.`,
1235
1302
  );
1236
1303
  } catch (err) {
1237
- reportFailure(err);
1304
+ reportContentFailure(err, argv);
1238
1305
  process.exitCode = 1;
1306
+ } finally {
1307
+ if (!argv[CONTENT_SESSION]) session?.dispose();
1239
1308
  }
1240
1309
  },
1241
1310
  };
@@ -1407,6 +1476,73 @@ function markdownCommand() {
1407
1476
  * @returns {object} The yargs command module.
1408
1477
  */
1409
1478
  // eslint-disable-next-line
1479
+ function contentCommand() {
1480
+ return {
1481
+ command: "content <action> <targets..>",
1482
+ describe: "Run selected content operations with one in-memory source capture",
1483
+ builder: (yargs) =>
1484
+ yargs
1485
+ .positional("action", { type: "string", choices: ["run"] })
1486
+ .positional("targets", { type: "string", array: true, choices: CONTENT_TARGETS })
1487
+ .option("references", {
1488
+ type: "boolean",
1489
+ default: true,
1490
+ describe: "Check frontmatter references during lint",
1491
+ })
1492
+ .option("pack", {
1493
+ type: "string",
1494
+ describe: "Select one pack for the compile target",
1495
+ })
1496
+ .option("out", {
1497
+ type: "string",
1498
+ describe: "Output directory for the index target",
1499
+ }),
1500
+ handler: async (argv) => {
1501
+ let session;
1502
+ const failures = new Set();
1503
+ const problems = new Set();
1504
+ const shared = { ...argv, [PREPARATION_REPORTED]: true, [REPORTED_FAILURES]: failures };
1505
+ try {
1506
+ const config = argv[CONTENT_CONFIG] ?? loadPackConfig();
1507
+ session = await createContentSession({ config });
1508
+ shared[CONTENT_SESSION] = session;
1509
+ const targets = CONTENT_TARGETS.filter((target) => argv.targets.includes(target));
1510
+ let preparationFailed = false;
1511
+ try {
1512
+ if (targets.includes("compile")) assertCompendiumOperation(config, "compile");
1513
+ await session.prepare(targets);
1514
+ } catch (error) {
1515
+ preparationFailed = true;
1516
+ process.exitCode = 1;
1517
+ if (error.findings) {
1518
+ for (const finding of error.findings) {
1519
+ emitDiagnostic(finding);
1520
+ problems.add(finding);
1521
+ }
1522
+ failures.add(error);
1523
+ } else reportContentFailure(error, shared);
1524
+ }
1525
+ for (const problem of session.problems) {
1526
+ if (!problems.has(problem)) emitDiagnostic(problem);
1527
+ process.exitCode = 1;
1528
+ }
1529
+ for (const target of targets) {
1530
+ if (preparationFailed && (target === "index" || target === "compile")) continue;
1531
+ if (target === "index") await contentIndexCommand().handler(shared);
1532
+ else if (target === "lint") await lintCommand().handler(shared);
1533
+ else if (target === "links") await linksCommand().handler(shared);
1534
+ else await packageCommand().handler({ ...shared, action: "compile" });
1535
+ }
1536
+ } catch (error) {
1537
+ reportContentFailure(error, shared);
1538
+ process.exitCode = 1;
1539
+ } finally {
1540
+ session?.dispose();
1541
+ }
1542
+ },
1543
+ };
1544
+ }
1545
+
1410
1546
  function linksCommand() {
1411
1547
  return {
1412
1548
  command: "links [root]",
@@ -1418,38 +1554,23 @@ function linksCommand() {
1418
1554
  });
1419
1555
  },
1420
1556
  handler: async (argv) => {
1557
+ let session;
1421
1558
  try {
1422
- const config = loadPackConfig();
1559
+ const config =
1560
+ argv[CONTENT_SESSION]?.config ?? argv[CONTENT_CONFIG] ?? loadPackConfig();
1423
1561
  const contentBase = argv.root ?? config.paths.content;
1424
1562
 
1425
1563
  const scope = { skipDirectories: config.skipDirectories };
1426
1564
  // Enumerated once and shared, as in `lint`, and a note
1427
1565
  // it cannot record is reported rather than thrown.
1428
- const corpusProblems = [];
1429
- const parsed = new Map();
1430
- const records = indexRecordsFor({
1431
- contentBase,
1432
- config,
1433
- ...scope,
1434
- problems: corpusProblems,
1435
- parsed,
1436
- });
1437
- for (const problem of corpusProblems) emitDiagnostic(problem);
1438
- // An error whatever the command's own strictness: the note is
1439
- // absent from every answer below, so reporting it and exiting 0
1440
- // would call the tree clean while silently omitting a note.
1566
+ session =
1567
+ argv[CONTENT_SESSION] ?? (await createContentSession({ config, contentBase }));
1568
+ session.assertContext({ config, contentBase });
1569
+ const { problems: corpusProblems } = session;
1570
+ if (!argv[PREPARATION_REPORTED])
1571
+ for (const problem of corpusProblems) emitDiagnostic(problem);
1441
1572
  if (corpusProblems.length) process.exitCode = 1;
1442
- const index = buildLinkIndex(contentBase, {
1443
- config,
1444
- records,
1445
- ...scope,
1446
- sqlTables: await prepareLinkSqlTables(contentBase, {
1447
- config,
1448
- records,
1449
- parsed,
1450
- ...scope,
1451
- }),
1452
- });
1573
+ const index = await session.audit("links");
1453
1574
 
1454
1575
  // An unusable manifest would otherwise surface as a pile of
1455
1576
  // dead addresses pointing at the notes that cite it, rather
@@ -1576,8 +1697,10 @@ function linksCommand() {
1576
1697
  );
1577
1698
  }
1578
1699
  } catch (err) {
1579
- reportFailure(err);
1700
+ reportContentFailure(err, argv);
1580
1701
  process.exitCode = 1;
1702
+ } finally {
1703
+ if (!argv[CONTENT_SESSION]) session?.dispose();
1581
1704
  }
1582
1705
  },
1583
1706
  };
@@ -1622,10 +1745,15 @@ function contentIndexCommand() {
1622
1745
  });
1623
1746
  },
1624
1747
  handler: async (argv) => {
1748
+ let session;
1625
1749
  try {
1626
- const config = loadPackConfig();
1750
+ const config =
1751
+ argv[CONTENT_SESSION]?.config ?? argv[CONTENT_CONFIG] ?? loadPackConfig();
1627
1752
  const contentBase = argv.root ?? config.paths.content;
1628
- const publication = await prepareContentIndex({ config, contentBase });
1753
+ session =
1754
+ argv[CONTENT_SESSION] ?? (await createContentSession({ config, contentBase }));
1755
+ session.assertContext({ config, contentBase });
1756
+ const publication = await session.publication();
1629
1757
  const { file, notes, bytes, full, draft, stub, dictionary, terms } =
1630
1758
  emitContentIndex({
1631
1759
  config,
@@ -1644,8 +1772,10 @@ function contentIndexCommand() {
1644
1772
  `(${written.rows} ${relation} rows)`,
1645
1773
  );
1646
1774
  } catch (err) {
1647
- reportFailure(err);
1775
+ reportContentFailure(err, argv);
1648
1776
  process.exitCode = 1;
1777
+ } finally {
1778
+ if (!argv[CONTENT_SESSION]) session?.dispose();
1649
1779
  }
1650
1780
  },
1651
1781
  };
@@ -2538,19 +2668,13 @@ function packageCommand() {
2538
2668
  // an unhandled-rejection stack trace. Report the message and set a
2539
2669
  // failing exit code, so a build guard reads as a build failure.
2540
2670
  try {
2541
- const config = loadPackConfig();
2671
+ const config =
2672
+ argv[CONTENT_SESSION]?.config ?? argv[CONTENT_CONFIG] ?? loadPackConfig();
2542
2673
  // The compile passes do not run for a package that declares no
2543
2674
  // packs by rule. Refused rather than exiting 0 having compiled
2544
2675
  // nothing: a build that succeeds and produces no documents is
2545
2676
  // the quiet failure this toolchain reports everywhere else.
2546
- if (!compilesFoundryDocuments(config)) {
2547
- throw new Error(
2548
- `\`packageKind: ${config.packageKind}\` compiles no ` +
2549
- `compendium, so there is nothing to ${action}. Build ` +
2550
- `its site with \`package-build site\` and its book ` +
2551
- `with \`package-build pdf\`.`,
2552
- );
2553
- }
2677
+ assertCompendiumOperation(config, action);
2554
2678
  // The one directory the pipeline creates rather than expects:
2555
2679
  // `unpack` writes the extracted JSON there and `compile` reads
2556
2680
  // it back. Created here rather than at module scope so that
@@ -2563,7 +2687,12 @@ function packageCommand() {
2563
2687
  // from the resolved configuration, so nothing is restated
2564
2688
  // here.
2565
2689
  case "compile":
2566
- return await compilePacks({ packName: pack });
2690
+ return await compilePacks({
2691
+ packName: pack,
2692
+ config,
2693
+ session: argv[CONTENT_SESSION],
2694
+ reportPreparation: !argv[PREPARATION_REPORTED],
2695
+ });
2567
2696
  case "clean":
2568
2697
  return await cleanPacks({
2569
2698
  packName: pack,
@@ -2577,7 +2706,7 @@ function packageCommand() {
2577
2706
  });
2578
2707
  }
2579
2708
  } catch (err) {
2580
- reportFailure(err);
2709
+ reportContentFailure(err, argv);
2581
2710
  process.exitCode = 1;
2582
2711
  }
2583
2712
  },