resurrect-esm 2.0.3 → 2.0.5

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.
@@ -8,14 +8,13 @@ jobs:
8
8
  runs-on: ubuntu-latest
9
9
  steps:
10
10
  - uses: actions/checkout@v4
11
- - uses: actions/setup-node@v4
12
- with:
13
- node-version: "24"
14
- registry-url: https://registry.npmjs.org
15
11
  - uses: pnpm/action-setup@v4
16
12
  with:
17
13
  version: 10
18
- cache: on
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ node-version: 24
17
+ cache: pnpm
19
18
  - run: pnpm install
20
19
  - run: pnpm test
21
20
  - run: pnpm prepare
package/README.md CHANGED
@@ -9,13 +9,14 @@ An ES6 module port of ResurrectTS.
9
9
  > * ResurrectError is now a top-level export, instead of living inside a Resurrect object.
10
10
  > * NamespaceResolver is now a top-level export, instead of living inside the Resurrect static constructor namespace.
11
11
  > * NamespaceResolver now has a new method, `getConstructor(name: string)`, which should return the constructor function to generate the object (it will not be called directly, so the parameters are irrelevant).
12
- > The bug found in skeeto/resurrect-js#11 has been fixed.
12
+ > * The bug found in skeeto/resurrect-js#11 has been fixed.
13
13
 
14
14
  ResurrectJS preserves object behavior (prototypes) and reference
15
15
  circularity with a special JSON encoding. Unlike flat JSON, it can
16
16
  also properly resurrect these types of values:
17
17
 
18
18
  * Date
19
+ * URL
19
20
  * RegExp
20
21
  * `undefined`
21
22
  * NaN, Infinity, -Infinity
@@ -80,8 +81,8 @@ properties:
80
81
 
81
82
  * *resolver* (`NamespaceResolver`): Converts between a name
82
83
  and a prototype. Create a custom resolver if your constructors
83
- are not stored in global variables. The resolver has two methods:
84
- getName(object) and getPrototype(string).
84
+ are not stored in global variables. The resolver has three methods:
85
+ getName(object), getConstructor(string), and getPrototype(string).
85
86
 
86
87
  > [!CAUTION]
87
88
  > If you're using ES6 modules for your custom classes, you MUST use a custom resolver since module scope is not global scope!
@@ -121,7 +122,7 @@ unwrapped. This means extra properties added to these objects will not
121
122
  be preserved.
122
123
 
123
124
  Functions cannot ever be serialized. Resurrect will throw an error if
124
- a function is found when traversing a data structure.
125
+ a function is found when traversing a data structure, rather than just silently dropping the property or replacing it with `null` like JSON.stringify does.
125
126
 
126
127
  ### Custom Resolvers
127
128
 
