resurrect-esm 2.0.2 → 2.0.4

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.
@@ -1,8 +1,5 @@
1
1
  name: Publish Package
2
- on:
3
- push:
4
- tags:
5
- - 'v*'
2
+ on: workflow_dispatch
6
3
  permissions:
7
4
  id-token: write # Required for OIDC
8
5
  contents: read
@@ -11,15 +8,14 @@ jobs:
11
8
  runs-on: ubuntu-latest
12
9
  steps:
13
10
  - uses: actions/checkout@v4
14
- - uses: actions/setup-node@v4
15
- with:
16
- node-version: "24"
17
- registry-url: https://registry.npmjs.org
18
11
  - uses: pnpm/action-setup@v4
19
12
  with:
20
13
  version: 10
21
- cache: on
22
- install: yes
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ node-version: 24
17
+ cache: pnpm
18
+ - run: pnpm install
23
19
  - run: pnpm test
24
20
  - run: pnpm prepare
25
21
  - run: npm publish
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cSpell.words": [
3
3
  "kybernetikos",
4
- "registrator"
4
+ "registrator",
5
+ "skeeto"
5
6
  ]
6
7
  }
package/README.md CHANGED
@@ -9,12 +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
13
 
13
14
  ResurrectJS preserves object behavior (prototypes) and reference
14
15
  circularity with a special JSON encoding. Unlike flat JSON, it can
15
16
  also properly resurrect these types of values:
16
17
 
17
18
  * Date
19
+ * URL
18
20
  * RegExp
19
21
  * `undefined`
20
22
  * NaN, Infinity, -Infinity
@@ -120,7 +122,7 @@ unwrapped. This means extra properties added to these objects will not
120
122
  be preserved.
121
123
 
122
124
  Functions cannot ever be serialized. Resurrect will throw an error if
123
- 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.
124
126
 
125
127
  ### Custom Resolvers
126
128
 
