@promptbook/utils 0.112.0-73 → 0.112.0-80

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 (74) hide show
  1. package/README.md +9 -9
  2. package/esm/index.es.js +310 -204
  3. package/esm/index.es.js.map +1 -1
  4. package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  5. package/esm/src/avatars/visuals/octopus3d2AvatarVisual.d.ts +7 -0
  6. package/esm/src/avatars/visuals/octopus3dAvatarVisualShared.d.ts +37 -0
  7. package/esm/src/book-components/Chat/save/_common/chatExportRendering.d.ts +47 -0
  8. package/esm/src/book-components/Chat/save/html/htmlSaveFormatDefinition.d.ts +12 -0
  9. package/esm/src/book-components/Chat/save/index.d.ts +2 -2
  10. package/esm/src/book-components/Chat/save/markdown/mdSaveFormatDefinition.d.ts +5 -3
  11. package/esm/src/book-components/Chat/save/pdf/buildChatPdf.d.ts +3 -3
  12. package/esm/src/book-components/Chat/save/pdf/pdfSaveFormatDefinition.d.ts +1 -1
  13. package/esm/src/cli/cli-commands/agent/agentProjectPaths.d.ts +8 -8
  14. package/esm/src/cli/cli-commands/agent/initializeAgentRunnerCommand.d.ts +1 -1
  15. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer.d.ts +56 -0
  16. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer.test.d.ts +1 -0
  17. package/esm/src/cli/cli-commands/agents-server/ensureAgentsServerEnvFile.d.ts +7 -0
  18. package/esm/src/cli/cli-commands/agents-server/ensureAgentsServerGitignoreFile.d.ts +7 -0
  19. package/esm/src/cli/cli-commands/agents-server/init.d.ts +9 -0
  20. package/esm/src/cli/cli-commands/agents-server/init.test.d.ts +1 -0
  21. package/esm/src/cli/cli-commands/agents-server/initializeAgentsServerProjectConfiguration.d.ts +17 -0
  22. package/esm/src/cli/cli-commands/agents-server/printAgentsServerInitializationSummary.d.ts +7 -0
  23. package/esm/src/cli/cli-commands/agents-server/run.d.ts +14 -0
  24. package/esm/src/cli/cli-commands/agents-server/run.test.d.ts +1 -0
  25. package/esm/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +23 -0
  26. package/esm/src/cli/cli-commands/agents-server.d.ts +8 -0
  27. package/esm/src/cli/cli-commands/common/projectInitialization.d.ts +65 -0
  28. package/esm/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +44 -0
  29. package/esm/src/cli/common/$deprecateCliCommand.d.ts +8 -0
  30. package/esm/src/cli/common/$deprecateCliCommand.test.d.ts +1 -0
  31. package/esm/src/utils/color/Color.d.ts +4 -44
  32. package/esm/src/utils/color/ColorValue.d.ts +55 -0
  33. package/esm/src/utils/color/isHexColorString.d.ts +10 -0
  34. package/esm/src/utils/color/parseColorString.d.ts +11 -0
  35. package/esm/src/version.d.ts +1 -1
  36. package/package.json +1 -1
  37. package/umd/index.umd.js +310 -204
  38. package/umd/index.umd.js.map +1 -1
  39. package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  40. package/umd/src/avatars/visuals/octopus3d2AvatarVisual.d.ts +7 -0
  41. package/umd/src/avatars/visuals/octopus3dAvatarVisualShared.d.ts +37 -0
  42. package/umd/src/book-components/Chat/save/_common/chatExportRendering.d.ts +47 -0
  43. package/umd/src/book-components/Chat/save/html/htmlSaveFormatDefinition.d.ts +12 -0
  44. package/umd/src/book-components/Chat/save/index.d.ts +2 -2
  45. package/umd/src/book-components/Chat/save/markdown/mdSaveFormatDefinition.d.ts +5 -3
  46. package/umd/src/book-components/Chat/save/pdf/buildChatPdf.d.ts +3 -3
  47. package/umd/src/book-components/Chat/save/pdf/pdfSaveFormatDefinition.d.ts +1 -1
  48. package/umd/src/cli/cli-commands/agent/agentProjectPaths.d.ts +8 -8
  49. package/umd/src/cli/cli-commands/agent/initializeAgentRunnerCommand.d.ts +1 -1
  50. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer.d.ts +56 -0
  51. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer.test.d.ts +1 -0
  52. package/umd/src/cli/cli-commands/agents-server/ensureAgentsServerEnvFile.d.ts +7 -0
  53. package/umd/src/cli/cli-commands/agents-server/ensureAgentsServerGitignoreFile.d.ts +7 -0
  54. package/umd/src/cli/cli-commands/agents-server/init.d.ts +9 -0
  55. package/umd/src/cli/cli-commands/agents-server/init.test.d.ts +1 -0
  56. package/umd/src/cli/cli-commands/agents-server/initializeAgentsServerProjectConfiguration.d.ts +17 -0
  57. package/umd/src/cli/cli-commands/agents-server/printAgentsServerInitializationSummary.d.ts +7 -0
  58. package/umd/src/cli/cli-commands/agents-server/run.d.ts +14 -0
  59. package/umd/src/cli/cli-commands/agents-server/run.test.d.ts +1 -0
  60. package/umd/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +23 -0
  61. package/umd/src/cli/cli-commands/agents-server.d.ts +8 -0
  62. package/umd/src/cli/cli-commands/common/projectInitialization.d.ts +65 -0
  63. package/umd/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +44 -0
  64. package/umd/src/cli/common/$deprecateCliCommand.d.ts +8 -0
  65. package/umd/src/cli/common/$deprecateCliCommand.test.d.ts +1 -0
  66. package/umd/src/utils/color/Color.d.ts +4 -44
  67. package/umd/src/utils/color/ColorValue.d.ts +55 -0
  68. package/umd/src/utils/color/isHexColorString.d.ts +10 -0
  69. package/umd/src/utils/color/parseColorString.d.ts +11 -0
  70. package/umd/src/version.d.ts +1 -1
  71. package/esm/src/cli/cli-commands/coder/appendBlock.d.ts +0 -6
  72. package/esm/src/cli/cli-commands/coder/readTextFileIfExists.d.ts +0 -6
  73. package/umd/src/cli/cli-commands/coder/appendBlock.d.ts +0 -6
  74. package/umd/src/cli/cli-commands/coder/readTextFileIfExists.d.ts +0 -6
