@zudojs/serialization 1.2.0 → 1.2.1

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/README.md CHANGED
@@ -98,8 +98,11 @@ picked up.
98
98
  or `createSerializer`) to use only your registry. A value that no transformer
99
99
  handles and that JSON would write as `{}` (a `Map`, `Set`, `WeakMap`, `Error`,
100
100
  `RegExp`, `ArrayBuffer`, ...) then throws `SerializeError` under
101
- `preserveTypes` instead of being silently emptied. To compose a registry by
102
- hand, start from `createBuiltinTransformers()`.
101
+ `preserveTypes` instead of being silently emptied. A type no built-in covers
102
+ (`RegExp`, `WeakMap`, `ArrayBuffer`, `DataView`, ...) throws the same way with
103
+ the built-ins on. The message names the fix: register a transformer, or, when
104
+ a built-in handles the type and you turned them off, keep them enabled. To
105
+ compose a registry by hand, start from `createBuiltinTransformers()`.
103
106
 
104
107
  ## Untrusted input
105
108
 
@@ -150,8 +153,19 @@ serializer.serialize({ error }, { preserveTypes: true }); // name, message, code
150
153
  serializer.serialize({ error }, { preserveTypes: true, includeStack: true });
151
154
  ```
152
155
 
153
- On the way back, a wire-supplied stack is attached as a non-enumerable
154
- `originalStack` rather than overwriting the reconstructed error's own.
156
+ On the way back:
157
+
158
+ - A built-in subclass (`TypeError`, `RangeError`, `SyntaxError`,
159
+ `ReferenceError`, `EvalError`, `URIError`, `AggregateError`) is rebuilt with
160
+ its own constructor, so `instanceof TypeError` still holds. Any other name
161
+ is rebuilt as a plain `Error` with `name` set. An `AggregateError` comes back
162
+ with an empty `errors` array, because the inner errors are not serialized.
163
+ - `.stack` is only the header line (`"TypeError: bad input"`) with no frames.
164
+ The reader's own frames would point at the deserializer, not at where the
165
+ error was thrown.
166
+ - A wire-supplied stack (sent with `includeStack`) is attached as a
167
+ non-enumerable `originalStack`. It never overwrites `.stack`, because it is
168
+ text the sender controls.
155
169
 
156
170
  ## Envelopes
157
171
 
@@ -18,6 +18,9 @@ export declare function toTaggedOutput(transformer: TypeTransformer, raw: unknow
18
18
  * contents. Only reachable when the built-in transformers are disabled or
19
19
  * a type has no transformer.
20
20
  *
21
+ * Suggests re-enabling the built-ins only when one of them handles the
22
+ * type; otherwise the fix is registering a transformer.
23
+ *
21
24
  * @throws {SerializeError} naming the type and how to fix it.
22
25
  */
23
26
  export declare function assertNotLossy(value: object): void;
@@ -4,6 +4,7 @@
4
4
  import { SerializationTags } from "@zudojs/constants";
5
5
  import { SerializeError } from "@zudojs/errors";
6
6
  import { isPlainObject } from "@zudojs/types";
7
+ import { createBuiltinTransformers, } from "./transformerLookup.core.js";
7
8
  /**
8
9
  * Normalises what a transformer's `serialize` returned into the tagged
9
10
  * `{ $type, $value }` form that is written to the wire.
@@ -37,21 +38,40 @@ const LOSSY_TYPES = [
37
38
  ["ArrayBuffer", (value) => value instanceof ArrayBuffer],
38
39
  ["DataView", (value) => value instanceof DataView],
39
40
  ];
41
+ let builtins;
42
+ /**
43
+ * Whether a built-in transformer handles `value`. When one does and the
44
+ * value still reached {@link assertNotLossy}, the built-ins were disabled.
45
+ */
46
+ function hasBuiltinTransformer(value) {
47
+ builtins ??= createBuiltinTransformers();
48
+ return builtins.findForValue(value) !== undefined;
49
+ }
50
+ /** `"a Map"`, `"an Error"`. */
51
+ function withArticle(name) {
52
+ return `${/^[AEIOU]/.test(name) ? "an" : "a"} ${name}`;
53
+ }
40
54
  /**
41
55
  * Throws when a value that no transformer handles would be written as `{}`
42
56
  * (a `Map`, `Set`, `Error`, `RegExp`, ...), instead of silently losing its
43
57
  * contents. Only reachable when the built-in transformers are disabled or
44
58
  * a type has no transformer.
45
59
  *
60
+ * Suggests re-enabling the built-ins only when one of them handles the
61
+ * type; otherwise the fix is registering a transformer.
62
+ *
46
63
  * @throws {SerializeError} naming the type and how to fix it.
47
64
  */
48
65
  export function assertNotLossy(value) {
49
66
  for (const [name, matches] of LOSSY_TYPES) {
50
67
  if (matches(value)) {
51
- throw new SerializeError(`Cannot serialize a ${name} with preserveTypes: no transformer is ` +
52
- `registered for it, and JSON would write it as {} and lose its ` +
53
- `contents. Register a transformer for ${name}, or keep the ` +
54
- `built-in transformers enabled (do not pass builtins: false).`, { format: "json" });
68
+ const fix = hasBuiltinTransformer(value)
69
+ ? `Register a transformer for ${name}, or keep the built-in ` +
70
+ `transformers enabled (do not pass builtins: false).`
71
+ : `Register a transformer for ${name}.`;
72
+ throw new SerializeError(`Cannot serialize ${withArticle(name)} with preserveTypes: no ` +
73
+ `transformer is registered for it, and JSON would write it as {} ` +
74
+ `and lose its contents. ${fix}`, { format: "json" });
55
75
  }
56
76
  }
57
77
  }
@@ -9,6 +9,33 @@
9
9
  */
10
10
  import { SerializationTags } from "@zudojs/constants";
11
11
  const ERROR_TYPE = "Error";
12
+ /**
13
+ * Built-in Error subclasses rebuilt with their own constructor, so a
14
+ * deserialized `TypeError` is still `instanceof TypeError`. Any other name
15
+ * falls back to `Error` with `name` set.
16
+ */
17
+ const BUILT_IN_ERRORS = Object.freeze({
18
+ TypeError: (message) => new TypeError(message),
19
+ RangeError: (message) => new RangeError(message),
20
+ SyntaxError: (message) => new SyntaxError(message),
21
+ ReferenceError: (message) => new ReferenceError(message),
22
+ EvalError: (message) => new EvalError(message),
23
+ URIError: (message) => new URIError(message),
24
+ AggregateError: (message) => new AggregateError([], message),
25
+ });
26
+ /** Creates the error for a serialized name, preferring a built-in subclass. */
27
+ function createError(name, message) {
28
+ const build = typeof name === "string" && Object.hasOwn(BUILT_IN_ERRORS, name)
29
+ ? BUILT_IN_ERRORS[name]
30
+ : undefined;
31
+ if (build !== undefined)
32
+ return build(message);
33
+ const error = new Error(message);
34
+ if (typeof name === "string" && name !== "Error") {
35
+ error.name = name;
36
+ }
37
+ return error;
38
+ }
12
39
  /** Transformer that handles Error round-trips. */
13
40
  export const ErrorTransformer = {
14
41
  type: ERROR_TYPE,
@@ -33,11 +60,10 @@ export const ErrorTransformer = {
33
60
  deserialize(value) {
34
61
  const data = value;
35
62
  const message = typeof data.message === "string" ? data.message : "";
36
- const error = new Error(message);
37
- const name = data.name;
38
- if (typeof name === "string" && name !== "Error") {
39
- error.name = name;
40
- }
63
+ const error = createError(data.name, message);
64
+ // The reader's own frames point at this deserializer, not at where the
65
+ // error was thrown, so the rebuilt stack is the header line only.
66
+ error.stack = message === "" ? error.name : `${error.name}: ${message}`;
41
67
  // A stack from the wire is attacker-controlled text. Restoring it over the
42
68
  // real one would make the reconstructed error lie about where it came
43
69
  // from, so it is carried as a separate, clearly-named field instead.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/serialization",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Data translation layer with JSON serializer, type transformers, envelopes, and registry for Zudojs applications.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",