@@ -131,9 +133,13 @@ the Foo constructor in this example,
131
133
  ```ts
132
134
  import { Resurrect, NamespaceResolver } from "resurrect-esm";
133
135
  const namespace = {
134
- Foo: class {
136
+ Foo: class Foo {
135
137
  constructor() {
136
138
  this.bar = true;
139
+ Foo.bax(this);
140
+ }
141
+ static bax(obj) {
142
+
137
143
  }
138
144
  }
139
145
  };
@@ -148,6 +154,9 @@ find the name of the constructor in the namespace when given the
148
154
  constructor. Keep in mind that using this form will bind the variable
149
155
  Foo to the surrounding function within the body of Foo.
150
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
+
151
160
  ## See Also
152
161
 
153
162
  * [HydrateJS](https://github.com/nanodeath/HydrateJS)
package/package.json CHANGED
@@ -1,42 +1,42 @@
1
1
  {
2
- "name": "resurrect-esm",
3
- "version": "2.0.2",
4
- "type": "module",
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
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/dragoncoder047/resurrect-esm.git"
9
- },
10
- "keywords": [],
11
- "author": "skeeto",
12
- "contributors": [
13
- {
14
- "name": "Andrew Bradley",
15
- "email": "cspotcode@gmail.com",
16
- "url": "https://github.com/cspotcode"
2
+ "name": "resurrect-esm",
3
+ "version": "2.0.4",
4
+ "type": "module",
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
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/dragoncoder047/resurrect-esm.git"
17
9
  },
18
- {
19
- "name": "dragoncoder047",
20
- "url": "https://github.com/dragoncoder047"
10
+ "keywords": [],
11
+ "author": "skeeto",
12
+ "contributors": [
13
+ {
14
+ "name": "Andrew Bradley",
15
+ "email": "cspotcode@gmail.com",
16
+ "url": "https://github.com/cspotcode"
17
+ },
18
+ {
19
+ "name": "dragoncoder047",
20
+ "url": "https://github.com/dragoncoder047"
21
+ }
22
+ ],
23
+ "license": "UNLICENSE",
24
+ "bugs": {
25
+ "url": "https://github.com/dragoncoder047/resurrect-esm/issues"
26
+ },
27
+ "homepage": "https://github.com/dragoncoder047/resurrect-esm#readme",
28
+ "devDependencies": {
29
+ "@happy-dom/global-registrator": "^20.6.1",
30
+ "@types/bun": "^1.3.9",
31
+ "bun": "^1.3.9",
32
+ "esbuild": "^0.25.0",
33
+ "typescript": "^5.6.2"
34
+ },
35
+ "main": "resurrect.ts",
36
+ "scripts": {
37
+ "build": "pnpm esbuild --sourcemap --platform=browser --target=esnext --format=esm resurrect.ts --outfile=resurrect.js",
38
+ "test": "pnpm bun test",
39
+ "test:watch": "pnpm test --watch",
40
+ "prepare": "pnpm build --minify --mangle-props=^_"
21
41
  }
22
- ],
23
- "license": "UNLICENSE",
24
- "bugs": {
25
- "url": "https://github.com/dragoncoder047/resurrect-esm/issues"
26
- },
27
- "homepage": "https://github.com/dragoncoder047/resurrect-esm#readme",
28
- "devDependencies": {
29
- "@happy-dom/global-registrator": "^20.6.1",
30
- "@types/bun": "^1.3.9",
31
- "bun": "^1.3.9",
32
- "esbuild": "^0.25.0",
33
- "typescript": "^5.6.2"
34
- },
35
- "main": "resurrect.ts",
36
- "scripts": {
37
- "build": "pnpm esbuild --sourcemap --platform=browser --target=esnext --format=esm resurrect.ts --outfile=resurrect.js",
38
- "test": "pnpm bun test",
39
- "test:watch": "pnpm test --watch",
40
- "prepare": "pnpm build --minify --mangle-props=^_"
41
- }
42
42
  }
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) { }
@@ -107,3 +126,11 @@ function suite(opt?: ResurrectOptions) {
107
126
  describe("default options", () => suite());
108
127
  describe("custom prefix", () => suite({ prefix: "qwerty" }));
109
128
  describe("no revive", () => suite({ revive: false }));
129
+
130
+ test("malformed state bug", () => {
131
+ // Test for the bug found in skeeto/resurrect-js#11
132
+ const r = new Resurrect();
133
+ const O = { test() { } };
134
+ expect(() => r.stringify(O)).toThrow(new ResurrectError("Can't serialize functions."));
135
+ expect(() => r.stringify(O)).toThrow(new ResurrectError("Can't serialize functions."));
136
+ });
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,16 +41,10 @@
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
@@ -60,8 +54,7 @@
60
54
  * For example,
61
55
  *
62
56
  * const necromancer = new Resurrect({
63
- * prefix: '__#',
64
- * cleanup: true
57
+ * prefix: "__#",
65
58
  * });
66
59
  *
67
60
  * ## Caveats
@@ -83,6 +76,7 @@
83
76
 
84
77
  export class Resurrect {
85
78
  private _table: any[] | null;
79
+ private _cleanups: (() => void)[] = [];
86
80
  prefix: string;
87
81
  cleanup: boolean;
88
82
  revive: boolean;
@@ -140,9 +134,10 @@ export class Resurrect {
140
134
  private static _isNumber = Resurrect._is("Number") as (obj: any) => obj is number;
141
135
  private static _isFunction = Resurrect._is("Function") as (obj: any) => obj is Function;
142
136
  private static _isDate = Resurrect._is("Date") as (obj: any) => obj is Date;
137
+ private static _isURL = Resurrect._is("URL") as (obj: any) => obj is URL;
143
138
  private static _isRegExp = Resurrect._is("RegExp") as (obj: any) => obj is RegExp;
144
139
  private static _isObject = Resurrect._is("Object") as (obj: any) => obj is object;
145
- private static isAtom(object: any) {
140
+ private static _isAtom(object: any) {
146
141
  return !Resurrect._isObject(object) && !Resurrect._isArray(object);
147
142
  }
148
143
  private static _isPrimitive(object: any) {
@@ -152,6 +147,11 @@ export class Resurrect {
152
147
  Resurrect._isBoolean(object);
153
148
  }
154
149
 
150
+ private _cleanup() {
151
+ this._cleanups.forEach(e => e());
152
+ this._cleanups = [];
153
+ }
154
+
155
155
  /**
156
156
  * Create a reference (encoding) to an object.
157
157
  */
@@ -180,10 +180,12 @@ export class Resurrect {
180
180
  throw new ResurrectError("Constructor mismatch!");
181
181
  } else {
182
182
  object[this._protocode] = constructor;
183
+ this._cleanups.push(() => delete object[this._protocode]);
183
184
  }
184
185
  }
185
186
  }
186
187
  object[this._refcode] = this._table!.length;
188
+ this._cleanups.push(() => delete object[this._refcode]);
187
189
  this._table!.push(object);
188
190
  return object[this._refcode];
189
191
  }
@@ -241,32 +243,30 @@ export class Resurrect {
241
243
  * Visit root and all its ancestors, visiting atoms with f.
242
244
  * @returns A fresh copy of root to be serialized.
243
245
  */
244
- private _visit(root: any, f: (obj: any) => any, replacer?: (k: string, v: any) => any): any {
245
- if (Resurrect.isAtom(root)) {
246
- return f(root);
246
+ private _visit(root: any, transform: (obj: any) => any, replacer?: (k: string, v: any) => any): any {
247
+ if (Resurrect._isAtom(root)) {
248
+ return transform(root);
247
249
  } else if (!this._isTagged(root)) {
248
250
  let copy: any = null;
249
251
  if (Resurrect._isArray(root)) {
250
252
  copy = [];
251
253
  root[this._refcode as any] = this._tag(copy);
254
+ this._cleanups.push(() => delete root[this._refcode as any]);
252
255
  for (let i = 0; i < root.length; i++) {
253
- copy.push(this._visit(root[i], f, replacer));
256
+ copy.push(this._visit(root[i], transform, replacer));
254
257
  }
255
258
  } else { /* Object */
256
259
  copy = Object.create(Object.getPrototypeOf(root));
257
260
  root[this._refcode as any] = this._tag(copy);
258
- for (const key in root) {
261
+ this._cleanups.push(() => delete root[this._refcode]);
262
+ for (const key of Object.getOwnPropertyNames(root)) {
259
263
  let value = root[key];
260
- if (root.hasOwnProperty(key)) {
261
- if (replacer && value !== undefined) {
262
- // Call replacer like JSON.stringify's replacer
263
- value = replacer.call(root, key, root[key]);
264
- if (value === undefined) {
265
- continue; // Omit from result
266
- }
267
- }
268
- copy[key] = this._visit(value, f, replacer);
264
+ if (replacer && value !== undefined) {
265
+ // Call replacer like JSON.stringify's replacer
266
+ value = replacer.call(root, key, root[key]);
267
+ if (value === undefined) continue; // Omit from result
269
268
  }
269
+ copy[key] = this._visit(value, transform, replacer);
270
270
  }
271
271
  }
272
272
  copy[this._origcode] = root;
@@ -287,6 +287,8 @@ export class Resurrect {
287
287
  return this._builder("Resurrect.Node", [new XMLSerializer().serializeToString(atom)]);
288
288
  } else if (Resurrect._isDate(atom)) {
289
289
  return this._builder("Date", [atom.toISOString()]);
290
+ } else if (Resurrect._isURL(atom)) {
291
+ return this._builder("URL", [atom.href]);
290
292
  } else if (Resurrect._isRegExp(atom)) {
291
293
  return this._builder("RegExp", ("" + atom).match(/\/(.+)\/([a-z]*)/)!.slice(1));
292
294
  } else if (atom === undefined) {
@@ -316,32 +318,39 @@ export class Resurrect {
316
318
  /**
317
319
  * Serialize an arbitrary JavaScript object, carefully preserving it.
318
320
  */
319
- stringify(object: any, replacer?: any[] | ((k: string, v: any) => any), space?: string) {
321
+ stringify(object: any, replacer?: any[] | ((k: string, v: any) => any), space?: string | number) {
320
322
  if (Resurrect._isFunction(replacer)) {
321
323
  replacer = this._replacerWrapper(replacer);
322
324
  } else if (Resurrect._isArray(replacer)) {
323
325
  const acceptKeys = replacer;
324
- replacer = function (k, v) {
325
- return acceptKeys.indexOf(k) >= 0 ? v : undefined;
326
- };
326
+ replacer = (k, v) => acceptKeys.includes(k) ? v : undefined;
327
327
  }
328
- if (Resurrect.isAtom(object)) {
328
+ if (Resurrect._isAtom(object)) {
329
329
  return JSON.stringify(this._handleAtom(object), replacer, space);
330
330
  } else {
331
- this._table = [];
332
- this._visit(object, this._handleAtom.bind(this), replacer);
333
- for (let i = 0; i < this._table.length; i++) {
334
- if (this.cleanup) {
335
- delete this._table[i][this._origcode][this._refcode];
336
- } else {
337
- this._table[i][this._origcode][this._refcode] = null;
331
+ this._cleanups = [];
332
+ const table = this._table = [] as any[];
333
+ try {
334
+ this._visit(object, this._handleAtom.bind(this), replacer);
335
+ } catch (e) {
336
+ this._cleanup();
337
+ throw e;
338
+ } finally {
339
+ for (let i = 0; i < table.length; i++) {
340
+ if (this.cleanup) {
341
+ delete table[i]?.[this._origcode]?.[this._refcode];
342
+ } else {
343
+ const obj = table[i]?.[this._origcode];
344
+ if (obj) obj[this._refcode] = null;
345
+ }
346
+ delete table[i]?.[this._refcode];
347
+ delete table[i]?.[this._origcode];
338
348
  }
339
- delete this._table[i][this._refcode];
340
- delete this._table[i][this._origcode];
349
+ this._table = null;
341
350
  }
342
- const table = this._table;
343
- this._table = null;
344
- return JSON.stringify(table, null, space);
351
+ const s = JSON.stringify(table, null, space);
352
+ if (this.cleanup) this._cleanup();
353
+ return s;
345
354
  }
346
355
  }
347
356
 
@@ -359,18 +368,17 @@ export class Resurrect {
359
368
  delete (object as any)[this._protocode];
360
369
  }
361
370
  return object;
362
- } else { // IE
363
- const copy = Object.create(prototype);
364
- for (const key in object) {
365
- if (object.hasOwnProperty(key) && key !== this.prefix) {
366
- copy[key] = object[key];
367
- }
371
+ }
372
+ // IE
373
+ const copy = Object.create(prototype);
374
+ for (const key of Object.getOwnPropertyNames(object)) {
375
+ if (key !== this._protocode) {
376
+ copy[key] = (object as any)[key];
368
377
  }
369
- return copy;
370
378
  }
371
- } else {
372
- return object;
379
+ return copy;
373
380
  }
381
+ return object;
374
382
  }
375
383
 
376
384
  /**
@@ -379,33 +387,34 @@ export class Resurrect {
379
387
  resurrect(string: string): any {
380
388
  let result = null;
381
389
  const data = JSON.parse(string);
382
- if (Resurrect._isArray(data)) {
383
- this._table = data;
384
- /* Restore __proto__. */
385
- if (this.revive) {
386
- for (let i = 0; i < this._table.length; i++) {
387
- this._table[i] = this._fixPrototype(this._table[i]);
390
+ try {
391
+ if (Resurrect._isArray(data)) {
392
+ this._table = data;
393
+ /* Restore __proto__. */
394
+ if (this.revive) {
395
+ for (let i = 0; i < this._table.length; i++) {
396
+ this._table[i] = this._fixPrototype(this._table[i]);
397
+ }
388
398
  }
389
- }
390
- /* Re-establish object references and construct atoms. */
391
- for (let i = 0; i < this._table.length; i++) {
392
- const object = this._table[i];
393
- for (const key in object) {
394
- if (object.hasOwnProperty(key)) {
395
- if (!(Resurrect.isAtom(object[key]))) {
399
+ /* Re-establish object references and construct atoms. */
400
+ for (let i = 0; i < this._table.length; i++) {
401
+ const object = this._table[i];
402
+ for (const key of Object.getOwnPropertyNames(object)) {
403
+ if (!(Resurrect._isAtom(object[key]))) {
396
404
  object[key] = this._decode(object[key]);
397
405
  }
398
406
  }
399
407
  }
408
+ result = this._table[0];
409
+ } else if (Resurrect._isObject(data)) {
410
+ this._table = [];
411
+ result = this._decode(data);
412
+ } else {
413
+ result = data;
400
414
  }
401
- result = this._table[0];
402
- } else if (Resurrect._isObject(data)) {
403
- this._table = [];
404
- result = this._decode(data);
405
- } else {
406
- result = data;
415
+ } finally {
416
+ this._table = null;
407
417
  }
408
- this._table = null;
409
418
  return result;
410
419
  }
411
420
  }
@@ -418,6 +427,7 @@ export interface ResurrectOptions {
418
427
  * important that you don't use any properties beginning with this
419
428
  * string. This option must be consistent between both
420
429
  * serialization and deserialization.
430
+ * @default "#"
421
431
  */
422
432
  prefix?: string;
423
433
  /**
@@ -426,6 +436,7 @@ export interface ResurrectOptions {
426
436
  * operator. This may cause performance penalties (breaking hidden
427
437
  * classes in V8) on objects that ResurrectJS touches, so enable
428
438
  * with care.
439
+ * @default false
429
440
  */
430
441
  cleanup?: boolean;
431
442
  /**
@@ -433,6 +444,7 @@ export interface ResurrectOptions {
433
444
  * been resurrected. If this is set to false during serialization,
434
445
  * resurrection information will not be encoded. You still get
435
446
  * circularity and Date support.
447
+ * @default true
436
448
  */
437
449
  revive?: boolean;
438
450
  /**
@@ -441,6 +453,7 @@ export interface ResurrectOptions {
441
453
  *
442
454
  * If you're using ES6 modules for your custom classes, you WILL need
443
455
  * to use this!
456
+ * @default undefined
444
457
  */
445
458
  resolver?: NamespaceResolver;
446
459
  }