@heroiclands/package-build 25.0.0 → 25.1.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 (105) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -0
  3. package/bin/content-commands.mjs +30 -96
  4. package/bin/encoding-commands.mjs +146 -0
  5. package/bin/package-build.mjs +35 -24
  6. package/config.mjs +25 -0
  7. package/convert.mjs +182 -0
  8. package/docs/api.md +137 -6
  9. package/docs/authoring/links-and-markup.md +260 -481
  10. package/docs/commands.md +110 -119
  11. package/docs/configuration.md +31 -13
  12. package/docs/content-format.md +11 -4
  13. package/docs/diagnostics.md +43 -16
  14. package/docs/encoding-core.md +135 -0
  15. package/docs/guides/book.md +2 -2
  16. package/docs/guides/module-project.md +23 -1
  17. package/docs/guides/site.md +2 -2
  18. package/docs/guides/writing-in-invented-scripts.md +450 -574
  19. package/docs/reference/format-details.md +289 -404
  20. package/docs/reference/note-types.md +80 -63
  21. package/docs/source-preview.md +8 -0
  22. package/e2e.mjs +1 -1
  23. package/engine/base-compiler.mjs +27 -9
  24. package/engine/content-charset.mjs +13 -1
  25. package/engine/content-format.yaml +6 -2
  26. package/engine/content-index.mjs +6 -2
  27. package/engine/content-links.mjs +1 -0
  28. package/engine/content-scripts.mjs +614 -459
  29. package/engine/content-scroll.mjs +4 -4
  30. package/engine/content-spans.mjs +40 -9
  31. package/engine/content-verse.mjs +1 -1
  32. package/engine/converter-data.mjs +70 -0
  33. package/engine/dictionary-rows.mjs +2 -34
  34. package/engine/encoding-integrity.mjs +94 -0
  35. package/engine/encoding-language.mjs +506 -0
  36. package/engine/encoding-lexicon.mjs +699 -0
  37. package/engine/encoding-notes.mjs +186 -0
  38. package/engine/encoding-standard.mjs +42 -0
  39. package/engine/encoding-template.mjs +313 -0
  40. package/engine/encoding-terms.mjs +141 -0
  41. package/engine/encoding.mjs +946 -0
  42. package/engine/extension-attributes.mjs +2 -0
  43. package/engine/helpers.mjs +1 -1
  44. package/engine/index.mjs +7 -0
  45. package/engine/language-terms.mjs +17 -16
  46. package/engine/{logographic.mjs → language-writings.mjs} +29 -38
  47. package/engine/markup-validation.mjs +1 -1
  48. package/engine/note-vocabulary.mjs +4 -158
  49. package/engine/pdf-render.mjs +31 -3
  50. package/engine/roman-characters.mjs +135 -16
  51. package/engine/roman-run.mjs +149 -0
  52. package/engine/roman-verse.mjs +95 -0
  53. package/engine/run-rows.mjs +29 -31
  54. package/engine/run-words.mjs +2 -2
  55. package/engine/script-emphasis-terms.mjs +3 -0
  56. package/engine/script-emphasis.mjs +3 -10
  57. package/engine/script-font-delivery.mjs +9 -12
  58. package/engine/script-fonts.mjs +2 -1
  59. package/engine/site-build.mjs +14 -5
  60. package/engine/site-preview.mjs +12 -2
  61. package/engine/sql-tables.mjs +36 -31
  62. package/engine/verse-metre.mjs +1 -1
  63. package/engine/word-forms.mjs +1 -1
  64. package/engine/word-inflections.mjs +48 -0
  65. package/engine/word-rows.mjs +1 -1
  66. package/engine/word-terms.mjs +16 -17
  67. package/package.json +11 -5
  68. package/types/config.d.mts +5 -0
  69. package/types/convert.d.mts +44 -0
  70. package/types/e2e.d.mts +1 -1
  71. package/types/engine/base-compiler.d.mts +4 -2
  72. package/types/engine/content-index.d.mts +4 -1
  73. package/types/engine/content-scripts.d.mts +103 -42
  74. package/types/engine/converter-data.d.mts +11 -0
  75. package/types/engine/dictionary-rows.d.mts +1 -25
  76. package/types/engine/encoding-integrity.d.mts +14 -0
  77. package/types/engine/encoding-language.d.mts +36 -0
  78. package/types/engine/encoding-lexicon.d.mts +259 -0
  79. package/types/engine/encoding-notes.d.mts +39 -0
  80. package/types/engine/encoding-standard.d.mts +11 -0
  81. package/types/engine/encoding-template.d.mts +43 -0
  82. package/types/engine/encoding-terms.d.mts +105 -0
  83. package/types/engine/encoding.d.mts +155 -0
  84. package/types/engine/helpers.d.mts +1 -1
  85. package/types/engine/index.d.mts +4 -0
  86. package/types/engine/language-terms.d.mts +1 -1
  87. package/types/engine/{logographic.d.mts → language-writings.d.mts} +6 -6
  88. package/types/engine/markup-validation.d.mts +1 -1
  89. package/types/engine/pdf-render.d.mts +1 -1
  90. package/types/engine/roman-characters.d.mts +2 -2
  91. package/types/engine/roman-run.d.mts +101 -0
  92. package/types/engine/roman-verse.d.mts +23 -0
  93. package/types/engine/run-rows.d.mts +13 -20
  94. package/types/engine/script-emphasis-terms.d.mts +2 -0
  95. package/types/engine/script-emphasis.d.mts +1 -1
  96. package/types/engine/script-font-delivery.d.mts +2 -3
  97. package/types/engine/script-fonts.d.mts +5 -0
  98. package/types/engine/site-preview.d.mts +1 -1
  99. package/types/engine/word-inflections.d.mts +25 -0
  100. package/engine/map-lexicon.mjs +0 -1045
  101. package/engine/text-maps.mjs +0 -654
  102. package/engine/written-in.mjs +0 -204
  103. package/types/engine/map-lexicon.d.mts +0 -330
  104. package/types/engine/text-maps.d.mts +0 -189
  105. package/types/engine/written-in.d.mts +0 -76
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 25.1.0
4
+
5
+ **Writing and conversion**
6
+
7
+ - Native writing now requires an encoding note that chooses its language and font, while unencoded text keeps its Roman spelling.
8
+ - Language runs share Roman punctuation and literal glyph codes, preserving typed spelling and case while locating invalid characters.
9
+ - The `encode` and `decode` commands replace `transliterate`; browser conversion can use vocabulary to recover words and names while preserving stress.
10
+
11
+ **Rendering and preview**
12
+
13
+ - Nested emphasis follows the surrounding script, using its red ink or overline convention.
14
+ - Indexed previews show source positions for text they cannot render while keeping the readable preview visible.
15
+ - Ch'alish P'ilam Common text no longer triggers a false character warning.
16
+
3
17
  ## 25.0.0
