Comparing version 0.9.0 to 0.10.1
@@ -1,19 +0,9 @@ | ||
import type { Options } from './context'; | ||
import type { SerovalNode } from './tree/types'; | ||
import { serialize } from './core/tree'; | ||
export type { AsyncServerValue, ServerValue, PrimitiveValue, CommonServerValue, SemiPrimitiveValue, ErrorValue, } from './types'; | ||
export { Feature } from './compat'; | ||
export declare function serialize<T>(source: T, options?: Partial<Options>): string; | ||
export declare function serializeAsync<T>(source: T, options?: Partial<Options>): Promise<string>; | ||
export declare function deserialize<T>(source: string): T; | ||
export interface SerovalJSON { | ||
t: SerovalNode; | ||
r: number; | ||
f: number; | ||
m: number[]; | ||
} | ||
export declare function toJSON<T>(source: T, options?: Partial<Options>): SerovalJSON; | ||
export declare function toJSONAsync<T>(source: T, options?: Partial<Options>): Promise<SerovalJSON>; | ||
export declare function compileJSON(source: SerovalJSON): string; | ||
export declare function fromJSON<T>(source: SerovalJSON): T; | ||
export { Feature } from './core/compat'; | ||
export { createReference } from './core/reference'; | ||
export * from './core/tree'; | ||
export * from './core/cross'; | ||
export { CROSS_REFERENCE_HEADER } from './core/keys'; | ||
export { default as Serializer } from './core/Serializer'; | ||
export default serialize; | ||
export { createReference } from './tree/reference'; |
@@ -1,2 +0,2 @@ | ||
import type { WellKnownSymbols } from './tree/symbols'; | ||
import type { WellKnownSymbols } from './core/constants'; | ||
export type PrimitiveValue = boolean | string | number | undefined | null | WellKnownSymbols; | ||
@@ -3,0 +3,0 @@ export type ErrorValue = Error | AggregateError | EvalError | RangeError | ReferenceError | TypeError | SyntaxError | URIError; |
{ | ||
"name": "seroval", | ||
"type": "module", | ||
"version": "0.9.0", | ||
"version": "0.10.1", | ||
"files": [ | ||
@@ -17,5 +17,5 @@ "dist", | ||
"devDependencies": { | ||
"@types/node": "^20.4.0", | ||
"@vitest/ui": "^0.33.0", | ||
"eslint": "^8.44.0", | ||
"@types/node": "^20.6.2", | ||
"@vitest/ui": "^0.34.4", | ||
"eslint": "^8.49.0", | ||
"eslint-config-lxsmnsyc": "^0.6.5", | ||
@@ -71,3 +71,3 @@ "node-fetch-native": "^1.1.0", | ||
}, | ||
"gitHead": "a3bfdc3ac2246b5b067f67af1f10d7a416bfed5f" | ||
"gitHead": "aa589c20369bfbeb28d7a3d9ab0e86ed92205195" | ||
} |
291
README.md
@@ -83,291 +83,8 @@ # seroval | ||
### Mutual cyclic example | ||
## Docs | ||
```js | ||
import { serialize } from 'seroval'; | ||
- [Serialization](https://github.com/lxsmnsyc/seroval/blob/main/docs/serialization.md) | ||
- [Compatibility](https://github.com/lxsmnsyc/seroval/blob/main/docs/compatibility.md) | ||
- [Isomorphic References](https://github.com/lxsmnsyc/seroval/blob/main/docs/isomorphic-refs.md) | ||
const a = new Map([['name', 'a']]); | ||
const b = new Map([['name', 'b']]); | ||
const c = new Map([['name', 'c']]); | ||
const d = new Map([['name', 'd']]); | ||
c.set('left', a); | ||
d.set('left', a); | ||
c.set('right', b); | ||
d.set('right', b); | ||
a.set('children', [c, d]); | ||
b.set('children', [c, d]); | ||
const result = serialize({ a, b, c, d }); | ||
console.log(result); | ||
``` | ||
Output (as a string): | ||
```js | ||
((h,j,k,m,o,q)=>(q={a:h=new Map([["name","a"],["children",[j=new Map([["name","c"],["right",o=new Map([["name","b"],["children",k=[,m=new Map([["name","d"]])]]])]]),m]]]),b:o,c:j,d:m},j.set("left",h),k[0]=j,m.set("left",h).set("right",o),q))() | ||
// Formatted | ||
((h, j, k, m, o, q) => (q = { | ||
a: h = new Map([ | ||
["name", "a"], | ||
["children", [j = new Map([ | ||
["name", "c"], | ||
["right", o = new Map([ | ||
["name", "b"], | ||
["children", k = [, m = new Map([ | ||
["name", "d"] | ||
])]] | ||
])] | ||
]), m]] | ||
]), | ||
b: o, | ||
c: j, | ||
d: m | ||
}, j.set("left", h), k[0] = j, m.set("left", h).set("right", o), q))() | ||
``` | ||
## Deserialization | ||
```js | ||
import { serialize, deserialize } from 'seroval'; | ||
const value = undefined; | ||
console.log(deserialize(serialize(value)) === value); | ||
``` | ||
## JSON | ||
`serialize` and `deserialize` is great for server-to-client communication, but what about the other way? `serialize` may cause an [RCE if used as a payload for requests](https://huntr.dev/bounties/63f1ff91-48f3-4886-a179-103f1ddd8ff8). `seroval` includes `toJSON` and `fromJSON` as an alternative form of serialization. | ||
First example above outputs the following JSON | ||
```js | ||
import { toJSON } from 'seroval'; | ||
// ... | ||
const result = toJSON(object); | ||
console.log(JSON.stringify(result)); | ||
``` | ||
```json | ||
{"t":{"t":16,"i":0,"d":{"k":["number","string","boolean","null","undefined","bigint","array","regexp","date","map","set","self"],"v":[{"t":15,"i":1,"l":5,"a":[{"t":0,"s":0.4350045546286634},{"t":5},{"t":8},{"t":6},{"t":7}]},{"t":15,"i":2,"l":2,"a":[{"t":1,"s":"hello world"},{"t":1,"s":"\\x3Cscript>Hello World\\x3C/script>"}]},{"t":15,"i":3,"l":2,"a":[{"t":2,"s":true},{"t":2,"s":false}]},{"t":3},{"t":4},{"t":9,"s":"9007199254740991"},{"t":15,"i":4,"l":5,"a":[null,null,null,{"t":10,"i":4},{"t":14,"i":5,"d":{"k":[{"t":1,"s":"hello"},{"t":1,"s":"self"},{"t":1,"s":"mutual"}],"v":[{"t":1,"s":"world"},{"t":10,"i":5},{"t":13,"i":6,"l":4,"a":[{"t":1,"s":"hello"},{"t":1,"s":"world"},{"t":10,"i":6},{"t":10,"i":4}]}],"s":3}}]},{"t":12,"i":7,"c":"[a-z0-9]+","m":"i"},{"t":11,"i":8,"s":"2023-03-22T02:55:33.504Z"},{"t":10,"i":5},{"t":10,"i":6},{"t":10,"i":0}],"s":12}},"r":0,"i":true,"f":8191,"m":[4,5,6,0]} | ||
``` | ||
Then you can feed it to `fromJSON`: | ||
```js | ||
import { fromJSON } from 'seroval'; | ||
const revived = fromJSON(result); | ||
``` | ||
Alternatively, if you want to compile the JSON output to JS (like `deserialize`), you can use `compileJSON` | ||
```js | ||
import { compileJSON, deserialize } from 'seroval'; | ||
const code = compileJSON(result); | ||
const revived = deserialize(code); | ||
``` | ||
## Promise serialization | ||
`seroval` allows Promise serialization through `serializeAsync` and `toJSONAsync`. | ||
```js | ||
import { serializeAsync } from 'seroval'; | ||
const value = Promise.resolve(100); | ||
const result = await serializeAsync(value); // "Promise.resolve(100)" | ||
console.log(await deserialize(result)); // 100 | ||
``` | ||
> **Note** | ||
> `seroval` can only serialize the resolved value and so the output will always be using `Promise.resolve`. If the Promise fulfills with rejection, the rejected value is thrown before serialization happens. | ||
## Serializable references | ||
There are values that has no way to be serializable at all, i.e. functions, but usually in an isomorphic code, functions can exist on both client and server-side. What if we can serialize these functions in such a way we can refer to their counterparts? | ||
`seroval` has `createReference` that you can use to map user-defined strings to their references. | ||
```js | ||
import { createReference } from 'seroval'; | ||
const thisIsAnIsomorphicFunction = createReference( | ||
// This is (ideally) a unique identifier | ||
// that is used to map the serialized value | ||
// to its actual reference (and vice versa) | ||
'my-function', | ||
() => { | ||
// Ideally this function should exist on both | ||
// server and client, but we want to add the ability | ||
// to serialize and deserialize this reference on | ||
// both sides | ||
} | ||
); | ||
// we can now serialize this | ||
const serialized = toJSON(thisIsAnIsomorphicFunction); // or any of the serializer | ||
thisIsAnIsomorphicFunction === fromJSON(serialized); // true | ||
``` | ||
> **Note** | ||
> It can only accept objects, functions and symbols and it doesn't actually | ||
> serialize their values but only the string you used to identify the reference | ||
## Supports | ||
The following values are the only values accepted by `seroval`: | ||
- Exact values | ||
- `NaN` | ||
- `Infinity` | ||
- `-Infinity` | ||
- `-0` | ||
- Primitives | ||
- `number` | ||
- `string` | ||
- `boolean` | ||
- `null` | ||
- `undefined` | ||
- `bigint` | ||
- `Array` + holes | ||
- `Object` | ||
- `RegExp` | ||
- `Date` | ||
- `Map` | ||
- `Set` | ||
- `Object.create(null)` | ||
- `ArrayBuffer` | ||
- `DataView` | ||
- `TypedArray` | ||
- `Int8Array` | ||
- `Int16Array` | ||
- `Int32Array` | ||
- `Uint8Array` | ||
- `Uint16Array` | ||
- `Uint32Array` | ||
- `Uint8ClampedArray` | ||
- `Float32Array` | ||
- `Float64Array` | ||
- `BigInt64Array` | ||
- `BigUint64Array` | ||
- `Error` | ||
- `AggregateError` | ||
- `EvalError` | ||
- `RangeError` | ||
- `ReferenceError` | ||
- `SyntaxError` | ||
- `TypeError` | ||
- `URIError` | ||
- `Promise` (with `serializeAsync` and `toJSONAsync`) | ||
- [`Iterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol) | ||
- [Well-known symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#static_properties) | ||
- Web API | ||
- [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) | ||
- [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) | ||
- [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) (with `serializeAsync` and `toJSONAsync`) | ||
- [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) (with `serializeAsync` and `toJSONAsync`) | ||
- [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) | ||
- [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) | ||
- If `FormData` has a `Blob`/`File` entry, it can only be serialized with `serializeAsync` and `toJSONAsync` | ||
- Cyclic references (both self and mutual) | ||
- Isomorphic references (a reference that exist on both the serializer and deserializer side) | ||
## Compat | ||
`serialize`, `serializeAsync`, `toJSON` and `toJSONAsync` can accept a `{ disabledFeatures: number }` option. The `disabledFeatures` defines how the output code would look like when serialized by `serialize`, `serializeAsync` and `compileJSON`. | ||
```js | ||
import { serialize, Feature } from 'seroval'; | ||
const y = Object.create(null); | ||
y.self = y; | ||
y.example = 'Hello World'; | ||
function serializeWithTarget(value, disabledFeatures) { | ||
const result = serialize(value, { | ||
disabledFeatures, | ||
}); | ||
console.log(result); | ||
} | ||
serializeWithTarget(y, Feature.ArrowFunction | Feature.ObjectAssign); | ||
serializeWithTarget(y, 0); | ||
``` | ||
```js | ||
(function(h){return (h=Object.create(null),h.self=h,h.example="Hello World",h)})() | ||
(h=>(h=Object.assign(Object.create(null),{example:"Hello World"}),h.self=h,h))() | ||
``` | ||
`disabledFeatures` uses bit flags for faster checking, so if you need to disable multiple features, you can use the logical OR symbol (`|`). | ||
Here's an `ES2017` flag: | ||
```js | ||
import { serialize, Feature } from 'seroval'; | ||
const ES2017FLAG = | ||
Feature.AggregateError // ES2021 | ||
| Feature.BigInt // ES2020 | ||
| Feature.BigIntTypedArray // ES2020; | ||
serialize(myValue, { | ||
disabledFeatures: ES2017FLAG, | ||
}) | ||
``` | ||
By default, all feature flags are enabled. The following are the feature flags and their behavior when disabled: | ||
- [`AggregateError`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError) | ||
- Compiles down to `Error` instead. | ||
- [`ArrayPrototypeValues`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values) | ||
- Used for `Iterable`, uses `Symbol.iterator` instead. | ||
- [`ArrowFunction`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) | ||
- Uses function expressions for top-level and for deferred `Promise` values | ||
- method shorthands (if `MethodShortand` is not set) or function expressions for `Iterable`. | ||
- [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) | ||
- Throws when attempted to use, includes `BigIntTypedArray` | ||
- Disables use of `BigInt`, `BigInt64Array` and `BigUint64Array` | ||
- [`ErrorPrototypeStack`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack) | ||
- Skipped when detected. | ||
- Affects both `Error` and `AggregateError` | ||
- [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) | ||
- Throws when attempted to use. | ||
- Disables serialization of `Map` | ||
- [`MethodShorthand`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions) | ||
- Uses function expressions instead. | ||
- Only affects `Iterable` | ||
- [`ObjectAssign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) | ||
- Uses manual object assignments instead. | ||
- Affects `Iterable`, `Error`, `AggregateError` and `Object.create(null)` | ||
- [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) | ||
- Throws when attempted to use in `serializeAsync` and `toJSONAsync`. | ||
- Disables serialization of `Promise` | ||
- [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) | ||
- Throws when attempted to use. | ||
- Disables serialization of `Set` | ||
- [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) | ||
- Throws when attempted to use. | ||
- This disables serialization of well-known symbols and `Iterable`. | ||
- [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) | ||
- Throws when attempted to use. | ||
- Disables serialization of `TypedArray` | ||
- [`BigIntTypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array) | ||
- Throws when attempted to use | ||
- Also throws if `BigInt` is disabled. | ||
- Disables serialization of `BigInt64Array` and `BigUint64Array` | ||
- `WebAPI` | ||
- Throws and disables the following usage: | ||
- [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) | ||
- [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) | ||
- [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) | ||
- [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) | ||
- [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) | ||
- [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) | ||
## Sponsors | ||
@@ -374,0 +91,0 @@ |
148
src/index.ts
@@ -1,19 +0,2 @@ | ||
/* eslint-disable no-await-in-loop */ | ||
import { Feature } from './compat'; | ||
import type { | ||
SerializationContext, | ||
Options, | ||
} from './context'; | ||
import { | ||
getRefParam, | ||
createParserContext, | ||
createSerializationContext, | ||
getRootID, | ||
} from './context'; | ||
import parseSync from './tree/sync'; | ||
import parseAsync from './tree/async'; | ||
import deserializeTree from './tree/deserialize'; | ||
import serializeTree, { resolvePatches } from './tree/serialize'; | ||
import type { SerovalNode } from './tree/types'; | ||
import { SerovalNodeType } from './tree/types'; | ||
import { serialize } from './core/tree'; | ||
@@ -28,129 +11,12 @@ export type { | ||
} from './types'; | ||
export { Feature } from './compat'; | ||
export { Feature } from './core/compat'; | ||
export { createReference } from './core/reference'; | ||
function finalize( | ||
ctx: SerializationContext, | ||
rootID: number, | ||
isObject: boolean, | ||
result: string, | ||
): string { | ||
// Shared references detected | ||
if (ctx.vars.length) { | ||
const patches = resolvePatches(ctx); | ||
let body = result; | ||
if (patches) { | ||
// Get (or create) a ref from the source | ||
const index = getRefParam(ctx, rootID); | ||
body = result + ',' + patches + index; | ||
if (!result.startsWith(index + '=')) { | ||
body = index + '=' + body; | ||
} | ||
} | ||
let params = ctx.vars.length > 1 | ||
? ctx.vars.join(',') | ||
: ctx.vars[0]; | ||
// Source is probably already assigned | ||
if (ctx.features & Feature.ArrowFunction) { | ||
params = ctx.vars.length > 1 || ctx.vars.length === 0 | ||
? '(' + params + ')' | ||
: params; | ||
return '(' + params + '=>(' + body + '))()'; | ||
} | ||
return '(function(' + params + '){return ' + body + '})()'; | ||
} | ||
if (isObject) { | ||
return '(' + result + ')'; | ||
} | ||
return result; | ||
} | ||
export * from './core/tree'; | ||
export * from './core/cross'; | ||
export function serialize<T>( | ||
source: T, | ||
options?: Partial<Options>, | ||
): string { | ||
const ctx = createParserContext(options); | ||
const tree = parseSync(ctx, source); | ||
const serial = createSerializationContext(ctx); | ||
const result = serializeTree(serial, tree); | ||
return finalize( | ||
serial, | ||
getRootID(ctx, source), | ||
tree.t === SerovalNodeType.Object, | ||
result, | ||
); | ||
} | ||
export { CROSS_REFERENCE_HEADER } from './core/keys'; | ||
export async function serializeAsync<T>( | ||
source: T, | ||
options?: Partial<Options>, | ||
): Promise<string> { | ||
const ctx = createParserContext(options); | ||
const tree = await parseAsync(ctx, source); | ||
const serial = createSerializationContext(ctx); | ||
const result = serializeTree(serial, tree); | ||
return finalize( | ||
serial, | ||
getRootID(ctx, source), | ||
tree.t === SerovalNodeType.Object, | ||
result, | ||
); | ||
} | ||
export { default as Serializer } from './core/Serializer'; | ||
export function deserialize<T>(source: string): T { | ||
// eslint-disable-next-line no-eval | ||
return (0, eval)(source) as T; | ||
} | ||
export interface SerovalJSON { | ||
t: SerovalNode; | ||
r: number; | ||
f: number; | ||
m: number[]; | ||
} | ||
export function toJSON<T>( | ||
source: T, | ||
options?: Partial<Options>, | ||
): SerovalJSON { | ||
const ctx = createParserContext(options); | ||
return { | ||
t: parseSync(ctx, source), | ||
r: getRootID(ctx, source), | ||
f: ctx.features, | ||
m: Array.from(ctx.markedRefs), | ||
}; | ||
} | ||
export async function toJSONAsync<T>( | ||
source: T, | ||
options?: Partial<Options>, | ||
): Promise<SerovalJSON> { | ||
const ctx = createParserContext(options); | ||
return { | ||
t: await parseAsync(ctx, source), | ||
r: getRootID(ctx, source), | ||
f: ctx.features, | ||
m: Array.from(ctx.markedRefs), | ||
}; | ||
} | ||
export function compileJSON(source: SerovalJSON): string { | ||
const serial = createSerializationContext({ | ||
features: source.f, | ||
markedRefs: source.m, | ||
}); | ||
const result = serializeTree(serial, source.t); | ||
return finalize(serial, source.r, source.t.i === SerovalNodeType.Object, result); | ||
} | ||
export function fromJSON<T>(source: SerovalJSON): T { | ||
const serial = createSerializationContext({ | ||
features: source.f, | ||
markedRefs: source.m, | ||
}); | ||
return deserializeTree(serial, source.t) as T; | ||
} | ||
export default serialize; | ||
export { createReference } from './tree/reference'; |
@@ -1,2 +0,2 @@ | ||
import type { WellKnownSymbols } from './tree/symbols'; | ||
import type { WellKnownSymbols } from './core/constants'; | ||
@@ -3,0 +3,0 @@ // Values that are non-recursive |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
1251697
81
18452
96
1