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.
- package/.github/workflows/publish.yml +4 -5
- package/README.md +13 -5
- package/package.json +4 -1
- package/resurrect.test.ts +29 -0
- package/resurrect.ts +46 -32
|
@@ -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
|
-
|
|
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
|
|
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
|
+
"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
|
|
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
|
|
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 =
|
|
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(() =>
|
|
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(() =>
|
|
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.
|
|
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(() =>
|
|
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(
|
|
265
|
+
copy = Object.create(getPrototypeOf(root));
|
|
261
266
|
root[this._refcode as any] = this._tag(copy);
|
|
262
|
-
this._cleanups.push(() =>
|
|
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.
|
|
328
|
-
return
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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("" +
|
|
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.");
|