seroval 0.4.0-alpha.3 → 0.4.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,563 @@
1
+ /* eslint-disable @typescript-eslint/no-use-before-define */
2
+ import { Feature } from '../compat';
3
+ import {
4
+ SerializationContext,
5
+ Assignment,
6
+ getRefParam,
7
+ markRef,
8
+ } from '../context';
9
+ import quote from '../quote';
10
+ import { isReferenceInStack } from './shared';
11
+ import { SerovalDictionaryNode, SerovalNode, SerovalNodeType } from './types';
12
+
13
+ function getAssignmentExpression(assignment: Assignment): string {
14
+ switch (assignment.type) {
15
+ case 'index':
16
+ return `${assignment.source}=${assignment.value}`;
17
+ case 'map':
18
+ return `${assignment.source}.set(${assignment.key},${assignment.value})`;
19
+ case 'set':
20
+ return `${assignment.source}.add(${assignment.value})`;
21
+ default:
22
+ return '';
23
+ }
24
+ }
25
+
26
+ function mergeAssignments(assignments: Assignment[]) {
27
+ const newAssignments = [];
28
+ let current = assignments[0];
29
+ let prev = current;
30
+ for (let i = 1, len = assignments.length; i < len; i++) {
31
+ const item = assignments[i];
32
+ if (item.type === prev.type) {
33
+ if (item.type === 'index' && item.value === prev.value) {
34
+ // Merge if the right-hand value is the same
35
+ // saves at least 2 chars
36
+ current = {
37
+ type: 'index',
38
+ source: item.source,
39
+ value: getAssignmentExpression(current),
40
+ };
41
+ } else if (item.type === 'map' && item.source === prev.source) {
42
+ // Maps has chaining methods, merge if source is the same
43
+ current = {
44
+ type: 'map',
45
+ source: getAssignmentExpression(current),
46
+ key: item.key,
47
+ value: item.value,
48
+ };
49
+ } else if (item.type === 'set' && item.source === prev.source) {
50
+ // Sets has chaining methods too
51
+ current = {
52
+ type: 'set',
53
+ source: getAssignmentExpression(current),
54
+ value: item.value,
55
+ };
56
+ } else {
57
+ // Different assignment, push current
58
+ newAssignments.push(current);
59
+ current = item;
60
+ }
61
+ } else {
62
+ newAssignments.push(current);
63
+ current = item;
64
+ }
65
+ prev = item;
66
+ }
67
+
68
+ newAssignments.push(current);
69
+
70
+ return newAssignments;
71
+ }
72
+
73
+ function resolveAssignments(assignments: Assignment[]) {
74
+ if (assignments.length) {
75
+ let result = '';
76
+ const merged = mergeAssignments(assignments);
77
+ for (let i = 0, len = merged.length; i < len; i++) {
78
+ result += `${getAssignmentExpression(merged[i])},`;
79
+ }
80
+ return result;
81
+ }
82
+ return undefined;
83
+ }
84
+
85
+ export function resolvePatches(ctx: SerializationContext) {
86
+ return resolveAssignments(ctx.assignments);
87
+ }
88
+
89
+ /**
90
+ * Generates the inlined assignment for the reference
91
+ * This is different from the assignments array as this one
92
+ * signifies creation rather than mutation
93
+ */
94
+ function assignRef(
95
+ ctx: SerializationContext,
96
+ index: number,
97
+ value: string,
98
+ ) {
99
+ if (ctx.markedRefs.has(index)) {
100
+ return `${getRefParam(ctx, index)}=${value}`;
101
+ }
102
+ return value;
103
+ }
104
+
105
+ function createAssignment(
106
+ ctx: SerializationContext,
107
+ source: string,
108
+ value: string,
109
+ ) {
110
+ ctx.assignments.push({
111
+ type: 'index',
112
+ source,
113
+ value,
114
+ });
115
+ }
116
+
117
+ function createSetAdd(
118
+ ctx: SerializationContext,
119
+ ref: number,
120
+ value: string,
121
+ ) {
122
+ markRef(ctx, ref);
123
+ ctx.assignments.push({
124
+ type: 'set',
125
+ source: getRefParam(ctx, ref),
126
+ value,
127
+ });
128
+ }
129
+
130
+ function createMapSet(
131
+ ctx: SerializationContext,
132
+ ref: number,
133
+ key: string,
134
+ value: string,
135
+ ) {
136
+ markRef(ctx, ref);
137
+ ctx.assignments.push({
138
+ type: 'map',
139
+ source: getRefParam(ctx, ref),
140
+ key,
141
+ value,
142
+ });
143
+ }
144
+
145
+ function createArrayAssign(
146
+ ctx: SerializationContext,
147
+ ref: number,
148
+ index: number,
149
+ value: string,
150
+ ) {
151
+ markRef(ctx, ref);
152
+ createAssignment(ctx, `${getRefParam(ctx, ref)}[${index}]`, value);
153
+ }
154
+
155
+ function createObjectAssign(
156
+ ctx: SerializationContext,
157
+ ref: number,
158
+ key: string,
159
+ value: string,
160
+ computed: boolean,
161
+ ) {
162
+ markRef(ctx, ref);
163
+ const member = computed ? `[${key}]` : `.${key}`;
164
+ createAssignment(ctx, `${getRefParam(ctx, ref)}${member}`, value);
165
+ }
166
+
167
+ const IDENTIFIER_CHECK = /^([$A-Z_][0-9A-Z_$]*)$/i;
168
+
169
+ function serializeAssignments(
170
+ ctx: SerializationContext,
171
+ targetRef: number,
172
+ [keys, values, size]: SerovalDictionaryNode,
173
+ ) {
174
+ ctx.stack.push(targetRef);
175
+ const mainAssignments: Assignment[] = [];
176
+ for (let i = 0; i < size; i++) {
177
+ const parentStack = ctx.stack;
178
+ ctx.stack = [];
179
+ const refParam = serializeTree(ctx, values[i]);
180
+ ctx.stack = parentStack;
181
+ const key = keys[i];
182
+ const check = Number(key);
183
+ const parentAssignment = ctx.assignments;
184
+ ctx.assignments = mainAssignments;
185
+ // Test if key is a valid number or JS identifier
186
+ // so that we don't have to serialize the key and wrap with brackets
187
+ const isIdentifier = check >= 0 || IDENTIFIER_CHECK.test(key);
188
+ createObjectAssign(
189
+ ctx,
190
+ targetRef,
191
+ isIdentifier ? key : quote(key),
192
+ refParam,
193
+ !(isIdentifier && Number.isNaN(check)),
194
+ );
195
+ ctx.assignments = parentAssignment;
196
+ }
197
+ ctx.stack.pop();
198
+ return resolveAssignments(mainAssignments);
199
+ }
200
+
201
+ function serializeObject(
202
+ ctx: SerializationContext,
203
+ sourceID: number,
204
+ [keys, values, size]: SerovalDictionaryNode,
205
+ ) {
206
+ if (size === 0) {
207
+ return '{}';
208
+ }
209
+ let result = '';
210
+ ctx.stack.push(sourceID);
211
+ for (let i = 0; i < size; i++) {
212
+ const key = keys[i];
213
+ const val = values[i];
214
+ const check = Number(key);
215
+ // Test if key is a valid number or JS identifier
216
+ // so that we don't have to serialize the key and wrap with brackets
217
+ const isIdentifier = check >= 0 || IDENTIFIER_CHECK.test(key);
218
+ const validKey = isIdentifier ? key : quote(key);
219
+ if (isReferenceInStack(ctx, val)) {
220
+ const refParam = getRefParam(ctx, val[1]);
221
+ createObjectAssign(
222
+ ctx,
223
+ sourceID,
224
+ validKey,
225
+ refParam,
226
+ !(isIdentifier && Number.isNaN(check)),
227
+ );
228
+ } else {
229
+ result += `${validKey}:${serializeTree(ctx, val)},`;
230
+ }
231
+ }
232
+ ctx.stack.pop();
233
+ return `{${result.substring(0, result.length - 1)}}`;
234
+ }
235
+
236
+ function serializePromise(
237
+ ctx: SerializationContext,
238
+ value: SerovalNode,
239
+ id: number,
240
+ ) {
241
+ let serialized: string;
242
+ // Check if resolved value is a parent expression
243
+ if (isReferenceInStack(ctx, value)) {
244
+ // A Promise trick, reference the value
245
+ // inside the `then` expression so that
246
+ // the Promise evaluates after the parent
247
+ // has initialized
248
+ serialized = `Promise.resolve().then(()=>${getRefParam(ctx, value[1])})`;
249
+ } else {
250
+ ctx.stack.push(id);
251
+ const result = serializeTree(ctx, value);
252
+ ctx.stack.pop();
253
+ // just inline the value/reference here
254
+ serialized = `Promise.resolve(${result})`;
255
+ }
256
+ return assignRef(ctx, id, serialized);
257
+ }
258
+
259
+ function serializeTypedArray(
260
+ ctx: SerializationContext,
261
+ constructor: string,
262
+ init: string,
263
+ byteOffset: number,
264
+ id: number,
265
+ ) {
266
+ let args = `[${init}]`;
267
+ if (byteOffset !== 0) {
268
+ args += `,${byteOffset}`;
269
+ }
270
+ return assignRef(ctx, id, `new ${constructor}(${args})`);
271
+ }
272
+
273
+ function serializeSet(
274
+ ctx: SerializationContext,
275
+ value: SerovalNode[],
276
+ id: number,
277
+ ) {
278
+ let serialized = 'new Set';
279
+ const size = value.length;
280
+ if (size) {
281
+ let result = '';
282
+ ctx.stack.push(id);
283
+ for (let i = 0; i < size; i++) {
284
+ const item = value[i];
285
+ if (isReferenceInStack(ctx, item)) {
286
+ createSetAdd(ctx, id, getRefParam(ctx, item[1]));
287
+ } else {
288
+ // Push directly
289
+ result += `${serializeTree(ctx, item)},`;
290
+ }
291
+ }
292
+ ctx.stack.pop();
293
+ if (result) {
294
+ serialized += `([${result.substring(0, result.length - 1)}])`;
295
+ }
296
+ }
297
+ return assignRef(ctx, id, serialized);
298
+ }
299
+
300
+ function serializeMap(
301
+ ctx: SerializationContext,
302
+ keys: SerovalNode[],
303
+ values: SerovalNode[],
304
+ size: number,
305
+ id: number,
306
+ ) {
307
+ let serialized = 'new Map';
308
+ if (size) {
309
+ let result = '';
310
+ ctx.stack.push(id);
311
+ for (let i = 0; i < size; i++) {
312
+ // Check if key is a parent
313
+ const key = keys[i];
314
+ const val = values[i];
315
+ if (isReferenceInStack(ctx, key)) {
316
+ // Create reference for the map instance
317
+ const keyRef = getRefParam(ctx, key[1]);
318
+ // Check if value is a parent
319
+ if (isReferenceInStack(ctx, val)) {
320
+ const valueRef = getRefParam(ctx, val[1]);
321
+ // Register an assignment since
322
+ // both key and value are a parent of this
323
+ // Map instance
324
+ createMapSet(ctx, id, keyRef, valueRef);
325
+ } else {
326
+ // Reset the stack
327
+ // This is required because the serialized
328
+ // value is no longer part of the expression
329
+ // tree and has been moved to the deferred
330
+ // assignment
331
+ const parent = ctx.stack;
332
+ ctx.stack = [];
333
+ createMapSet(ctx, id, keyRef, serializeTree(ctx, val));
334
+ ctx.stack = parent;
335
+ }
336
+ } else if (isReferenceInStack(ctx, val)) {
337
+ // Create ref for the Map instance
338
+ const valueRef = getRefParam(ctx, val[1]);
339
+ // Reset stack for the key serialization
340
+ const parent = ctx.stack;
341
+ ctx.stack = [];
342
+ createMapSet(ctx, id, serializeTree(ctx, key), valueRef);
343
+ ctx.stack = parent;
344
+ } else {
345
+ result += `[${serializeTree(ctx, key)},${serializeTree(ctx, val)}],`;
346
+ }
347
+ }
348
+ ctx.stack.pop();
349
+ // Check if there are any values
350
+ // so that the empty Map constructor
351
+ // can be used instead
352
+ if (result) {
353
+ serialized += `([${result.substring(0, result.length - 1)}])`;
354
+ }
355
+ }
356
+ return assignRef(ctx, id, serialized);
357
+ }
358
+
359
+ function serializeNodeList(
360
+ ctx: SerializationContext,
361
+ value: SerovalNode[],
362
+ id: number,
363
+ ) {
364
+ // This is different than Map and Set
365
+ // because we also need to serialize
366
+ // the holes of the Array
367
+ const size = value.length;
368
+ let values = '';
369
+ for (let i = 0; i < size; i++) {
370
+ const item = value[i];
371
+ // Check if index is a hole
372
+ if (item) {
373
+ // Check if item is a parent
374
+ if (isReferenceInStack(ctx, item)) {
375
+ createArrayAssign(ctx, id, i, getRefParam(ctx, item[1]));
376
+ values += ',';
377
+ } else {
378
+ values += serializeTree(ctx, item);
379
+ if (i < size - 1) {
380
+ values += ',';
381
+ }
382
+ }
383
+ } else {
384
+ // Add an empty item
385
+ values += ',';
386
+ }
387
+ }
388
+ return `[${values}]`;
389
+ }
390
+
391
+ function serializeArray(
392
+ ctx: SerializationContext,
393
+ value: SerovalNode[],
394
+ id: number,
395
+ ) {
396
+ ctx.stack.push(id);
397
+ const result = serializeNodeList(ctx, value, id);
398
+ ctx.stack.pop();
399
+ return assignRef(ctx, id, result);
400
+ }
401
+
402
+ function serializeWithObjectAssign(
403
+ ctx: SerializationContext,
404
+ value: SerovalDictionaryNode,
405
+ id: number,
406
+ serialized: string,
407
+ ) {
408
+ const fields = serializeObject(ctx, id, value);
409
+ if (fields !== '{}') {
410
+ return `Object.assign(${serialized},${fields})`;
411
+ }
412
+ return serialized;
413
+ }
414
+
415
+ function serializeAggregateError(
416
+ ctx: SerializationContext,
417
+ message: string,
418
+ options: SerovalDictionaryNode | undefined,
419
+ errors: SerovalNode,
420
+ id: number,
421
+ ) {
422
+ // Serialize the required arguments
423
+ ctx.stack.push(id);
424
+ let serialized = `new AggregateError(${serializeTree(ctx, errors)},${quote(message)})`;
425
+ ctx.stack.pop();
426
+ // `AggregateError` might've been extended
427
+ // either through class or custom properties
428
+ // Make sure to assign extra properties
429
+ if (options) {
430
+ if (ctx.features & Feature.ObjectAssign) {
431
+ serialized = serializeWithObjectAssign(ctx, options, id, serialized);
432
+ } else {
433
+ markRef(ctx, id);
434
+ const assignments = serializeAssignments(ctx, id, options);
435
+ if (assignments) {
436
+ const ref = getRefParam(ctx, id);
437
+ return `(${assignRef(ctx, id, serialized)},${assignments}${ref})`;
438
+ }
439
+ }
440
+ }
441
+ return assignRef(ctx, id, serialized);
442
+ }
443
+
444
+ function serializeError(
445
+ ctx: SerializationContext,
446
+ constructor: string,
447
+ message: string,
448
+ options: SerovalDictionaryNode | undefined,
449
+ id: number,
450
+ ) {
451
+ let serialized = `new ${constructor}(${quote(message)})`;
452
+ if (options) {
453
+ if (ctx.features & Feature.ObjectAssign) {
454
+ serialized = serializeWithObjectAssign(ctx, options, id, serialized);
455
+ } else {
456
+ markRef(ctx, id);
457
+ const assignments = serializeAssignments(ctx, id, options);
458
+ if (assignments) {
459
+ const ref = getRefParam(ctx, id);
460
+ return `(${assignRef(ctx, id, serialized)},${assignments}${ref})`;
461
+ }
462
+ }
463
+ }
464
+ return assignRef(ctx, id, serialized);
465
+ }
466
+
467
+ function serializeIterable(
468
+ ctx: SerializationContext,
469
+ options: SerovalDictionaryNode | undefined,
470
+ items: SerovalNode[],
471
+ id: number,
472
+ ) {
473
+ const parent = ctx.stack;
474
+ ctx.stack = [];
475
+ const values = serializeNodeList(ctx, items, id);
476
+ ctx.stack = parent;
477
+ let serialized: string;
478
+ if (ctx.features & Feature.ArrayPrototypeValues) {
479
+ serialized = `${values}.values()`;
480
+ } else {
481
+ serialized = `${values}[Symbol.iterator]()`;
482
+ }
483
+ if (ctx.features & Feature.ArrowFunction) {
484
+ serialized = `{[Symbol.iterator]:()=>${serialized}}`;
485
+ } else if (ctx.features & Feature.MethodShorthand) {
486
+ serialized = `{[Symbol.iterator](){return ${serialized}}}`;
487
+ } else {
488
+ serialized = `{[Symbol.iterator]:function(){return ${serialized}}}`;
489
+ }
490
+ if (options) {
491
+ if (ctx.features & Feature.ObjectAssign) {
492
+ serialized = serializeWithObjectAssign(ctx, options, id, serialized);
493
+ } else {
494
+ markRef(ctx, id);
495
+ const assignments = serializeAssignments(ctx, id, options);
496
+ if (assignments) {
497
+ const ref = getRefParam(ctx, id);
498
+ return `(${assignRef(ctx, id, serialized)},${assignments}${ref})`;
499
+ }
500
+ }
501
+ }
502
+ return assignRef(ctx, id, serialized);
503
+ }
504
+
505
+ function serializeNullConstructor(
506
+ ctx: SerializationContext,
507
+ value: SerovalDictionaryNode,
508
+ id: number,
509
+ ) {
510
+ let serialized = 'Object.create(null)';
511
+ if (ctx.features & Feature.ObjectAssign) {
512
+ serialized = serializeWithObjectAssign(ctx, value, id, serialized);
513
+ } else {
514
+ markRef(ctx, id);
515
+ const assignments = serializeAssignments(ctx, id, value);
516
+ if (assignments) {
517
+ const ref = getRefParam(ctx, id);
518
+ return `(${assignRef(ctx, id, serialized)},${assignments}${ref})`;
519
+ }
520
+ }
521
+ return assignRef(ctx, id, serialized);
522
+ }
523
+
524
+ export default function serializeTree(
525
+ ctx: SerializationContext,
526
+ [type, value, id]: SerovalNode,
527
+ ): string {
528
+ switch (type) {
529
+ case SerovalNodeType.Primitive:
530
+ return String(value);
531
+ case SerovalNodeType.Reference:
532
+ return getRefParam(ctx, value);
533
+ case SerovalNodeType.Promise:
534
+ return serializePromise(ctx, value, id);
535
+ case SerovalNodeType.BigInt:
536
+ return value;
537
+ case SerovalNodeType.Date:
538
+ return assignRef(ctx, id, `new Date("${value}")`);
539
+ case SerovalNodeType.RegExp:
540
+ return assignRef(ctx, id, value);
541
+ case SerovalNodeType.BigIntTypedArray:
542
+ case SerovalNodeType.TypedArray:
543
+ return serializeTypedArray(ctx, value[0], value[1], value[2], id);
544
+ case SerovalNodeType.Set:
545
+ return serializeSet(ctx, value, id);
546
+ case SerovalNodeType.Map:
547
+ return serializeMap(ctx, value[0], value[1], value[2], id);
548
+ case SerovalNodeType.Array:
549
+ return serializeArray(ctx, value, id);
550
+ case SerovalNodeType.AggregateError:
551
+ return serializeAggregateError(ctx, value[0], value[1], value[2], id);
552
+ case SerovalNodeType.Error:
553
+ return serializeError(ctx, value[0], value[1], value[2], id);
554
+ case SerovalNodeType.Iterable:
555
+ return serializeIterable(ctx, value[0], value[1], id);
556
+ case SerovalNodeType.NullConstructor:
557
+ return serializeNullConstructor(ctx, value, id);
558
+ case SerovalNodeType.Object:
559
+ return assignRef(ctx, id, serializeObject(ctx, id, value));
560
+ default:
561
+ throw new Error('Unsupported type');
562
+ }
563
+ }