@@ -132,9 +133,13 @@ the Foo constructor in this example,
132
133
  ```ts
133
134
  import { Resurrect, NamespaceResolver } from "resurrect-esm";
134
135
  const namespace = {
135
- Foo: class {
136
+ Foo: class Foo {
136
137
  constructor() {
137
138
  this.bar = true;
139
+ Foo.bax(this);
140
+ }
141
+ static bax(obj) {
142
+
138
143
  }
139
144
  }
140
145
  };
@@ -149,6 +154,9 @@ find the name of the constructor in the namespace when given the
149
154
  constructor. Keep in mind that using this form will bind the variable
150
155
  Foo to the surrounding function within the body of Foo.
151
156
 
157
+ If you're using a bundler, you **must** enable the "keep names" option
158
+ for at least the classes that will be stringified, so that Resurrect.js can get the correct `.name` from the constructor functions. (For esbuild, the option is `--keep-names`.)
159
+
152
160
  ## See Also
153
161
 
154
162
  * [HydrateJS](https://github.com/nanodeath/HydrateJS)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resurrect-esm",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "type": "module",
5
5
  "description": "ResurrectJS preserves object behavior (prototypes) and reference circularity with a special JSON encoding. Unlike flat JSON, it can also properly resurrect self-referential objects, objects with shared structure, Dates, RegExps, HTMLElements, NaN, Infinity, undefined (which won't get turned into null), and any other custom object type given the proper resolver.",
6
6
  "repository": {
@@ -38,5 +38,8 @@
38
38
  "test": "pnpm bun test",
39
39
  "test:watch": "pnpm test --watch",
40
40
  "prepare": "pnpm build --minify --mangle-props=^_"
41
+ },
42
+ "dependencies": {
43
+ "lib0": "^0.2.117"
41
44
  }
42
45
  }
package/resurrect.test.ts CHANGED
@@ -66,7 +66,26 @@ function suite(opt?: ResurrectOptions) {
66
66
  expect(() => roundtrip(obj)).toThrow(new ResurrectError("Can't serialize functions."));
67
67
  });
68
68
 
69
+ test("can serialize URL objects ok", () => {
70
+ const obj = { a_url_object: new URL("about:test") };
71
+ expect(roundtrip(obj).a_url_object).toBeInstanceOf(URL);
72
+ });
73
+
69
74
  if (defOpts.revive) {
75
+ test("can revive classes with cleanup true and false", () => {
76
+ const ns = {
77
+ Foo: class Foo {
78
+ bar: number;
79
+ constructor() {
80
+ this.bar = 1;
81
+ Foo.bax();
82
+ }
83
+ static bax() { }
84
+ }
85
+ };
86
+ expect(roundtrip(new ns.Foo(), { cleanup: false, resolver: new NamespaceResolver(ns) })).toBeInstanceOf(ns.Foo);
87
+ expect(roundtrip(new ns.Foo(), { cleanup: true, resolver: new NamespaceResolver(ns) })).toBeInstanceOf(ns.Foo);
88
+ });
70
89
  test("revive and custom resolver", () => {
71
90
  class Dog {
72
91
  constructor(public loudness: number, public sound: string) { }
@@ -79,6 +98,16 @@ function suite(opt?: ResurrectOptions) {
79
98
  expect(roundtripped).toBeInstanceOf(Dog);
80
99
  expect(roundtripped.woof()).toEqual("wowwowwow!");
81
100
  });
101
+ test("revive/serialize works with minifier-renamed classes", () => {
102
+ class z {
103
+ constructor(public foo: number) { };
104
+ }
105
+ const obj = new z(1);
106
+ const roundtripped = roundtrip(obj, {
107
+ resolver: new NamespaceResolver({ Foo: z }),
108
+ });
109
+ expect(roundtripped).toBeInstanceOf(z);
110
+ })
82
111
  test("can't serialize anonymous classes", () => {
83
112
  const obj = new class {
84
113
  foo: number;
package/resurrect.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * ResurrectJS preserves object behavior (prototypes) and reference
7
7
  * circularity with a special JSON encoding. Unlike regular JSON,
8
- * Date, RegExp, DOM objects, and `undefined` are also properly
8
+ * Date, URL, RegExp, DOM objects, and `undefined` are also properly
9
9
  * preserved.
10
10
  *
11
11
  * ## Examples
@@ -41,27 +41,21 @@
41
41
  * string. This option must be consistent between both
42
42
  * serialization and deserialization.
43
43
  *
44
- * cleanup (false): Perform full property cleanup after both
45
- * serialization and deserialization using the `delete`
46
- * operator. This may cause performance penalties (breaking hidden
47
- * classes in V8) on objects that ResurrectJS touches, so enable
48
- * with care.
49
- *
50
44
  * revive (true): Restore behavior (__proto__) to objects that have
51
45
  * been resurrected. If this is set to false during serialization,
52
46
  * resurrection information will not be encoded. You still get
53
- * circularity and Date support.
47
+ * circularity and Date/URL support.
54
48
  *
55
49
  * resolver (Resurrect.NamespaceResolver(window)): Converts between
56
50
  * a name and a prototype. Create a custom resolver if your
57
51
  * constructors are not stored in global variables. The resolver
58
- * has two methods: getName(object) and getPrototype(string).
52
+ * has three methods: getName(object), getConstructor(string), and
53
+ * getPrototype(string).
59
54
  *
60
55
  * For example,
61
56
  *
62
57
  * const necromancer = new Resurrect({
63
- * prefix: '__#',
64
- * cleanup: true
58
+ * prefix: "__#",
65
59
  * });
66
60
  *
67
61
  * ## Caveats
@@ -81,6 +75,11 @@
81
75
  * @see http://nullprogram.com/blog/2013/03/28/
82
76
  */
83
77
 
78
+ import { parse, stringify } from "lib0/json";
79
+ import { keys } from "lib0/object";
80
+
81
+ const getPrototypeOf = Object.getPrototypeOf;
82
+
84
83
  export class Resurrect {
85
84
  private _table: any[] | null;
86
85
  private _cleanups: (() => void)[] = [];
@@ -141,9 +140,10 @@ export class Resurrect {
141
140
  private static _isNumber = Resurrect._is("Number") as (obj: any) => obj is number;
142
141
  private static _isFunction = Resurrect._is("Function") as (obj: any) => obj is Function;
143
142
  private static _isDate = Resurrect._is("Date") as (obj: any) => obj is Date;
143
+ private static _isURL = Resurrect._is("URL") as (obj: any) => obj is URL;
144
144
  private static _isRegExp = Resurrect._is("RegExp") as (obj: any) => obj is RegExp;
145
145
  private static _isObject = Resurrect._is("Object") as (obj: any) => obj is object;
146
- private static isAtom(object: any) {
146
+ private static _isAtom(object: any) {
147
147
  return !Resurrect._isObject(object) && !Resurrect._isArray(object);
148
148
  }
149
149
  private static _isPrimitive(object: any) {
@@ -153,6 +153,11 @@ export class Resurrect {
153
153
  Resurrect._isBoolean(object);
154
154
  }
155
155
 
156
+ private _cleanup() {
157
+ this._cleanups.forEach(e => e());
158
+ this._cleanups = [];
159
+ }
160
+
156
161
  /**
157
162
  * Create a reference (encoding) to an object.
158
163
  */
@@ -176,17 +181,17 @@ export class Resurrect {
176
181
  if (this.revive) {
177
182
  const constructor = this.resolver.getName(object);
178
183
  if (constructor) {
179
- const proto = Object.getPrototypeOf(object);
184
+ const proto = getPrototypeOf(object);
180
185
  if (this.resolver.getPrototype(constructor) !== proto) {
181
186
  throw new ResurrectError("Constructor mismatch!");
182
187
  } else {
183
188
  object[this._protocode] = constructor;
184
- this._cleanups.push(() => { delete object[this._protocode as any]; })
189
+ this._cleanups.push(() => delete object[this._protocode]);
185
190
  }
186
191
  }
187
192
  }
188
193
  object[this._refcode] = this._table!.length;
189
- this._cleanups.push(() => { delete object[this._refcode as any]; })
194
+ this._cleanups.push(() => delete object[this._refcode]);
190
195
  this._table!.push(object);
191
196
  return object[this._refcode];
192
197
  }
@@ -245,21 +250,21 @@ export class Resurrect {
245
250
  * @returns A fresh copy of root to be serialized.
246
251
  */
247
252
  private _visit(root: any, transform: (obj: any) => any, replacer?: (k: string, v: any) => any): any {
248
- if (Resurrect.isAtom(root)) {
253
+ if (Resurrect._isAtom(root)) {
249
254
  return transform(root);
250
255
  } else if (!this._isTagged(root)) {
251
256
  let copy: any = null;
252
257
  if (Resurrect._isArray(root)) {
253
258
  copy = [];
254
259
  root[this._refcode as any] = this._tag(copy);
255
- this._cleanups.push(() => { delete root[this._refcode as any]; })
260
+ this._cleanups.push(() => delete root[this._refcode as any]);
256
261
  for (let i = 0; i < root.length; i++) {
257
262
  copy.push(this._visit(root[i], transform, replacer));
258
263
  }
259
264
  } else { /* Object */
260
- copy = Object.create(Object.getPrototypeOf(root));
265
+ copy = Object.create(getPrototypeOf(root));
261
266
  root[this._refcode as any] = this._tag(copy);
262
- this._cleanups.push(() => { delete root[this._refcode as any]; })
267
+ this._cleanups.push(() => delete root[this._refcode]);
263
268
  for (const key of Object.getOwnPropertyNames(root)) {
264
269
  let value = root[key];
265
270
  if (replacer && value !== undefined) {
@@ -288,6 +293,8 @@ export class Resurrect {
288
293
  return this._builder("Resurrect.Node", [new XMLSerializer().serializeToString(atom)]);
289
294
  } else if (Resurrect._isDate(atom)) {
290
295
  return this._builder("Date", [atom.toISOString()]);
296
+ } else if (Resurrect._isURL(atom)) {
297
+ return this._builder("URL", [atom.href]);
291
298
  } else if (Resurrect._isRegExp(atom)) {
292
299
  return this._builder("RegExp", ("" + atom).match(/\/(.+)\/([a-z]*)/)!.slice(1));
293
300
  } else if (atom === undefined) {
@@ -317,37 +324,39 @@ export class Resurrect {
317
324
  /**
318
325
  * Serialize an arbitrary JavaScript object, carefully preserving it.
319
326
  */
320
- stringify(object: any, replacer?: any[] | ((k: string, v: any) => any), space?: string) {
327
+ stringify(object: any, replacer?: any[] | ((k: string, v: any) => any), space?: string | number) {
321
328
  if (Resurrect._isFunction(replacer)) {
322
329
  replacer = this._replacerWrapper(replacer);
323
330
  } else if (Resurrect._isArray(replacer)) {
324
331
  const acceptKeys = replacer;
325
332
  replacer = (k, v) => acceptKeys.includes(k) ? v : undefined;
326
333
  }
327
- if (Resurrect.isAtom(object)) {
328
- return JSON.stringify(this._handleAtom(object), replacer, space);
334
+ if (Resurrect._isAtom(object)) {
335
+ return stringify(this._handleAtom(object), replacer, space);
329
336
  } else {
330
337
  this._cleanups = [];
331
338
  const table = this._table = [] as any[];
332
339
  try {
333
340
  this._visit(object, this._handleAtom.bind(this), replacer);
334
341
  } catch (e) {
335
- this._cleanups.forEach(e => e());
342
+ this._cleanup();
336
343
  throw e;
337
344
  } finally {
338
345
  for (let i = 0; i < table.length; i++) {
339
346
  if (this.cleanup) {
340
- delete table[i][this._origcode][this._refcode];
347
+ delete table[i]?.[this._origcode]?.[this._refcode];
341
348
  } else {
342
- const obj = table[i][this._origcode];
349
+ const obj = table[i]?.[this._origcode];
343
350
  if (obj) obj[this._refcode] = null;
344
351
  }
345
- delete table[i][this._refcode];
346
- delete table[i][this._origcode];
352
+ delete table[i]?.[this._refcode];
353
+ delete table[i]?.[this._origcode];
347
354
  }
348
355
  this._table = null;
349
356
  }
350
- return JSON.stringify(table, null, space);
357
+ const s = stringify(table, null, space);
358
+ if (this.cleanup) this._cleanup();
359
+ return s;
351
360
  }
352
361
  }
353
362
 
@@ -383,7 +392,7 @@ export class Resurrect {
383
392
  */
384
393
  resurrect(string: string): any {
385
394
  let result = null;
386
- const data = JSON.parse(string);
395
+ const data = parse(string);
387
396
  try {
388
397
  if (Resurrect._isArray(data)) {
389
398
  this._table = data;
@@ -397,7 +406,7 @@ export class Resurrect {
397
406
  for (let i = 0; i < this._table.length; i++) {
398
407
  const object = this._table[i];
399
408
  for (const key of Object.getOwnPropertyNames(object)) {
400
- if (!(Resurrect.isAtom(object[key]))) {
409
+ if (!(Resurrect._isAtom(object[key]))) {
401
410
  object[key] = this._decode(object[key]);
402
411
  }
403
412
  }
@@ -424,6 +433,7 @@ export interface ResurrectOptions {
424
433
  * important that you don't use any properties beginning with this
425
434
  * string. This option must be consistent between both
426
435
  * serialization and deserialization.
436
+ * @default "#"
427
437
  */
428
438
  prefix?: string;
429
439
  /**
@@ -432,6 +442,7 @@ export interface ResurrectOptions {
432
442
  * operator. This may cause performance penalties (breaking hidden
433
443
  * classes in V8) on objects that ResurrectJS touches, so enable
434
444
  * with care.
445
+ * @default false
435
446
  */
436
447
  cleanup?: boolean;
437
448
  /**
@@ -439,6 +450,7 @@ export interface ResurrectOptions {
439
450
  * been resurrected. If this is set to false during serialization,
440
451
  * resurrection information will not be encoded. You still get
441
452
  * circularity and Date support.
453
+ * @default true
442
454
  */
443
455
  revive?: boolean;
444
456
  /**
@@ -447,6 +459,7 @@ export interface ResurrectOptions {
447
459
  *
448
460
  * If you're using ES6 modules for your custom classes, you WILL need
449
461
  * to use this!
462
+ * @default undefined
450
463
  */
451
464
  resolver?: NamespaceResolver;
452
465
  }
@@ -476,9 +489,10 @@ export class NamespaceResolver {
476
489
  * @returns null if the constructor is `Object` or `Array`.
477
490
  */
478
491
  getName(object: object): string | null {
479
- let constructor = object.constructor.name;
492
+ const constructorFun = object.constructor;
493
+ let constructor = keys(this.scope).find(realName => this.scope[realName] === constructorFun) ?? constructorFun.name;
480
494
  if (constructor == null) { // IE
481
- constructor = /^\s*function\s*([A-Za-z0-9_$]*)/.exec("" + object.constructor)?.[1] ?? "";
495
+ constructor = /^\s*function\s*([A-Za-z0-9_$]*)/.exec("" + constructorFun)?.[1] ?? "";
482
496
  }
483
497
  if (constructor === "") {
484
498
  throw new ResurrectError("Can't serialize objects with anonymous constructors.");