diff --git a/packages/seroval/src/context.ts b/packages/seroval/src/context.ts index a037842c..5b1f2452 100644 --- a/packages/seroval/src/context.ts +++ b/packages/seroval/src/context.ts @@ -8,25 +8,33 @@ interface IndexAssignment { v: string; } -interface MapAssignment { - t: 'map'; +interface SetAssignment { + t: 'set'; s: string; k: string; v: string; } -interface SetAssignment { - t: 'set'; +interface AddAssignment { + t: 'add'; s: string; k: undefined; v: string; } +interface AppendAssignment { + t: 'append'; + s: string; + k: string; + v: string; +} + // Array of assignments to be done (used for recursion) export type Assignment = | IndexAssignment - | MapAssignment - | SetAssignment; + | AddAssignment + | SetAssignment + | AppendAssignment; export interface ParserContext { refs: Map; diff --git a/packages/seroval/src/tree/async.ts b/packages/seroval/src/tree/async.ts index d1d2f80f..20ab510b 100644 --- a/packages/seroval/src/tree/async.ts +++ b/packages/seroval/src/tree/async.ts @@ -42,6 +42,7 @@ import { SerovalAggregateErrorNode, SerovalArrayNode, SerovalErrorNode, + SerovalFormDataNode, SerovalHeadersNode, SerovalIterableNode, SerovalMapNode, @@ -357,7 +358,7 @@ async function generateHeadersNode( id: number, current: Headers, ): Promise { - assert(ctx.features & Feature.WebAPI, 'Unsupported type "File"'); + assert(ctx.features & Feature.WebAPI, 'Unsupported type "Headers"'); const items: [string, string][] = []; current.forEach((value, key) => { items.push([key, value]); @@ -376,6 +377,30 @@ async function generateHeadersNode( }; } +async function generateFormDataNode( + ctx: ParserContext, + id: number, + current: FormData, +): Promise { + assert(ctx.features & Feature.WebAPI, 'Unsupported type "FormData"'); + const items: Record = {}; + current.forEach((value, key) => { + items[key] = value; + }); + return { + t: SerovalNodeType.FormData, + i: id, + s: undefined, + l: undefined, + c: undefined, + m: undefined, + d: await generateProperties(ctx, items), + a: undefined, + f: undefined, + b: undefined, + }; +} + async function parse( ctx: ParserContext, current: T, @@ -492,6 +517,8 @@ async function parse( return createFileNode(ctx, id, current as unknown as File); case Headers: return generateHeadersNode(ctx, id, current as unknown as Headers); + case FormData: + return generateFormDataNode(ctx, id, current as unknown as FormData); default: break; } diff --git a/packages/seroval/src/tree/deserialize.ts b/packages/seroval/src/tree/deserialize.ts index ff1d2db7..474cb50e 100644 --- a/packages/seroval/src/tree/deserialize.ts +++ b/packages/seroval/src/tree/deserialize.ts @@ -1,9 +1,9 @@ +/* eslint-disable prefer-spread */ /* eslint-disable @typescript-eslint/no-use-before-define */ import { SerializationContext, } from '../context'; import { deserializeString } from '../string'; -import { AsyncServerValue } from '../types'; import { getReference } from './reference'; import { getErrorConstructor, getTypedArrayConstructor } from './shared'; import { SYMBOL_REF } from './symbols'; @@ -17,6 +17,7 @@ import { SerovalDateNode, SerovalErrorNode, SerovalFileNode, + SerovalFormDataNode, SerovalHeadersNode, SerovalIterableNode, SerovalMapNode, @@ -39,22 +40,23 @@ function assignIndexedValue( index: number, value: T, ) { - ctx.valueMap.set(index, value); + if (ctx.markedRefs.has(index)) { + ctx.valueMap.set(index, value); + } return value; } -type SerovalNodeListNode = - | SerovalArrayNode - | SerovalIterableNode - | SerovalHeadersNode; - -function deserializeNodeList( +function deserializeArray( ctx: SerializationContext, - node: SerovalNodeListNode, - result: unknown[], + node: SerovalArrayNode, ) { + const result: unknown[] = assignIndexedValue( + ctx, + node.i, + new Array(node.l), + ); let item: SerovalNode; - for (let i = 0, len = node.a.length; i < len; i++) { + for (let i = 0, len = node.l; i < len; i++) { item = node.a[i]; if (item) { result[i] = deserializeTree(ctx, item); @@ -63,21 +65,6 @@ function deserializeNodeList( return result; } -function deserializeArray( - ctx: SerializationContext, - node: SerovalArrayNode, -) { - const result: AsyncServerValue[] = assignIndexedValue( - ctx, - node.i, - new Array(node.l), - ); - ctx.stack.push(node.i); - deserializeNodeList(ctx, node, result); - ctx.stack.pop(); - return result; -} - function deserializeProperties( ctx: SerializationContext, node: SerovalObjectRecordNode, @@ -99,11 +86,9 @@ function deserializeNullConstructor( const result = assignIndexedValue( ctx, node.i, - Object.create(null) as Record, + Object.create(null) as Record, ); - ctx.stack.push(node.i); deserializeProperties(ctx, node.d, result); - ctx.stack.pop(); return result; } @@ -111,10 +96,8 @@ function deserializeObject( ctx: SerializationContext, node: SerovalObjectNode, ) { - const result = assignIndexedValue(ctx, node.i, {} as Record); - ctx.stack.push(node.i); + const result = assignIndexedValue(ctx, node.i, {} as Record); deserializeProperties(ctx, node.d, result); - ctx.stack.pop(); return result; } @@ -123,11 +106,9 @@ function deserializeSet( node: SerovalSetNode, ) { const result = assignIndexedValue(ctx, node.i, new Set()); - ctx.stack.push(node.i); - for (let i = 0, len = node.a.length; i < len; i++) { + for (let i = 0, len = node.l; i < len; i++) { result.add(deserializeTree(ctx, node.a[i])); } - ctx.stack.pop(); return result; } @@ -140,18 +121,16 @@ function deserializeMap( node.i, new Map(), ); - ctx.stack.push(node.i); - for (let i = 0; i < node.d.s; i++) { + for (let i = 0, len = node.d.s; i < len; i++) { result.set( deserializeTree(ctx, node.d.k[i]), deserializeTree(ctx, node.d.v[i]), ); } - ctx.stack.pop(); return result; } -type AssignableValue = AggregateError | Error | Iterable +type AssignableValue = AggregateError | Error | Iterable type AssignableNode = SerovalAggregateErrorNode | SerovalErrorNode | SerovalIterableNode; function deserializeDictionary( @@ -160,9 +139,7 @@ function deserializeDictionary( result: T, ) { if (node.d) { - ctx.stack.push(node.i); const fields = deserializeProperties(ctx, node.d, {}); - ctx.stack.pop(); Object.assign(result, fields); } return result; @@ -247,9 +224,15 @@ function deserializeIterable( ctx: SerializationContext, node: SerovalIterableNode, ) { - const values: AsyncServerValue[] = []; - deserializeNodeList(ctx, node, values); - const result = assignIndexedValue(ctx, node.i, { + const values: unknown[] = []; + let item: SerovalNode; + for (let i = 0, len = node.l; i < len; i++) { + item = node.a[i]; + if (item) { + values[i] = deserializeTree(ctx, item); + } + } + const result: Iterable = assignIndexedValue(ctx, node.i, { [Symbol.iterator]: () => values.values(), }); return deserializeDictionary(ctx, node, result); @@ -332,9 +315,31 @@ function deserializeHeaders( ctx: SerializationContext, node: SerovalHeadersNode, ) { - const values: [string, string][] = []; - deserializeNodeList(ctx, node, values); - return assignIndexedValue(ctx, node.i, new Headers(values)); + const result = assignIndexedValue(ctx, node.i, new Headers()); + let item: SerovalNode; + let entry: [string, string]; + for (let i = 0, len = node.l; i < len; i++) { + item = node.a[i]; + if (item) { + entry = deserializeTree(ctx, item) as [string, string]; + result.append.apply(result, entry); + } + } + return result; +} + +function deserializeFormData( + ctx: SerializationContext, + node: SerovalFormDataNode, +) { + const result = assignIndexedValue(ctx, node.i, new FormData()); + for (let i = 0, len = node.d.s; i < len; i++) { + result.set( + deserializeString(node.d.k[i]), + deserializeTree(ctx, node.d.v[i]) as FormDataEntryValue, + ); + } + return result; } export default function deserializeTree( @@ -406,6 +411,8 @@ export default function deserializeTree( return deserializeFile(ctx, node); case SerovalNodeType.Headers: return deserializeHeaders(ctx, node); + case SerovalNodeType.FormData: + return deserializeFormData(ctx, node); default: throw new Error('Unsupported type'); } diff --git a/packages/seroval/src/tree/serialize.ts b/packages/seroval/src/tree/serialize.ts index 85f5b27e..95fe3c5b 100644 --- a/packages/seroval/src/tree/serialize.ts +++ b/packages/seroval/src/tree/serialize.ts @@ -35,16 +35,19 @@ import { SerovalURLNode, SerovalURLSearchParamsNode, SerovalReferenceNode, + SerovalFormDataNode, } from './types'; function getAssignmentExpression(assignment: Assignment): string { switch (assignment.t) { case 'index': return assignment.s + '=' + assignment.v; - case 'map': - return assignment.s + '.set(' + assignment.k + ',' + assignment.v + ')'; case 'set': + return assignment.s + '.set(' + assignment.k + ',' + assignment.v + ')'; + case 'add': return assignment.s + '.add(' + assignment.v + ')'; + case 'append': + return assignment.s + '.append(' + assignment.k + ',' + assignment.v + ')'; default: return ''; } @@ -75,11 +78,11 @@ function mergeAssignments(assignments: Assignment[]) { current = item; } break; - case 'map': + case 'set': if (item.s === prev.s) { // Maps has chaining methods, merge if source is the same current = { - t: 'map', + t: 'set', s: getAssignmentExpression(current), k: item.k, v: item.v, @@ -90,11 +93,11 @@ function mergeAssignments(assignments: Assignment[]) { current = item; } break; - case 'set': + case 'add': if (item.s === prev.s) { // Sets has chaining methods too current = { - t: 'set', + t: 'add', s: getAssignmentExpression(current), k: undefined, v: item.v, @@ -105,6 +108,11 @@ function mergeAssignments(assignments: Assignment[]) { current = item; } break; + case 'append': + // Different assignment, push current + newAssignments.push(current); + current = item; + break; default: break; } @@ -155,21 +163,36 @@ function createAssignment( }); } -function createSetAdd( +function createAddAssignment( ctx: SerializationContext, ref: number, value: string, ) { markRef(ctx, ref); ctx.assignments.push({ - t: 'set', + t: 'add', s: getRefParam(ctx, ref), k: undefined, v: value, }); } -function createMapSet( +function createSetAssignment( + ctx: SerializationContext, + ref: number, + key: string, + value: string, +) { + markRef(ctx, ref); + ctx.assignments.push({ + t: 'set', + s: getRefParam(ctx, ref), + k: key, + v: value, + }); +} + +function createAppendAssignment( ctx: SerializationContext, ref: number, key: string, @@ -177,7 +200,7 @@ function createMapSet( ) { markRef(ctx, ref); ctx.assignments.push({ - t: 'map', + t: 'append', s: getRefParam(ctx, ref), k: key, v: value, @@ -409,7 +432,7 @@ function serializeSet( for (let i = 0; i < size; i++) { item = node.a[i]; if (isIndexedValueInStack(ctx, item)) { - createSetAdd(ctx, node.i, getRefParam(ctx, item.i)); + createAddAssignment(ctx, node.i, getRefParam(ctx, item.i)); } else { // Push directly result += (hasPrev ? ',' : '') + serializeTree(ctx, item); @@ -451,7 +474,7 @@ function serializeMap( // Register an assignment since // both key and value are a parent of this // Map instance - createMapSet(ctx, node.i, keyRef, valueRef); + createSetAssignment(ctx, node.i, keyRef, valueRef); } else { // Reset the stack // This is required because the serialized @@ -460,7 +483,7 @@ function serializeMap( // assignment parent = ctx.stack; ctx.stack = []; - createMapSet(ctx, node.i, keyRef, serializeTree(ctx, val)); + createSetAssignment(ctx, node.i, keyRef, serializeTree(ctx, val)); ctx.stack = parent; } } else if (isIndexedValueInStack(ctx, val)) { @@ -469,7 +492,7 @@ function serializeMap( // Reset stack for the key serialization parent = ctx.stack; ctx.stack = []; - createMapSet(ctx, node.i, serializeTree(ctx, key), valueRef); + createSetAssignment(ctx, node.i, serializeTree(ctx, key), valueRef); ctx.stack = parent; } else { result += (hasPrev ? ',[' : '[') + serializeTree(ctx, key) + ',' + serializeTree(ctx, val) + ']'; @@ -650,6 +673,45 @@ function serializeHeaders( return assignIndexedValue(ctx, node.i, 'new Headers(' + serializeNodeList(ctx, node) + ')'); } +function serializeFormDataEntries( + ctx: SerializationContext, + node: SerovalFormDataNode, +) { + ctx.stack.push(node.i); + const mainAssignments: Assignment[] = []; + let parentStack: number[]; + let value: string; + let key: string; + let parentAssignment: Assignment[]; + for (let i = 0; i < node.d.s; i++) { + parentStack = ctx.stack; + ctx.stack = []; + value = serializeTree(ctx, node.d.v[i]); + key = node.d.k[i]; + ctx.stack = parentStack; + parentAssignment = ctx.assignments; + ctx.assignments = mainAssignments; + createAppendAssignment(ctx, node.i, '"' + key + '"', value); + ctx.assignments = parentAssignment; + } + ctx.stack.pop(); + return resolveAssignments(mainAssignments); +} + +function serializeFormData( + ctx: SerializationContext, + node: SerovalFormDataNode, +) { + if (node.d.s) { + markRef(ctx, node.i); + } + const result = assignIndexedValue(ctx, node.i, 'new FormData()'); + if (node.d.s) { + return '(' + result + ',' + serializeFormDataEntries(ctx, node) + getRefParam(ctx, node.i) + ')'; + } + return result; +} + export default function serializeTree( ctx: SerializationContext, node: SerovalNode, @@ -720,6 +782,8 @@ export default function serializeTree( return serializeFile(ctx, node); case SerovalNodeType.Headers: return serializeHeaders(ctx, node); + case SerovalNodeType.FormData: + return serializeFormData(ctx, node); default: throw new Error('Unsupported type'); } diff --git a/packages/seroval/src/tree/sync.ts b/packages/seroval/src/tree/sync.ts index 33f9559d..49b30291 100644 --- a/packages/seroval/src/tree/sync.ts +++ b/packages/seroval/src/tree/sync.ts @@ -40,6 +40,7 @@ import { SerovalAggregateErrorNode, SerovalArrayNode, SerovalErrorNode, + SerovalFormDataNode, SerovalHeadersNode, SerovalIterableNode, SerovalMapNode, @@ -313,7 +314,7 @@ function generateHeadersNode( id: number, current: Headers, ): SerovalHeadersNode { - assert(ctx.features & Feature.WebAPI, 'Unsupported type "File"'); + assert(ctx.features & Feature.WebAPI, 'Unsupported type "Headers"'); const items: [string, string][] = []; current.forEach((value, key) => { items.push([key, value]); @@ -332,6 +333,30 @@ function generateHeadersNode( }; } +function generateFormDataNode( + ctx: ParserContext, + id: number, + current: FormData, +): SerovalFormDataNode { + assert(ctx.features & Feature.WebAPI, 'Unsupported type "FormData"'); + const items: Record = {}; + current.forEach((value, key) => { + items[key] = value; + }); + return { + t: SerovalNodeType.FormData, + i: id, + s: undefined, + l: undefined, + c: undefined, + m: undefined, + d: generateProperties(ctx, items), + a: undefined, + f: undefined, + b: undefined, + }; +} + function parse( ctx: ParserContext, current: T, @@ -434,6 +459,8 @@ function parse( return createURLSearchParamsNode(ctx, id, current as unknown as URLSearchParams); case Headers: return generateHeadersNode(ctx, id, current as unknown as Headers); + case FormData: + return generateFormDataNode(ctx, id, current as unknown as FormData); default: break; } diff --git a/packages/seroval/src/tree/types.ts b/packages/seroval/src/tree/types.ts index efb97de4..ea483726 100644 --- a/packages/seroval/src/tree/types.ts +++ b/packages/seroval/src/tree/types.ts @@ -34,6 +34,7 @@ export const enum SerovalNodeType { Blob, File, Headers, + FormData, } export interface SerovalBaseNode { @@ -341,6 +342,12 @@ export interface SerovalHeadersNode extends SerovalBaseNode { a: SerovalNode[]; } +export interface SerovalFormDataNode extends SerovalBaseNode { + t: SerovalNodeType.FormData; + i: number; + d: SerovalObjectRecordNode; +} + export type SerovalNode = | SerovalPrimitiveNode | SerovalIndexedValueNode @@ -362,4 +369,5 @@ export type SerovalNode = | SerovalDataViewNode | SerovalBlobNode | SerovalFileNode - | SerovalHeadersNode; + | SerovalHeadersNode + | SerovalFormDataNode; diff --git a/packages/seroval/test.js b/packages/seroval/test.js index 0d30d501..4c02933b 100644 --- a/packages/seroval/test.js +++ b/packages/seroval/test.js @@ -1,10 +1,15 @@ import { serialize, Feature } from 'seroval'; -const example = new AggregateError([]); - -example.errors = [example]; +const errors = []; +const example = Object.assign(new AggregateError(errors), { + errors, +}); +errors[0] = example; +console.log(errors) console.dir(serialize(example, { disabledFeatures: Feature.ErrorPrototypeStack }), { depth: null -}); \ No newline at end of file +}); + +console.log(((h,j)=>(h=Object.assign(new AggregateError([],""),{name:"AggregateError",errors:j=[,]}),j[0]=h,h))()); \ No newline at end of file diff --git a/packages/seroval/test/web-api/__snapshots__/form-data.test.ts.snap b/packages/seroval/test/web-api/__snapshots__/form-data.test.ts.snap new file mode 100644 index 00000000..28936be5 --- /dev/null +++ b/packages/seroval/test/web-api/__snapshots__/form-data.test.ts.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1 + +exports[`FormData > serialize > supports FormData 1`] = `"(h=>((h=new FormData(),h.append(\\"hello\\",\\"world\\"),h)))()"`; + +exports[`FormData > serializeAsync > supports FormData 1`] = `"(h=>(Promise.resolve((h=new FormData(),h.append(\\"example\\",new File([new Uint8Array([72,101,108,108,111,32,87,111,114,108,100]).buffer],\\"hello.txt\\",{type:\\"text/plain\\",lastModified:1681027542680})),h))))()"`; + +exports[`FormData > toJSON > supports FormData 1`] = `"{\\"t\\":{\\"t\\":33,\\"i\\":0,\\"d\\":{\\"k\\":[\\"hello\\"],\\"v\\":[{\\"t\\":1,\\"s\\":\\"world\\"}],\\"s\\":1}},\\"r\\":0,\\"i\\":false,\\"f\\":16383,\\"m\\":[]}"`; + +exports[`FormData > toJSONAsync > supports FormData 1`] = `"{\\"t\\":{\\"t\\":18,\\"i\\":0,\\"f\\":{\\"t\\":33,\\"i\\":1,\\"d\\":{\\"k\\":[\\"example\\"],\\"v\\":[{\\"t\\":31,\\"i\\":2,\\"c\\":\\"text/plain\\",\\"m\\":\\"hello.txt\\",\\"f\\":{\\"t\\":28,\\"i\\":3,\\"s\\":[72,101,108,108,111,32,87,111,114,108,100]},\\"b\\":1681027542680}],\\"s\\":1}}},\\"r\\":0,\\"i\\":false,\\"f\\":16383,\\"m\\":[]}"`; diff --git a/packages/seroval/test/web-api/form-data.test.ts b/packages/seroval/test/web-api/form-data.test.ts new file mode 100644 index 00000000..b824b224 --- /dev/null +++ b/packages/seroval/test/web-api/form-data.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from 'vitest'; +import 'node-fetch-native/polyfill'; +import { + deserialize, + fromJSON, + serialize, + serializeAsync, + toJSON, + toJSONAsync, +} from '../../src'; + +describe('FormData', () => { + describe('serialize', () => { + it('supports FormData', () => { + const example = new FormData(); + example.set('hello', 'world'); + const result = serialize(example); + expect(result).toMatchSnapshot(); + const back = deserialize(result); + expect(back).toBeInstanceOf(FormData); + }); + }); + describe('serializeAsync', () => { + it('supports FormData', async () => { + const example = new FormData(); + example.set('example', new File(['Hello World'], 'hello.txt', { + type: 'text/plain', + lastModified: 1681027542680, + })); + const result = await serializeAsync(Promise.resolve(example)); + expect(result).toMatchSnapshot(); + const back = await deserialize>(result); + expect(back).toBeInstanceOf(FormData); + }); + }); + describe('toJSON', () => { + it('supports FormData', () => { + const example = new FormData(); + example.set('hello', 'world'); + const result = toJSON(example); + expect(JSON.stringify(result)).toMatchSnapshot(); + const back = fromJSON(result); + expect(back).toBeInstanceOf(FormData); + }); + }); + describe('toJSONAsync', () => { + it('supports FormData', async () => { + const example = new FormData(); + example.set('example', new File(['Hello World'], 'hello.txt', { + type: 'text/plain', + lastModified: 1681027542680, + })); + const result = await toJSONAsync(Promise.resolve(example)); + expect(JSON.stringify(result)).toMatchSnapshot(); + const back = await fromJSON>(result); + expect(back).toBeInstanceOf(FormData); + expect(String(back)).toBe(String(example)); + }); + }); +});