package/README.md CHANGED
@@ -758,28 +758,28 @@ npx ts-node ./src/cli/test/ptbk.ts coder verify
758
758
 
759
759
  #### Using `ptbk coder` in an external project
760
760
 
761
- If you want to use the workflow in another repository, install the package and invoke the `ptbk` binary. After local installation, `npx ptbk ...` is the most portable form; plain `ptbk ...` also works when your environment exposes the local binary on `PATH`.
761
+ If you want to use the workflow in another repository, install the package and invoke the `ptbk` binary directly.
762
762
 
763
763
  ```bash
764
764
  npm install ptbk
765
765
 
766
766
  ptbk coder init
767
767
 
768
- npx ptbk coder generate-boilerplates
768
+ ptbk coder generate-boilerplates
769
769
 
770
- npx ptbk coder generate-boilerplates --template prompts/templates/common.md
770
+ ptbk coder generate-boilerplates --template prompts/templates/common.md
771
771
 
772
- npx ptbk coder run --agent github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --test npm run test
772
+ ptbk coder run --agent github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --test npm run test
773
773
 
774
- npx ptbk coder run --agent github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --auto-push
774
+ ptbk coder run --agent github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --auto-push
775
775
 
776
- npx ptbk coder run --agent github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --test npm run test --ignore-git-changes --no-wait
776
+ ptbk coder run --agent github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --test npm run test --ignore-git-changes --no-wait
777
777
 
778
- npx ptbk coder find-refactor-candidates
778
+ ptbk coder find-refactor-candidates
779
779
 
780
- npx ptbk coder find-refactor-candidates --level xhigh
780
+ ptbk coder find-refactor-candidates --level xhigh
781
781
 
782
- npx ptbk coder verify
782
+ ptbk coder verify
783
783
  ```
784
784
 