4
18
 
5
19
  ### Major Changes
package/README.md CHANGED
@@ -78,6 +78,7 @@ The documentation lives in the Markdown files in this repository. Start with [th
78
78
  | Foundry integration testing | [End-to-end testing](docs/guides/e2e.md), including an optional S3 server |
79
79
  | Release and deployment | [Release and deploy](docs/guides/release-and-deploy.md) |
80
80
  | Invented languages and scripts | [Write in invented scripts](docs/guides/writing-in-invented-scripts.md) |
81
+ | Encoding rules and the conversion core | [Encoding core](docs/encoding-core.md) |
81
82
  | Buffer-only preview and Gitea rendering | [Source preview](docs/source-preview.md) |
82
83
  | Every CLI command | [Commands](docs/commands.md) |
83
84
  | Every build setting | [Configuration](docs/configuration.md) |
@@ -30,7 +30,6 @@
30
30
  * npx package-build site [--out <dir>]
31
31
  * npx package-build map [--tree] [--from <shortcode>] [--chart <shortcode>] [--travel] [--out <dir>]
32
32
  * npx package-build reachability <dir> [file] [--index <shortcode>]
33
- * npx package-build transliterate <map> [--lang <language>] [--font <slug>] [--hex] [text..]
34
33
  * npx package-build lexicon <language> [--map <map>] [--font <slug>] [--derived] [--json]
35
34
  * npx package-build addresses diff --from <zip|dir> [--strict]
36
35
  * npx package-build npc new (--spec <json> | --specfile <path>) [--seed <text>] [--out <path>]
@@ -110,11 +109,10 @@ import { coreReport, coreReportText } from "../engine/core-report.mjs";
110
109
  import { checkScriptNoteFonts } from "../engine/script-emphasis.mjs";
111
110
  import {
112
111
  checkLexiconNatives,
113
- checkMapFonts,
112
+ checkEncodingLexicon,
114
113
  languageNoteFindings,
115
114
  lexiconEntries,
116
- transliterate,
117
- } from "../engine/map-lexicon.mjs";
115
+ } from "../engine/encoding-lexicon.mjs";
118
116
  import { checkFormatting, checkPrettierConventions, lintMarkdown } from "../engine/prose-lint.mjs";
