seroval 0.1.0
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/LICENSE +7 -0
- package/README.md +153 -0
- package/dist/cjs/development/index.cjs +481 -0
- package/dist/cjs/development/index.cjs.map +7 -0
- package/dist/cjs/production/index.cjs +2 -0
- package/dist/esm/development/index.mjs +458 -0
- package/dist/esm/development/index.mjs.map +7 -0
- package/dist/esm/production/index.mjs +2 -0
- package/dist/types/array.d.ts +2 -0
- package/dist/types/get-identifier.d.ts +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/quote.d.ts +1 -0
- package/package.json +68 -0
- package/src/array.ts +18 -0
- package/src/get-identifier.ts +16 -0
- package/src/index.ts +479 -0
- package/src/quote.ts +45 -0
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "seroval",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"src"
|
|
8
|
+
],
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=10"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pridepack"
|
|
15
|
+
],
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^18.11.18",
|
|
18
|
+
"eslint": "^8.32.0",
|
|
19
|
+
"eslint-config-lxsmnsyc": "^0.4.8",
|
|
20
|
+
"pridepack": "2.4.0",
|
|
21
|
+
"tslib": "^2.4.1",
|
|
22
|
+
"typescript": "^4.9.4",
|
|
23
|
+
"vitest": "^0.28.1"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"prepublishOnly": "pridepack clean && pridepack build",
|
|
27
|
+
"build": "pridepack build",
|
|
28
|
+
"type-check": "pridepack check",
|
|
29
|
+
"lint": "pridepack lint",
|
|
30
|
+
"clean": "pridepack clean",
|
|
31
|
+
"watch": "pridepack watch",
|
|
32
|
+
"start": "pridepack start",
|
|
33
|
+
"dev": "pridepack dev",
|
|
34
|
+
"test": "vitest"
|
|
35
|
+
},
|
|
36
|
+
"private": false,
|
|
37
|
+
"description": "Stringify JS values",
|
|
38
|
+
"repository": {
|
|
39
|
+
"url": "https://github.com/lxsmnsyc/seroval.git",
|
|
40
|
+
"type": "git"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/lxsmnsyc/seroval/tree/main/packages/seroval",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/lxsmnsyc/seroval/issues"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"author": "Alexis Munsayac",
|
|
50
|
+
"types": "./dist/types/index.d.ts",
|
|
51
|
+
"main": "./dist/cjs/production/index.cjs",
|
|
52
|
+
"module": "./dist/esm/production/index.mjs",
|
|
53
|
+
"exports": {
|
|
54
|
+
".": {
|
|
55
|
+
"development": {
|
|
56
|
+
"require": "./dist/cjs/development/index.cjs",
|
|
57
|
+
"import": "./dist/esm/development/index.mjs"
|
|
58
|
+
},
|
|
59
|
+
"require": "./dist/cjs/production/index.cjs",
|
|
60
|
+
"import": "./dist/esm/production/index.mjs",
|
|
61
|
+
"types": "./dist/types/index.d.ts"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"typesVersions": {
|
|
65
|
+
"*": {}
|
|
66
|
+
},
|
|
67
|
+
"gitHead": "c7db9d01e7f66662e4f890ded4533be0b0cb0ea1"
|
|
68
|
+
}
|
package/src/array.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function forEach<T>(arr: T[], callback: (value: T, index: number) => (boolean | void)) {
|
|
2
|
+
for (let i = 0, len = arr.length; i < len; i++) {
|
|
3
|
+
if (callback(arr[i], i)) {
|
|
4
|
+
break;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function join(arr: string[], joiner: string): string {
|
|
10
|
+
let result = '';
|
|
11
|
+
forEach(arr, (value, index) => {
|
|
12
|
+
result += value;
|
|
13
|
+
if (index < arr.length - 1) {
|
|
14
|
+
result += joiner;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const REF_START_CHARS = 'hjkmoquxzABCDEFGHIJKLNPQRTUVWXYZ$_'; // Avoids chars that could evaluate to a reserved word.
|
|
2
|
+
const REF_START_CHARS_LEN = REF_START_CHARS.length;
|
|
3
|
+
const REF_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$_';
|
|
4
|
+
const REF_CHARS_LEN = REF_CHARS.length;
|
|
5
|
+
|
|
6
|
+
export default function getIdentifier(index: number) {
|
|
7
|
+
let mod = index % REF_START_CHARS_LEN;
|
|
8
|
+
let ref = REF_START_CHARS[mod];
|
|
9
|
+
index = (index - mod) / REF_START_CHARS_LEN;
|
|
10
|
+
while (index > 0) {
|
|
11
|
+
mod = index % REF_CHARS_LEN;
|
|
12
|
+
ref += REF_CHARS[mod];
|
|
13
|
+
index = (index - mod) / REF_CHARS_LEN;
|
|
14
|
+
}
|
|
15
|
+
return ref;
|
|
16
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { forEach, join } from './array';
|
|
2
|
+
import getIdentifier from './get-identifier';
|
|
3
|
+
import quote from './quote';
|
|
4
|
+
|
|
5
|
+
const EMPTY_SET = 'new Set';
|
|
6
|
+
const EMPTY_MAP = 'new Map';
|
|
7
|
+
const EMPTY_ARRAY = '[]';
|
|
8
|
+
|
|
9
|
+
// Values that are non-recursive
|
|
10
|
+
export type PrimitiveValue =
|
|
11
|
+
| boolean
|
|
12
|
+
| string
|
|
13
|
+
| number
|
|
14
|
+
| undefined
|
|
15
|
+
| null
|
|
16
|
+
| bigint
|
|
17
|
+
| Date
|
|
18
|
+
| RegExp;
|
|
19
|
+
|
|
20
|
+
export type ServerValue =
|
|
21
|
+
| PrimitiveValue
|
|
22
|
+
| Array<ServerValue>
|
|
23
|
+
| { [key: string | number]: ServerValue }
|
|
24
|
+
| Set<ServerValue>
|
|
25
|
+
| Map<ServerValue, ServerValue>;
|
|
26
|
+
|
|
27
|
+
// Reference counter
|
|
28
|
+
const REF_COUNT = new Map<ServerValue, number>();
|
|
29
|
+
|
|
30
|
+
const STACK = new Set();
|
|
31
|
+
|
|
32
|
+
// Value-to-ref map
|
|
33
|
+
const REFS = new Map<ServerValue, number>();
|
|
34
|
+
|
|
35
|
+
// Set to check if ref id already has an assigned value
|
|
36
|
+
const ASSIGNED_REFS = new Set<number>();
|
|
37
|
+
|
|
38
|
+
// Variables
|
|
39
|
+
let VARS: string[] = [];
|
|
40
|
+
|
|
41
|
+
// Array of assignments to be done (used for recursion)
|
|
42
|
+
type Assignment = [source: string, value: string];
|
|
43
|
+
let ASSIGNMENTS: Assignment[] = [];
|
|
44
|
+
|
|
45
|
+
// Array of Map.prototype.set calls
|
|
46
|
+
type MapSet = [source: string, key: string, value: string];
|
|
47
|
+
let MAP_SETS: MapSet[] = [];
|
|
48
|
+
|
|
49
|
+
// Array of Set.prototype.add calls
|
|
50
|
+
type SetAdd = [source: string, value: string];
|
|
51
|
+
let SET_ADDS: SetAdd[] = [];
|
|
52
|
+
|
|
53
|
+
function reset() {
|
|
54
|
+
ASSIGNMENTS = [];
|
|
55
|
+
MAP_SETS = [];
|
|
56
|
+
SET_ADDS = [];
|
|
57
|
+
VARS = [];
|
|
58
|
+
REF_COUNT.clear();
|
|
59
|
+
REFS.clear();
|
|
60
|
+
ASSIGNED_REFS.clear();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveAssignments() {
|
|
64
|
+
if (ASSIGNMENTS.length) {
|
|
65
|
+
const result: Record<string, string> = {};
|
|
66
|
+
|
|
67
|
+
// Merge all assignments with similar source
|
|
68
|
+
forEach(ASSIGNMENTS, ([source, value]) => {
|
|
69
|
+
if (value in result) {
|
|
70
|
+
result[value] = `${source}=${result[value]}`;
|
|
71
|
+
} else {
|
|
72
|
+
result[value] = `${source}=${value}`;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return `${join(Object.values(result), ',')},`;
|
|
77
|
+
}
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveMapSets() {
|
|
82
|
+
if (MAP_SETS.length) {
|
|
83
|
+
const result: Record<string, string> = {};
|
|
84
|
+
|
|
85
|
+
// Merge all assignments with similar source
|
|
86
|
+
forEach(MAP_SETS, ([source, key, value]) => {
|
|
87
|
+
if (source in result) {
|
|
88
|
+
result[source] = `${result[source]}.set(${key},${value})`;
|
|
89
|
+
} else {
|
|
90
|
+
result[source] = `${source}.set(${key},${value})`;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return `${join(Object.values(result), ',')},`;
|
|
95
|
+
}
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resolveSetAdds() {
|
|
100
|
+
if (SET_ADDS.length) {
|
|
101
|
+
const result: Record<string, string> = {};
|
|
102
|
+
|
|
103
|
+
// Merge all assignments with similar source
|
|
104
|
+
forEach(SET_ADDS, ([source, value]) => {
|
|
105
|
+
if (source in result) {
|
|
106
|
+
result[source] = `${result[source]}.add(${value})`;
|
|
107
|
+
} else {
|
|
108
|
+
result[source] = `${source}.add(${value})`;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return `${join(Object.values(result), ',')},`;
|
|
113
|
+
}
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolvePatches() {
|
|
118
|
+
return `${resolveAssignments()}${resolveMapSets()}${resolveSetAdds()}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function insertRef(current: ServerValue) {
|
|
122
|
+
const count = REF_COUNT.get(current) || 0;
|
|
123
|
+
REF_COUNT.set(current, count + 1);
|
|
124
|
+
return count === 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function constructorCheck<T extends NonNullable<ServerValue>>(
|
|
128
|
+
value: NonNullable<ServerValue>,
|
|
129
|
+
constructor: unknown,
|
|
130
|
+
): value is T {
|
|
131
|
+
return value.constructor === constructor;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isPrimitive(current: ServerValue): current is PrimitiveValue {
|
|
135
|
+
if (!current || current === true) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
const type = typeof current;
|
|
139
|
+
const classConst = current.constructor;
|
|
140
|
+
return type === 'number'
|
|
141
|
+
|| type === 'string'
|
|
142
|
+
|| type === 'bigint'
|
|
143
|
+
|| classConst === Date
|
|
144
|
+
|| classConst === RegExp;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function lookupRefs(current: ServerValue) {
|
|
148
|
+
if (isPrimitive(current)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(current)) {
|
|
152
|
+
if (insertRef(current)) {
|
|
153
|
+
forEach(current, lookupRefs);
|
|
154
|
+
}
|
|
155
|
+
} else if (constructorCheck<Set<ServerValue>>(current, Set)) {
|
|
156
|
+
if (insertRef(current)) {
|
|
157
|
+
for (const item of current.keys()) {
|
|
158
|
+
lookupRefs(item);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else if (constructorCheck<Map<ServerValue, ServerValue>>(current, Map)) {
|
|
162
|
+
if (insertRef(current)) {
|
|
163
|
+
for (const [key, value] of current.entries()) {
|
|
164
|
+
lookupRefs(key);
|
|
165
|
+
lookupRefs(value);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} else if (current.constructor === Object && insertRef(current)) {
|
|
169
|
+
forEach(Object.values(current), lookupRefs);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function createRef(value: ServerValue) {
|
|
174
|
+
const current = REFS.get(value);
|
|
175
|
+
if (current != null) {
|
|
176
|
+
return current;
|
|
177
|
+
}
|
|
178
|
+
const id = REFS.size;
|
|
179
|
+
REFS.set(value, id);
|
|
180
|
+
return id;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getRefParam(index: number) {
|
|
184
|
+
if (VARS[index]) {
|
|
185
|
+
return VARS[index];
|
|
186
|
+
}
|
|
187
|
+
const result = getIdentifier(index);
|
|
188
|
+
VARS[index] = result;
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function assignRef(index: number, value: string) {
|
|
193
|
+
return `${getRefParam(index)}=${value}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function createAssignment(source: string, value: string) {
|
|
197
|
+
ASSIGNMENTS.push([source, value]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function createSetAdd(ref: number, value: string) {
|
|
201
|
+
SET_ADDS.push([getRefParam(ref), value]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createMapSet(ref: number, key: string, value: string) {
|
|
205
|
+
MAP_SETS.push([getRefParam(ref), key, value]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function createArrayAssign(ref: number, index: number, value: string) {
|
|
209
|
+
createAssignment(`${getRefParam(ref)}[${index}]`, value);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function createObjectIdentifierAssign(ref: number, key: string, value: string) {
|
|
213
|
+
createAssignment(`${getRefParam(ref)}.${key}`, value);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function createObjectStringAssign(ref: number, key: string, value: string) {
|
|
217
|
+
createAssignment(`${getRefParam(ref)}[${quote(key)}]`, value);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function serializePrimitive(value: PrimitiveValue): string {
|
|
221
|
+
// Shortened forms
|
|
222
|
+
if (value === true) {
|
|
223
|
+
return '!0';
|
|
224
|
+
}
|
|
225
|
+
if (value === false) {
|
|
226
|
+
return '!1';
|
|
227
|
+
}
|
|
228
|
+
if (value === undefined) {
|
|
229
|
+
return 'void 0';
|
|
230
|
+
}
|
|
231
|
+
if (value === null) {
|
|
232
|
+
return 'null';
|
|
233
|
+
}
|
|
234
|
+
// negative 0 isn't the same as 0
|
|
235
|
+
if (Object.is(value, -0)) {
|
|
236
|
+
return '-0';
|
|
237
|
+
}
|
|
238
|
+
if (typeof value === 'bigint') {
|
|
239
|
+
return `BigInt("${value}")`;
|
|
240
|
+
}
|
|
241
|
+
if (typeof value === 'string') {
|
|
242
|
+
return quote(value);
|
|
243
|
+
}
|
|
244
|
+
if (value.constructor === Date) {
|
|
245
|
+
return `new Date("${value.toISOString()}")`;
|
|
246
|
+
}
|
|
247
|
+
return String(value);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function traverse(current: ServerValue): string | { referred: string } {
|
|
251
|
+
// Skip primitives
|
|
252
|
+
if (isPrimitive(current)) {
|
|
253
|
+
return serializePrimitive(current);
|
|
254
|
+
}
|
|
255
|
+
// Get reference of the current object
|
|
256
|
+
let refResult = REFS.get(current);
|
|
257
|
+
if (refResult != null) {
|
|
258
|
+
// Return the reference if already assigned a value
|
|
259
|
+
if (ASSIGNED_REFS.has(refResult)) {
|
|
260
|
+
return { referred: getRefParam(refResult) };
|
|
261
|
+
}
|
|
262
|
+
// Early sign (to prevent recursion)
|
|
263
|
+
ASSIGNED_REFS.add(refResult);
|
|
264
|
+
}
|
|
265
|
+
// Transform Set
|
|
266
|
+
if (constructorCheck<Set<ServerValue>>(current, Set)) {
|
|
267
|
+
if (current.size) {
|
|
268
|
+
const values: string[] = [];
|
|
269
|
+
|
|
270
|
+
STACK.add(current);
|
|
271
|
+
for (const value of current.keys()) {
|
|
272
|
+
if (STACK.has(value)) {
|
|
273
|
+
// This object received a ref, we must generate a setter ref
|
|
274
|
+
if (refResult == null) {
|
|
275
|
+
refResult = createRef(current);
|
|
276
|
+
}
|
|
277
|
+
// Received a ref, this might be a recursive ref, defer an assignment
|
|
278
|
+
createSetAdd(refResult, getRefParam(createRef(value)));
|
|
279
|
+
} else {
|
|
280
|
+
const result = traverse(value);
|
|
281
|
+
if (typeof result === 'string') {
|
|
282
|
+
values.push(result);
|
|
283
|
+
} else {
|
|
284
|
+
values.push(result.referred);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
STACK.delete(current);
|
|
289
|
+
|
|
290
|
+
const value = values.length ? `new Set([${join(values, ',')}])` : EMPTY_SET;
|
|
291
|
+
if (refResult != null) {
|
|
292
|
+
return assignRef(refResult, value);
|
|
293
|
+
}
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
return EMPTY_SET;
|
|
297
|
+
}
|
|
298
|
+
if (constructorCheck<Map<ServerValue, ServerValue>>(current, Map)) {
|
|
299
|
+
if (current.size) {
|
|
300
|
+
const values: string[] = [];
|
|
301
|
+
|
|
302
|
+
STACK.add(current);
|
|
303
|
+
for (const [key, value] of current.entries()) {
|
|
304
|
+
if (STACK.has(key)) {
|
|
305
|
+
if (refResult == null) {
|
|
306
|
+
refResult = createRef(current);
|
|
307
|
+
}
|
|
308
|
+
const keyRef = getRefParam(createRef(key));
|
|
309
|
+
if (STACK.has(value)) {
|
|
310
|
+
createMapSet(refResult, keyRef, getRefParam(createRef(value)));
|
|
311
|
+
} else {
|
|
312
|
+
const valueResult = traverse(value);
|
|
313
|
+
if (typeof valueResult === 'string') {
|
|
314
|
+
createMapSet(refResult, keyRef, valueResult);
|
|
315
|
+
} else {
|
|
316
|
+
createMapSet(refResult, keyRef, valueResult.referred);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} else if (STACK.has(value)) {
|
|
320
|
+
if (refResult == null) {
|
|
321
|
+
refResult = createRef(current);
|
|
322
|
+
}
|
|
323
|
+
const valueRef = getRefParam(createRef(value));
|
|
324
|
+
const keyResult = traverse(key);
|
|
325
|
+
if (typeof keyResult === 'string') {
|
|
326
|
+
createMapSet(refResult, keyResult, valueRef);
|
|
327
|
+
} else {
|
|
328
|
+
createMapSet(refResult, keyResult.referred, valueRef);
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
const keyResult = traverse(key);
|
|
332
|
+
const valueResult = traverse(value);
|
|
333
|
+
if (typeof keyResult === 'string') {
|
|
334
|
+
if (typeof valueResult === 'string') {
|
|
335
|
+
values.push(`[${keyResult},${valueResult}]`);
|
|
336
|
+
} else {
|
|
337
|
+
values.push(`[${keyResult},${valueResult.referred}]`);
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
if (typeof valueResult === 'string') {
|
|
341
|
+
values.push(`[${keyResult.referred},${valueResult}]`);
|
|
342
|
+
} else {
|
|
343
|
+
values.push(`[${keyResult.referred},${valueResult.referred}]`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
STACK.delete(current);
|
|
349
|
+
const value = values.length ? `new Map([${join(values, ',')}])` : EMPTY_MAP;
|
|
350
|
+
if (refResult != null) {
|
|
351
|
+
return assignRef(refResult, value);
|
|
352
|
+
}
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
return EMPTY_MAP;
|
|
356
|
+
}
|
|
357
|
+
if (Array.isArray(current)) {
|
|
358
|
+
if (current.length) {
|
|
359
|
+
let values = '';
|
|
360
|
+
|
|
361
|
+
STACK.add(current);
|
|
362
|
+
forEach(current, (item, i) => {
|
|
363
|
+
if (i in current) {
|
|
364
|
+
if (STACK.has(item)) {
|
|
365
|
+
if (refResult == null) {
|
|
366
|
+
refResult = createRef(current);
|
|
367
|
+
}
|
|
368
|
+
createArrayAssign(refResult, i, getRefParam(createRef(item)));
|
|
369
|
+
values += ',';
|
|
370
|
+
} else {
|
|
371
|
+
const result = traverse(item);
|
|
372
|
+
if (typeof result === 'string') {
|
|
373
|
+
values += result;
|
|
374
|
+
} else {
|
|
375
|
+
values += result.referred;
|
|
376
|
+
}
|
|
377
|
+
if (i < current.length - 1) {
|
|
378
|
+
values += ',';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
values += ',';
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
STACK.delete(current);
|
|
386
|
+
|
|
387
|
+
const value = `[${values}]`;
|
|
388
|
+
if (refResult != null) {
|
|
389
|
+
return assignRef(refResult, value);
|
|
390
|
+
}
|
|
391
|
+
return value;
|
|
392
|
+
}
|
|
393
|
+
return EMPTY_ARRAY;
|
|
394
|
+
}
|
|
395
|
+
if (current.constructor === Object) {
|
|
396
|
+
const values: string[] = [];
|
|
397
|
+
|
|
398
|
+
STACK.add(current);
|
|
399
|
+
forEach(Object.entries(current), ([key, value]) => {
|
|
400
|
+
const check = Number(key);
|
|
401
|
+
// Test if key is a valid number or JS identifier
|
|
402
|
+
// so that we don't have to serialize the key and wrap with brackets
|
|
403
|
+
if (Number.isNaN(check) || /^([$A-Z_][0-9A-Z_$]*)$/i.test(key)) {
|
|
404
|
+
if (STACK.has(value)) {
|
|
405
|
+
if (refResult == null) {
|
|
406
|
+
refResult = createRef(current);
|
|
407
|
+
}
|
|
408
|
+
createObjectIdentifierAssign(refResult, key, getRefParam(createRef(value)));
|
|
409
|
+
} else {
|
|
410
|
+
const result = traverse(value);
|
|
411
|
+
if (typeof result === 'string') {
|
|
412
|
+
values.push(`${key}:${result}`);
|
|
413
|
+
} else {
|
|
414
|
+
values.push(`${key}:${result.referred}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} else if (STACK.has(value)) {
|
|
418
|
+
if (refResult == null) {
|
|
419
|
+
refResult = createRef(current);
|
|
420
|
+
}
|
|
421
|
+
createObjectStringAssign(refResult, key, getRefParam(createRef(value)));
|
|
422
|
+
} else {
|
|
423
|
+
const result = traverse(value);
|
|
424
|
+
if (typeof result === 'string') {
|
|
425
|
+
values.push(`${quote(key)}:${result}`);
|
|
426
|
+
} else {
|
|
427
|
+
values.push(`${quote(key)}:${result.referred}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
STACK.delete(current);
|
|
432
|
+
|
|
433
|
+
const value = `{${join(values, ',')}}`;
|
|
434
|
+
if (refResult != null) {
|
|
435
|
+
return assignRef(refResult, value);
|
|
436
|
+
}
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
throw new Error('Unserializable value');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export default function serialize(source: ServerValue) {
|
|
443
|
+
if (isPrimitive(source)) {
|
|
444
|
+
return serializePrimitive(source);
|
|
445
|
+
}
|
|
446
|
+
reset();
|
|
447
|
+
// Lookup possible shared references
|
|
448
|
+
lookupRefs(source);
|
|
449
|
+
// Add shared references to refs
|
|
450
|
+
for (const [key, value] of REF_COUNT.entries()) {
|
|
451
|
+
if (value > 1) {
|
|
452
|
+
createRef(key);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Get top-level serialization
|
|
456
|
+
const result = traverse(source);
|
|
457
|
+
if (typeof result !== 'string') {
|
|
458
|
+
throw new Error('Unreachable error');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Shared references detected
|
|
462
|
+
if (VARS.length) {
|
|
463
|
+
// Get (or create) a ref from the source
|
|
464
|
+
const index = getRefParam(createRef(source));
|
|
465
|
+
const patches = resolvePatches();
|
|
466
|
+
const params = VARS.length > 1
|
|
467
|
+
? `(${join(VARS, ',')})`
|
|
468
|
+
: VARS[0];
|
|
469
|
+
// Source is probably already assigned
|
|
470
|
+
if (result.startsWith(index)) {
|
|
471
|
+
return `(${params}=>(${result},${patches}${index}))()`;
|
|
472
|
+
}
|
|
473
|
+
return `(${params}=>(${index}=${result},${patches}${index}))()`;
|
|
474
|
+
}
|
|
475
|
+
if (source.constructor === Object) {
|
|
476
|
+
return `(${result})`;
|
|
477
|
+
}
|
|
478
|
+
return result;
|
|
479
|
+
}
|
package/src/quote.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Written by https://github.com/DylanPiercey and is distributed under the MIT license.
|
|
2
|
+
// Creates a JavaScript double quoted string and escapes all characters
|
|
3
|
+
// not listed as DoubleStringCharacters on
|
|
4
|
+
// Also includes "<" to escape "</script>" and "\" to avoid invalid escapes in the output.
|
|
5
|
+
// http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
|
|
6
|
+
export default function quote(str: string) {
|
|
7
|
+
let result = '';
|
|
8
|
+
let lastPos = 0;
|
|
9
|
+
for (let i = 0, len = str.length; i < len; i++) {
|
|
10
|
+
let replacement;
|
|
11
|
+
switch (str[i]) {
|
|
12
|
+
case '"':
|
|
13
|
+
replacement = '\\"';
|
|
14
|
+
break;
|
|
15
|
+
case '\\':
|
|
16
|
+
replacement = '\\\\';
|
|
17
|
+
break;
|
|
18
|
+
case '<':
|
|
19
|
+
replacement = '\\x3C';
|
|
20
|
+
break;
|
|
21
|
+
case '\n':
|
|
22
|
+
replacement = '\\n';
|
|
23
|
+
break;
|
|
24
|
+
case '\r':
|
|
25
|
+
replacement = '\\r';
|
|
26
|
+
break;
|
|
27
|
+
case '\u2028':
|
|
28
|
+
replacement = '\\u2028';
|
|
29
|
+
break;
|
|
30
|
+
case '\u2029':
|
|
31
|
+
replacement = '\\u2029';
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
result += str.slice(lastPos, i) + replacement;
|
|
37
|
+
lastPos = i + 1;
|
|
38
|
+
}
|
|
39
|
+
if (lastPos === 0) {
|
|
40
|
+
result = str;
|
|
41
|
+
} else {
|
|
42
|
+
result += str.slice(lastPos);
|
|
43
|
+
}
|
|
44
|
+
return `"${result}"`;
|
|
45
|
+
}
|