UNPKG

seroval

Version:
1,485 lines (1,384 loc) 39.5 kB
import { Feature } from '../compat'; import { CONSTANT_STRING, ERROR_CONSTRUCTOR_STRING, NIL, SerovalNodeType, SerovalObjectFlags, SYMBOL_STRING, } from '../constants'; import { SERIALIZED_ASYNC_ITERATOR_CONSTRUCTOR, SERIALIZED_ITERATOR_CONSTRUCTOR, } from '../constructors'; import { SerovalMissingPluginError, SerovalSerializationError, SerovalUnsupportedNodeError, } from '../errors'; import { createEffectfulFunction, createFunction } from '../function-string'; import { GLOBAL_CONTEXT_REFERENCES, REFERENCES_KEY } from '../keys'; import type { PluginAccessOptions } from '../plugin'; import { SerovalMode } from '../plugin'; import { SPECIAL_REF_STRING } from '../special-reference'; import { serializeString } from '../string'; import type { SerovalAggregateErrorNode, SerovalArrayBufferNode, SerovalArrayNode, SerovalAsyncIteratorFactoryInstanceNode, SerovalAsyncIteratorFactoryNode, SerovalBigIntTypedArrayNode, SerovalBoxedNode, SerovalDataViewNode, SerovalDateNode, SerovalErrorNode, SerovalIndexedValueNode, SerovalIteratorFactoryInstanceNode, SerovalIteratorFactoryNode, SerovalMapNode, SerovalNode, SerovalNodeWithID, SerovalNullConstructorNode, SerovalObjectNode, SerovalObjectRecordKey, SerovalObjectRecordNode, SerovalPluginNode, SerovalPromiseConstructorNode, SerovalPromiseNode, SerovalPromiseRejectNode, SerovalPromiseResolveNode, SerovalReferenceNode, SerovalRegExpNode, SerovalSequenceNode, SerovalSetNode, SerovalStreamConstructorNode, SerovalStreamNextNode, SerovalStreamReturnNode, SerovalStreamThrowNode, SerovalTypedArrayNode, } from '../types'; import getIdentifier from '../utils/get-identifier'; import { isValidIdentifier } from '../utils/is-valid-identifier'; const enum AssignmentType { Index = 0, Add = 1, Set = 2, Delete = 3, } interface IndexAssignment { t: AssignmentType.Index; s: string; k: undefined; v: string; } interface SetAssignment { t: AssignmentType.Set; s: string; k: string; v: string; } interface AddAssignment { t: AssignmentType.Add; s: string; k: undefined; v: string; } interface DeleteAssignment { t: AssignmentType.Delete; s: string; k: string; v: undefined; } // Array of assignments to be done (used for recursion) type Assignment = | IndexAssignment | AddAssignment | SetAssignment | DeleteAssignment; export interface FlaggedObject { type: SerovalObjectFlags; value: string; } function getAssignmentExpression(assignment: Assignment): string { switch (assignment.t) { case AssignmentType.Index: return assignment.s + '=' + assignment.v; case AssignmentType.Set: return assignment.s + '.set(' + assignment.k + ',' + assignment.v + ')'; case AssignmentType.Add: return assignment.s + '.add(' + assignment.v + ')'; case AssignmentType.Delete: return assignment.s + '.delete(' + assignment.k + ')'; } } function mergeAssignments(assignments: Assignment[]): Assignment[] { const newAssignments: Assignment[] = []; let current = assignments[0]; for ( let i = 1, len = assignments.length, item: Assignment, prev = current; i < len; i++ ) { item = assignments[i]; if (item.t === AssignmentType.Index && item.v === prev.v) { // Merge if the right-hand value is the same // saves at least 2 chars current = { t: AssignmentType.Index, s: item.s, k: NIL, v: getAssignmentExpression(current), } as IndexAssignment; } else if (item.t === AssignmentType.Set && item.s === prev.s) { // Maps has chaining methods, merge if source is the same current = { t: AssignmentType.Set, s: getAssignmentExpression(current), k: item.k, v: item.v, } as SetAssignment; } else if (item.t === AssignmentType.Add && item.s === prev.s) { // Sets has chaining methods too current = { t: AssignmentType.Add, s: getAssignmentExpression(current), k: NIL, v: item.v, } as AddAssignment; } else if (item.t === AssignmentType.Delete && item.s === prev.s) { // Maps has chaining methods, merge if source is the same current = { t: AssignmentType.Delete, s: getAssignmentExpression(current), k: item.k, v: NIL, } as DeleteAssignment; } else { // Different assignment, push current newAssignments.push(current); current = item; } prev = item; } newAssignments.push(current); return newAssignments; } function resolveAssignments(assignments: Assignment[]): string | undefined { if (assignments.length) { let result = ''; const merged = mergeAssignments(assignments); for (let i = 0, len = merged.length; i < len; i++) { result += getAssignmentExpression(merged[i]) + ','; } return result; } return NIL; } const NULL_CONSTRUCTOR = 'Object.create(null)'; const SET_CONSTRUCTOR = 'new Set'; const MAP_CONSTRUCTOR = 'new Map'; const PROMISE_RESOLVE = 'Promise.resolve'; const PROMISE_REJECT = 'Promise.reject'; const OBJECT_FLAG_CONSTRUCTOR: Record<SerovalObjectFlags, string | undefined> = { [SerovalObjectFlags.Frozen]: 'Object.freeze', [SerovalObjectFlags.Sealed]: 'Object.seal', [SerovalObjectFlags.NonExtensible]: 'Object.preventExtensions', [SerovalObjectFlags.None]: NIL, }; type SerovalNodeWithProperties = | SerovalObjectNode | SerovalNullConstructorNode | SerovalAggregateErrorNode | SerovalErrorNode; export interface BaseSerializerContextOptions extends PluginAccessOptions { features: number; markedRefs: number[] | Set<number>; } export interface BaseSerializerContext extends PluginAccessOptions { readonly mode: SerovalMode; features: number; /* * To check if an object is synchronously referencing itself */ stack: number[]; /** * Array of object mutations */ flags: FlaggedObject[]; /** * Array of assignments to be done (used for recursion) */ assignments: Assignment[]; /** * Refs that are...referenced */ marked: Set<number>; } export interface CrossContextOptions { scopeId?: string; } export function createBaseSerializerContext( mode: SerovalMode, options: BaseSerializerContextOptions, ): BaseSerializerContext { return { mode, plugins: options.plugins, features: options.features, marked: new Set(options.markedRefs), stack: [], flags: [], assignments: [], }; } export interface VanillaSerializerState { valid: Map<number, number>; vars: string[]; } function createVanillaSerializerState(): VanillaSerializerState { return { valid: new Map(), vars: [], }; } export interface VanillaSerializerContext { mode: SerovalMode.Vanilla; base: BaseSerializerContext; state: VanillaSerializerState; child: SerializePluginContext | undefined; } export type VanillaSerializerContextOptions = BaseSerializerContextOptions; export function createVanillaSerializerContext( options: VanillaSerializerContextOptions, ): VanillaSerializerContext { return { mode: SerovalMode.Vanilla, base: createBaseSerializerContext(SerovalMode.Vanilla, options), state: createVanillaSerializerState(), child: NIL, }; } export interface CrossSerializerContext { mode: SerovalMode.Cross; base: BaseSerializerContext; state: CrossContextOptions; child: SerializePluginContext | undefined; } export interface CrossSerializerContextOptions extends BaseSerializerContextOptions, CrossContextOptions { // empty } export function createCrossSerializerContext( options: CrossSerializerContextOptions, ): CrossSerializerContext { return { mode: SerovalMode.Cross, base: createBaseSerializerContext(SerovalMode.Cross, options), state: options, child: NIL, }; } type SerializerContext = VanillaSerializerContext | CrossSerializerContext; export class SerializePluginContext { constructor(private _p: SerializerContext) {} serialize(node: SerovalNode) { return serialize(this._p, node); } } /** * Creates the reference param (identifier) from the given reference ID * Calling this function means the value has been referenced somewhere */ function getVanillaRefParam( state: VanillaSerializerState, index: number, ): string { /** * Creates a new reference ID from a given reference ID * This new reference ID means that the reference itself * has been referenced at least once, and is used to generate * the variables */ let actualIndex = state.valid.get(index); if (actualIndex == null) { actualIndex = state.valid.size; state.valid.set(index, actualIndex); } let identifier = state.vars[actualIndex]; if (identifier == null) { identifier = getIdentifier(actualIndex); state.vars[actualIndex] = identifier; } return identifier; } function getCrossRefParam(id: number): string { return GLOBAL_CONTEXT_REFERENCES + '[' + id + ']'; } /** * Converts the ID of a reference into a identifier string * that is used to refer to the object instance in the * generated script. */ function getRefParam(ctx: SerializerContext, id: number): string { return ctx.mode === SerovalMode.Vanilla ? getVanillaRefParam(ctx.state, id) : getCrossRefParam(id); } function markSerializerRef(ctx: BaseSerializerContext, id: number): void { ctx.marked.add(id); } function isSerializerRefMarked( ctx: BaseSerializerContext, id: number, ): boolean { return ctx.marked.has(id); } function pushObjectFlag( ctx: SerializerContext, flag: SerovalObjectFlags, id: number, ): void { if (flag !== SerovalObjectFlags.None) { markSerializerRef(ctx.base, id); ctx.base.flags.push({ type: flag, value: getRefParam(ctx, id), }); } } function resolveFlags(ctx: BaseSerializerContext): string | undefined { let result = ''; for (let i = 0, current = ctx.flags, len = current.length; i < len; i++) { const flag = current[i]; result += OBJECT_FLAG_CONSTRUCTOR[flag.type] + '(' + flag.value + '),'; } return result; } function resolvePatches(ctx: BaseSerializerContext): string | undefined { const assignments = resolveAssignments(ctx.assignments); const flags = resolveFlags(ctx); if (assignments) { if (flags) { return assignments + flags; } return assignments; } return flags; } /** * Generates the inlined assignment for the reference * This is different from the assignments array as this one * signifies creation rather than mutation */ function createAssignment( ctx: BaseSerializerContext, source: string, value: string, ): void { ctx.assignments.push({ t: AssignmentType.Index, s: source, k: NIL, v: value, }); } function createAddAssignment( ctx: SerializerContext, ref: number, value: string, ): void { ctx.base.assignments.push({ t: AssignmentType.Add, s: getRefParam(ctx, ref), k: NIL, v: value, }); } function createSetAssignment( ctx: SerializerContext, ref: number, key: string, value: string, ): void { ctx.base.assignments.push({ t: AssignmentType.Set, s: getRefParam(ctx, ref), k: key, v: value, }); } function createDeleteAssignment( ctx: SerializerContext, ref: number, key: string, ): void { ctx.base.assignments.push({ t: AssignmentType.Delete, s: getRefParam(ctx, ref), k: key, v: NIL, }); } function createArrayAssign( ctx: SerializerContext, ref: number, index: number | string, value: string, ): void { createAssignment(ctx.base, getRefParam(ctx, ref) + '[' + index + ']', value); } function createObjectAssign( ctx: SerializerContext, ref: number, key: string, value: string, ): void { createAssignment(ctx.base, getRefParam(ctx, ref) + '.' + key, value); } function createSequenceAssign( ctx: SerializerContext, ref: number, index: number | string, value: string, ): void { createAssignment( ctx.base, getRefParam(ctx, ref) + '.v[' + index + ']', value, ); } /** * Checks if the value is in the stack. Stack here is a reference * structure to know if a object is to be accessed in a TDZ. */ function isIndexedValueInStack( ctx: BaseSerializerContext, node: SerovalNode, ): boolean { return node.t === SerovalNodeType.IndexedValue && ctx.stack.includes(node.i); } /** * Produces an assignment expression. `id` generates a reference * parameter (through `getRefParam`) and has the option to * return the reference parameter directly or assign a value to * it. */ function assignIndexedValue( ctx: SerializerContext, index: number, value: string, ): string { if ( ctx.mode === SerovalMode.Vanilla && !isSerializerRefMarked(ctx.base, index) ) { return value; } /** * In cross-reference, we have to assume that * every reference are going to be referenced * in the future, and so we need to store * all of it into the reference array. * * otherwise in vanilla, we only do this if it * is actually referenced */ return getRefParam(ctx, index) + '=' + value; } function serializeReference(node: SerovalReferenceNode): string { return REFERENCES_KEY + '.get("' + node.s + '")'; } function serializeArrayItem( ctx: SerializerContext, id: number, item: SerovalNode | 0, index: number, ): string { // Check if index is a hole if (item) { // Check if item is a parent if (isIndexedValueInStack(ctx.base, item)) { markSerializerRef(ctx.base, id); createArrayAssign( ctx, id, index, getRefParam(ctx, (item as SerovalIndexedValueNode).i), ); return ''; } return serialize(ctx, item); } return ''; } function serializeArray( ctx: SerializerContext, node: SerovalArrayNode, ): string { const id = node.i; const list = node.a; const len = list.length; if (len > 0) { ctx.base.stack.push(id); let values = serializeArrayItem(ctx, id, list[0], 0); // This is different than Map and Set // because we also need to serialize // the holes of the Array let isHoley = values === ''; for (let i = 1, item: string; i < len; i++) { item = serializeArrayItem(ctx, id, list[i], i); values += ',' + item; isHoley = item === ''; } ctx.base.stack.pop(); pushObjectFlag(ctx, node.o, node.i); return '[' + values + (isHoley ? ',]' : ']'); } return '[]'; } function serializeProperty( ctx: SerializerContext, source: SerovalNodeWithProperties, key: SerovalObjectRecordKey, val: SerovalNode, ): string { if (typeof key === 'string') { const check = Number(key); const isIdentifier = // Test if key is a valid positive number or JS identifier // so that we don't have to serialize the key and wrap with brackets (check >= 0 && // It's also important to consider that if the key is // indeed numeric, we need to make sure that when // converted back into a string, it's still the same // to the original key. This allows us to differentiate // keys that has numeric formats but in a different // format, which can cause unintentional key declaration // Example: { 0x1: 1 } vs { '0x1': 1 } check.toString() === key) || isValidIdentifier(key); if (isIndexedValueInStack(ctx.base, val)) { const refParam = getRefParam(ctx, (val as SerovalIndexedValueNode).i); markSerializerRef(ctx.base, source.i); // Strict identifier check, make sure // that it isn't numeric (except NaN) if (isIdentifier && check !== check) { createObjectAssign(ctx, source.i, key, refParam); } else { createArrayAssign( ctx, source.i, isIdentifier ? key : '"' + key + '"', refParam, ); } return ''; } return (isIdentifier ? key : '"' + key + '"') + ':' + serialize(ctx, val); } return '[' + serialize(ctx, key) + ']:' + serialize(ctx, val); } function serializeProperties( ctx: SerializerContext, source: SerovalNodeWithProperties, record: SerovalObjectRecordNode, ): string { const keys = record.k; const len = keys.length; if (len > 0) { const values = record.v; ctx.base.stack.push(source.i); let result = serializeProperty(ctx, source, keys[0], values[0]); for (let i = 1, item = result; i < len; i++) { item = serializeProperty(ctx, source, keys[i], values[i]); result += (item && result && ',') + item; } ctx.base.stack.pop(); return '{' + result + '}'; } return '{}'; } function serializeObject( ctx: SerializerContext, node: SerovalObjectNode, ): string { pushObjectFlag(ctx, node.o, node.i); return serializeProperties(ctx, node, node.p); } function serializeWithObjectAssign( ctx: SerializerContext, source: SerovalNodeWithProperties, value: SerovalObjectRecordNode, serialized: string, ): string { const fields = serializeProperties(ctx, source, value); if (fields !== '{}') { return 'Object.assign(' + serialized + ',' + fields + ')'; } return serialized; } function serializeStringKeyAssignment( ctx: SerializerContext, source: SerovalNodeWithProperties, mainAssignments: Assignment[], key: string, value: SerovalNode, ): void { const base = ctx.base; const serialized = serialize(ctx, value); const check = Number(key); const isIdentifier = // Test if key is a valid positive number or JS identifier // so that we don't have to serialize the key and wrap with brackets (check >= 0 && // It's also important to consider that if the key is // indeed numeric, we need to make sure that when // converted back into a string, it's still the same // to the original key. This allows us to differentiate // keys that has numeric formats but in a different // format, which can cause unintentional key declaration // Example: { 0x1: 1 } vs { '0x1': 1 } check.toString() === key) || isValidIdentifier(key); if (isIndexedValueInStack(base, value)) { // Strict identifier check, make sure // that it isn't numeric (except NaN) if (isIdentifier && check !== check) { createObjectAssign(ctx, source.i, key, serialized); } else { createArrayAssign( ctx, source.i, isIdentifier ? key : '"' + key + '"', serialized, ); } } else { const parentAssignment = base.assignments; base.assignments = mainAssignments; if (isIdentifier && check !== check) { createObjectAssign(ctx, source.i, key, serialized); } else { createArrayAssign( ctx, source.i, isIdentifier ? key : '"' + key + '"', serialized, ); } base.assignments = parentAssignment; } } function serializeAssignment( ctx: SerializerContext, source: SerovalNodeWithProperties, mainAssignments: Assignment[], key: SerovalObjectRecordKey, value: SerovalNode, ): void { if (typeof key === 'string') { serializeStringKeyAssignment(ctx, source, mainAssignments, key, value); } else { const base = ctx.base; const parent = base.stack; base.stack = []; const serialized = serialize(ctx, value); base.stack = parent; const parentAssignment = base.assignments; base.assignments = mainAssignments; createArrayAssign(ctx, source.i, serialize(ctx, key), serialized); base.assignments = parentAssignment; } } function serializeAssignments( ctx: SerializerContext, source: SerovalNodeWithProperties, node: SerovalObjectRecordNode, ): string | undefined { const keys = node.k; const len = keys.length; if (len > 0) { const mainAssignments: Assignment[] = []; const values = node.v; ctx.base.stack.push(source.i); for (let i = 0; i < len; i++) { serializeAssignment(ctx, source, mainAssignments, keys[i], values[i]); } ctx.base.stack.pop(); return resolveAssignments(mainAssignments); } return NIL; } function serializeDictionary( ctx: SerializerContext, node: SerovalNodeWithProperties, init: string, ): string { if (node.p) { const base = ctx.base; if (base.features & Feature.ObjectAssign) { init = serializeWithObjectAssign(ctx, node, node.p, init); } else { markSerializerRef(base, node.i); const assignments = serializeAssignments(ctx, node, node.p); if (assignments) { return ( '(' + assignIndexedValue(ctx, node.i, init) + ',' + assignments + getRefParam(ctx, node.i) + ')' ); } } } return init; } function serializeNullConstructor( ctx: SerializerContext, node: SerovalNullConstructorNode, ): string { pushObjectFlag(ctx, node.o, node.i); return serializeDictionary(ctx, node, NULL_CONSTRUCTOR); } function serializeDate(node: SerovalDateNode): string { return 'new Date("' + node.s + '")'; } function serializeRegExp( ctx: SerializerContext, node: SerovalRegExpNode, ): string { if (ctx.base.features & Feature.RegExp) { return '/' + node.c + '/' + node.m; } throw new SerovalUnsupportedNodeError(node); } function serializeSetItem( ctx: SerializerContext, id: number, item: SerovalNode, ): string { const base = ctx.base; if (isIndexedValueInStack(base, item)) { markSerializerRef(base, id); createAddAssignment( ctx, id, getRefParam(ctx, (item as SerovalIndexedValueNode).i), ); return ''; } return serialize(ctx, item); } function serializeSet(ctx: SerializerContext, node: SerovalSetNode): string { let serialized = SET_CONSTRUCTOR; const items = node.a; const size = items.length; const id = node.i; if (size > 0) { ctx.base.stack.push(id); let result = serializeSetItem(ctx, id, items[0]); for (let i = 1, item = result; i < size; i++) { item = serializeSetItem(ctx, id, items[i]); result += (item && result && ',') + item; } ctx.base.stack.pop(); if (result) { serialized += '([' + result + '])'; } } return serialized; } function serializeMapEntry( ctx: SerializerContext, id: number, key: SerovalNode, val: SerovalNode, sentinel: string, ): string { const base = ctx.base; if (isIndexedValueInStack(base, key)) { // Create reference for the map instance const keyRef = getRefParam(ctx, (key as SerovalIndexedValueNode).i); markSerializerRef(base, id); // Check if value is a parent if (isIndexedValueInStack(base, val)) { const valueRef = getRefParam(ctx, (val as SerovalIndexedValueNode).i); // Register an assignment since // both key and value are a parent of this // Map instance createSetAssignment(ctx, id, keyRef, valueRef); return ''; } // Reset the stack // This is required because the serialized // value is no longer part of the expression // tree and has been moved to the deferred // assignment if ( val.t !== SerovalNodeType.IndexedValue && val.i != null && isSerializerRefMarked(base, val.i) ) { // We use a trick here using sequence (or comma) expressions // basically we serialize the intended object in place WITHOUT // actually returning it, this is by returning a placeholder // value that we will remove sometime after. const serialized = '(' + serialize(ctx, val) + ',[' + sentinel + ',' + sentinel + '])'; createSetAssignment(ctx, id, keyRef, getRefParam(ctx, val.i)); createDeleteAssignment(ctx, id, sentinel); return serialized; } const parent = base.stack; base.stack = []; createSetAssignment(ctx, id, keyRef, serialize(ctx, val)); base.stack = parent; return ''; } if (isIndexedValueInStack(base, val)) { // Create ref for the Map instance const valueRef = getRefParam(ctx, (val as SerovalIndexedValueNode).i); markSerializerRef(base, id); if ( key.t !== SerovalNodeType.IndexedValue && key.i != null && isSerializerRefMarked(base, key.i) ) { const serialized = '(' + serialize(ctx, key) + ',[' + sentinel + ',' + sentinel + '])'; createSetAssignment(ctx, id, getRefParam(ctx, key.i), valueRef); createDeleteAssignment(ctx, id, sentinel); return serialized; } // Reset stack for the key serialization const parent = base.stack; base.stack = []; createSetAssignment(ctx, id, serialize(ctx, key), valueRef); base.stack = parent; return ''; } return '[' + serialize(ctx, key) + ',' + serialize(ctx, val) + ']'; } function serializeMap(ctx: SerializerContext, node: SerovalMapNode): string { let serialized = MAP_CONSTRUCTOR; const keys = node.e.k; const size = keys.length; const id = node.i; const sentinel = node.f; const sentinelId = getRefParam(ctx, sentinel.i); const base = ctx.base; if (size > 0) { const vals = node.e.v; base.stack.push(id); let result = serializeMapEntry(ctx, id, keys[0], vals[0], sentinelId); for (let i = 1, item = result; i < size; i++) { item = serializeMapEntry(ctx, id, keys[i], vals[i], sentinelId); result += (item && result && ',') + item; } base.stack.pop(); // Check if there are any values // so that the empty Map constructor // can be used instead if (result) { serialized += '([' + result + '])'; } } if (sentinel.t === SerovalNodeType.SpecialReference) { markSerializerRef(base, sentinel.i); serialized = '(' + serialize(ctx, sentinel) + ',' + serialized + ')'; } return serialized; } function serializeArrayBuffer( ctx: SerializerContext, node: SerovalArrayBufferNode, ): string { return getConstructor(ctx, node.f) + '("' + node.s + '")'; } function serializeTypedArray( ctx: SerializerContext, node: SerovalTypedArrayNode | SerovalBigIntTypedArrayNode, ): string { return ( 'new ' + node.c + '(' + serialize(ctx, node.f) + ',' + node.b + ',' + node.l + ')' ); } function serializeDataView( ctx: SerializerContext, node: SerovalDataViewNode, ): string { return ( 'new DataView(' + serialize(ctx, node.f) + ',' + node.b + ',' + node.l + ')' ); } function serializeAggregateError( ctx: SerializerContext, node: SerovalAggregateErrorNode, ): string { const id = node.i; // `AggregateError` might've been extended // either through class or custom properties // Make sure to assign extra properties ctx.base.stack.push(id); const serialized = serializeDictionary( ctx, node, 'new AggregateError([],"' + node.m + '")', ); ctx.base.stack.pop(); return serialized; } function serializeError( ctx: SerializerContext, node: SerovalErrorNode, ): string { return serializeDictionary( ctx, node, 'new ' + ERROR_CONSTRUCTOR_STRING[node.s] + '("' + node.m + '")', ); } function serializePromise( ctx: SerializerContext, node: SerovalPromiseNode, ): string { let serialized: string; // Check if resolved value is a parent expression const fulfilled = node.f; const id = node.i; const promiseConstructor = node.s ? PROMISE_RESOLVE : PROMISE_REJECT; const base = ctx.base; if (isIndexedValueInStack(base, fulfilled)) { // A Promise trick, reference the value // inside the `then` expression so that // the Promise evaluates after the parent // has initialized const ref = getRefParam(ctx, (fulfilled as SerovalIndexedValueNode).i); serialized = promiseConstructor + (node.s ? '().then(' + createFunction([], ref) + ')' : '().catch(' + createEffectfulFunction([], 'throw ' + ref) + ')'); } else { base.stack.push(id); const result = serialize(ctx, fulfilled); base.stack.pop(); // just inline the value/reference here serialized = promiseConstructor + '(' + result + ')'; } return serialized; } function serializeBoxed( ctx: SerializerContext, node: SerovalBoxedNode, ): string { return 'Object(' + serialize(ctx, node.f) + ')'; } function getConstructor( ctx: SerializerContext, node: SerovalNodeWithID, ): string { const current = serialize(ctx, node); return node.t === SerovalNodeType.IndexedValue ? current : '(' + current + ')'; } function serializePromiseConstructor( ctx: SerializerContext, node: SerovalPromiseConstructorNode, ): string { if (ctx.mode === SerovalMode.Vanilla) { throw new SerovalUnsupportedNodeError(node); } const resolver = assignIndexedValue( ctx, node.s, getConstructor(ctx, node.f) + '()', ); return '(' + resolver + ').p'; } function serializePromiseResolve( ctx: SerializerContext, node: SerovalPromiseResolveNode, ): string { if (ctx.mode === SerovalMode.Vanilla) { throw new SerovalUnsupportedNodeError(node); } return ( getConstructor(ctx, node.a[0]) + '(' + getRefParam(ctx, node.i) + ',' + serialize(ctx, node.a[1]) + ')' ); } function serializePromiseReject( ctx: SerializerContext, node: SerovalPromiseRejectNode, ): string { if (ctx.mode === SerovalMode.Vanilla) { throw new SerovalUnsupportedNodeError(node); } return ( getConstructor(ctx, node.a[0]) + '(' + getRefParam(ctx, node.i) + ',' + serialize(ctx, node.a[1]) + ')' ); } function serializePlugin( ctx: SerializerContext, node: SerovalPluginNode, ): string { const currentPlugins = ctx.base.plugins; if (currentPlugins) { for (let i = 0, len = currentPlugins.length; i < len; i++) { const plugin = currentPlugins[i]; if (plugin.tag === node.c) { if (ctx.child == null) { ctx.child = new SerializePluginContext(ctx); } return plugin.serialize(node.s, ctx.child, { id: node.i, }); } } } throw new SerovalMissingPluginError(node.c); } function serializeIteratorFactory( ctx: SerializerContext, node: SerovalIteratorFactoryNode, ): string { let result = ''; let initialized = false; if (node.f.t !== SerovalNodeType.IndexedValue) { markSerializerRef(ctx.base, node.f.i); result = '(' + serialize(ctx, node.f) + ','; initialized = true; } result += assignIndexedValue( ctx, node.i, '(' + SERIALIZED_ITERATOR_CONSTRUCTOR + ')(' + getRefParam(ctx, node.f.i) + ')', ); if (initialized) { result += ')'; } return result; } function serializeIteratorFactoryInstance( ctx: SerializerContext, node: SerovalIteratorFactoryInstanceNode, ): string { return getConstructor(ctx, node.a[0]) + '(' + serialize(ctx, node.a[1]) + ')'; } function serializeAsyncIteratorFactory( ctx: SerializerContext, node: SerovalAsyncIteratorFactoryNode, ): string { const promise = node.a[0]; const symbol = node.a[1]; const base = ctx.base; let result = ''; if (promise.t !== SerovalNodeType.IndexedValue) { markSerializerRef(base, promise.i); result += '(' + serialize(ctx, promise); } if (symbol.t !== SerovalNodeType.IndexedValue) { markSerializerRef(base, symbol.i); result += (result ? ',' : '(') + serialize(ctx, symbol); } if (result) { result += ','; } const iterator = assignIndexedValue( ctx, node.i, '(' + SERIALIZED_ASYNC_ITERATOR_CONSTRUCTOR + ')(' + getRefParam(ctx, symbol.i) + ',' + getRefParam(ctx, promise.i) + ')', ); if (result) { return result + iterator + ')'; } return iterator; } function serializeAsyncIteratorFactoryInstance( ctx: SerializerContext, node: SerovalAsyncIteratorFactoryInstanceNode, ): string { return getConstructor(ctx, node.a[0]) + '(' + serialize(ctx, node.a[1]) + ')'; } function serializeStreamConstructor( ctx: SerializerContext, node: SerovalStreamConstructorNode, ): string { const result = assignIndexedValue( ctx, node.i, getConstructor(ctx, node.f) + '()', ); const len = node.a.length; if (len) { let values = serialize(ctx, node.a[0]); for (let i = 1; i < len; i++) { values += ',' + serialize(ctx, node.a[i]); } return '(' + result + ',' + values + ',' + getRefParam(ctx, node.i) + ')'; } return result; } function serializeStreamNext( ctx: SerializerContext, node: SerovalStreamNextNode, ): string { return getRefParam(ctx, node.i) + '.next(' + serialize(ctx, node.f) + ')'; } function serializeStreamThrow( ctx: SerializerContext, node: SerovalStreamThrowNode, ): string { return getRefParam(ctx, node.i) + '.throw(' + serialize(ctx, node.f) + ')'; } function serializeStreamReturn( ctx: SerializerContext, node: SerovalStreamReturnNode, ): string { return getRefParam(ctx, node.i) + '.return(' + serialize(ctx, node.f) + ')'; } function serializeSequenceItem( ctx: SerializerContext, id: number, index: number, item: SerovalNode, ): string { const base = ctx.base; if (isIndexedValueInStack(base, item)) { markSerializerRef(base, id); createSequenceAssign( ctx, id, index, getRefParam(ctx, (item as SerovalIndexedValueNode).i), ); return ''; } return serialize(ctx, item); } function serializeSequence( ctx: SerializerContext, node: SerovalSequenceNode, ): string { const items = node.a; const size = items.length; const id = node.i; if (size > 0) { ctx.base.stack.push(id); let result = serializeSequenceItem(ctx, id, 0, items[0]); for (let i = 1, item = result; i < size; i++) { item = serializeSequenceItem(ctx, id, i, items[i]); result += (item && result && ',') + item; } ctx.base.stack.pop(); if (result) { return ( '{__SEROVAL_SEQUENCE__:!0,v:[' + result + '],t:' + node.s + ',d:' + node.l + '}' ); } } return '{__SEROVAL_SEQUENCE__:!0,v:[],t:-1,d:0}'; } function serializeAssignable( ctx: SerializerContext, node: SerovalNode, ): string { switch (node.t) { case SerovalNodeType.WKSymbol: return SYMBOL_STRING[node.s]; case SerovalNodeType.Reference: return serializeReference(node); case SerovalNodeType.Array: return serializeArray(ctx, node); case SerovalNodeType.Object: return serializeObject(ctx, node); case SerovalNodeType.NullConstructor: return serializeNullConstructor(ctx, node); case SerovalNodeType.Date: return serializeDate(node); case SerovalNodeType.RegExp: return serializeRegExp(ctx, node); case SerovalNodeType.Set: return serializeSet(ctx, node); case SerovalNodeType.Map: return serializeMap(ctx, node); case SerovalNodeType.ArrayBuffer: return serializeArrayBuffer(ctx, node); case SerovalNodeType.BigIntTypedArray: case SerovalNodeType.TypedArray: return serializeTypedArray(ctx, node); case SerovalNodeType.DataView: return serializeDataView(ctx, node); case SerovalNodeType.AggregateError: return serializeAggregateError(ctx, node); case SerovalNodeType.Error: return serializeError(ctx, node); case SerovalNodeType.Promise: return serializePromise(ctx, node); case SerovalNodeType.Boxed: return serializeBoxed(ctx, node); case SerovalNodeType.PromiseConstructor: return serializePromiseConstructor(ctx, node); case SerovalNodeType.Plugin: return serializePlugin(ctx, node); case SerovalNodeType.SpecialReference: return SPECIAL_REF_STRING[node.s]; case SerovalNodeType.Sequence: return serializeSequence(ctx, node); default: throw new SerovalUnsupportedNodeError(node); } } function serialize(ctx: SerializerContext, node: SerovalNode): string { switch (node.t) { case SerovalNodeType.Constant: return CONSTANT_STRING[node.s]; case SerovalNodeType.Number: return '' + node.s; case SerovalNodeType.String: return '"' + node.s + '"'; case SerovalNodeType.BigInt: return node.s + 'n'; case SerovalNodeType.IndexedValue: return getRefParam(ctx, node.i); case SerovalNodeType.PromiseSuccess: return serializePromiseResolve(ctx, node); case SerovalNodeType.PromiseFailure: return serializePromiseReject(ctx, node); case SerovalNodeType.IteratorFactory: return serializeIteratorFactory(ctx, node); case SerovalNodeType.IteratorFactoryInstance: return serializeIteratorFactoryInstance(ctx, node); case SerovalNodeType.AsyncIteratorFactory: return serializeAsyncIteratorFactory(ctx, node); case SerovalNodeType.AsyncIteratorFactoryInstance: return serializeAsyncIteratorFactoryInstance(ctx, node); case SerovalNodeType.StreamConstructor: return serializeStreamConstructor(ctx, node); case SerovalNodeType.StreamNext: return serializeStreamNext(ctx, node); case SerovalNodeType.StreamThrow: return serializeStreamThrow(ctx, node); case SerovalNodeType.StreamReturn: return serializeStreamReturn(ctx, node); default: return assignIndexedValue(ctx, node.i, serializeAssignable(ctx, node)); } } export function serializeRoot( ctx: SerializerContext, node: SerovalNode, ): string { try { return serialize(ctx, node); } catch (error) { throw error instanceof SerovalSerializationError ? error : new SerovalSerializationError(error); } } export function serializeTopVanilla( ctx: VanillaSerializerContext, tree: SerovalNode, ): string { const result = serialize(ctx, tree); // Shared references detected if (tree.i != null && ctx.state.vars.length) { const patches = resolvePatches(ctx.base); let body = result; if (patches) { // Get (or create) a ref from the source const index = getRefParam(ctx, tree.i); body = result + ',' + patches + index; if (!result.startsWith(index + '=')) { body = index + '=' + body; } body = '(' + body + ')'; } return '(' + createFunction(ctx.state.vars, body) + ')()'; } if (tree.t === SerovalNodeType.Object) { return '(' + result + ')'; } return result; } export function serializeTopCross( ctx: CrossSerializerContext, tree: SerovalNode, ): string { // Get the serialized result const result = serialize(ctx, tree); // If the node is a non-reference, return // the result immediately const id = tree.i; if (id == null) { return result; } // Get the patches const patches = resolvePatches(ctx.base); // Get the variable that represents the root const ref = getRefParam(ctx, id); const scopeId = ctx.state.scopeId; // Parameters needed for scoping const params = scopeId == null ? '' : GLOBAL_CONTEXT_REFERENCES; // If there are patches, append it after the result const body = patches ? '(' + result + ',' + patches + ref + ')' : result; // If there are no params, there's no need to generate a function if (params === '') { if (tree.t === SerovalNodeType.Object && !patches) { return '(' + body + ')'; } return body; } // Get the arguments for the IIFE const args = scopeId == null ? '()' : '(' + GLOBAL_CONTEXT_REFERENCES + '["' + serializeString(scopeId) + '"])'; // Create the IIFE return '(' + createFunction([params], body) + ')' + args; }