119
117
  import { lintProse, scoreProseTree } from "../engine/readability-lint.mjs";
120
118
  import { loadPackageBuildConfig } from "../config.mjs";
@@ -321,7 +319,6 @@ export function registerContentCommands(cli) {
321
319
  )
322
320
  .command(withIndexPreflight(mapCommand()))
323
321
  .command(withIndexPreflight(reachabilityCommand()))
324
- .command(transliterateCommand())
325
322
  .command(lexiconCommand())
326
323
  .command(coreCommand())
327
324
  .command(addressesCommand())
@@ -1285,15 +1282,22 @@ function lintCommand() {
1285
1282
  // the languages this package declares, the faces it installed
1286
1283
  // and the maps its notes declare.
1287
1284
  const runs = runContext(config, records);
1285
+ const loaderLocations = new Set(
1286
+ (runs.encodingFindings ?? []).map((one) =>
1287
+ JSON.stringify([one.file, one.line, one.column, one.message]),
1288
+ ),
1289
+ );
1290
+ const reportedLoaders = new Set();
1288
1291
  const findings = [
1289
1292
  ...index.notes.flatMap((note) =>
1290
1293
  markupFindings(note.raw, note.file, session.syntax, runs),
1291
1294
  ),
1292
1295
  // Each mapping note's faces, each language's native forms
1293
1296
  // and each language's `note`, against the faces and maps.
1294
- ...index.notes.flatMap((note) => checkMapFonts(note, runs)),
1295
1297
  ...index.notes.flatMap((note) => checkScriptNoteFonts(note, runs)),
1296
1298
  ...index.notes.flatMap((note) => checkLexiconNatives(note, runs)),
1299
+ ...index.notes.flatMap((note) => checkEncodingLexicon(note, runs)),
1300
+ ...(runs.encodingFindings ?? []),
1297
1301
  ...languageNoteFindings({ ...config, configFile: packConfigPath() }, records),
1298
1302
  ...index.notes.flatMap(firstUseChecker),
1299
1303
  ...(names ? [names] : []),
@@ -1309,7 +1313,13 @@ function lintCommand() {
1309
1313
  ...states.findings,
1310
1314
  ...generatedArt,
1311
1315
  ...artSizes,
1312
- ];
1316
+ ].filter((one) => {
1317
+ const key = JSON.stringify([one.file, one.line, one.column, one.message]);
1318
+ if (!loaderLocations.has(key)) return true;
1319
+ if (reportedLoaders.has(key)) return false;
1320
+ reportedLoaders.add(key);
1321
+ return true;
1322
+ });
1313
1323
  // Only an **error** fails the run. `reportFindings` draws that
1314
1324
  // line, so the rule is the shared one rather than a second
1315
1325
  // copy here. Each finding names its own file.
@@ -2566,7 +2576,7 @@ function mapCommand() {
2566
2576
  * `undefined` when the content tree could not be read whole, which is
2567
2577
  * reported.
2568
2578
  */
2569
- function mappingContext() {
2579
+ function encodingContext() {
2570
2580
  const config = loadPackConfig();
2571
2581
  const problems = [];
2572
2582
  const records = indexRecordsFor({
@@ -2590,7 +2600,7 @@ function mappingContext() {
2590
2600
  * @param {{problem: string, about: string}} found - What is wrong.
2591
2601
  * @param {string} [lang] - The language asked for.
2592
2602
  */
2593
- function reportMapping(config, found) {
2603
+ function reportEncoding(config, found) {
2594
2604
  if (found.about === "lang") {
2595
2605
  const file = packConfigPath();
2596
2606
  let text;
@@ -2613,84 +2623,10 @@ function reportMapping(config, found) {
2613
2623
  }
2614
2624
 
2615
2625
  /**
2616
- * `package-build transliterate <map> [text..]` — map a passage exactly as the
2617
- * build maps a run marked with the same `map=`, `lang=` and `font=`.
2618
- *
2619
- * @returns {object} The yargs command module.
2620
- */
2621
- // eslint-disable-next-line
2622
- function transliterateCommand() {
2623
- return {
2624
- command: "transliterate <map> [text..]",
2625
- describe: "Map a passage into a script exactly as the build maps a marked run",
2626
- builder: (yargs) =>
2627
- yargs
2628
- .positional("map", {
2629
- type: "string",
2630
- describe: "The `data.map.name` of a mapping note",
2631
- })
2632
- .positional("text", {
2633
- type: "string",
2634
- array: true,
2635
- describe: "The passage; `-` or nothing reads it from stdin",
2636
- })
2637
- .option("lang", {
2638
- type: "string",
2639
- describe: "A declared language, whose lexicon is read first",
2640
- })
2641
- .option("font", {
2642
- type: "string",
2643
- describe:
2644
- "The face; the language's `data.writtenIn` face where the map is drawn in it, and required when neither it nor the map gives one",
2645
- })
2646
- .option("hex", {
2647
- type: "boolean",
2648
- default: false,
2649
- describe: "Print the code points, as `U+XXXX` separated by spaces",
2650
- }),
2651
- handler: async (argv) => {
2652
- try {
2653
- const read = mappingContext();
2654
- if (!read) {
2655
- process.exitCode = 1;
2656
- return;
2657
- }
2658
- const words = (argv.text ?? []).map(String);
2659
- const text =
2660
- !words.length || (words.length === 1 && words[0] === "-") ?
2661
- (await readStdin()).replace(/\r?\n$/, "")
2662
- : words.join(" ");
2663
- const mapped = transliterate(
2664
- read.ctx,
2665
- { map: String(argv.map), lang: argv.lang, font: argv.font },
2666
- text,
2667
- );
2668
- if ("problem" in mapped) {
2669
- reportMapping(read.config, mapped);
2670
- return;
2671
- }
2672
- console.log(
2673
- argv.hex ?
2674
- Array.from(
2675
- mapped.text,
2676
- (char) =>
2677
- `U+${char.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`,
2678
- ).join(" ")
2679
- : mapped.text,
2680
- );
2681
- } catch (err) {
2682
- reportFailure(err);
2683
- process.exitCode = 1;
2684
- }
2685
- },
2686
- };
2687
- }
2688
-
2689
- /**
2690
- * `package-build lexicon <language> [--map <map>]` — every word of a language's
2691
- * lexicon with its native form under a map, as the build writes it; with
2626
+ * `package-build lexicon <language> [--enc <enc>]` — every word of a language's
2627
+ * lexicon with its native form under an encoding, as the build writes it; with
2692
2628
  * `--derived`, the forms its rules derive follow, each naming its word in a
2693
- * sixth column. Under a logographic map the third column says where a
2629
+ * sixth column. Under a logographic enc the third column says where a
2694
2630
  * word's writing comes from, `own` or `parts`, or marks it `none` or
2695
2631
  * `ambiguous`.
2696
2632
  *
@@ -2700,22 +2636,20 @@ function transliterateCommand() {
2700
2636
  function lexiconCommand() {
2701
2637
  return {
2702
2638
  command: "lexicon <language>",
2703
- describe: "List a language's words with their native forms under a map",
2639
+ describe: "List a language's words with their native forms under an encoding",
2704
2640
  builder: (yargs) =>
2705
2641
  yargs
2706
2642
  .positional("language", {
2707
2643
  type: "string",
2708
2644
  describe: "A declared language whose `note` names its language note",
2709
2645
  })
2710
- .option("map", {
2646
+ .option("enc", {
2711
2647
  type: "string",
2712
- describe:
2713
- "The `data.map.name` of a mapping note; the language's `data.writtenIn` map where left out",
2648
+ describe: "The encoding document Address, with doc as default type",
2714
2649
  })
2715
2650
  .option("font", {
2716
2651
  type: "string",
2717
- describe:
2718
- "The face; the language's `data.writtenIn` face where the map is drawn in it, and required when neither it nor the map gives one",
2652
+ describe: "The encoding document's font; an explicit value must agree",
2719
2653
  })
2720
2654
  .option("derived", {
2721
2655
  type: "boolean",
@@ -2730,19 +2664,19 @@ function lexiconCommand() {
2730
2664
  }),
2731
2665
  handler: (argv) => {
2732
2666
  try {
2733
- const read = mappingContext();
2667
+ const read = encodingContext();
2734
2668
  if (!read) {
2735
2669
  process.exitCode = 1;
2736
2670
  return;
2737
2671
  }
2738
2672
  const listed = lexiconEntries(read.ctx, {
2739
2673
  lang: String(argv.language),
2740
- map: argv.map === undefined ? undefined : String(argv.map),
2674
+ enc: argv.enc === undefined ? undefined : String(argv.enc),
2741
2675
  font: argv.font,
2742
2676
  derived: Boolean(argv.derived),
2743
2677
  });
2744
2678
  if ("problem" in listed) {
2745
- reportMapping(read.config, listed);
2679
+ reportEncoding(read.config, listed);
2746
2680
  return;
2747
2681
  }
2748
2682
  if (argv.json) console.log(JSON.stringify(listed.entries, null, 2));
@@ -3339,7 +3273,7 @@ function readNpcSpec(argv) {
3339
3273
  *
3340
3274
  * @param {{file?: string, raw?: string}} note - Where the note is.
3341
3275
  * @param {Array<string|number>} keyPath - The frontmatter key at fault, or the
3342
- * map a missing key belongs in.
3276
+ * enc a missing key belongs in.
3343
3277
  * @param {string} message - What is wrong.
3344
3278
  * @returns {Error} The error, for `reportFailure` to print as a diagnostic.
3345
3279
  */
@@ -0,0 +1,146 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+ /** Bare encoding commands: a published definition, UTF-8 input and exact output. */
14
+ import fs from "node:fs";
15
+ import { encodeText, decodeText } from "../engine/encoding.mjs";
16
+ import { encodingAddress } from "../engine/encoding-notes.mjs";
17
+ import { readStdin } from "../engine/read-stdin.mjs";
18
+
19
+ /** Apply one of the bare commands; all conversion is shared with the build. */
20
+ async function execute(direction, argv) {
21
+ const strings = argv.text ?? [];
22
+ if (
23
+ !argv.enc ||
24
+ (argv.in !== undefined && strings.length) ||
25
+ (argv.in === undefined && strings.length !== 1)
26
+ ) {
27
+ console.error(
28
+ `package-build ${direction}: --enc is required; give one string or --in <file|->`,
29
+ );
30
+ process.exitCode = 2;
31
+ return;
32
+ }
33
+ let source = "<text>";
34
+ try {
35
+ let encoding;
36
+ if (argv.enc !== "default") {
37
+ const [{ loadPackConfig }, { indexRecordsFor }, { runContext }] = await Promise.all([
38
+ import("../engine/pack-config.mjs"),
39
+ import("../engine/content-index.mjs"),
40
+ import("../engine/content-scripts.mjs"),
41
+ ]);
42
+ const config = loadPackConfig(),
43
+ problems = [];
44
+ const records = indexRecordsFor({ config, problems });
45
+ if (problems.length) throw new Error(problems.map((one) => one.message).join("\n"));
46
+ const context = runContext(config, records);
47
+ if (context.encodingFindings?.length)
48
+ throw Object.assign(
49
+ new Error(context.encodingFindings[0].message),
50
+ context.encodingFindings[0],
51
+ );
52
+ encoding =
53
+ context.encodings.get(String(argv.enc)) ??
54
+ context.encodings.get(encodingAddress(String(argv.enc), config.contentPackage));
55
+ if (!encoding) throw new Error(`Encoding document \`${argv.enc}\` does not resolve`);
56
+ }
57
+ source =
58
+ argv.in === "-" ? "<stdin>"
59
+ : argv.in === undefined ? "<text>"
60
+ : String(argv.in);
61
+ const input =
62
+ argv.in === undefined ? String(strings[0])
63
+ : argv.in === "-" ? await readStdin()
64
+ : fs.readFileSync(String(argv.in), "utf8");
65
+ let output;
66
+ try {
67
+ output =
68
+ !encoding ? input
69
+ : direction === "encode" ? encodeText(encoding, input)
70
+ : decodeText(encoding, input);
71
+ } catch (error) {
72
+ error.file = source;
73
+ throw error;
74
+ }
75
+ if (argv.out !== undefined) fs.writeFileSync(String(argv.out), output, "utf8");
76
+ else process.stdout.write(output + (argv.in === undefined ? "\n" : ""));
77
+ } catch (error) {
78
+ const file = error.file ?? source;
79
+ const position = error.line !== undefined ? `:${error.line}:${error.column ?? 1}` : "";
80
+ console.error(`${file}${position}: error: ${error.message}`);
81
+ process.exitCode = 1;
82
+ }
83
+ }
84
+
85
+ /** Encode roman text through one explicitly selected encoding. */
86
+ function encodeCommand() {
87
+ return {
88
+ command: "encode [text..]",
89
+ describe: "Encode roman text using an encoding document",
90
+ builder: (yargs) =>
91
+ yargs
92
+ .positional("text", { type: "string", array: true, describe: "One input string" })
93
+ .option("enc", {
94
+ type: "string",
95
+ requiresArg: true,
96
+ describe: "Required encoding document Address, or default",
97
+ })
98
+ .option("in", {
99
+ alias: "i",
100
+ type: "string",
101
+ requiresArg: true,
102
+ describe: "Read UTF-8 input from a file or - for stdin",
103
+ })
104
+ .option("out", {
105
+ alias: "o",
106
+ type: "string",
107
+ requiresArg: true,
108
+ describe: "Write the exact result to a file",
109
+ }),
110
+ handler: (argv) => execute("encode", argv),
111
+ };
112
+ }
113
+
114
+ /** Decode code points through one explicitly selected encoding. */
115
+ function decodeCommand() {
116
+ return {
117
+ command: "decode [text..]",
118
+ describe: "Decode code points using an encoding document",
119
+ builder: (yargs) =>
120
+ yargs
121
+ .positional("text", { type: "string", array: true, describe: "One input string" })
122
+ .option("enc", {
123
+ type: "string",
124
+ requiresArg: true,
125
+ describe: "Required encoding document Address, or default",
126
+ })
127
+ .option("in", {
128
+ alias: "i",
129
+ type: "string",
130
+ requiresArg: true,
131
+ describe: "Read UTF-8 input from a file or - for stdin",
132
+ })
133
+ .option("out", {
134
+ alias: "o",
135
+ type: "string",
136
+ requiresArg: true,
137
+ describe: "Write the exact result to a file",
138
+ }),
139
+ handler: (argv) => execute("decode", argv),
140
+ };
141
+ }
142
+
143
+ /** Register the two bare conversion commands on the shared CLI. */
144
+ export function registerEncodingCommands(yargs) {
145
+ return yargs.command(encodeCommand()).command(decodeCommand());
146
+ }
@@ -145,6 +145,7 @@ import {
145
145
  INIT_KINDS,
146
146
  INIT_LICENSES,
147
147
  } from "../engine/project-init.mjs";
148
+ import { registerEncodingCommands } from "./encoding-commands.mjs";
148
149
  import { registerContentCommands } from "./content-commands.mjs";
149
150
 
150
151
  /**
@@ -711,6 +712,10 @@ function manifestCommand() {
711
712
  // So a `packFolders` finding names the line it is about.
712
713
  configFile: packConfigPath(),
713
714
  });
715
+ if (config.converterData) {
716
+ const { writeConverterData } = await import("../engine/converter-data.mjs");
717
+ writeConverterData({ config: shared, packageBuild: config });
718
+ }
714
719
  console.log(
715
720
  `✅ Wrote ${path.relative(config.rootDir, written)} ` +
716
721
  `(${Object.keys(manifest).length} keys, ` +
@@ -1746,30 +1751,32 @@ function bumpCommand() {
1746
1751
  }
1747
1752
 
1748
1753
  registerContentCommands(
1749
- yargs(hideBin(process.argv))
1750
- .scriptName("package-build")
1751
- .command(initCommand())
1752
- .command(ciCommand())
1753
- .command(cleanCommand())
1754
- .command(assetsCommand())
1755
- .command(stageCommand())
1756
- .command(calendarsCommand())
1757
- .command(datefromCommand())
1758
- .command(datetoCommand())
1759
- .command(manifestCommand())
1760
- .command(siteRootCommand())
1761
- .command(typesCommand())
1762
- .command(schemaCommand())
1763
- .command(langCommand())
1764
- .command(bumpCommand())
1765
- .command(yamlCommand())
1766
- .command(changelogCommand())
1767
- .command(bundleCommand())
1768
- .command(releaseCommand())
1769
- .command(deployCommand())
1770
- .command(cdnCommand())
1771
- .command(containerCommand())
1772
- .command(e2eCommand()),
1754
+ registerEncodingCommands(
1755
+ yargs(hideBin(process.argv))
1756
+ .scriptName("package-build")
1757
+ .command(initCommand())
1758
+ .command(ciCommand())
1759
+ .command(cleanCommand())
1760
+ .command(assetsCommand())
1761
+ .command(stageCommand())
1762
+ .command(calendarsCommand())
1763
+ .command(datefromCommand())
1764
+ .command(datetoCommand())
1765
+ .command(manifestCommand())
1766
+ .command(siteRootCommand())
1767
+ .command(typesCommand())
1768
+ .command(schemaCommand())
1769
+ .command(langCommand())
1770
+ .command(bumpCommand())
1771
+ .command(yamlCommand())
1772
+ .command(changelogCommand())
1773
+ .command(bundleCommand())
1774
+ .command(releaseCommand())
1775
+ .command(deployCommand())
1776
+ .command(cdnCommand())
1777
+ .command(containerCommand())
1778
+ .command(e2eCommand()),
1779
+ ),
1773
1780
  )
1774
1781
  .demandCommand(1, "Name a command.")
1775
1782
  .strict()
@@ -1779,5 +1786,9 @@ registerContentCommands(
1779
1786
  .epilog("Run package-build <command> --help for that command's actions and options.")
1780
1787
  .fail((message, err, cli) => {
1781
1788
  if (!err) cli.showHelp("error");
1789
+ if (["encode", "decode"].includes(process.argv[2])) {
1790
+ console.error(`package-build: ${err?.message ?? message}`);
1791
+ process.exit(2);
1792
+ }
1782
1793
  die(err ?? message);
1783
1794
  }).argv;
package/config.mjs CHANGED
@@ -70,6 +70,7 @@ const SECTION_KEYS = [
70
70
  "baseStyles",
71
71
  "manifest",
72
72
  "manifestFlags",
73
+ "converterData",
73
74
  "schema",
74
75
  "clean",
75
76
  "lang",
@@ -338,6 +339,28 @@ function normalizeManifest(value) {
338
339
  return Object.freeze(structuredClone(input));
339
340
  }
340
341
 
342
+ /** The optional converter artifact is a JSON file beneath the package stage. */
343
+ function normalizeConverterData(value) {
344
+ if (value === undefined) return null;
345
+ const declared = requireNonEmptyString(value, "packageBuild.converterData");
346
+ const pieces = declared.split(/[\\/]/);
347
+ if (
348
+ path.posix.isAbsolute(declared) ||
349
+ path.win32.isAbsolute(declared) ||
350
+ pieces.includes("..") ||
351
+ pieces.includes("") ||
352
+ !declared.endsWith(".json") ||
353
+ declared.includes("\0") ||
354
+ ["module.json", "system.json", "package.json"].includes(declared)
355
+ ) {
356
+ fail(
357
+ "packageBuild.converterData",
358
+ "must name a JSON artifact within the build stage, such as data/converter.json",
359
+ );
360
+ }
361
+ return pieces.join("/");
362
+ }
363
+
341
364
  /**
342
365
  * Normalize a setting that is one glob or several.
343
366
  *
@@ -565,6 +588,7 @@ function normalizeExceptions(value, field, where) {
565
588
  * named first in the manifest's `styles`.
566
589
  * @property {Readonly<Record<string, unknown>>} manifest The manifest
567
590
  * specification, emitted as declared.
591
+ * @property {string|null} converterData Optional JSON path relative to the build stage.
568
592
  * @property {string|null} manifestFlags Module to load a `flags` function
569
593
  * from, for namespaced flags a repository has
570
594
  * to compute. `null` when it declares none.
@@ -938,6 +962,7 @@ export function resolvePackageBuildConfig(shared) {
938
962
  shared.rootDir,
939
963
  ),
940
964
  manifest: normalizeManifest(section.manifest),
965
+ converterData: normalizeConverterData(section.converterData),
941
966
  manifestFlags:
942
967
  section.manifestFlags === undefined ?
943
968
  null