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.
@@ -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
@@ -0,0 +1,6 @@
1
+ {
2
+ "cSpell.words": [
3
+ "kybernetikos",
4
+ "registrator"
5
+ ]
6
+ }
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,3 @@
1
+ onlyBuiltDependencies:
2
+ - bun
3
+ - esbuild
@@ -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
+ }