785
785
  `ptbk coder init` also bootstraps a starter `AGENTS.md`, adds `package.json` scripts for the four main coder commands, adds the shared `/.promptbook` temp ignore to `.gitignore`, and configures `.vscode/settings.json` so pasted images from `prompts/*.md` land in `prompts/screenshots/`.
package/esm/index.es.js CHANGED
@@ -18,7 +18,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
18
18
  * @generated
19
19
  * @see https://github.com/webgptorg/promptbook
20
20
  */
21
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-73';
21
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-80';
22
22
  /**
23
23
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
24
24
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -268,6 +268,111 @@ function checkChannelValue(channelName, value) {
268
268
  }
269
269
  }
270
270
 
271
+ /**
272
+ * Shared immutable channel storage and serialization helpers for `Color`.
273
+ *
274
+ * @private base class of Color
275
+ */
276
+ class ColorValue {
277
+ constructor(red, green, blue, alpha = 255) {
278
+ this.red = red;
279
+ this.green = green;
280
+ this.blue = blue;
281
+ this.alpha = alpha;
282
+ checkChannelValue('Red', red);
283
+ checkChannelValue('Green', green);
284
+ checkChannelValue('Blue', blue);
285
+ checkChannelValue('Alpha', alpha);
286
+ }
287
+ /**
288
+ * Shortcut for `red` property
289
+ * Number from 0 to 255
290
+ * @alias red
291
+ */
292
+ get r() {
293
+ return this.red;
294
+ }
295
+ /**
296
+ * Shortcut for `green` property
297
+ * Number from 0 to 255
298
+ * @alias green
299
+ */
300
+ get g() {
301
+ return this.green;
302
+ }
303
+ /**
304
+ * Shortcut for `blue` property
305
+ * Number from 0 to 255
306
+ * @alias blue
307
+ */
308
+ get b() {
309
+ return this.blue;
310
+ }
311
+ /**
312
+ * Shortcut for `alpha` property
313
+ * Number from 0 (transparent) to 255 (opaque)
314
+ * @alias alpha
315
+ */
316
+ get a() {
317
+ return this.alpha;
318
+ }
319
+ /**
320
+ * Shortcut for `alpha` property
321
+ * Number from 0 (transparent) to 255 (opaque)
322
+ * @alias alpha
323
+ */
324
+ get opacity() {
325
+ return this.alpha;
326
+ }
327
+ /**
328
+ * Shortcut for 1-`alpha` property
329
+ */
330
+ get transparency() {
331
+ return 255 - this.alpha;
332
+ }
333
+ clone() {
334
+ return take(this.createColor(this.red, this.green, this.blue, this.alpha));
335
+ }
336
+ toString() {
337
+ return this.toHex();
338
+ }
339
+ toHex() {
340
+ if (this.alpha === 255) {
341
+ return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
342
+ .toString(16)
343
+ .padStart(2, '0')}`;
344
+ }
345
+ else {
346
+ return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
347
+ .toString(16)
348
+ .padStart(2, '0')}${this.alpha.toString(16).padStart(2, '0')}`;
349
+ }
350
+ }
351
+ toRgb() {
352
+ if (this.alpha === 255) {
353
+ return `rgb(${this.red}, ${this.green}, ${this.blue})`;
354
+ }
355
+ else {
356
+ return `rgba(${this.red}, ${this.green}, ${this.blue}, ${Math.round((this.alpha / 255) * 100)}%)`;
357
+ }
358
+ }
359
+ toHsl() {
360
+ throw new Error(`Getting HSL is not implemented`);
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Checks if the given value is a valid hex color string
366
+ *
367
+ * @param value - value to check
368
+ * @returns true if the value is a valid hex color string (e.g., `#009edd`, `#fff`, etc.)
369
+ *
370
+ * @private function of Color
371
+ */
372
+ function isHexColorString(value) {
373
+ return (typeof value === 'string' && /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value));
374
+ }
375
+
271
376
  /**
272
377
  * Constant for short hex lengths.
273
378
  */
