seroval 0.4.0-alpha.4 → 0.4.0-alpha.6

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,558 @@
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
+ record: SerovalDictionaryNode,
173
+ ) {
174
+ ctx.stack.push(targetRef);
175
+ const mainAssignments: Assignment[] = [];
176
+ for (const key of Object.keys(record)) {
177
+ const parentStack = ctx.stack;
178
+ ctx.stack = [];
179
+ const refParam = serializeTree(ctx, record[key]);
180
+ ctx.stack = parentStack;
181
+ const check = Number(key);
182
+ const parentAssignment = ctx.assignments;
183
+ ctx.assignments = mainAssignments;
184
+ // Test if key is a valid number or JS identifier
185
+ // so that we don't have to serialize the key and wrap with brackets
186
+ const isIdentifier = check >= 0 || IDENTIFIER_CHECK.test(key);
187
+ createObjectAssign(
188
+ ctx,
189
+ targetRef,
190
+ isIdentifier ? key : quote(key),
191
+ refParam,
192
+ !(isIdentifier && Number.isNaN(check)),
193
+ );
194
+ ctx.assignments = parentAssignment;
195
+ }
196
+ ctx.stack.pop();
197
+ return resolveAssignments(mainAssignments);
198
+ }
199
+
200
+ function serializeDictionary(
201
+ ctx: SerializationContext,
202
+ sourceID: number,
203
+ record: SerovalDictionaryNode,
204
+ ) {
205
+ let result = '';
206
+ ctx.stack.push(sourceID);
207
+ for (const key of Object.keys(record)) {
208
+ const val = record[key];
209
+ const check = Number(key);
210
+ // Test if key is a valid number or JS identifier
211
+ // so that we don't have to serialize the key and wrap with brackets
212
+ const isIdentifier = check >= 0 || IDENTIFIER_CHECK.test(key);
213
+ const validKey = isIdentifier ? key : quote(key);
214
+ if (isReferenceInStack(ctx, val)) {
215
+ const refParam = getRefParam(ctx, val[1]);
216
+ createObjectAssign(
217
+ ctx,
218
+ sourceID,
219
+ validKey,
220
+ refParam,
221
+ !(isIdentifier && Number.isNaN(check)),
222
+ );
223
+ } else {
224
+ result += `${validKey}:${serializeTree(ctx, val)},`;
225
+ }
226
+ }
227
+ ctx.stack.pop();
228
+ return `{${result.substring(0, result.length - 1)}}`;
229
+ }
230
+
231
+ function serializePromise(
232
+ ctx: SerializationContext,
233
+ value: SerovalNode,
234
+ id: number,
235
+ ) {
236
+ let serialized: string;
237
+ // Check if resolved value is a parent expression
238
+ if (isReferenceInStack(ctx, value)) {
239
+ // A Promise trick, reference the value
240
+ // inside the `then` expression so that
241
+ // the Promise evaluates after the parent
242
+ // has initialized
243
+ serialized = `Promise.resolve().then(()=>${getRefParam(ctx, value[1])})`;
244
+ } else {
245
+ ctx.stack.push(id);
246
+ const result = serializeTree(ctx, value);
247
+ ctx.stack.pop();
248
+ // just inline the value/reference here
249
+ serialized = `Promise.resolve(${result})`;
250
+ }
251
+ return assignRef(ctx, id, serialized);
252
+ }
253
+
254
+ function serializeTypedArray(
255
+ ctx: SerializationContext,
256
+ constructor: string,
257
+ init: string,
258
+ byteOffset: number,
259
+ id: number,
260
+ ) {
261
+ let args = `[${init}]`;
262
+ if (byteOffset !== 0) {
263
+ args += `,${byteOffset}`;
264
+ }
265
+ return assignRef(ctx, id, `new ${constructor}(${args})`);
266
+ }
267
+
268
+ function serializeSet(
269
+ ctx: SerializationContext,
270
+ value: SerovalNode[],
271
+ id: number,
272
+ ) {
273
+ let serialized = 'new Set';
274
+ const size = value.length;
275
+ if (size) {
276
+ let result = '';
277
+ ctx.stack.push(id);
278
+ for (let i = 0; i < size; i++) {
279
+ const item = value[i];
280
+ if (isReferenceInStack(ctx, item)) {
281
+ createSetAdd(ctx, id, getRefParam(ctx, item[1]));
282
+ } else {
283
+ // Push directly
284
+ result += `${serializeTree(ctx, item)},`;
285
+ }
286
+ }
287
+ ctx.stack.pop();
288
+ if (result) {
289
+ serialized += `([${result.substring(0, result.length - 1)}])`;
290
+ }
291
+ }
292
+ return assignRef(ctx, id, serialized);
293
+ }
294
+
295
+ function serializeMap(
296
+ ctx: SerializationContext,
297
+ keys: SerovalNode[],
298
+ values: SerovalNode[],
299
+ size: number,
300
+ id: number,
301
+ ) {
302
+ let serialized = 'new Map';
303
+ if (size) {
304
+ let result = '';
305
+ ctx.stack.push(id);
306
+ for (let i = 0; i < size; i++) {
307
+ // Check if key is a parent
308
+ const key = keys[i];
309
+ const val = values[i];
310
+ if (isReferenceInStack(ctx, key)) {
311
+ // Create reference for the map instance
312
+ const keyRef = getRefParam(ctx, key[1]);
313
+ // Check if value is a parent
314
+ if (isReferenceInStack(ctx, val)) {
315
+ const valueRef = getRefParam(ctx, val[1]);
316
+ // Register an assignment since
317
+ // both key and value are a parent of this
318
+ // Map instance
319
+ createMapSet(ctx, id, keyRef, valueRef);
320
+ } else {
321
+ // Reset the stack
322
+ // This is required because the serialized
323
+ // value is no longer part of the expression
324
+ // tree and has been moved to the deferred
325
+ // assignment
326
+ const parent = ctx.stack;
327
+ ctx.stack = [];
328
+ createMapSet(ctx, id, keyRef, serializeTree(ctx, val));
329
+ ctx.stack = parent;
330
+ }
331
+ } else if (isReferenceInStack(ctx, val)) {
332
+ // Create ref for the Map instance
333
+ const valueRef = getRefParam(ctx, val[1]);
334
+ // Reset stack for the key serialization
335
+ const parent = ctx.stack;
336
+ ctx.stack = [];
337
+ createMapSet(ctx, id, serializeTree(ctx, key), valueRef);
338
+ ctx.stack = parent;
339
+ } else {
340
+ result += `[${serializeTree(ctx, key)},${serializeTree(ctx, val)}],`;
341
+ }
342
+ }
343
+ ctx.stack.pop();
344
+ // Check if there are any values
345
+ // so that the empty Map constructor
346
+ // can be used instead
347
+ if (result) {
348
+ serialized += `([${result.substring(0, result.length - 1)}])`;
349
+ }
350
+ }
351
+ return assignRef(ctx, id, serialized);
352
+ }
353
+
354
+ function serializeNodeList(
355
+ ctx: SerializationContext,
356
+ value: SerovalNode[],
357
+ id: number,
358
+ ) {
359
+ // This is different than Map and Set
360
+ // because we also need to serialize
361
+ // the holes of the Array
362
+ const size = value.length;
363
+ let values = '';
364
+ for (let i = 0; i < size; i++) {
365
+ const item = value[i];
366
+ // Check if index is a hole
367
+ if (item) {
368
+ // Check if item is a parent
369
+ if (isReferenceInStack(ctx, item)) {
370
+ createArrayAssign(ctx, id, i, getRefParam(ctx, item[1]));
371
+ values += ',';
372
+ } else {
373
+ values += serializeTree(ctx, item);
374
+ if (i < size - 1) {
375
+ values += ',';
376
+ }
377
+ }
378
+ } else {
379
+ // Add an empty item
380
+ values += ',';
381
+ }
382
+ }
383
+ return `[${values}]`;
384
+ }
385
+
386
+ function serializeArray(
387
+ ctx: SerializationContext,
388
+ value: SerovalNode[],
389
+ id: number,
390
+ ) {
391
+ ctx.stack.push(id);
392
+ const result = serializeNodeList(ctx, value, id);
393
+ ctx.stack.pop();
394
+ return assignRef(ctx, id, result);
395
+ }
396
+
397
+ function serializeWithObjectAssign(
398
+ ctx: SerializationContext,
399
+ value: SerovalDictionaryNode,
400
+ id: number,
401
+ serialized: string,
402
+ ) {
403
+ const fields = serializeDictionary(ctx, id, value);
404
+ if (fields !== '{}') {
405
+ return `Object.assign(${serialized},${fields})`;
406
+ }
407
+ return serialized;
408
+ }
409
+
410
+ function serializeAggregateError(
411
+ ctx: SerializationContext,
412
+ message: string,
413
+ options: SerovalDictionaryNode | undefined,
414
+ errors: SerovalNode,
415
+ id: number,
416
+ ) {
417
+ // Serialize the required arguments
418
+ ctx.stack.push(id);
419
+ let serialized = `new AggregateError(${serializeTree(ctx, errors)},${quote(message)})`;
420
+ ctx.stack.pop();
421
+ // `AggregateError` might've been extended
422
+ // either through class or custom properties
423
+ // Make sure to assign extra properties
424
+ if (options) {
425
+ if (ctx.features & Feature.ObjectAssign) {
426
+ serialized = serializeWithObjectAssign(ctx, options, id, serialized);
427
+ } else {
428
+ markRef(ctx, id);
429
+ const assignments = serializeAssignments(ctx, id, options);
430
+ if (assignments) {
431
+ const ref = getRefParam(ctx, id);
432
+ return `(${assignRef(ctx, id, serialized)},${assignments}${ref})`;
433
+ }
434
+ }
435
+ }
436
+ return assignRef(ctx, id, serialized);
437
+ }
438
+
439
+ function serializeError(
440
+ ctx: SerializationContext,
441
+ constructor: string,
442
+ message: string,
443
+ options: SerovalDictionaryNode | undefined,
444
+ id: number,
445
+ ) {
446
+ let serialized = `new ${constructor}(${quote(message)})`;
447
+ if (options) {
448
+ if (ctx.features & Feature.ObjectAssign) {
449
+ serialized = serializeWithObjectAssign(ctx, options, id, serialized);
450
+ } else {
451
+ markRef(ctx, id);
452
+ const assignments = serializeAssignments(ctx, id, options);
453
+ if (assignments) {
454
+ const ref = getRefParam(ctx, id);
455
+ return `(${assignRef(ctx, id, serialized)},${assignments}${ref})`;
456
+ }
457
+ }
458
+ }
459
+ return assignRef(ctx, id, serialized);
460
+ }
461
+
462
+ function serializeIterable(
463
+ ctx: SerializationContext,
464
+ options: SerovalDictionaryNode | undefined,
465
+ items: SerovalNode[],
466
+ id: number,
467
+ ) {
468
+ const parent = ctx.stack;
469
+ ctx.stack = [];
470
+ const values = serializeNodeList(ctx, items, id);
471
+ ctx.stack = parent;
472
+ let serialized: string;
473
+ if (ctx.features & Feature.ArrayPrototypeValues) {
474
+ serialized = `${values}.values()`;
475
+ } else {
476
+ serialized = `${values}[Symbol.iterator]()`;
477
+ }
478
+ if (ctx.features & Feature.ArrowFunction) {
479
+ serialized = `{[Symbol.iterator]:()=>${serialized}}`;
480
+ } else if (ctx.features & Feature.MethodShorthand) {
481
+ serialized = `{[Symbol.iterator](){return ${serialized}}}`;
482
+ } else {
483
+ serialized = `{[Symbol.iterator]:function(){return ${serialized}}}`;
484
+ }
485
+ if (options) {
486
+ if (ctx.features & Feature.ObjectAssign) {
487
+ serialized = serializeWithObjectAssign(ctx, options, id, serialized);
488
+ } else {
489
+ markRef(ctx, id);
490
+ const assignments = serializeAssignments(ctx, id, options);
491
+ if (assignments) {
492
+ const ref = getRefParam(ctx, id);
493
+ return `(${assignRef(ctx, id, serialized)},${assignments}${ref})`;
494
+ }
495
+ }
496
+ }
497
+ return assignRef(ctx, id, serialized);
498
+ }
499
+
500
+ function serializeNullConstructor(
501
+ ctx: SerializationContext,
502
+ value: SerovalDictionaryNode,
503
+ id: number,
504
+ ) {
505
+ let serialized = 'Object.create(null)';
506
+ if (ctx.features & Feature.ObjectAssign) {
507
+ serialized = serializeWithObjectAssign(ctx, value, id, serialized);
508
+ } else {
509
+ markRef(ctx, id);
510
+ const assignments = serializeAssignments(ctx, id, value);
511
+ if (assignments) {
512
+ const ref = getRefParam(ctx, id);
513
+ return `(${assignRef(ctx, id, serialized)},${assignments}${ref})`;
514
+ }
515
+ }
516
+ return assignRef(ctx, id, serialized);
517
+ }
518
+
519
+ export default function serializeTree(
520
+ ctx: SerializationContext,
521
+ [type, value, id]: SerovalNode,
522
+ ): string {
523
+ switch (type) {
524
+ case SerovalNodeType.Primitive:
525
+ return String(value);
526
+ case SerovalNodeType.Reference:
527
+ return getRefParam(ctx, value);
528
+ case SerovalNodeType.Promise:
529
+ return serializePromise(ctx, value, id);
530
+ case SerovalNodeType.BigInt:
531
+ return value;
532
+ case SerovalNodeType.Date:
533
+ return assignRef(ctx, id, `new Date("${value}")`);
534
+ case SerovalNodeType.RegExp:
535
+ return assignRef(ctx, id, value);
536
+ case SerovalNodeType.BigIntTypedArray:
537
+ case SerovalNodeType.TypedArray:
538
+ return serializeTypedArray(ctx, value[0], value[1], value[2], id);
539
+ case SerovalNodeType.Set:
540
+ return serializeSet(ctx, value, id);
541
+ case SerovalNodeType.Map:
542
+ return serializeMap(ctx, value[0], value[1], value[2], id);
543
+ case SerovalNodeType.Array:
544
+ return serializeArray(ctx, value, id);
545
+ case SerovalNodeType.AggregateError:
546
+ return serializeAggregateError(ctx, value[0], value[1], value[2], id);
547
+ case SerovalNodeType.Error:
548
+ return serializeError(ctx, value[0], value[1], value[2], id);
549
+ case SerovalNodeType.Iterable:
550
+ return serializeIterable(ctx, value[0], value[1], id);
551
+ case SerovalNodeType.NullConstructor:
552
+ return serializeNullConstructor(ctx, value, id);
553
+ case SerovalNodeType.Object:
554
+ return assignRef(ctx, id, serializeDictionary(ctx, id, value));
555
+ default:
556
+ throw new Error('Unsupported type');
557
+ }
558
+ }