resurrect-esm 2.0.2
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 +25 -0
- package/.vscode/settings.json +6 -0
- package/README.md +155 -0
- package/UNLICENSE +24 -0
- package/package.json +42 -0
- package/pnpm-workspace.yaml +3 -0
- package/resurrect.test.ts +109 -0
- package/resurrect.ts +493 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- 'v*'
|
|
6
|
+
permissions:
|
|
7
|
+
id-token: write # Required for OIDC
|
|
8
|
+
contents: read
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-node@v4
|
|
15
|
+
with:
|
|
16
|
+
node-version: "24"
|
|
17
|
+
registry-url: https://registry.npmjs.org
|
|
18
|
+
- uses: pnpm/action-setup@v4
|
|
19
|
+
with:
|
|
20
|
+
version: 10
|
|
21
|
+
cache: on
|
|
22
|
+
install: yes
|
|
23
|
+
- run: pnpm test
|
|
24
|
+
- run: pnpm prepare
|
|
25
|
+
- run: npm publish
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# ResurrectESM
|
|
2
|
+
|
|
3
|
+
An ES6 module port of ResurrectTS.
|
|
4
|
+
|
|
5
|
+
> [!CAUTION]
|
|
6
|
+
> This is *not* just a naive `exports.foo` => `export {foo}` rewrite. Some API changes have been made:
|
|
7
|
+
>
|
|
8
|
+
> * All of the "irrelevant" methods of Resurrect have been renamed using private names (that are then mangled further by esbuild).
|
|
9
|
+
> * ResurrectError is now a top-level export, instead of living inside a Resurrect object.
|
|
10
|
+
> * NamespaceResolver is now a top-level export, instead of living inside the Resurrect static constructor namespace.
|
|
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
|
+
|
|
13
|
+
ResurrectJS preserves object behavior (prototypes) and reference
|
|
14
|
+
circularity with a special JSON encoding. Unlike flat JSON, it can
|
|
15
|
+
also properly resurrect these types of values:
|
|
16
|
+
|
|
17
|
+
* Date
|
|
18
|
+
* RegExp
|
|
19
|
+
* `undefined`
|
|
20
|
+
* NaN, Infinity, -Infinity
|
|
21
|
+
|
|
22
|
+
Supported Browsers:
|
|
23
|
+
|
|
24
|
+
* Chrome
|
|
25
|
+
* Firefox
|
|
26
|
+
* Safari
|
|
27
|
+
* Opera
|
|
28
|
+
* IE9+
|
|
29
|
+
|
|
30
|
+
Read about [how it works](http://nullprogram.com/blog/2013/03/28/).
|
|
31
|
+
|
|
32
|
+
## Examples
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { Resurrect } from "resurrect-esm";
|
|
36
|
+
window.Foo = class Foo {
|
|
37
|
+
greet() { return "hello"; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Behavior is preserved:
|
|
41
|
+
const necromancer = new Resurrect();
|
|
42
|
+
const json = necromancer.stringify(new Foo());
|
|
43
|
+
const foo = necromancer.resurrect(json);
|
|
44
|
+
foo.greet(); // => "hello"
|
|
45
|
+
|
|
46
|
+
// References to the same object are preserved:
|
|
47
|
+
json = necromancer.stringify([foo, foo]);
|
|
48
|
+
const array = necromancer.resurrect(json);
|
|
49
|
+
array[0] === array[1]; // => true
|
|
50
|
+
array[1].greet(); // => "hello"
|
|
51
|
+
|
|
52
|
+
// Dates are restored properly
|
|
53
|
+
json = necromancer.stringify(new Date());
|
|
54
|
+
const date = necromancer.resurrect(json);
|
|
55
|
+
{}.toString.call(date); // => "[object Date]"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Options
|
|
59
|
+
|
|
60
|
+
Options are provided to the constructor as an object with these
|
|
61
|
+
properties:
|
|
62
|
+
|
|
63
|
+
* *prefix* (`"#"`): A prefix string used for temporary properties added
|
|
64
|
+
to objects during serialization and deserialization. It is
|
|
65
|
+
important that you don't use any properties beginning with this
|
|
66
|
+
string. This option must be consistent between both serialization
|
|
67
|
+
and deserialization.
|
|
68
|
+
|
|
69
|
+
* *cleanup* (`false`): Perform full property cleanup after both
|
|
70
|
+
serialization and deserialization using the `delete` operator.
|
|
71
|
+
This may cause performance penalties (i.e. breaking hidden
|
|
72
|
+
classes in V8) on objects that ResurrectJS touches, so enable
|
|
73
|
+
with care.
|
|
74
|
+
|
|
75
|
+
* *revive* (`true`): Restore behavior (`__proto__`) to objects that
|
|
76
|
+
have been resurrected. If this is set to false during
|
|
77
|
+
serialization, resurrection information will not be encoded. You
|
|
78
|
+
still get circularity and Date support.
|
|
79
|
+
|
|
80
|
+
* *resolver* (`NamespaceResolver`): Converts between a name
|
|
81
|
+
and a prototype. Create a custom resolver if your constructors
|
|
82
|
+
are not stored in global variables. The resolver has two methods:
|
|
83
|
+
getName(object) and getPrototype(string).
|
|
84
|
+
|
|
85
|
+
> [!CAUTION]
|
|
86
|
+
> If you're using ES6 modules for your custom classes, you MUST use a custom resolver since module scope is not global scope!
|
|
87
|
+
|
|
88
|
+
For example,
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
import { Resurrect } from "resurrect-esm";
|
|
92
|
+
const necromancer = new Resurrect({
|
|
93
|
+
prefix: "__#",
|
|
94
|
+
cleanup: true
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Methods
|
|
99
|
+
|
|
100
|
+
Only two methods are significant when using ResurrectJS.
|
|
101
|
+
|
|
102
|
+
* `.stringify(object[, replacer[, space]])`: Serializes an arbitrary
|
|
103
|
+
object or value into a string. The `replacer` and `space`
|
|
104
|
+
arguments are the same as [JSON.stringify][json-mdn], being
|
|
105
|
+
passed through to this method. Note that the replacer will *not*
|
|
106
|
+
be called for ResurrectJS's intrusive keys.
|
|
107
|
+
|
|
108
|
+
* `.resurrect(string)`: Deserializes an object stored in a string by
|
|
109
|
+
a previous call to `.stringify()`. Circularity and, optionally,
|
|
110
|
+
behavior (prototype chain) will be restored.
|
|
111
|
+
|
|
112
|
+
## Restrictions
|
|
113
|
+
|
|
114
|
+
With the default resolver, all constructors must be named and stored
|
|
115
|
+
in the global variable under that name. This is required so that the
|
|
116
|
+
prototypes can be looked up and reconnected at resurrection time.
|
|
117
|
+
|
|
118
|
+
The wrapper objects Boolean, String, and Number will be
|
|
119
|
+
unwrapped. This means extra properties added to these objects will not
|
|
120
|
+
be preserved.
|
|
121
|
+
|
|
122
|
+
Functions cannot ever be serialized. Resurrect will throw an error if
|
|
123
|
+
a function is found when traversing a data structure.
|
|
124
|
+
|
|
125
|
+
### Custom Resolvers
|
|
126
|
+
|
|
127
|
+
There is a caveat with the provided resolver, NamespaceResolver: all
|
|
128
|
+
constructors *must* be explicitly named when defined. For example, see
|
|
129
|
+
the Foo constructor in this example,
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { Resurrect, NamespaceResolver } from "resurrect-esm";
|
|
133
|
+
const namespace = {
|
|
134
|
+
Foo: class {
|
|
135
|
+
constructor() {
|
|
136
|
+
this.bar = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const necromancer = new Resurrect({
|
|
141
|
+
resolver: new NamespaceResolver(namespace)
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The constructor been assigned to the Foo property *and* the function
|
|
146
|
+
itself has been given a matching name. This is how the resolver will
|
|
147
|
+
find the name of the constructor in the namespace when given the
|
|
148
|
+
constructor. Keep in mind that using this form will bind the variable
|
|
149
|
+
Foo to the surrounding function within the body of Foo.
|
|
150
|
+
|
|
151
|
+
## See Also
|
|
152
|
+
|
|
153
|
+
* [HydrateJS](https://github.com/nanodeath/HydrateJS)
|
|
154
|
+
|
|
155
|
+
[json-mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
|
package/UNLICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <http://unlicense.org/>
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
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"
|
|
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=^_"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { GlobalRegistrator } from "@happy-dom/global-registrator";
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
import { NamespaceResolver, Resurrect, ResurrectError, ResurrectOptions } from "./resurrect";
|
|
4
|
+
|
|
5
|
+
GlobalRegistrator.register();
|
|
6
|
+
|
|
7
|
+
function suite(opt?: ResurrectOptions) {
|
|
8
|
+
|
|
9
|
+
const defOpts = new Resurrect(opt);
|
|
10
|
+
|
|
11
|
+
function roundtrip<T>(obj: T, options?: ResurrectOptions): T {
|
|
12
|
+
const serializer = new Resurrect({ ...options, ...opt });
|
|
13
|
+
const str = serializer.stringify(obj);
|
|
14
|
+
return serializer.resurrect(str);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("primitive serialization", () => {
|
|
18
|
+
expect(roundtrip(1)).toBe(1);
|
|
19
|
+
expect(roundtrip(null)).toBeNull();
|
|
20
|
+
expect(roundtrip(undefined)).toBeUndefined();
|
|
21
|
+
expect(roundtrip("foo")).toBe("foo");
|
|
22
|
+
expect(roundtrip(NaN)).toBeNaN();
|
|
23
|
+
expect(roundtrip(Infinity)).toBeGreaterThan(Number.MAX_VALUE);
|
|
24
|
+
expect(roundtrip(-Infinity)).toBeLessThan(Number.MIN_VALUE);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("basic JSON serialization", () => {
|
|
28
|
+
const obj = { a: 1, b: 2, c: [1, 2, { d: 3 }], e: null, f: true };
|
|
29
|
+
expect(roundtrip(obj, { cleanup: true })).toEqual(obj);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("non-JSON atoms in an object", () => {
|
|
33
|
+
const obj = { a: null, b: undefined, c: NaN, d: Infinity, e: -Infinity };
|
|
34
|
+
expect(roundtrip(obj, { cleanup: true })).toEqual(obj);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("serialization with circular references", () => {
|
|
38
|
+
const obj = { a: 1, b: 2, c: null as any };
|
|
39
|
+
obj.c = obj;
|
|
40
|
+
expect(roundtrip(obj, { cleanup: true })).toEqual(obj);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("serialization with shared structure", () => {
|
|
44
|
+
const obj = { a: 1, b: 2 };
|
|
45
|
+
const arr = [obj, obj, { obj }] as const;
|
|
46
|
+
const roundtripped = roundtrip(arr, { cleanup: true });
|
|
47
|
+
expect(roundtripped).toEqual(arr);
|
|
48
|
+
expect(roundtripped[0]).toBe(roundtripped[1]);
|
|
49
|
+
expect(roundtripped[1]).toBe(roundtripped[2].obj);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("serialization with Date and RegExp", () => {
|
|
53
|
+
const obj = { a: new Date, b: /abc/gu };
|
|
54
|
+
expect(roundtrip(obj, { cleanup: true })).toEqual(obj);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("serialization with DOM elements", () => {
|
|
58
|
+
const obj = new Resurrect.Node("<span id=foo><a id=1></a></span>");
|
|
59
|
+
const roundtripped = roundtrip(obj);
|
|
60
|
+
expect(roundtripped).toBeInstanceOf(HTMLSpanElement);
|
|
61
|
+
expect(roundtripped.firstChild).toBeInstanceOf(HTMLAnchorElement);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("doesn't try to serialize a function", () => {
|
|
65
|
+
const obj = { foo() { } };
|
|
66
|
+
expect(() => roundtrip(obj)).toThrow(new ResurrectError("Can't serialize functions."));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (defOpts.revive) {
|
|
70
|
+
test("revive and custom resolver", () => {
|
|
71
|
+
class Dog {
|
|
72
|
+
constructor(public loudness: number, public sound: string) { }
|
|
73
|
+
woof() { return this.sound.repeat(this.loudness) + "!"; }
|
|
74
|
+
}
|
|
75
|
+
const obj = new Dog(3, "wow");
|
|
76
|
+
const roundtripped = roundtrip(obj, {
|
|
77
|
+
resolver: new NamespaceResolver({ Dog }),
|
|
78
|
+
});
|
|
79
|
+
expect(roundtripped).toBeInstanceOf(Dog);
|
|
80
|
+
expect(roundtripped.woof()).toEqual("wowwowwow!");
|
|
81
|
+
});
|
|
82
|
+
test("can't serialize anonymous classes", () => {
|
|
83
|
+
const obj = new class {
|
|
84
|
+
foo: number;
|
|
85
|
+
constructor() {
|
|
86
|
+
this.foo = 1;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
expect(() => roundtrip(obj)).toThrow(new ResurrectError("Can't serialize objects with anonymous constructors."))
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
test("no revive preserves own properties but not functionality", () => {
|
|
93
|
+
class Dog {
|
|
94
|
+
constructor(public loudness: number, public sound: string) { }
|
|
95
|
+
woof() { return this.sound.repeat(this.loudness) + "!"; }
|
|
96
|
+
}
|
|
97
|
+
const obj = new Dog(3, "wow");
|
|
98
|
+
const roundtripped = roundtrip(obj, {
|
|
99
|
+
resolver: new NamespaceResolver({ Dog }),
|
|
100
|
+
});
|
|
101
|
+
expect(roundtripped).not.toBeInstanceOf(Dog);
|
|
102
|
+
expect(roundtripped.woof).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe("default options", () => suite());
|
|
108
|
+
describe("custom prefix", () => suite({ prefix: "qwerty" }));
|
|
109
|
+
describe("no revive", () => suite({ revive: false }));
|
package/resurrect.ts
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* # ResurrectJS
|
|
3
|
+
* @version 1.0.3
|
|
4
|
+
* @license Public Domain
|
|
5
|
+
*
|
|
6
|
+
* ResurrectJS preserves object behavior (prototypes) and reference
|
|
7
|
+
* circularity with a special JSON encoding. Unlike regular JSON,
|
|
8
|
+
* Date, RegExp, DOM objects, and `undefined` are also properly
|
|
9
|
+
* preserved.
|
|
10
|
+
*
|
|
11
|
+
* ## Examples
|
|
12
|
+
*
|
|
13
|
+
* function Foo() {}
|
|
14
|
+
* Foo.prototype.greet = function() { return "hello"; };
|
|
15
|
+
*
|
|
16
|
+
* // Behavior is preserved:
|
|
17
|
+
* const necromancer = new Resurrect();
|
|
18
|
+
* const json = necromancer.stringify(new Foo());
|
|
19
|
+
* const foo = necromancer.resurrect(json);
|
|
20
|
+
* foo.greet(); // => "hello"
|
|
21
|
+
*
|
|
22
|
+
* // References to the same object are preserved:
|
|
23
|
+
* json = necromancer.stringify([foo, foo]);
|
|
24
|
+
* const array = necromancer.resurrect(json);
|
|
25
|
+
* array[0] === array[1]; // => true
|
|
26
|
+
* array[1].greet(); // => "hello"
|
|
27
|
+
*
|
|
28
|
+
* // Dates are restored properly
|
|
29
|
+
* json = necromancer.stringify(new Date());
|
|
30
|
+
* const date = necromancer.resurrect(json);
|
|
31
|
+
* Object.prototype.toString.call(date); // => "[object Date]"
|
|
32
|
+
*
|
|
33
|
+
* ## Options
|
|
34
|
+
*
|
|
35
|
+
* Options are provided to the constructor as an object with these
|
|
36
|
+
* properties:
|
|
37
|
+
*
|
|
38
|
+
* prefix ('#'): A prefix string used for temporary properties added
|
|
39
|
+
* to objects during serialization and deserialization. It is
|
|
40
|
+
* important that you don't use any properties beginning with this
|
|
41
|
+
* string. This option must be consistent between both
|
|
42
|
+
* serialization and deserialization.
|
|
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
|
+
* revive (true): Restore behavior (__proto__) to objects that have
|
|
51
|
+
* been resurrected. If this is set to false during serialization,
|
|
52
|
+
* resurrection information will not be encoded. You still get
|
|
53
|
+
* circularity and Date support.
|
|
54
|
+
*
|
|
55
|
+
* resolver (Resurrect.NamespaceResolver(window)): Converts between
|
|
56
|
+
* a name and a prototype. Create a custom resolver if your
|
|
57
|
+
* constructors are not stored in global variables. The resolver
|
|
58
|
+
* has two methods: getName(object) and getPrototype(string).
|
|
59
|
+
*
|
|
60
|
+
* For example,
|
|
61
|
+
*
|
|
62
|
+
* const necromancer = new Resurrect({
|
|
63
|
+
* prefix: '__#',
|
|
64
|
+
* cleanup: true
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* ## Caveats
|
|
68
|
+
*
|
|
69
|
+
* * With the default resolver, all constructors must be named and
|
|
70
|
+
* stored in the global variable under that name. This is required
|
|
71
|
+
* so that the prototypes can be looked up and reconnected at
|
|
72
|
+
* resurrection time.
|
|
73
|
+
*
|
|
74
|
+
* * The wrapper objects Boolean, String, and Number will be
|
|
75
|
+
* unwrapped. This means extra properties added to these objects
|
|
76
|
+
* will not be preserved.
|
|
77
|
+
*
|
|
78
|
+
* * Functions cannot ever be serialized. Resurrect will throw an
|
|
79
|
+
* error if a function is found when traversing a data structure.
|
|
80
|
+
*
|
|
81
|
+
* @see http://nullprogram.com/blog/2013/03/28/
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
export class Resurrect {
|
|
85
|
+
private _table: any[] | null;
|
|
86
|
+
prefix: string;
|
|
87
|
+
cleanup: boolean;
|
|
88
|
+
revive: boolean;
|
|
89
|
+
get _refcode() { return this.prefix + "#" };
|
|
90
|
+
get _backrefcode() { return this.prefix + "=" };
|
|
91
|
+
get _protocode() { return this.prefix + "+" };
|
|
92
|
+
get _origcode() { return this.prefix + "&" };
|
|
93
|
+
get _buildcode() { return this.prefix + "@" };
|
|
94
|
+
get _valuecode() { return this.prefix + "_" };
|
|
95
|
+
resolver: NamespaceResolver;
|
|
96
|
+
constructor(opt: ResurrectOptions = {}) {
|
|
97
|
+
this._table = null;
|
|
98
|
+
this.prefix = opt.prefix ?? "#";
|
|
99
|
+
this.cleanup = opt.cleanup ?? false;
|
|
100
|
+
this.revive = opt.revive ?? true;
|
|
101
|
+
this.resolver = opt.resolver ?? new NamespaceResolver(Resurrect.GLOBAL as any);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Portable access to the global object (window, global).
|
|
106
|
+
* Uses indirect eval.
|
|
107
|
+
* @constant
|
|
108
|
+
*/
|
|
109
|
+
static readonly GLOBAL: typeof globalThis = globalThis ?? (0, eval)("this");
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Escape special regular expression characters in a string.
|
|
113
|
+
* Uses `RegExp.escape` if available, otherwise falls back to http://stackoverflow.com/a/6969486.
|
|
114
|
+
* @param {string} string
|
|
115
|
+
* @returns {string} The string escaped for exact matches.
|
|
116
|
+
*/
|
|
117
|
+
private static _escapeRegExp = RegExp.escape ?? ((string: string) => string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"));
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create a DOM node from HTML source; behaves like a constructor.
|
|
121
|
+
*/
|
|
122
|
+
static Node = class {
|
|
123
|
+
constructor(html: string) {
|
|
124
|
+
const div = document.createElement("a");
|
|
125
|
+
div.innerHTML = html;
|
|
126
|
+
return div.firstChild as HTMLElement;
|
|
127
|
+
}
|
|
128
|
+
} as new (x: string) => HTMLElement;
|
|
129
|
+
|
|
130
|
+
private static _is(type: string) {
|
|
131
|
+
const string = `[object ${type}]`;
|
|
132
|
+
return (obj: any) => {
|
|
133
|
+
return {}.toString.call(obj) === string;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private static _isArray = Resurrect._is("Array") as (obj: any) => obj is any[];
|
|
138
|
+
private static _isString = Resurrect._is("String") as (obj: any) => obj is string;
|
|
139
|
+
private static _isBoolean = Resurrect._is("Boolean") as (obj: any) => obj is boolean;
|
|
140
|
+
private static _isNumber = Resurrect._is("Number") as (obj: any) => obj is number;
|
|
141
|
+
private static _isFunction = Resurrect._is("Function") as (obj: any) => obj is Function;
|
|
142
|
+
private static _isDate = Resurrect._is("Date") as (obj: any) => obj is Date;
|
|
143
|
+
private static _isRegExp = Resurrect._is("RegExp") as (obj: any) => obj is RegExp;
|
|
144
|
+
private static _isObject = Resurrect._is("Object") as (obj: any) => obj is object;
|
|
145
|
+
private static isAtom(object: any) {
|
|
146
|
+
return !Resurrect._isObject(object) && !Resurrect._isArray(object);
|
|
147
|
+
}
|
|
148
|
+
private static _isPrimitive(object: any) {
|
|
149
|
+
return object == null ||
|
|
150
|
+
Resurrect._isNumber(object) ||
|
|
151
|
+
Resurrect._isString(object) ||
|
|
152
|
+
Resurrect._isBoolean(object);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create a reference (encoding) to an object.
|
|
157
|
+
*/
|
|
158
|
+
private _ref(object: any) {
|
|
159
|
+
return {
|
|
160
|
+
[this._backrefcode]: object === undefined ? -1 : object[this._refcode],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Lookup an object in the table by reference object.
|
|
166
|
+
*/
|
|
167
|
+
private _deref(ref: any) {
|
|
168
|
+
return this._table![ref[this._backrefcode]];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Put a temporary identifier on an object and store it in the table.
|
|
173
|
+
*/
|
|
174
|
+
private _tag(object: any): number {
|
|
175
|
+
if (this.revive) {
|
|
176
|
+
const constructor = this.resolver.getName(object);
|
|
177
|
+
if (constructor) {
|
|
178
|
+
const proto = Object.getPrototypeOf(object);
|
|
179
|
+
if (this.resolver.getPrototype(constructor) !== proto) {
|
|
180
|
+
throw new ResurrectError("Constructor mismatch!");
|
|
181
|
+
} else {
|
|
182
|
+
object[this._protocode] = constructor;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
object[this._refcode] = this._table!.length;
|
|
187
|
+
this._table!.push(object);
|
|
188
|
+
return object[this._refcode];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create a builder object (encoding) for serialization.
|
|
193
|
+
* @param value The value to pass to the constructor.
|
|
194
|
+
*/
|
|
195
|
+
private _builder(name: string, value: any): object {
|
|
196
|
+
return {
|
|
197
|
+
[this._buildcode]: name,
|
|
198
|
+
[this._valuecode]: value
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build a value from a deserialized builder.
|
|
204
|
+
* @see http://stackoverflow.com/a/14378462
|
|
205
|
+
* @see http://nullprogram.com/blog/2013/03/24/
|
|
206
|
+
*/
|
|
207
|
+
private _build(ref: any): any {
|
|
208
|
+
const type = this.resolver.getConstructor(ref[this._buildcode]);
|
|
209
|
+
/* Brilliant hack by kybernetikos: */
|
|
210
|
+
const result: any = new (type.bind.apply(type, [null].concat(ref[this._valuecode]) as [any, any[]]))();
|
|
211
|
+
if (Resurrect._isPrimitive(result)) {
|
|
212
|
+
return result.valueOf(); // unwrap
|
|
213
|
+
} else {
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Dereference or build an object or value from an encoding.
|
|
220
|
+
* @method
|
|
221
|
+
*/
|
|
222
|
+
private _decode(ref: object): object | undefined {
|
|
223
|
+
if (this._backrefcode in ref) {
|
|
224
|
+
return this._deref(ref);
|
|
225
|
+
} else if (this._buildcode in ref) {
|
|
226
|
+
return this._build(ref);
|
|
227
|
+
} else {
|
|
228
|
+
throw new ResurrectError("Unknown encoding.");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @returns {boolean} True if the provided object is tagged for serialization.
|
|
234
|
+
*/
|
|
235
|
+
private _isTagged(object: any): boolean {
|
|
236
|
+
return (this._refcode in object) && (object[this._refcode] != null);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Visit root and all its ancestors, visiting atoms with f.
|
|
242
|
+
* @returns A fresh copy of root to be serialized.
|
|
243
|
+
*/
|
|
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);
|
|
247
|
+
} else if (!this._isTagged(root)) {
|
|
248
|
+
let copy: any = null;
|
|
249
|
+
if (Resurrect._isArray(root)) {
|
|
250
|
+
copy = [];
|
|
251
|
+
root[this._refcode as any] = this._tag(copy);
|
|
252
|
+
for (let i = 0; i < root.length; i++) {
|
|
253
|
+
copy.push(this._visit(root[i], f, replacer));
|
|
254
|
+
}
|
|
255
|
+
} else { /* Object */
|
|
256
|
+
copy = Object.create(Object.getPrototypeOf(root));
|
|
257
|
+
root[this._refcode as any] = this._tag(copy);
|
|
258
|
+
for (const key in root) {
|
|
259
|
+
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);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
copy[this._origcode] = root;
|
|
273
|
+
return this._ref(copy);
|
|
274
|
+
} else {
|
|
275
|
+
return this._ref(root);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Manage special atom values, possibly returning an encoding.
|
|
281
|
+
*/
|
|
282
|
+
private _handleAtom(atom: any): any {
|
|
283
|
+
const Node = Resurrect.GLOBAL.Node || function () { };
|
|
284
|
+
if (Resurrect._isFunction(atom)) {
|
|
285
|
+
throw new ResurrectError("Can't serialize functions.");
|
|
286
|
+
} else if (atom instanceof Node) {
|
|
287
|
+
return this._builder("Resurrect.Node", [new XMLSerializer().serializeToString(atom)]);
|
|
288
|
+
} else if (Resurrect._isDate(atom)) {
|
|
289
|
+
return this._builder("Date", [atom.toISOString()]);
|
|
290
|
+
} else if (Resurrect._isRegExp(atom)) {
|
|
291
|
+
return this._builder("RegExp", ("" + atom).match(/\/(.+)\/([a-z]*)/)!.slice(1));
|
|
292
|
+
} else if (atom === undefined) {
|
|
293
|
+
return this._ref(undefined);
|
|
294
|
+
} else if (Resurrect._isNumber(atom) && (isNaN(atom) || !isFinite(atom))) {
|
|
295
|
+
return this._builder("Number", ["" + atom]);
|
|
296
|
+
} else {
|
|
297
|
+
return atom;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Hides intrusive keys from a user-supplied replacer.
|
|
303
|
+
* @method
|
|
304
|
+
*/
|
|
305
|
+
private _replacerWrapper<K extends string, V, U>(replacer: (k: K, v: V) => U): (k: K, v: V) => U | V {
|
|
306
|
+
const skip = new RegExp("^" + Resurrect._escapeRegExp(this.prefix));
|
|
307
|
+
return (k, v) => {
|
|
308
|
+
if (skip.test(k)) {
|
|
309
|
+
return v;
|
|
310
|
+
} else {
|
|
311
|
+
return replacer(k, v);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Serialize an arbitrary JavaScript object, carefully preserving it.
|
|
318
|
+
*/
|
|
319
|
+
stringify(object: any, replacer?: any[] | ((k: string, v: any) => any), space?: string) {
|
|
320
|
+
if (Resurrect._isFunction(replacer)) {
|
|
321
|
+
replacer = this._replacerWrapper(replacer);
|
|
322
|
+
} else if (Resurrect._isArray(replacer)) {
|
|
323
|
+
const acceptKeys = replacer;
|
|
324
|
+
replacer = function (k, v) {
|
|
325
|
+
return acceptKeys.indexOf(k) >= 0 ? v : undefined;
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (Resurrect.isAtom(object)) {
|
|
329
|
+
return JSON.stringify(this._handleAtom(object), replacer, space);
|
|
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;
|
|
338
|
+
}
|
|
339
|
+
delete this._table[i][this._refcode];
|
|
340
|
+
delete this._table[i][this._origcode];
|
|
341
|
+
}
|
|
342
|
+
const table = this._table;
|
|
343
|
+
this._table = null;
|
|
344
|
+
return JSON.stringify(table, null, space);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Restore the `__proto__` of the given object to the proper value.
|
|
350
|
+
* @method
|
|
351
|
+
*/
|
|
352
|
+
private _fixPrototype<T extends object>(object: T): T {
|
|
353
|
+
if (this._protocode in object) {
|
|
354
|
+
const name = (object as any)[this._protocode];
|
|
355
|
+
const prototype = this.resolver.getPrototype(name);
|
|
356
|
+
if ("__proto__" in object) {
|
|
357
|
+
object.__proto__ = prototype;
|
|
358
|
+
if (this.cleanup) {
|
|
359
|
+
delete (object as any)[this._protocode];
|
|
360
|
+
}
|
|
361
|
+
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
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return copy;
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
return object;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Deserialize an encoded object, restoring circularity and behavior.
|
|
378
|
+
*/
|
|
379
|
+
resurrect(string: string): any {
|
|
380
|
+
let result = null;
|
|
381
|
+
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]);
|
|
388
|
+
}
|
|
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]))) {
|
|
396
|
+
object[key] = this._decode(object[key]);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
result = this._table[0];
|
|
402
|
+
} else if (Resurrect._isObject(data)) {
|
|
403
|
+
this._table = [];
|
|
404
|
+
result = this._decode(data);
|
|
405
|
+
} else {
|
|
406
|
+
result = data;
|
|
407
|
+
}
|
|
408
|
+
this._table = null;
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
export interface ResurrectOptions {
|
|
415
|
+
/**
|
|
416
|
+
* A prefix string used for temporary properties added
|
|
417
|
+
* to objects during serialization and deserialization. It is
|
|
418
|
+
* important that you don't use any properties beginning with this
|
|
419
|
+
* string. This option must be consistent between both
|
|
420
|
+
* serialization and deserialization.
|
|
421
|
+
*/
|
|
422
|
+
prefix?: string;
|
|
423
|
+
/**
|
|
424
|
+
* Perform full property cleanup after both
|
|
425
|
+
* serialization and deserialization using the `delete`
|
|
426
|
+
* operator. This may cause performance penalties (breaking hidden
|
|
427
|
+
* classes in V8) on objects that ResurrectJS touches, so enable
|
|
428
|
+
* with care.
|
|
429
|
+
*/
|
|
430
|
+
cleanup?: boolean;
|
|
431
|
+
/**
|
|
432
|
+
* Restore behavior (`__proto__`) to objects that have
|
|
433
|
+
* been resurrected. If this is set to false during serialization,
|
|
434
|
+
* resurrection information will not be encoded. You still get
|
|
435
|
+
* circularity and Date support.
|
|
436
|
+
*/
|
|
437
|
+
revive?: boolean;
|
|
438
|
+
/**
|
|
439
|
+
* Converts between a name and a prototype. Create a custom
|
|
440
|
+
* resolver if your constructors are not stored in global variables.
|
|
441
|
+
*
|
|
442
|
+
* If you're using ES6 modules for your custom classes, you WILL need
|
|
443
|
+
* to use this!
|
|
444
|
+
*/
|
|
445
|
+
resolver?: NamespaceResolver;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export class ResurrectError extends Error { }
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Resolves prototypes through the properties on an object and
|
|
452
|
+
* constructor names.
|
|
453
|
+
*/
|
|
454
|
+
export class NamespaceResolver {
|
|
455
|
+
constructor(public scope: Record<string, new (...args: any[]) => any>) { }
|
|
456
|
+
/**
|
|
457
|
+
* Gets the prototype of the given property name from an object. If
|
|
458
|
+
* not found, throws an error.
|
|
459
|
+
*/
|
|
460
|
+
getPrototype(name: string): any {
|
|
461
|
+
const constructor = this.scope[name];
|
|
462
|
+
if (constructor) {
|
|
463
|
+
return constructor.prototype;
|
|
464
|
+
}
|
|
465
|
+
throw new ResurrectError("Unknown constructor: " + name);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Get the prototype name for an object, to be fetched later with
|
|
469
|
+
* {@link getPrototype} and {@link getConstructor}.
|
|
470
|
+
* @returns null if the constructor is `Object` or `Array`.
|
|
471
|
+
*/
|
|
472
|
+
getName(object: object): string | null {
|
|
473
|
+
let constructor = object.constructor.name;
|
|
474
|
+
if (constructor == null) { // IE
|
|
475
|
+
constructor = /^\s*function\s*([A-Za-z0-9_$]*)/.exec("" + object.constructor)?.[1] ?? "";
|
|
476
|
+
}
|
|
477
|
+
if (constructor === "") {
|
|
478
|
+
throw new ResurrectError("Can't serialize objects with anonymous constructors.");
|
|
479
|
+
}
|
|
480
|
+
return constructor === "Object" || constructor === "Array" ? null : constructor;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Get the constructor function for the object prototype name. For backwards compatibility
|
|
485
|
+
* purposes, falls back to treating the string as a dot-separated path on `globalThis` if the
|
|
486
|
+
* object's constructor isn't in the namespace.
|
|
487
|
+
*/
|
|
488
|
+
getConstructor(name: string): new (...args: any[]) => any {
|
|
489
|
+
return (name === "Resurrect.Node" ? Resurrect.Node : this.scope[name] ?? name.split(/\./).reduce((object, name) => {
|
|
490
|
+
return (object as any)[name];
|
|
491
|
+
}, Resurrect.GLOBAL)) as unknown as new () => any;
|
|
492
|
+
}
|
|
493
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"noEmit": true,
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"resolveJsonModule": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": false,
|
|
9
|
+
"noImplicitAny": true,
|
|
10
|
+
"noFallthroughCasesInSwitch": true,
|
|
11
|
+
"strictNullChecks": true,
|
|
12
|
+
"moduleResolution": "Bundler",
|
|
13
|
+
"module": "ESNext",
|
|
14
|
+
"lib": [
|
|
15
|
+
"esnext",
|
|
16
|
+
"dom",
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|