@@ -479,16 +584,53 @@ function parseAlphaValue(value) {
479
584
 
480
585
  /**
481
586
  * Pattern matching hsl regex.
587
+ *
588
+ * @private function of Color
482
589
  */
483
590
  const HSL_REGEX_PATTERN = /^hsl\(\s*([0-9.]+)\s*,\s*([0-9.]+)%\s*,\s*([0-9.]+)%\s*\)$/;
484
591
  /**
485
592
  * Pattern matching RGB regex.
593
+ *
594
+ * @private function of Color
486
595
  */
487
596
  const RGB_REGEX_PATTERN = /^rgb\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/;
488
597
  /**
489
598
  * Pattern matching rgba regex.
599
+ *
600
+ * @private function of Color
490
601
  */
491
602
  const RGBA_REGEX_PATTERN = /^rgba\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/;
603
+ /**
604
+ * Parses a supported color string into RGBA channels.
605
+ *
606
+ * @param color as a string for example `#009edd`, `rgb(0,158,221)`, `rgb(0%,62%,86.7%)`, `hsl(197.1,100%,43.3%)`, `red`, `darkgrey`,...
607
+ * @returns RGBA channel values.
608
+ *
609
+ * @private function of Color
610
+ */
611
+ function parseColorString(color) {
612
+ const trimmed = color.trim();
613
+ const cssColor = CSS_COLORS[trimmed];
614
+ if (cssColor) {
615
+ return parseColorString(cssColor);
616
+ }
617
+ else if (isHexColorString(trimmed)) {
618
+ return parseHexColor(trimmed);
619
+ }
620
+ if (HSL_REGEX_PATTERN.test(trimmed)) {
621
+ return parseHslColor(trimmed);
622
+ }
623
+ else if (RGB_REGEX_PATTERN.test(trimmed)) {
624
+ return parseRgbColor(trimmed);
625
+ }
626
+ else if (RGBA_REGEX_PATTERN.test(trimmed)) {
627
+ return parseRgbaColor(trimmed);
628
+ }
629
+ else {
630
+ throw new Error(`Can not create a new Color instance from string "${trimmed}".`);
631
+ }
632
+ }
633
+
492
634
  /**
493
635
  * Color object represents an RGB color with alpha channel
494
636
  *
@@ -496,7 +638,7 @@ const RGBA_REGEX_PATTERN = /^rgba\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.
496
638
  *
497
639
  * @public exported from `@promptbook/color`
498
640
  */
499
- class Color {
641
+ class Color extends ColorValue {
500
642
  /**
501
643
  * Creates a new Color instance from miscellaneous formats
502
644
  * - It can receive Color instance and just return the same instance
@@ -569,25 +711,7 @@ class Color {
569
711
  * @returns Color object
570
712
  */
571
713
  static fromString(color) {
572
- const trimmed = color.trim();
573
- if (CSS_COLORS[trimmed]) {
574
- return Color.fromString(CSS_COLORS[trimmed]);
575
- }
576
- else if (Color.isHexColorString(trimmed)) {
577
- return Color.fromHex(trimmed);
578
- }
579
- if (HSL_REGEX_PATTERN.test(trimmed)) {
580
- return Color.fromHsl(trimmed);
581
- }
582
- else if (RGB_REGEX_PATTERN.test(trimmed)) {
583
- return Color.fromRgbString(trimmed);
584
- }
585
- else if (RGBA_REGEX_PATTERN.test(trimmed)) {
586
- return Color.fromRgbaString(trimmed);
587
- }
588
- else {
589
- throw new Error(`Can not create a new Color instance from string "${trimmed}".`);
590
- }
714
+ return Color.fromColorChannels(parseColorString(color));
591
715
  }
592
716
  /**
593
717
  * Gets common color
@@ -617,8 +741,7 @@ class Color {
617
741
  * @returns Color object
618
742
  */
619
743
  static fromHex(hex) {
620
- const { red, green, blue, alpha } = parseHexColor(hex);
621
- return take(new Color(red, green, blue, alpha));
744
+ return Color.fromColorChannels(parseHexColor(hex));
622
745
  }
623
746
  /**
624
747
  * Creates a new Color instance from color in hsl format
@@ -627,8 +750,7 @@ class Color {
627
750
  * @returns Color object
628
751
  */
629
752
  static fromHsl(hsl) {
630
- const { red, green, blue, alpha } = parseHslColor(hsl);
631
- return take(new Color(red, green, blue, alpha));
753
+ return Color.fromColorChannels(parseHslColor(hsl));
632
754
  }
633
755
  /**
634
756
  * Creates a new Color instance from color in rgb format
@@ -637,8 +759,7 @@ class Color {
637
759
  * @returns Color object
638
760
  */
639
761
  static fromRgbString(rgb) {
640
- const { red, green, blue, alpha } = parseRgbColor(rgb);
641
- return take(new Color(red, green, blue, alpha));
762
+ return Color.fromColorChannels(parseRgbColor(rgb));
642
763
  }
643
764
  /**
644
765
  * Creates a new Color instance from color in rbga format
@@ -647,8 +768,7 @@ class Color {
647
768
  * @returns Color object
648
769
  */
649
770
  static fromRgbaString(rgba) {
650
- const { red, green, blue, alpha } = parseRgbaColor(rgba);
651
- return take(new Color(red, green, blue, alpha));
771
+ return Color.fromColorChannels(parseRgbaColor(rgba));
652
772
  }
653
773
  /**
654
774
  * Creates a new Color for color channels values
@@ -660,7 +780,7 @@ class Color {
660
780
  * @returns Color object
661
781
  */
662
782
  static fromValues(red, green, blue, alpha = 255) {
663
- return take(new Color(red, green, blue, alpha));
783
+ return Color.fromColorChannels({ red, green, blue, alpha });
664
784
  }
665
785
  /**
666
786
  * Checks if the given value is a valid Color object.
@@ -693,8 +813,7 @@ class Color {
693
813
  * @returns true if the value is a valid hex color string (e.g., `#009edd`, `#fff`, etc.)
694
814
  */
695
815
  static isHexColorString(value) {
696
- return (typeof value === 'string' &&
697
- /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value));
816
+ return isHexColorString(value);
698
817
  }
699
818
  /**
700
819
  * Creates new Color object
@@ -707,89 +826,13 @@ class Color {
707
826
  * @param alpha number from 0 (transparent) to 255 (opaque)
708
827
  */
709
828
  constructor(red, green, blue, alpha = 255) {
710
- this.red = red;
711
- this.green = green;
712
- this.blue = blue;
713
- this.alpha = alpha;
714
- checkChannelValue('Red', red);
715
- checkChannelValue('Green', green);
716
- checkChannelValue('Blue', blue);
717
- checkChannelValue('Alpha', alpha);
718
- }
719
- /**
720
- * Shortcut for `red` property
721
- * Number from 0 to 255
722
- * @alias red
723
- */
724
- get r() {
725
- return this.red;
726
- }
727
- /**
728
- * Shortcut for `green` property
729
- * Number from 0 to 255
730
- * @alias green
731
- */
732
- get g() {
733
- return this.green;
734
- }
735
- /**
736
- * Shortcut for `blue` property
737
- * Number from 0 to 255
738
- * @alias blue
739
- */
740
- get b() {
741
- return this.blue;
742
- }
743
- /**
744
- * Shortcut for `alpha` property
745
- * Number from 0 (transparent) to 255 (opaque)
746
- * @alias alpha
747
- */
748
- get a() {
749
- return this.alpha;
829
+ super(red, green, blue, alpha);
750
830
  }
751
- /**
752
- * Shortcut for `alpha` property
753
- * Number from 0 (transparent) to 255 (opaque)
754
- * @alias alpha
755
- */
756
- get opacity() {
757
- return this.alpha;
831
+ createColor(red, green, blue, alpha) {
832
+ return new Color(red, green, blue, alpha);
758
833
  }
759
- /**
760
- * Shortcut for 1-`alpha` property
761
- */
762
- get transparency() {
763
- return 255 - this.alpha;
764
- }
765
- clone() {
766
- return take(new Color(this.red, this.green, this.blue, this.alpha));
767
- }
768
- toString() {
769
- return this.toHex();
770
- }
771
- toHex() {
772
- if (this.alpha === 255) {
773
- return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
774
- .toString(16)
775
- .padStart(2, '0')}`;
776
- }
777
- else {
778
- return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
779
- .toString(16)
780
- .padStart(2, '0')}${this.alpha.toString(16).padStart(2, '0')}`;
781
- }
782
- }
783
- toRgb() {
784
- if (this.alpha === 255) {
785
- return `rgb(${this.red}, ${this.green}, ${this.blue})`;
786
- }
787
- else {
788
- return `rgba(${this.red}, ${this.green}, ${this.blue}, ${Math.round((this.alpha / 255) * 100)}%)`;
789
- }
790
- }
791
- toHsl() {
792
- throw new Error(`Getting HSL is not implemented`);
834
+ static fromColorChannels({ red, green, blue, alpha }) {
835
+ return take(new Color(red, green, blue, alpha));
793
836
  }
794
837
  }
795
838
 
@@ -1289,120 +1332,183 @@ function assertsError(whatWasThrown) {
1289
1332
  * @public exported from `@promptbook/utils`
1290
1333
  */
1291
1334
  function checkSerializableAsJson(options) {
1292
- const { value, name, message } = options;
1335
+ checkSerializableValue(options);
1336
+ }
1337
+ // TODO: Can be return type more type-safe? like `asserts options.value is JsonValue`
1338
+ // TODO: [🧠][main] !!3 In-memory cache of same values to prevent multiple checks
1339
+ // Note: [🐠] This is how `checkSerializableAsJson` + `isSerializableAsJson` together can just retun true/false or rich error message
1340
+ /**
1341
+ * Checks one value and dispatches to the appropriate specialized validator.
1342
+ *
1343
+ * @private function of `checkSerializableAsJson`
1344
+ */
1345
+ function checkSerializableValue(options) {
1346
+ const { value } = options;
1347
+ if (isSerializablePrimitive(value)) {
1348
+ return;
1349
+ }
1293
1350
  if (value === undefined) {
1294
- throw new UnexpectedError(`${name} is undefined`);
1351
+ throw new UnexpectedError(`${options.name} is undefined`);
1295
1352
  }
1296
- else if (value === null) {
1297
- return;
1353
+ if (typeof value === 'symbol') {
1354
+ throw new UnexpectedError(`${options.name} is symbol`);
1298
1355
  }
1299
- else if (typeof value === 'boolean') {
1300
- return;
1356
+ if (typeof value === 'function') {
1357
+ throw new UnexpectedError(`${options.name} is function`);
1301
1358
  }
1302
- else if (typeof value === 'number' && !isNaN(value)) {
1359
+ if (Array.isArray(value)) {
1360
+ checkSerializableArray(options, value);
1303
1361
  return;
1304
1362
  }
1305
- else if (typeof value === 'string') {
1363
+ if (value !== null && typeof value === 'object') {
1364
+ checkSerializableObject(options, value);
1306
1365
  return;
1307
1366
  }
1308
- else if (typeof value === 'symbol') {
1309
- throw new UnexpectedError(`${name} is symbol`);
1310
- }
1311
- else if (typeof value === 'function') {
1312
- throw new UnexpectedError(`${name} is function`);
1313
- }
1314
- else if (typeof value === 'object' && Array.isArray(value)) {
1315
- for (let i = 0; i < value.length; i++) {
1316
- checkSerializableAsJson({ name: `${name}[${i}]`, value: value[i], message });
1317
- }
1367
+ throwUnknownTypeError(options);
1368
+ }
1369
+ /**
1370
+ * Checks the primitive values that are directly JSON serializable.
1371
+ *
1372
+ * @private function of `checkSerializableAsJson`
1373
+ */
1374
+ function isSerializablePrimitive(value) {
1375
+ return (value === null ||
1376
+ typeof value === 'boolean' ||
1377
+ (typeof value === 'number' && !isNaN(value)) ||
1378
+ typeof value === 'string');
1379
+ }
1380
+ /**
1381
+ * Recursively checks JSON array items.
1382
+ *
1383
+ * @private function of `checkSerializableAsJson`
1384
+ */
1385
+ function checkSerializableArray(context, arrayValue) {
1386
+ for (let index = 0; index < arrayValue.length; index++) {
1387
+ checkSerializableAsJson({
1388
+ ...context,
1389
+ name: `${context.name}[${index}]`,
1390
+ value: arrayValue[index],
1391
+ });
1318
1392
  }
1319
- else if (typeof value === 'object') {
1320
- if (value instanceof Date) {
1321
- throw new UnexpectedError(spaceTrim$1((block) => `
1322
- \`${name}\` is Date
1393
+ }
1394
+ /**
1395
+ * Checks object-like values and dispatches special unsupported built-ins.
1396
+ *
1397
+ * @private function of `checkSerializableAsJson`
1398
+ */
1399
+ function checkSerializableObject(context, objectValue) {
1400
+ checkUnsupportedObjectType(context, objectValue);
1401
+ checkSerializableObjectEntries(context, objectValue);
1402
+ assertJsonStringificationSucceeds(context, objectValue);
1403
+ }
1404
+ /**
1405
+ * Rejects built-in objects that must be converted before JSON serialization.
1406
+ *
1407
+ * @private function of `checkSerializableAsJson`
1408
+ */
1409
+ function checkUnsupportedObjectType(context, objectValue) {
1410
+ if (objectValue instanceof Date) {
1411
+ throw new UnexpectedError(spaceTrim$1((block) => `
1412
+ \`${context.name}\` is Date
1323
1413
 
1324
- Use \`string_date_iso8601\` instead
1414
+ Use \`string_date_iso8601\` instead
1325
1415
 
1326
- Additional message for \`${name}\`:
1327
- ${block(message || '(nothing)')}
1328
- `));
1329
- }
1330
- else if (value instanceof Map) {
1331
- throw new UnexpectedError(`${name} is Map`);
1332
- }
1333
- else if (value instanceof Set) {
1334
- throw new UnexpectedError(`${name} is Set`);
1335
- }
1336
- else if (value instanceof RegExp) {
1337
- throw new UnexpectedError(`${name} is RegExp`);
1338
- }
1339
- else if (value instanceof Error) {
1340
- throw new UnexpectedError(spaceTrim$1((block) => `
1341
- \`${name}\` is unserialized Error
1416
+ Additional message for \`${context.name}\`:
1417
+ ${block(context.message || '(nothing)')}
1418
+ `));
1419
+ }
1420
+ if (objectValue instanceof Map) {
1421
+ throw new UnexpectedError(`${context.name} is Map`);
1422
+ }
1423
+ if (objectValue instanceof Set) {
1424
+ throw new UnexpectedError(`${context.name} is Set`);
1425
+ }
1426
+ if (objectValue instanceof RegExp) {
1427
+ throw new UnexpectedError(`${context.name} is RegExp`);
1428
+ }
1429
+ if (objectValue instanceof Error) {
1430
+ throw new UnexpectedError(spaceTrim$1((block) => `
1431
+ \`${context.name}\` is unserialized Error
1342
1432
 
1343
- Use function \`serializeError\`
1433
+ Use function \`serializeError\`
1344
1434
 
1345
- Additional message for \`${name}\`:
1346
- ${block(message || '(nothing)')}
1435
+ Additional message for \`${context.name}\`:
1436
+ ${block(context.message || '(nothing)')}
1347
1437
 
1348
- `));
1438
+ `));
1439
+ }
1440
+ }
1441
+ /**
1442
+ * Recursively checks object properties while preserving omitted `undefined` keys.
1443
+ *
1444
+ * @private function of `checkSerializableAsJson`
1445
+ */
1446
+ function checkSerializableObjectEntries(context, objectValue) {
1447
+ for (const [subName, subValue] of Object.entries(objectValue)) {
1448
+ if (subValue === undefined) {
1449
+ // Note: undefined in object is serializable - it is just omitted
1450
+ continue;
1349
1451
  }
1350
- else {
1351
- for (const [subName, subValue] of Object.entries(value)) {
1352
- if (subValue === undefined) {
1353
- // Note: undefined in object is serializable - it is just omitted
1354
- continue;
1355
- }
1356
- checkSerializableAsJson({ name: `${name}.${subName}`, value: subValue, message });
1357
- }
1358
- try {
1359
- JSON.stringify(value); // <- TODO: [0]
1360
- }
1361
- catch (error) {
1362
- assertsError(error);
1363
- throw new UnexpectedError(spaceTrim$1((block) => `
1364
- \`${name}\` is not serializable
1452
+ checkSerializableAsJson({
1453
+ ...context,
1454
+ name: `${context.name}.${subName}`,
1455
+ value: subValue,
1456
+ });
1457
+ }
1458
+ }
1459
+ /**
1460
+ * Uses `JSON.stringify` as the final guard for cases like circular references.
1461
+ *
1462
+ * @private function of `checkSerializableAsJson`
1463
+ */
1464
+ function assertJsonStringificationSucceeds(context, objectValue) {
1465
+ try {
1466
+ JSON.stringify(objectValue); // <- TODO: [0]
1467
+ }
1468
+ catch (error) {
1469
+ assertsError(error);
1470
+ throw new UnexpectedError(spaceTrim$1((block) => `
1471
+ \`${context.name}\` is not serializable
1365
1472
 
1366
- ${block(error.stack || error.message)}
1473
+ ${block(error.stack || error.message)}
1367
1474
 
1368
- Additional message for \`${name}\`:
1369
- ${block(message || '(nothing)')}
1370
- `));
1475
+ Additional message for \`${context.name}\`:
1476
+ ${block(context.message || '(nothing)')}
1477
+ `));
1478
+ }
1479
+ /*
1480
+ TODO: [0] Is there some more elegant way to check circular references?
1481
+ const seen = new Set();
1482
+ const stack = [{ value }];
1483
+ while (stack.length > 0) {
1484
+ const { value } = stack.pop()!;
1485
+ if (typeof value === 'object' && value !== null) {
1486
+ if (seen.has(value)) {
1487
+ throw new UnexpectedError(`${name} has circular reference`);
1371
1488
  }
1372
- /*
1373
- TODO: [0] Is there some more elegant way to check circular references?
1374
- const seen = new Set();
1375
- const stack = [{ value }];
1376
- while (stack.length > 0) {
1377
- const { value } = stack.pop()!;
1378
- if (typeof value === 'object' && value !== null) {
1379
- if (seen.has(value)) {
1380
- throw new UnexpectedError(`${name} has circular reference`);
1381
- }
1382
- seen.add(value);
1383
- if (Array.isArray(value)) {
1384
- stack.push(...value.map((value) => ({ value })));
1385
- } else {
1386
- stack.push(...Object.values(value).map((value) => ({ value })));
1387
- }
1388
- }
1489
+ seen.add(value);
1490
+ if (Array.isArray(value)) {
1491
+ stack.push(...value.map((value) => ({ value })));
1492
+ } else {
1493
+ stack.push(...Object.values(value).map((value) => ({ value })));
1389
1494
  }
1390
- */
1391
- return;
1392
1495
  }
1393
1496
  }
1394
- else {
1395
- throw new UnexpectedError(spaceTrim$1((block) => `
1396
- \`${name}\` is unknown type
1497
+ */
1498
+ }
1499
+ /**
1500
+ * Throws the fallback error for unsupported value types like `bigint` and `NaN`.
1501
+ *
1502
+ * @private function of `checkSerializableAsJson`
1503
+ */
1504
+ function throwUnknownTypeError(context) {
1505
+ throw new UnexpectedError(spaceTrim$1((block) => `
1506
+ \`${context.name}\` is unknown type
1397
1507
 
1398
- Additional message for \`${name}\`:
1399
- ${block(message || '(nothing)')}
1400
- `));
1401
- }
1508
+ Additional message for \`${context.name}\`:
1509
+ ${block(context.message || '(nothing)')}
1510
+ `));
1402
1511
  }
1403
- // TODO: Can be return type more type-safe? like `asserts options.value is JsonValue`
1404
- // TODO: [🧠][main] !!3 In-memory cache of same values to prevent multiple checks
1405
- // Note: [🐠] This is how `checkSerializableAsJson` + `isSerializableAsJson` together can just retun true/false or rich error message
1406
1512
 
1407
1513
  /**
1408
1514
  * Creates a deep clone of the given object