Phase 1 PWA: prayer times, qibla, quran, hijri, tasbih, 99 names

This commit is contained in:
Hermes Bot
2026-08-06 11:55:47 +08:00
parent 4def05d0ef
commit 0d37e7ec1e
10869 changed files with 959465 additions and 18 deletions
+7
View File
@@ -0,0 +1,7 @@
Copyright (c) 2018-19 [these people](https://github.com/rich-harris/devalue/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+328
View File
@@ -0,0 +1,328 @@
# devalue
Like `JSON.stringify`, but handles
- cyclical references (`obj.self = obj`)
- repeated references (`[value, value]`)
- `undefined`, `Infinity`, `NaN`, `-0`
- regular expressions
- dates
- `Map` and `Set`
- `BigInt`
- `ArrayBuffer` and Typed Arrays
- `URL` and `URLSearchParams`
- `Temporal`
- custom types via replacers, reducers and revivers
- promises (via `stringifyAsync`)
Try it out [here](https://svelte.dev/repl/138d70def7a748ce9eda736ef1c71239?version=3.49.0).
## Goals:
- Performance
- Security (see [XSS mitigation](#xss-mitigation))
- Compact output
## Non-goals:
- Human-readable output
- Stringifying functions
- Stability of serialization mechanisms between versions (i.e. if you `devalue.stringify` with one version and `devalue.parse` with another, things may break)
## Usage
There are two ways to use `devalue`:
### `uneval`
This function takes a JavaScript value and returns the JavaScript code to create an equivalent value — sort of like `eval` in reverse:
```js
import * as devalue from 'devalue';
let obj = { message: 'hello' };
devalue.uneval(obj); // '{message:"hello"}'
obj.self = obj;
devalue.uneval(obj); // '(function(a){a.message="hello";a.self=a;return a}({}))'
```
Use `uneval` when you want the most compact possible output and don't want to include any code for parsing the serialized value.
### `stringify` and `parse`
These two functions are analogous to `JSON.stringify` and `JSON.parse`:
```js
import * as devalue from 'devalue';
let obj = { message: 'hello' };
let stringified = devalue.stringify(obj); // '[{"message":1},"hello"]'
devalue.parse(stringified); // { message: 'hello' }
obj.self = obj;
stringified = devalue.stringify(obj); // '[{"message":1,"self":0},"hello"]'
devalue.parse(stringified); // { message: 'hello', self: [Circular] }
```
Use `stringify` and `parse` when evaluating JavaScript isn't an option.
### `stringifyAsync`
`stringifyAsync` is an async version of `stringify` that can handle promises:
```js
import * as devalue from 'devalue';
let obj = {
quick: 'data',
slow: fetch('/api/slow').then((r) => r.json())
};
let stringified = await devalue.stringifyAsync(obj);
devalue.parse(stringified); // { quick: 'data', slow: { ... } }
```
Promises are awaited and their resolved values are serialized. The output format is identical to `stringify`, so `parse` and `unflatten` work unchanged.
### `unflatten`
In the case where devalued data is one part of a larger JSON string, `unflatten` allows you to revive just the bit you need:
```js
import * as devalue from 'devalue';
const json = `{
"type": "data",
"data": ${devalue.stringify(data)}
}`;
const data = devalue.unflatten(JSON.parse(json).data);
```
## Custom types
You can serialize and deserialize custom types by passing a second argument to `stringify` containing an object of types and their _reducers_, and a second argument to `parse` or `unflatten` containing an object of types and their _revivers_:
```js
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
magnitude() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
}
const stringified = devalue.stringify(new Vector(30, 40), {
Vector: (value) => value instanceof Vector && [value.x, value.y]
});
console.log(stringified); // [["Vector",1],[2,3],30,40]
const vector = devalue.parse(stringified, {
Vector: ([x, y]) => new Vector(x, y)
});
console.log(vector.magnitude()); // 50
```
If a function passed to `stringify` returns a truthy value, it's treated as a match.
You can also use custom types with `uneval` by specifying a custom replacer:
```js
devalue.uneval(vector, (value, uneval) => {
if (value instanceof Vector) {
return `new Vector(${value.x},${value.y})`;
}
}); // `new Vector(30,40)`
```
Note that any variables referenced in the resulting JavaScript (like `Vector` in the example above) must be in scope when it runs.
## Custom operations
Every introspection `stringify` performs on the value being serialized — property reads, prototype method calls, iteration, type classification — goes through an operations interface that you can override via the `operations` option. Omitted members fall back to the defaults (exported as `defaultStringifyOperations`), which behave exactly as devalue always has.
This is useful in two situations:
**Side-effect-free serialization.** By default, serializing a value can execute user code: getters and proxy traps fire during property reads, `Object.prototype.toString` consults (potentially getter-defined) `Symbol.toStringTag`, and patched prototype methods like `Date.prototype.toISOString` or `Map.prototype[Symbol.iterator]` are invoked. Deterministic or sandboxed runtimes can replace these operations with implementations based on captured intrinsics and property descriptors:
```js
const originalToISOString = Date.prototype.toISOString;
const stringified = devalue.stringify(value, undefined, {
operations: {
// use a captured intrinsic instead of a (possibly patched) prototype method
toISOString: (date) => originalToISOString.call(date),
// read through descriptors so getters are never invoked
get: (object, key) => {
const descriptor = Object.getOwnPropertyDescriptor(object, key);
if (descriptor?.get) throw new Error(`refusing to invoke getter for "${key}"`);
return descriptor?.value;
}
}
});
```
**Foreign-runtime serialization.** The `stringify` algorithm never touches the value directly, so "value" can be an opaque handle to something living in another JavaScript runtime — a `node:vm` context, a WASM-hosted engine, a remote process — as long as the operations know how to inspect it. Implement `typeOf`/`tagOf` for classification, `toPrimitive`/`get`/`entriesOf`/etc. for extraction, and `identify` to key deduplication and cycle detection on the underlying value's identity rather than the handle's:
```js
const stringified = devalue.stringify(rootHandle, undefined, {
operations: {
identify: (handle) => handle.pointer,
typeOf: (handle) => handle.typeOf(),
get: (handle, key) => handle.getProperty(key)
// ... see StringifyOperations for the full interface
}
});
```
Some operations have a non-obvious contract that is easy to get subtly wrong. Where the work is not specific to your values, devalue exports the pieces so you don't have to reimplement them — `filterArrayIndices` does the array-index filtering that `indicesOf` needs, given keys you already have:
```js
indicesOf: (handle) => devalue.filterArrayIndices(handle.ownEnumerableStringKeys())
```
Reducers compose with custom operations: they receive the raw value/handle, and whatever they return is serialized through the same operations.
### Customizing `parse`
The mirror image: `parse` and `unflatten` build every value through construction operations (`ParseOperations`, defaults exported as `defaultParseOperations`), so you can control what gets created. The members mirror `StringifyOperations` with the host/value-space boundary running the other way: each `fromXxx` inverts the corresponding `toXxx`, `fromXxxInfo` inverts `xxxInfo`, and the bare-verb mutators invert the bare-verb accessors (`set`/`get`, `addValue`/`valuesOf`, `addEntry`/`entriesOf`, `box`/`unbox`).
**Cross-realm revival.** By default the revived value is built from the intrinsics of whichever realm devalue is running in, so `instanceof` checks fail elsewhere. Constructing from a target realm's intrinsics fixes that:
```js
const revived = devalue.parse(serialized, undefined, {
operations: {
fromISOString: (iso) => new sandbox.Date(iso),
createMap: () => new sandbox.Map(),
createObject: () => sandbox.makeObject()
}
});
```
**Foreign-runtime revival.** `parse` never inspects the values it creates — it only passes them back into other operations — so the operations can build values inside another runtime and return opaque handles:
```js
const rootHandle = devalue.parse(serialized, undefined, {
operations: {
fromPrimitive: (primitive) => vm.toHandle(primitive),
createObject: () => vm.newObject(),
set: (handle, key, value) => handle.setProp(key, value)
// ... see ParseOperations for the full interface
}
});
```
Containers are created empty and populated afterwards (`createMap` then `addEntry`, `createObject` then `set`, and so on) — that ordering is what allows cyclic values to be revived, since the empty container is cached before its contents are built.
Revivers compose the same way reducers do: they receive whatever the operations built, and their return value is used as-is.
## Error handling
If `uneval` or `stringify` encounters a function or a non-POJO that isn't handled by a custom replacer/reducer, it will throw an error. You can find where in the input data the offending value lives by inspecting `error.path`:
```js
try {
const map = new Map();
map.set('key', function invalid() {});
uneval({
object: {
array: [map]
}
});
} catch (e) {
console.log(e.path); // '.object.array[0].get("key")'
}
```
## XSS mitigation
Say you're server-rendering a page and want to serialize some state, which could include user input. `JSON.stringify` doesn't protect against XSS attacks:
```js
const state = {
userinput: `</script><script src='https://evil.com/mwahaha.js'>`
};
const template = `
<script>
// NEVER DO THIS
var preloaded = ${JSON.stringify(state)};
</script>`;
```
Which would result in this:
```html
<script>
// NEVER DO THIS
var preloaded = {"userinput":"
</script>
<script src="https://evil.com/mwahaha.js">
"};
</script>
```
Using `uneval` or `stringify`, we're protected against that attack:
```js
const template = `
<script>
var preloaded = ${uneval(state)};
</script>`;
```
```html
<script>
var preloaded = {
userinput:
"\\u003C\\u002Fscript\\u003E\\u003Cscript src='https:\\u002F\\u002Fevil.com\\u002Fmwahaha.js'\\u003E"
};
</script>
```
This, along with the fact that `uneval` and `stringify` bail on functions and non-POJOs, stops attackers from executing arbitrary code. Strings generated by `uneval` can be safely deserialized with `eval` or `new Function`:
```js
const value = (0, eval)('(' + str + ')');
```
## Other security considerations
While `uneval` prevents the XSS vulnerability shown above, meaning you can use it to send data from server to client, **you should not send user data from client to server** using the same method. Since it has to be evaluated, an attacker that successfully submitted data that bypassed `uneval` would have access to your system.
When using `eval`, ensure that you call it _indirectly_ so that the evaluated code doesn't have access to the surrounding scope:
```js
{
const sensitiveData = 'Setec Astronomy';
eval('sendToEvilServer(sensitiveData)'); // pwned :(
(0, eval)('sendToEvilServer(sensitiveData)'); // nice try, evildoer!
}
```
Using `new Function(code)` is akin to using indirect eval.
## See also
- [lave](https://github.com/jed/lave) by Jed Schmidt
- [arson](https://github.com/benjamn/arson) by Ben Newman. The `stringify`/`parse` approach in `devalue` was inspired by `arson`
- [oson](https://github.com/KnorpelSenf/oson) by Steffen Trog
- [tosource](https://github.com/marcello3d/node-tosource) by Marcello Bastéa-Forte
- [serialize-javascript](https://github.com/yahoo/serialize-javascript) by Eric Ferraiuolo
- [jsesc](https://github.com/mathiasbynens/jsesc) by Mathias Bynens
- [superjson](https://github.com/blitz-js/superjson) by Blitz
- [next-json](https://github.com/iccicci/next-json) by Daniele Ricci
## License
[MIT](LICENSE)
+17
View File
@@ -0,0 +1,17 @@
export { uneval } from './src/uneval.js';
export { parse, unflatten } from './src/parse.js';
export { stringify, stringifyAsync } from './src/stringify.js';
export {
default_stringify_operations as defaultStringifyOperations,
default_parse_operations as defaultParseOperations
} from './src/operations.js';
export { DevalueError, filter_array_indices as filterArrayIndices } from './src/utils.js';
/** @typedef {import('./src/types.js').StringValueTag} StringValueTag */
/** @typedef {import('./src/types.js').ViewTag} ViewTag */
/** @typedef {import('./src/types.js').StringifyOperations} StringifyOperations */
/** @typedef {import('./src/types.js').DefaultStringifyOperations} DefaultStringifyOperations */
/** @typedef {import('./src/types.js').StringifyOptions} StringifyOptions */
/** @typedef {import('./src/types.js').ParseOperations} ParseOperations */
/** @typedef {import('./src/types.js').DefaultParseOperations} DefaultParseOperations */
/** @typedef {import('./src/types.js').ParseOptions} ParseOptions */
+39
View File
@@ -0,0 +1,39 @@
{
"name": "devalue",
"description": "Gets the job done when JSON.stringify can't",
"version": "5.9.0",
"repository": "sveltejs/devalue",
"sideEffects": false,
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./index.js",
"default": "./index.js"
}
},
"files": [
"index.js",
"src",
"types"
],
"types": "./types/index.d.ts",
"devDependencies": {
"@changesets/cli": "^2.29.6",
"@js-temporal/polyfill": "^0.5.1",
"@types/node": "^24.12.0",
"dts-buddy": "^0.6.2",
"publint": "^0.3.12",
"typescript": "^5.9.2",
"uvu": "^0.5.6"
},
"license": "MIT",
"type": "module",
"scripts": {
"changeset:version": "changeset version",
"changeset:publish": "changeset publish",
"build": "dts-buddy",
"test": "uvu",
"bench": "node --allow-natives-syntax ./benchmarking/run.js",
"bench:compare": "node --allow-natives-syntax ./benchmarking/compare/index.js"
}
}
+60
View File
@@ -0,0 +1,60 @@
/* Baseline 2025 runtimes */
/** @type {(array_buffer: ArrayBuffer) => string} */
export function encode_native(array_buffer) {
return new Uint8Array(array_buffer).toBase64();
}
/** @type {(base64: string) => ArrayBuffer} */
export function decode_native(base64) {
return Uint8Array.fromBase64(base64).buffer;
}
/* Node-compatible runtimes */
/** @type {(array_buffer: ArrayBuffer) => string} */
export function encode_buffer(array_buffer) {
return Buffer.from(array_buffer).toString('base64');
}
/** @type {(base64: string) => ArrayBuffer} */
export function decode_buffer(base64) {
return Uint8Array.from(Buffer.from(base64, 'base64')).buffer;
}
/* Legacy runtimes */
/** @type {(array_buffer: ArrayBuffer) => string} */
export function encode_legacy(array_buffer) {
const array = new Uint8Array(array_buffer);
let binary = '';
// the maximum number of arguments to String.fromCharCode.apply
// should be around 0xFFFF in modern engines
const chunk_size = 0x8000;
for (let i = 0; i < array.length; i += chunk_size) {
const chunk = array.subarray(i, i + chunk_size);
binary += String.fromCharCode.apply(null, chunk);
}
return btoa(binary);
}
/** @type {(base64: string) => ArrayBuffer} */
export function decode_legacy(base64) {
const binary_string = atob(base64);
const len = binary_string.length;
const array = new Uint8Array(len);
for (let i = 0; i < len; i++) {
array[i] = binary_string.charCodeAt(i);
}
return array.buffer;
}
const native = typeof Uint8Array.fromBase64 === 'function';
const buffer = typeof process === 'object' && process.versions?.node !== undefined;
export const encode64 = native ? encode_native : buffer ? encode_buffer : encode_legacy;
export const decode64 = native ? decode_native : buffer ? decode_buffer : decode_legacy;
+44
View File
@@ -0,0 +1,44 @@
import * as assert from 'uvu/assert';
import { suite } from 'uvu';
import * as base64 from './base64.js';
const strings = [
'',
'a',
'ab',
'abc',
'a\r\nb',
'\xFF\xFE',
'\x00',
'\x00\x00\x00',
'the quick brown fox etc',
'é',
'中文',
'+/',
'😎'
];
const test = suite('base64_encode_decode');
const encoder = new TextEncoder();
const decoder = new TextDecoder();
for (const string of strings) {
test(string, () => {
const data = encoder.encode(string);
const with_buffer = base64.encode_buffer(data);
const with_legacy = base64.encode_legacy(data);
assert.equal(with_buffer, with_legacy);
assert.equal(decoder.decode(base64.decode_buffer(with_buffer)), string);
assert.equal(decoder.decode(base64.decode_legacy(with_legacy)), string);
if (typeof Uint8Array.fromBase64 === 'function') {
const with_native = base64.encode_native(data);
assert.equal(decoder.decode(base64.decode_native(with_native)), string);
}
});
}
test.run();
+12
View File
@@ -0,0 +1,12 @@
export const UNDEFINED = -1;
export const HOLE = -2;
export const NAN = -3;
export const POSITIVE_INFINITY = -4;
export const NEGATIVE_INFINITY = -5;
export const NEGATIVE_ZERO = -6;
export const SPARSE = -7;
// The largest valid value for a JavaScript array's `length` property,
// and the largest valid array index (one less than the max length).
export const MAX_ARRAY_LEN = 2 ** 32 - 1;
export const MAX_ARRAY_INDEX = MAX_ARRAY_LEN - 1;
+193
View File
@@ -0,0 +1,193 @@
import { MAX_ARRAY_INDEX } from './constants.js';
import {
enumerable_symbols,
get_type,
is_plain_object,
valid_array_indices
} from './utils.js';
/**
* Merges caller-provided operation overrides over the defaults. Iterating the
* default keys (rather than the override's own keys) means nullish members
* fall back to the default, and inherited members — e.g. from a class
* instance — are picked up.
*
* @template {Record<string, any>} T
* @param {T} defaults
* @param {Partial<T> | undefined} overrides
* @returns {T}
*/
export function merge_operations(defaults, overrides) {
if (!overrides) return defaults;
const merged = /** @type {T} */ ({});
for (const key of /** @type {(keyof T)[]} */ (Object.keys(defaults))) {
merged[key] = overrides[key] ?? defaults[key];
}
return merged;
}
/** @type {{ kind: 'not-plain' }} */
const NOT_PLAIN = Object.freeze({ kind: 'not-plain' });
/** @type {{ kind: 'symbol-keys' }} */
const SYMBOL_KEYS = Object.freeze({ kind: 'symbol-keys' });
/**
* The default implementations of every introspection/extraction operation
* `stringify` performs on the value being serialized. Each one uses native
* JavaScript semantics (property access, iteration, prototype methods, etc).
*
* Pass overrides via the `operations` option of `stringify`/`stringifyAsync`
* to customize how values are inspected — e.g. to serialize values without
* triggering getters, proxy traps, or patched prototype methods, or to
* serialize values that live in a different JavaScript runtime (a `node:vm`
* context, a WASM-hosted engine, a remote process) through handle objects.
*
* The object is frozen — it is shared by every `stringify` call that does
* not override a given operation.
*
*/
/** @type {import('./types.js').DefaultStringifyOperations} */
const stringify_operations = {
identify: (value) => value,
typeOf: (value) => (value === null ? 'null' : typeof value),
toPrimitive: (value) => value,
tagOf: (value) => get_type(value),
isThenable: (value) => typeof value.then === 'function',
toPromise: (thenable) => Promise.resolve(thenable),
unbox: (boxed) => boxed.valueOf(),
toISOString: (date) => (isNaN(date.getDate()) ? '' : date.toISOString()),
toStringValue: (value) => value.toString(),
regExpInfo: (regexp) => ({ source: regexp.source, flags: regexp.flags }),
valuesOf: (set) => set,
entriesOf: (map) => map,
viewInfo: (view) => ({
buffer: view.buffer,
byteOffset: view.byteOffset,
byteLength: view.byteLength,
length: view.length,
bufferByteLength: view.buffer.byteLength
}),
toArrayBuffer: (buffer) => buffer,
lengthOf: (array) => array.length,
hasOwn: (value, key) => Object.hasOwn(value, key),
indicesOf: (array) => valid_array_indices(array),
shapeOf: (value) => {
if (!is_plain_object(value)) return NOT_PLAIN;
if (enumerable_symbols(value).length > 0) return SYMBOL_KEYS;
return {
kind: Object.getPrototypeOf(value) === null ? 'null-proto' : 'plain',
keys: Object.keys(value)
};
},
get: (value, key) => value[key]
};
export const default_stringify_operations = Object.freeze(stringify_operations);
/**
* The default implementations of every construction operation `parse` and
* `unflatten` perform while reviving a value. Each one uses native
* JavaScript semantics (built-in constructors, property assignment, etc).
*
* Pass overrides via the `operations` option of `parse`/`unflatten` to
* customize how values are built — e.g. to construct them from the
* intrinsics of a different realm (a `node:vm` context), or to build up
* values inside another JavaScript runtime (a WASM-hosted engine, a remote
* process) through handle objects.
*
* The object is frozen — it is shared by every `parse` call that does not
* override a given operation.
*
*/
/** @type {import('./types.js').DefaultParseOperations} */
const parse_operations = {
fromPrimitive: (primitive) => primitive,
fromISOString: (iso) => new Date(iso),
fromStringValue: (tag, text) => {
if (tag === 'URL') return new URL(text);
if (tag === 'URLSearchParams') return new URLSearchParams(text);
// 'Temporal.Instant', 'Temporal.PlainDate', ...
// @ts-expect-error TS doesn't know about Temporal yet
return Temporal[tag.slice(9)].from(text);
},
fromArrayBuffer: (buffer) => buffer,
fromRegExpInfo: (source, flags) => new RegExp(source, flags),
fromViewInfo: (tag, buffer, byteOffset, length) => {
const Constructor = /** @type {any} */ (globalThis)[tag];
return byteOffset !== undefined
? new Constructor(buffer, byteOffset, length)
: new Constructor(buffer);
},
box: (value) => Object(value),
createArray: (length) => new Array(length),
createSparseArray: (length) => {
/** @type {any[]} */
const array = [];
// Setting `array.length = length` (or equivalently calling
// `new Array(length)`) on an untrusted length is a DoS vector: V8
// eagerly allocates a contiguous backing store for array lengths below
// ~10^8, so a small payload with a huge declared length can force
// arbitrary memory allocation. Touching the largest-possible index
// first forces V8 into dictionary-elements mode, where `length` is
// just a number and no contiguous allocation occurs.
array[MAX_ARRAY_INDEX] = undefined;
delete array[MAX_ARRAY_INDEX];
array.length = length;
return array;
},
createObject: () => ({}),
createNullPrototypeObject: () => Object.create(null),
createSet: () => new Set(),
createMap: () => new Map(),
set: (target, key, value) => {
target[key] = value;
},
addValue: (set, value) => {
set.add(value);
},
addEntry: (map, key, value) => {
map.set(key, value);
}
};
export const default_parse_operations = Object.freeze(parse_operations);
+270
View File
@@ -0,0 +1,270 @@
import { decode64 } from './base64.js';
import {
HOLE,
NAN,
NEGATIVE_INFINITY,
NEGATIVE_ZERO,
POSITIVE_INFINITY,
SPARSE,
UNDEFINED
} from './constants.js';
import { default_parse_operations, merge_operations } from './operations.js';
import { is_valid_array_index, is_valid_array_len } from './utils.js';
/**
* Revive a value serialized with `devalue.stringify`
* @param {string} serialized
* @param {Record<string, (value: any) => any>} [revivers]
* @param {import('./types.js').ParseOptions} [options]
*/
export function parse(serialized, revivers, options) {
return unflatten(JSON.parse(serialized), revivers, options);
}
/**
* Revive a value flattened with `devalue.stringify`
* @param {number | any[]} parsed
* @param {Record<string, (value: any) => any>} [revivers]
* @param {import('./types.js').ParseOptions} [options]
*/
export function unflatten(parsed, revivers, options) {
/** @type {import('./types.js').ParseOperations} */
const ops = merge_operations(default_parse_operations, options?.operations);
if (typeof parsed === 'number') return hydrate(parsed, true);
if (!Array.isArray(parsed) || parsed.length === 0) {
throw new Error('Invalid input');
}
const values = /** @type {any[]} */ (parsed);
const hydrated = Array(values.length);
/**
* A set of values currently being hydrated with custom revivers,
* used to detect invalid cyclical dependencies
* @type {Set<number> | null}
*/
let hydrating = null;
/**
* @param {number} index
* @returns {any}
*/
function hydrate(index, standalone = false) {
if (index === UNDEFINED) return ops.fromPrimitive(undefined);
if (index === NAN) return ops.fromPrimitive(NaN);
if (index === POSITIVE_INFINITY) return ops.fromPrimitive(Infinity);
if (index === NEGATIVE_INFINITY) return ops.fromPrimitive(-Infinity);
if (index === NEGATIVE_ZERO) return ops.fromPrimitive(-0);
if (standalone || typeof index !== 'number') {
throw new Error(`Invalid input`);
}
if (index in hydrated) return hydrated[index];
const value = values[index];
if (!value || typeof value !== 'object') {
hydrated[index] = ops.fromPrimitive(value);
} else if (Array.isArray(value)) {
if (typeof value[0] === 'string') {
const type = value[0];
const reviver = revivers && Object.hasOwn(revivers, type) ? revivers[type] : undefined;
if (reviver) {
let i = value[1];
if (typeof i !== 'number') {
// if it's not a number, it was serialized by a builtin reviver
// so we need to munge it into the format expected by a custom reviver
i = values.push(value[1]) - 1;
}
// If the payload is already hydrated, its recursion has already
// terminated (e.g. a self-referential object cached itself before
// following its own back-reference), so revive it directly. Falling
// through to the `hydrating` guard here would wrongly reject a valid
// cycle. An actually infinite payload (e.g. `[["Custom", 0]]`) is never
// cached, so it still hits the guard below.
if (Object.hasOwn(hydrated, i)) {
return (hydrated[index] = reviver(hydrated[i]));
}
hydrating ??= new Set();
if (hydrating.has(i)) {
throw new Error('Invalid circular reference');
}
hydrating.add(i);
hydrated[index] = reviver(hydrate(i));
hydrating.delete(i);
return hydrated[index];
}
switch (type) {
case 'Date':
hydrated[index] = ops.fromISOString(value[1]);
break;
case 'Set':
const set = ops.createSet();
hydrated[index] = set;
for (let i = 1; i < value.length; i += 1) {
ops.addValue(set, hydrate(value[i]));
}
break;
case 'Map':
const map = ops.createMap();
hydrated[index] = map;
for (let i = 1; i < value.length; i += 2) {
ops.addEntry(map, hydrate(value[i]), hydrate(value[i + 1]));
}
break;
case 'RegExp':
hydrated[index] = ops.fromRegExpInfo(value[1], value[2]);
break;
case 'Object': {
const wrapped_index = value[1];
if (
typeof values[wrapped_index] === 'object' &&
values[wrapped_index][0] !== 'BigInt'
) {
// avoid infinite recusion in case of malformed input
throw new Error('Invalid input');
}
hydrated[index] = ops.box(hydrate(wrapped_index));
break;
}
case 'BigInt':
hydrated[index] = ops.fromPrimitive(BigInt(value[1]));
break;
case 'null':
const obj = ops.createNullPrototypeObject();
hydrated[index] = obj;
for (let i = 1; i < value.length; i += 2) {
if (value[i] === '__proto__') {
throw new Error('Cannot parse an object with a `__proto__` property');
}
ops.set(obj, value[i], hydrate(value[i + 1]));
}
break;
case 'Int8Array':
case 'Uint8Array':
case 'Uint8ClampedArray':
case 'Int16Array':
case 'Uint16Array':
case 'Float16Array':
case 'Int32Array':
case 'Uint32Array':
case 'Float32Array':
case 'Float64Array':
case 'BigInt64Array':
case 'BigUint64Array':
case 'DataView': {
if (values[value[1]][0] !== 'ArrayBuffer') {
// without this, if we receive malformed input we could
// end up trying to hydrate in a circle or allocate
// huge amounts of memory when we call `new TypedArrayConstructor(buffer)`
throw new Error('Invalid data');
}
const buffer = hydrate(value[1]);
hydrated[index] = ops.fromViewInfo(type, buffer, value[2], value[3]);
break;
}
case 'ArrayBuffer': {
const base64 = value[1];
if (typeof base64 !== 'string') {
throw new Error('Invalid ArrayBuffer encoding');
}
hydrated[index] = ops.fromArrayBuffer(decode64(base64));
break;
}
case 'URL':
case 'URLSearchParams':
case 'Temporal.Duration':
case 'Temporal.Instant':
case 'Temporal.PlainDate':
case 'Temporal.PlainTime':
case 'Temporal.PlainDateTime':
case 'Temporal.PlainMonthDay':
case 'Temporal.PlainYearMonth':
case 'Temporal.ZonedDateTime': {
// the same tags `toStringValue` serializes on the stringify side
hydrated[index] = ops.fromStringValue(type, value[1]);
break;
}
default:
throw new Error(`Unknown type ${type}`);
}
} else if (value[0] === SPARSE) {
// Sparse array encoding: [SPARSE, length, idx, val, idx, val, ...]
const len = value[1];
if (!is_valid_array_len(len)) {
throw new Error('Invalid input');
}
// `len` comes from the input rather than being bounded by it, so
// `createSparseArray` is responsible for not allocating storage
// proportional to it.
const array = ops.createSparseArray(len);
hydrated[index] = array;
for (let i = 2; i < value.length; i += 2) {
const idx = value[i];
if (!is_valid_array_index(idx) || idx >= len) {
throw new Error('Invalid input');
}
ops.set(array, idx, hydrate(value[i + 1]));
}
} else {
const array = ops.createArray(value.length);
hydrated[index] = array;
for (let i = 0; i < value.length; i += 1) {
const n = value[i];
if (n === HOLE) continue;
ops.set(array, i, hydrate(n));
}
}
} else {
const object = ops.createObject();
hydrated[index] = object;
for (const key of Object.keys(value)) {
if (key === '__proto__') {
throw new Error('Cannot parse an object with a `__proto__` property');
}
ops.set(object, key, hydrate(value[key]));
}
}
return hydrated[index];
}
return hydrate(0);
}
+428
View File
@@ -0,0 +1,428 @@
import { DevalueError, stringify_key, stringify_string } from './utils.js';
import {
HOLE,
NAN,
NEGATIVE_INFINITY,
NEGATIVE_ZERO,
POSITIVE_INFINITY,
SPARSE,
UNDEFINED
} from './constants.js';
import { encode64 } from './base64.js';
import { default_stringify_operations, merge_operations } from './operations.js';
/**
* Turn a value into a JSON string that can be parsed with `devalue.parse`
* @param {any} value
* @param {Record<string, (value: any) => any>} [reducers]
* @param {import('./types.js').StringifyOptions} [options]
*/
export function stringify(value, reducers, options) {
const stringified = run(false, value, reducers, options);
return typeof stringified === 'string' ? stringified : `[${stringified.join(',')}]`;
}
/**
* Turn a value into a JSON string that can be parsed with `devalue.parse`
* @param {any} value
* @param {Record<string, (value: any) => any>} [reducers]
* @param {import('./types.js').StringifyOptions} [options]
*/
export async function stringifyAsync(value, reducers, options) {
const stringified = run(true, value, reducers, options);
if (typeof stringified === 'string') {
return stringified;
}
let out = '[';
for (let i = 0; i < stringified.length; i += 1) {
let value = stringified[i];
if (typeof value !== 'string') {
await value;
value = stringified[i];
if (i === 0 && value < 0) {
return `${value}`;
}
}
out += value;
if (i < stringified.length - 1) {
out += ',';
}
}
out += ']';
return out;
}
/**
* @param {boolean} async
* @param {any} value
* @param {Record<string, (value: any) => any>} [reducers]
* @param {import('./types.js').StringifyOptions} [options]
*/
function run(async, value, reducers, options) {
const ops = merge_operations(default_stringify_operations, options?.operations);
/** @type {any[]} */
const stringified = [];
/** @type {Map<any, number>} */
const indexes = new Map();
/** @type {Array<{ key: string, fn: (value: any) => any }>} */
const custom = [];
if (reducers) {
for (const key of Object.getOwnPropertyNames(reducers)) {
custom.push({ key, fn: reducers[key] });
}
}
/** @type {string[]} */
const keys = [];
let p = 0;
/**
* @param {any} thing
* @param {number} [index]
*/
function flatten(thing, index) {
const type = ops.typeOf(thing);
if (type === 'undefined') return UNDEFINED;
/** @type {number | undefined} */
let number;
// `ops.toPrimitive` is the boundary between the value being serialized and
// plain host JavaScript: everything below operates on the extracted host
// primitive, so native comparisons and arithmetic are correct there.
if (type === 'number') {
number = /** @type {number} */ (ops.toPrimitive(thing));
if (Number.isNaN(number)) return NAN;
if (number === Infinity) return POSITIVE_INFINITY;
if (number === -Infinity) return NEGATIVE_INFINITY;
if (number === 0 && 1 / number < 0) return NEGATIVE_ZERO;
}
const id = ops.identify(thing);
if (indexes.has(id)) return /** @type {number} */ (indexes.get(id));
index ??= p++;
indexes.set(id, index);
for (const { key, fn } of custom) {
const value = fn(thing);
if (value) {
stringified[index] = `["${key}",${flatten(value)}]`;
return index;
}
}
if (type === 'function') {
throw new DevalueError(`Cannot stringify a function`, keys, thing, value);
} else if (type === 'symbol') {
throw new DevalueError(`Cannot stringify a Symbol primitive`, keys, thing, value);
}
/** @type {string | Promise<any>} */
let str = '';
if (type !== 'object') {
str = stringify_primitive(type === 'number' ? number : ops.toPrimitive(thing));
} else if (ops.isThenable(thing)) {
if (!async) {
throw new DevalueError(
`Cannot stringify a Promise or thenable — use stringifyAsync instead`,
keys,
thing,
value
);
}
str = ops.toPromise(thing).then((value) => {
const i = flatten(value, index);
if (i < 0) stringified[index] = i;
});
} else {
const tag = ops.tagOf(thing);
switch (tag) {
case 'Number':
case 'String':
case 'Boolean':
case 'BigInt':
str = `["Object",${flatten(ops.unbox(thing))}]`;
break;
case 'Date':
str = `["Date","${ops.toISOString(thing)}"]`;
break;
case 'URL':
str = `["URL",${stringify_string(ops.toStringValue(thing))}]`;
break;
case 'URLSearchParams':
str = `["URLSearchParams",${stringify_string(ops.toStringValue(thing))}]`;
break;
case 'RegExp':
const { source, flags } = ops.regExpInfo(thing);
str = flags
? `["RegExp",${stringify_string(source)},"${flags}"]`
: `["RegExp",${stringify_string(source)}]`;
break;
case 'Array': {
// For dense arrays (no holes), we iterate normally.
// When we encounter the first hole, we call Object.keys
// to determine the sparseness, then decide between:
// - HOLE encoding: [-2, val, -2, ...] (default)
// - Sparse encoding: [-7, length, idx, val, ...] (for very sparse arrays)
// Only the sparse path avoids iterating every slot, which
// is what protects against the DoS of e.g. `arr[1000000] = 1`.
let mostly_dense = false;
const length = ops.lengthOf(thing);
str = '[';
for (let i = 0; i < length; i += 1) {
if (i > 0) str += ',';
if (ops.hasOwn(thing, i)) {
keys.push(`[${i}]`);
str += flatten(ops.get(thing, i));
keys.pop();
} else if (mostly_dense) {
// Use dense encoding. The heuristic guarantees the
// array is only mildly sparse, so iterating over every
// slot is fine.
str += HOLE;
} else {
// Decide between HOLE encoding and sparse encoding.
//
// HOLE encoding: each hole is serialized as the HOLE
// sentinel (-2). For example, [, "a", ,] becomes
// [-2, 0, -2]. Each hole costs 3 chars ("-2" + comma).
//
// Sparse encoding: lists only populated indices.
// For example, [, "a", ,] becomes [-7, 3, 1, 0] — the
// -7 sentinel, the array length (3), then index-value
// pairs. This avoids paying per-hole, but each element
// costs extra chars to write its index.
//
// The values are the same size either way, so the
// choice comes down to structural overhead:
//
// HOLE overhead:
// 3 chars per hole ("-2" + comma)
// = (L - P) * 3
//
// Sparse overhead:
// "-7," — 3 chars (sparse sentinel + comma)
// + length + "," — (d + 1) chars (array length + comma)
// + per element: index + "," — (d + 1) chars
// = (4 + d) + P * (d + 1)
//
// where L is the array length, P is the number of
// populated elements, and d is the number of digits
// in L (an upper bound on the digits in any index).
//
// Sparse encoding is cheaper when:
// (4 + d) + P * (d + 1) < (L - P) * 3
const populated_keys = ops.indicesOf(thing);
const population = populated_keys.length;
const d = String(length).length;
const hole_cost = (length - population) * 3;
const sparse_cost = 4 + d + population * (d + 1);
if (hole_cost > sparse_cost) {
str = '[' + SPARSE + ',' + length;
for (let j = 0; j < populated_keys.length; j++) {
const key = populated_keys[j];
keys.push(`[${key}]`);
str += ',' + key + ',' + flatten(ops.get(thing, key));
keys.pop();
}
break;
} else {
mostly_dense = true;
str += HOLE;
}
}
}
str += ']';
break;
}
case 'Set':
str = '["Set"';
for (const value of ops.valuesOf(thing)) {
str += `,${flatten(value)}`;
}
str += ']';
break;
case 'Map':
str = '["Map"';
for (const [key, value] of ops.entriesOf(thing)) {
const key_type = ops.typeOf(key);
const key_is_primitive =
key_type !== 'object' && key_type !== 'function' && key_type !== 'symbol';
keys.push(
`.get(${key_is_primitive ? stringify_primitive(ops.toPrimitive(key)) : '...'})`
);
str += `,${flatten(key)},${flatten(value)}`;
keys.pop();
}
str += ']';
break;
case 'Int8Array':
case 'Uint8Array':
case 'Uint8ClampedArray':
case 'Int16Array':
case 'Uint16Array':
case 'Float16Array':
case 'Int32Array':
case 'Uint32Array':
case 'Float32Array':
case 'Float64Array':
case 'BigInt64Array':
case 'BigUint64Array': {
const info = ops.viewInfo(thing);
str = '["' + tag + '",' + flatten(info.buffer);
// handle subarrays
if (info.byteLength !== info.bufferByteLength) {
str += `,${info.byteOffset},${info.length}`;
}
str += ']';
break;
}
case 'DataView': {
const info = ops.viewInfo(thing);
str = '["' + tag + '",' + flatten(info.buffer);
if (info.byteLength !== info.bufferByteLength) {
str += `,${info.byteOffset},${info.byteLength}`;
}
str += ']';
break;
}
case 'ArrayBuffer': {
const base64 = encode64(ops.toArrayBuffer(thing));
str = `["ArrayBuffer","${base64}"]`;
break;
}
case 'Temporal.Duration':
case 'Temporal.Instant':
case 'Temporal.PlainDate':
case 'Temporal.PlainTime':
case 'Temporal.PlainDateTime':
case 'Temporal.PlainMonthDay':
case 'Temporal.PlainYearMonth':
case 'Temporal.ZonedDateTime':
str = `["${tag}",${stringify_string(ops.toStringValue(thing))}]`;
break;
default: {
const shape = ops.shapeOf(thing);
if (shape.kind === 'not-plain') {
throw new DevalueError(`Cannot stringify arbitrary non-POJOs`, keys, thing, value);
}
if (shape.kind === 'symbol-keys') {
throw new DevalueError(`Cannot stringify POJOs with symbolic keys`, keys, thing, value);
}
if (shape.kind === 'null-proto') {
str = '["null"';
for (const key of shape.keys) {
if (key === '__proto__') {
throw new DevalueError(
`Cannot stringify objects with __proto__ keys`,
keys,
thing,
value
);
}
keys.push(stringify_key(key));
str += `,${stringify_string(key)},${flatten(ops.get(thing, key))}`;
keys.pop();
}
str += ']';
} else {
str = '{';
let started = false;
for (const key of shape.keys) {
if (key === '__proto__') {
throw new DevalueError(
`Cannot stringify objects with __proto__ keys`,
keys,
thing,
value
);
}
if (started) str += ',';
started = true;
keys.push(stringify_key(key));
str += `${stringify_string(key)}:${flatten(ops.get(thing, key))}`;
keys.pop();
}
str += '}';
}
}
}
}
stringified[index] = str;
return index;
}
const index = flatten(value);
// special case — value is represented as a negative index
if (index < 0) return `${index}`;
return stringified;
}
/**
* @param {any} thing
* @returns {string}
*/
function stringify_primitive(thing) {
const type = typeof thing;
if (type === 'string') return stringify_string(thing);
if (thing === void 0) return UNDEFINED.toString();
if (thing === 0 && 1 / thing < 0) return NEGATIVE_ZERO.toString();
if (type === 'bigint') return `["BigInt","${thing}"]`;
return String(thing);
}
+452
View File
@@ -0,0 +1,452 @@
export type StringValueTag =
| 'URL'
| 'URLSearchParams'
| 'Temporal.Duration'
| 'Temporal.Instant'
| 'Temporal.PlainDate'
| 'Temporal.PlainTime'
| 'Temporal.PlainDateTime'
| 'Temporal.PlainMonthDay'
| 'Temporal.PlainYearMonth'
| 'Temporal.ZonedDateTime';
export type ViewTag =
| 'Int8Array'
| 'Uint8Array'
| 'Uint8ClampedArray'
| 'Int16Array'
| 'Uint16Array'
| 'Float16Array'
| 'Int32Array'
| 'Uint32Array'
| 'Float32Array'
| 'Float64Array'
| 'BigInt64Array'
| 'BigUint64Array'
| 'DataView';
export type TypedArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Float16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| BigInt64Array
| BigUint64Array;
/**
* The introspection/extraction operations `stringify` performs on the value
* being serialized. Every dynamic operation — property reads, prototype
* method calls, iteration, type classification — goes through this
* interface, so overriding members lets you control exactly how values are
* inspected.
*
* Use cases:
* - **Side-effect-free serialization**: replace operations that can execute
* user code (getters, proxy traps, patched prototypes, `Symbol.toStringTag`
* accessors) with implementations based on captured intrinsics, internal
* slots, or property descriptors.
* - **Foreign-runtime serialization**: serialize values that live in another
* JavaScript runtime (a `node:vm` context, a WASM-hosted engine, a remote
* process) by implementing the operations over handle objects. The
* `stringify` algorithm never touches the value directly, so "value" can
* be any opaque token as long as the operations agree on what it means.
*
* All members are optional when passed to `stringify` — omitted members fall
* back to the defaults (native behavior, exported as
* `defaultStringifyOperations`).
*
* Members are named by what they do with the value:
* - `isXxx`/`hasXxx` — predicates returning booleans
* - `toXxx` — conversions whose whole result crosses into host JavaScript
* (`toPrimitive`, `toISOString`) or into a native container (`toPromise`)
* - `xxxOf` — queries returning host data *about* the value (`typeOf`,
* `tagOf`, `lengthOf`) or its constituents, which remain in value space
* (`valuesOf`, `entriesOf`)
* - `xxxInfo` — multi-field descriptors mixing host data and constituent
* values (`viewInfo`, `regExpInfo`)
* - bare verbs (`get`, `unbox`, `identify`) — accessors whose results remain
* in value space
*
* (`toStringValue` and `unbox` deliberately avoid the names `toString` and
* `valueOf`, which would shadow `Object.prototype` methods on the operations
* object.)
*/
export interface StringifyOperations {
/**
* Returns the key used for deduplication and cycle detection (compared
* with `Map` key semantics). Two values that represent the same logical
* object must return the same key. Default: the value itself.
*
* Override this when serializing through handles, where two distinct
* handle objects may refer to the same underlying value.
*
* Keys are compared across *every* value in the payload, including
* primitives, so an implementation that derives keys for objects must
* make sure they cannot collide with a primitive that appears in the
* same payload — returning e.g. the string `'42'` as an object's key
* would alias it to the string `'42'` elsewhere in the payload and emit
* a wrong back-reference. Prefer keys that are unforgeable, such as the
* underlying object itself, a symbol, or a wrapper object.
*/
identify(value: any): unknown;
/**
* Classifies a value. Same contract as the `typeof` operator, except
* `null` must be reported as `'null'` (not `'object'`).
*/
typeOf(value: any):
| 'undefined'
| 'null'
| 'boolean'
| 'number'
| 'bigint'
| 'string'
| 'symbol'
| 'function'
| 'object';
/**
* Extracts the host-JavaScript primitive from a value whose `typeOf` is
* `'null'`, `'boolean'`, `'number'`, `'bigint'` or `'string'`.
* Default: the value itself (it already is the primitive).
*/
toPrimitive(value: any): undefined | null | boolean | number | bigint | string;
/**
* Returns the brand of an object value — the strings produced by
* `Object.prototype.toString` without the wrapping (`'Date'`, `'Array'`,
* `'Map'`, `'Object'`, `'Temporal.Instant'`, …). This decides which
* serialization strategy is used, so hardened implementations should use
* engine-level brand checks rather than (spoofable, getter-invoking)
* `Symbol.toStringTag` lookups.
*/
tagOf(value: any): string;
/** Returns true if the object value should be treated as a thenable. */
isThenable(value: any): boolean;
/**
* Converts a thenable into a native promise, whose settled value is then
* serialized. The returned promise may reject, in which case
* `stringifyAsync` rejects. Only called from `stringifyAsync`, for values
* where `isThenable` returned true.
*/
toPromise(thenable: any): Promise<any>;
/**
* Extracts the inner value of a boxed primitive (`Number`, `String`,
* `Boolean`, `BigInt` objects). Equivalent to `boxed.valueOf()`. The
* result is serialized recursively, so it may be a foreign value/handle.
*/
unbox(boxed: any): any;
/**
* Returns the ISO string for a `Date` value, or `''` for an invalid
* date. Equivalent to `date.toISOString()`.
*/
toISOString(date: any): string;
/**
* Returns the string form of a `URL`, `URLSearchParams` or `Temporal.*`
* value. Equivalent to `value.toString()`.
*/
toStringValue(value: any): string;
/** Returns the source and flags of a `RegExp` value. */
regExpInfo(regexp: any): { source: string; flags: string };
/**
* Returns an iterable over the elements of a `Set` value. The iterable
* is consumed on the host; elements may be foreign values/handles.
*/
valuesOf(set: any): Iterable<any>;
/**
* Returns an iterable over the `[key, value]` entries of a `Map` value.
* The iterable is consumed on the host; keys/values may be foreign
* values/handles.
*/
entriesOf(map: any): Iterable<[any, any]>;
/**
* Returns the view metadata of a typed array or `DataView` value.
* `length` is only meaningful for typed arrays. `buffer` is serialized
* recursively, so it may be a foreign value/handle.
*/
viewInfo(view: any): {
buffer: any;
byteOffset: number;
byteLength: number;
length?: number;
bufferByteLength: number;
};
/**
* Returns a host `ArrayBuffer` with the bytes of an `ArrayBuffer` value.
* Default: the value itself. Foreign-runtime implementations should copy
* the bytes into a host buffer.
*/
toArrayBuffer(buffer: any): ArrayBuffer;
/** Returns the length of an `Array` value. */
lengthOf(array: any): number;
/**
* Returns true if a value has an own property at `key`. Same contract as
* `Object.hasOwn(value, key)`.
*/
hasOwn(value: any, key: string | number): boolean;
/**
* Returns the populated indices of a (sparse) `Array` value as strings,
* in ascending order.
*
* Implementations that already have the value's own enumerable string
* keys — as a foreign-runtime implementation typically does — should pass
* them through the exported `filterArrayIndices` helper rather than
* reimplementing the filtering, which encodes the sparse-array heuristic.
*
* Equivalent to `Object.keys(array)` filtered to
* valid array indices.
*/
indicesOf(array: any): string[];
/**
* Classifies a plain-object candidate:
* - `{ kind: 'plain' | 'null-proto', keys }` — a serializable POJO and
* its own enumerable string keys
* - `{ kind: 'not-plain' }` — a non-POJO (stringify throws)
* - `{ kind: 'symbol-keys' }` — a POJO with enumerable symbol keys
* (stringify throws)
*/
shapeOf(
value: any
):
| { kind: 'plain' | 'null-proto'; keys: string[] }
| { kind: 'not-plain' }
| { kind: 'symbol-keys' };
/**
* Reads a property from an `Array` or plain-object value. Equivalent to
* `value[key]`. Hardened implementations can read through property
* descriptors to control what happens for accessor properties.
*/
get(value: any, key: string | number): any;
}
/** The native JavaScript implementation exported as `defaultStringifyOperations`. */
export interface DefaultStringifyOperations extends StringifyOperations {
identify(value: any): any;
toPrimitive(
value: undefined | null | boolean | number | bigint | string
): undefined | null | boolean | number | bigint | string;
toISOString(date: Date): string;
regExpInfo(regexp: RegExp): { source: string; flags: string };
valuesOf(set: Set<any>): Set<any>;
entriesOf(map: Map<any, any>): Map<any, any>;
viewInfo(view: any): {
buffer: ArrayBufferLike;
byteOffset: number;
byteLength: number;
length?: number;
bufferByteLength: number;
};
toArrayBuffer(buffer: ArrayBuffer): ArrayBuffer;
lengthOf(array: any[]): number;
indicesOf(array: any[]): string[];
}
/** Options for `stringify` and `stringifyAsync`. */
export interface StringifyOptions {
/**
* Overrides for the introspection/extraction operations used while
* serializing. Omitted members fall back to `defaultStringifyOperations`.
*/
operations?: Partial<StringifyOperations>;
}
/**
* The construction operations `parse` and `unflatten` perform while reviving
* a value. Every value the algorithm creates — primitives, built-in
* instances, containers — and every mutation it performs to populate those
* containers goes through this interface, so overriding members lets you
* control exactly what gets built.
*
* Use cases:
* - **Cross-realm revival**: construct values from the intrinsics of a
* different realm (e.g. a `node:vm` context) so that the result passes
* `instanceof` checks inside that realm.
* - **Foreign-runtime revival**: build values inside another JavaScript
* runtime (a WASM-hosted engine, a remote process) by implementing the
* operations over handle objects. The algorithm never inspects the values
* it creates — it only passes them back into other operations — so
* "value" can be any opaque token.
*
* The naming follows the same scheme as `StringifyOperations`, with the
* host/value-space boundary running the other way:
*
* - `fromXxx` — conversions whose input is entirely host data and whose
* result crosses into value space; each is the inverse of the
* corresponding `toXxx` (`fromPrimitive` / `toPrimitive`,
* `fromISOString` / `toISOString`, `fromStringValue` / `toStringValue`,
* `fromArrayBuffer` / `toArrayBuffer`).
* - `fromXxxInfo` — construction from a multi-field descriptor, the inverse
* of the corresponding `xxxInfo` (`fromRegExpInfo` / `regExpInfo`,
* `fromViewInfo` / `viewInfo`).
* - `createXxx` — empty value-space containers, populated afterwards by the
* mutators. That ordering is what makes cyclic values possible: the empty
* container is cached before its contents are revived.
* - bare verbs — value-space operations whose operands and results stay in
* value space (`box` inverts `unbox`, `set` inverts `get`, `addValue`
* inverts `valuesOf`, `addEntry` inverts `entriesOf`).
*
* All members are optional when passed to `parse`/`unflatten` — omitted
* members fall back to the defaults (native behavior, exported as
* `defaultParseOperations`).
*/
export interface ParseOperations {
/**
* Wraps a host primitive (`string`, `number`, `boolean`, `bigint`,
* `null`, `undefined`, and the special values `NaN`, `±Infinity`, `-0`)
* into the representation the other operations expect. The inverse of
* `toPrimitive`. Default: the value itself.
*/
fromPrimitive(
primitive: string | number | boolean | bigint | null | undefined
): any;
/**
* Creates a `Date` from an ISO string. The inverse of `toISOString`.
* An empty string represents an invalid date (as produced for
* `new Date(NaN)`).
*/
fromISOString(iso: string): any;
/**
* Creates a `URL`, `URLSearchParams` or `Temporal.*` value from its
* string form — the same tags `toStringValue` serializes, and its
* inverse. `tag` distinguishes them (e.g. `'URL'`,
* `'Temporal.Instant'`).
*/
fromStringValue(tag: StringValueTag, text: string): any;
/**
* Creates an `ArrayBuffer` from a host `ArrayBuffer` holding the decoded
* bytes. The inverse of `toArrayBuffer`. Default: the buffer itself.
* Foreign-runtime implementations should copy the bytes into the target
* runtime.
*/
fromArrayBuffer(buffer: ArrayBuffer): any;
/**
* Creates a `RegExp` from its source and flags. The inverse of
* `regExpInfo`. `flags` is `undefined` when the pattern had no flags.
*/
fromRegExpInfo(source: string, flags: string | undefined): any;
/**
* Creates a typed array or `DataView` over an already-revived buffer.
* The inverse of `viewInfo`. `tag` is the constructor name (e.g.
* `'Uint8Array'`, `'DataView'`). `byteOffset` and `length` are
* `undefined` when the view spans the whole buffer; otherwise `length`
* is the element count for typed arrays and the byte length for
* `DataView`, matching the constructor signatures.
*/
fromViewInfo(
tag: ViewTag,
buffer: any,
byteOffset: number | undefined,
length: number | undefined
): any;
/**
* Creates a boxed primitive object (`Number`, `String`, `Boolean`,
* `BigInt` wrapper) around an already-revived inner primitive. The
* inverse of `unbox`. Equivalent to `Object(value)`.
*/
box(value: any): any;
/**
* Creates an array of the given length, to be populated with `set`.
* The length is bounded by the size of the input, so it is safe to
* allocate eagerly. Indices that are never set must remain holes.
*/
createArray(length: number): any;
/**
* Creates a sparse array of the given length, to be populated with
* `set`. Unlike `createArray`, the length comes from the input rather
* than being bounded by it, so implementations must not allocate
* storage proportional to it.
*/
createSparseArray(length: number): any;
/** Creates an empty object, to be populated with `set`. */
createObject(): any;
/**
* Creates an empty null-prototype object, to be populated with `set`.
* Equivalent to `Object.create(null)`.
*/
createNullPrototypeObject(): any;
/** Creates an empty `Set`, to be populated with `addValue`. */
createSet(): any;
/** Creates an empty `Map`, to be populated with `addEntry`. */
createMap(): any;
/**
* Sets an element or property on a value created by `createArray`,
* `createSparseArray`, `createObject` or `createNullPrototypeObject`.
* The inverse of `get`, which likewise serves both arrays and objects.
*/
set(target: any, key: string | number, value: any): void;
/** Adds a value to a `Set` created by `createSet`. The inverse of `valuesOf`. */
addValue(set: any, value: any): void;
/** Adds an entry to a `Map` created by `createMap`. The inverse of `entriesOf`. */
addEntry(map: any, key: any, value: any): void;
}
/** The native JavaScript implementation exported as `defaultParseOperations`. */
export interface DefaultParseOperations extends ParseOperations {
fromPrimitive(
primitive: string | number | boolean | bigint | null | undefined
): string | number | boolean | bigint | null | undefined;
fromISOString(iso: string): Date;
fromStringValue(tag: StringValueTag, text: string): URL | URLSearchParams | object;
fromArrayBuffer(buffer: ArrayBuffer): ArrayBuffer;
fromRegExpInfo(source: string, flags: string | undefined): RegExp;
fromViewInfo(
tag: ViewTag,
buffer: ArrayBufferLike,
byteOffset: number | undefined,
length: number | undefined
): TypedArray | DataView;
box(value: any): object;
createArray(length: number): any[];
createSparseArray(length: number): any[];
createObject(): Record<string, any>;
createNullPrototypeObject(): Record<string, any>;
createSet(): Set<any>;
createMap(): Map<any, any>;
addValue(set: Set<any>, value: any): void;
addEntry(map: Map<any, any>, key: any, value: any): void;
}
/** Options for `parse` and `unflatten`. */
export interface ParseOptions {
/**
* Overrides for the construction operations used while reviving.
* Omitted members fall back to `defaultParseOperations`.
*/
operations?: Partial<ParseOperations>;
}
+582
View File
@@ -0,0 +1,582 @@
import {
DevalueError,
enumerable_symbols,
escaped,
get_type,
is_plain_object,
is_primitive,
stringify_key,
stringify_string,
valid_array_indices
} from './utils.js';
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
const unsafe_chars = /[<\b\f\n\r\t\0\u2028\u2029]/g;
const reserved =
/^(?:do|if|in|for|int|let|new|try|var|byte|case|char|else|enum|goto|long|this|void|with|await|break|catch|class|const|final|float|short|super|throw|while|yield|delete|double|export|import|native|return|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/;
/**
* Turn a value into the JavaScript that creates an equivalent value
* @param {any} value
* @param {(value: any, uneval: (value: any) => string) => string | void} [replacer]
*/
export function uneval(value, replacer) {
const counts = new Map();
/** @type {string[]} */
const keys = [];
const custom = new Map();
/** @param {any} thing */
function walk(thing) {
if (!is_primitive(thing)) {
if (counts.has(thing)) {
counts.set(thing, counts.get(thing) + 1);
return;
}
counts.set(thing, 1);
if (replacer) {
const str = replacer(thing, (value) => uneval(value, replacer));
if (typeof str === 'string') {
custom.set(thing, str);
return;
}
}
if (typeof thing === 'function') {
throw new DevalueError(`Cannot stringify a function`, keys, thing, value);
}
const type = get_type(thing);
switch (type) {
case 'Number':
case 'BigInt':
case 'String':
case 'Boolean':
case 'Date':
case 'RegExp':
case 'URL':
case 'URLSearchParams':
return;
case 'Array':
/** @type {any[]} */ (thing).forEach((value, i) => {
keys.push(`[${i}]`);
walk(value);
keys.pop();
});
break;
case 'Set':
Array.from(thing).forEach(walk);
break;
case 'Map':
for (const [key, value] of thing) {
keys.push(`.get(${is_primitive(key) ? stringify_primitive(key) : '...'})`);
walk(key);
walk(value);
keys.pop();
}
break;
case 'Int8Array':
case 'Uint8Array':
case 'Uint8ClampedArray':
case 'Int16Array':
case 'Uint16Array':
case 'Float16Array':
case 'Int32Array':
case 'Uint32Array':
case 'Float32Array':
case 'Float64Array':
case 'BigInt64Array':
case 'BigUint64Array':
case 'DataView':
walk(thing.buffer);
return;
case 'ArrayBuffer':
return;
case 'Temporal.Duration':
case 'Temporal.Instant':
case 'Temporal.PlainDate':
case 'Temporal.PlainTime':
case 'Temporal.PlainDateTime':
case 'Temporal.PlainMonthDay':
case 'Temporal.PlainYearMonth':
case 'Temporal.ZonedDateTime':
return;
default:
if (!is_plain_object(thing)) {
throw new DevalueError(`Cannot stringify arbitrary non-POJOs`, keys, thing, value);
}
if (enumerable_symbols(thing).length > 0) {
throw new DevalueError(`Cannot stringify POJOs with symbolic keys`, keys, thing, value);
}
for (const key of Object.keys(thing)) {
if (key === '__proto__') {
throw new DevalueError(
`Cannot stringify objects with __proto__ keys`,
keys,
thing,
value
);
}
keys.push(stringify_key(key));
walk(thing[key]);
keys.pop();
}
}
} else if (typeof thing === 'symbol') {
throw new DevalueError(`Cannot stringify a Symbol primitive`, keys, thing, value);
}
}
walk(value);
const names = new Map();
Array.from(counts)
.filter((entry) => entry[1] > 1)
.sort((a, b) => b[1] - a[1])
.forEach((entry, i) => {
names.set(entry[0], get_name(i));
});
/**
* @param {any} thing
* @returns {string}
*/
function stringify(thing) {
if (names.has(thing)) {
return names.get(thing);
}
if (is_primitive(thing)) {
return stringify_primitive(thing);
}
if (custom.has(thing)) {
return custom.get(thing);
}
const type = get_type(thing);
switch (type) {
case 'Number':
case 'String':
case 'Boolean':
case 'BigInt':
return `Object(${stringify(thing.valueOf())})`;
case 'RegExp':
const { source, flags } = thing;
return flags
? `new RegExp(${stringify_string(source)},"${flags}")`
: `new RegExp(${stringify_string(source)})`;
case 'Date':
return `new Date(${thing.getTime()})`;
case 'URL':
return `new URL(${stringify_string(thing.toString())})`;
case 'URLSearchParams':
return `new URLSearchParams(${stringify_string(thing.toString())})`;
case 'Array': {
// For dense arrays (no holes), we iterate normally.
// When we encounter the first hole, we call Object.keys
// to determine the sparseness, then decide between:
// - Array literal with holes: [,"a",,] (default)
// - Object.assign: Object.assign(Array(n),{...}) (for very sparse arrays)
// Only the Object.assign path avoids iterating every slot, which
// is what protects against the DoS of e.g. `arr[1000000] = 1`.
let has_holes = false;
let result = '[';
for (let i = 0; i < thing.length; i += 1) {
if (i > 0) result += ',';
if (Object.hasOwn(thing, i)) {
result += stringify(thing[i]);
} else if (!has_holes) {
// Decide between array literal and Object.assign.
//
// Array literal: holes are consecutive commas.
// For example, [, "a", ,] is written as [,"a",,].
// Each hole costs 1 char (a comma).
//
// Object.assign: populated indices are listed explicitly.
// For example, [, "a", ,] would be written as
// Object.assign(Array(3),{1:"a"}). This avoids paying
// per-hole, but has a large fixed overhead for the
// "Object.assign(Array(n),{...})" wrapper, and each
// element costs extra chars for its index and colon.
//
// The serialized values are the same size either way, so
// the choice comes down to the structural overhead:
//
// Array literal overhead:
// 1 char per element or hole (comma separators)
// + 2 chars for "[" and "]"
// = L + 2
//
// Object.assign overhead:
// "Object.assign(Array(" — 20 chars
// + length — d chars
// + "),{" — 3 chars
// + for each populated element:
// index + ":" + "," — (d + 2) chars
// + "})" — 2 chars
// = (25 + d) + P * (d + 2)
//
// where L is the array length, P is the number of
// populated elements, and d is the number of digits
// in L (an upper bound on the digits in any index).
//
// Object.assign is cheaper when:
// (25 + d) + P * (d + 2) < L + 2
const populated_keys = valid_array_indices(/** @type {any[]} */ (thing));
const population = populated_keys.length;
const d = String(thing.length).length;
const hole_cost = thing.length + 2;
const sparse_cost = 25 + d + population * (d + 2);
if (hole_cost > sparse_cost) {
const entries = populated_keys.map((k) => `${k}:${stringify(thing[k])}`).join(',');
return `Object.assign(Array(${thing.length}),{${entries}})`;
}
has_holes = true;
}
// else: already decided on array literal, hole is just an empty slot
// (the comma separator is all we need — no content for this position)
}
const tail = thing.length === 0 || thing.length - 1 in thing ? '' : ',';
return result + tail + ']';
}
case 'Set':
case 'Map':
return `new ${type}([${Array.from(thing).map(stringify).join(',')}])`;
case 'Int8Array':
case 'Uint8Array':
case 'Uint8ClampedArray':
case 'Int16Array':
case 'Uint16Array':
case 'Float16Array':
case 'Int32Array':
case 'Uint32Array':
case 'Float32Array':
case 'Float64Array':
case 'BigInt64Array':
case 'BigUint64Array': {
let str = `new ${type}`;
if (!names.has(thing.buffer)) {
str += `([${stringify_typed_array_elements(new thing.constructor(thing.buffer))}])`;
} else {
str += `(${stringify(thing.buffer)})`;
}
// handle subarrays
if (thing.byteLength !== thing.buffer.byteLength) {
const start = thing.byteOffset / thing.BYTES_PER_ELEMENT;
const end = start + thing.length;
str += `.subarray(${start},${end})`;
}
return str;
}
case 'DataView': {
let str = `new DataView`;
if (!names.has(thing.buffer)) {
str += `(new Uint8Array([${new Uint8Array(thing.buffer)}]).buffer`;
} else {
str += `(${stringify(thing.buffer)}`;
}
// handle subviews
if (thing.byteLength !== thing.buffer.byteLength) {
str += `,${thing.byteOffset},${thing.byteLength}`;
}
return str + ')';
}
case 'ArrayBuffer': {
const ui8 = new Uint8Array(thing);
return `new Uint8Array([${ui8.toString()}]).buffer`;
}
case 'Temporal.Duration':
case 'Temporal.Instant':
case 'Temporal.PlainDate':
case 'Temporal.PlainTime':
case 'Temporal.PlainDateTime':
case 'Temporal.PlainMonthDay':
case 'Temporal.PlainYearMonth':
case 'Temporal.ZonedDateTime':
return `${type}.from(${stringify_string(thing.toString())})`;
default:
const keys = Object.keys(thing);
const obj = keys.map((key) => `${safe_key(key)}:${stringify(thing[key])}`).join(',');
const proto = Object.getPrototypeOf(thing);
if (proto === null) {
return keys.length > 0 ? `{${obj},__proto__:null}` : `{__proto__:null}`;
}
return `{${obj}}`;
}
}
const str = stringify(value);
if (names.size) {
/** @type {string[]} */
const params = [];
/** @type {string[]} */
const statements = [];
/** @type {string[]} */
const values = [];
// Reconstructions (e.g. `b = new Uint8Array(...)`) reassign a placeholder
// parameter. They must run before the `statements` that reference them,
// otherwise those statements capture the placeholder. They only depend on
// IIFE arguments (never on each other), so emitting them first is safe.
/** @type {string[]} */
const reconstructions = [];
names.forEach((name, thing) => {
params.push(name);
if (custom.has(thing)) {
values.push(/** @type {string} */ (custom.get(thing)));
return;
}
if (is_primitive(thing)) {
values.push(stringify_primitive(thing));
return;
}
const type = get_type(thing);
switch (type) {
case 'Number':
case 'String':
case 'Boolean':
case 'BigInt':
values.push(`Object(${stringify(thing.valueOf())})`);
break;
case 'RegExp':
const { source, flags } = thing;
const regexp = flags
? `new RegExp(${stringify_string(source)},"${flags}")`
: `new RegExp(${stringify_string(source)})`
values.push(regexp);
break;
case 'Date':
values.push(`new Date(${thing.getTime()})`);
break;
case 'URL':
values.push(`new URL(${stringify_string(thing.toString())})`);
break;
case 'URLSearchParams':
values.push(`new URLSearchParams(${stringify_string(thing.toString())})`);
break;
case 'Array':
values.push(`Array(${thing.length})`);
/** @type {any[]} */ (thing).forEach((v, i) => {
statements.push(`${name}[${i}]=${stringify(v)}`);
});
break;
case 'Set': {
values.push(`new Set`);
const adds = Array.from(thing).map((v) => `.add(${stringify(v)})`);
// An empty Set is fully built by `new Set`; a chained statement would
// otherwise be a dangling `name.`.
if (adds.length > 0) statements.push(name + adds.join(''));
break;
}
case 'Map': {
values.push(`new Map`);
const sets = Array.from(thing).map(
([k, v]) => `.set(${stringify(k)}, ${stringify(v)})`
);
if (sets.length > 0) statements.push(name + sets.join(''));
break;
}
case 'Int8Array':
case 'Uint8Array':
case 'Uint8ClampedArray':
case 'Int16Array':
case 'Uint16Array':
case 'Float16Array':
case 'Int32Array':
case 'Uint32Array':
case 'Float32Array':
case 'Float64Array':
case 'BigInt64Array':
case 'BigUint64Array': {
let str = `new ${type}`;
if (!names.has(thing.buffer)) {
str += `([${stringify_typed_array_elements(new thing.constructor(thing.buffer))}])`;
} else {
str += `(${stringify(thing.buffer)})`;
}
// handle subarrays
if (thing.byteLength !== thing.buffer.byteLength) {
const start = thing.byteOffset / thing.BYTES_PER_ELEMENT;
const end = start + thing.length;
str += `.subarray(${start},${end})`;
}
values.push(`{}`);
reconstructions.push(`${name}=${str}`);
break;
}
case 'DataView': {
let str = `new DataView`;
if (!names.has(thing.buffer)) {
str += `(new Uint8Array([${new Uint8Array(thing.buffer)}]).buffer`;
} else {
str += `(${stringify(thing.buffer)}`;
}
// handle subviews
if (thing.byteLength !== thing.buffer.byteLength) {
str += `,${thing.byteOffset},${thing.byteLength}`;
}
str += ')';
values.push(`{}`);
reconstructions.push(`${name}=${str}`);
break;
}
case 'ArrayBuffer':
values.push(`new Uint8Array([${new Uint8Array(thing)}]).buffer`);
break;
case 'Temporal.Duration':
case 'Temporal.Instant':
case 'Temporal.PlainDate':
case 'Temporal.PlainTime':
case 'Temporal.PlainDateTime':
case 'Temporal.PlainMonthDay':
case 'Temporal.PlainYearMonth':
case 'Temporal.ZonedDateTime':
values.push(`${type}.from(${stringify_string(thing.toString())})`);
break;
default:
values.push(Object.getPrototypeOf(thing) === null ? 'Object.create(null)' : '{}');
Object.keys(thing).forEach((key) => {
statements.push(`${name}${safe_prop(key)}=${stringify(thing[key])}`);
});
}
});
statements.push(`return ${str}`);
const body = [...reconstructions, ...statements].join(';');
return `(function(${params.join(',')}){${body}}(${values.join(',')}))`;
} else {
return str;
}
}
/**
* Serialize the elements of a typed array as a comma-separated list.
* `BigInt64Array`/`BigUint64Array` elements are bigints and must be written
* with an `n` suffix, otherwise the emitted `new BigInt64Array([...])` throws.
* @param {import('./types.js').TypedArray} array
*/
function stringify_typed_array_elements(array) {
if (array instanceof BigInt64Array || array instanceof BigUint64Array) {
return Array.from(array, (element) => `${element}n`).join(',');
}
return array.toString();
}
/** @param {number} num */
function get_name(num) {
let name = '';
do {
name = chars[num % chars.length] + name;
num = ~~(num / chars.length) - 1;
} while (num >= 0);
return reserved.test(name) ? `${name}0` : name;
}
/** @param {string} c */
function escape_unsafe_char(c) {
return escaped[c] || c;
}
/** @param {string} str */
function escape_unsafe_chars(str) {
return str.replace(unsafe_chars, escape_unsafe_char);
}
/** @param {string} key */
function safe_key(key) {
return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) ? key : escape_unsafe_chars(JSON.stringify(key));
}
/** @param {string} key */
function safe_prop(key) {
return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key)
? `.${key}`
: `[${escape_unsafe_chars(JSON.stringify(key))}]`;
}
/** @param {any} thing */
function stringify_primitive(thing) {
const type = typeof thing;
if (type === 'string') return stringify_string(thing);
if (thing === void 0) return 'void 0';
if (thing === 0 && 1 / thing < 0) return '-0';
const str = String(thing);
if (type === 'number') return str.replace(/^(-)?0\./, '$1.');
if (type === 'bigint') return thing + 'n';
return str;
}
+185
View File
@@ -0,0 +1,185 @@
import { MAX_ARRAY_INDEX, MAX_ARRAY_LEN } from './constants.js';
/** @type {Record<string, string>} */
export const escaped = {
'<': '\\u003C',
'\\': '\\\\',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
};
export class DevalueError extends Error {
/**
* @param {string} message
* @param {string[]} keys
* @param {any} [value] - The value that failed to be serialized
* @param {any} [root] - The root value being serialized
*/
constructor(message, keys, value, root) {
super(message);
this.name = 'DevalueError';
this.path = keys.join('');
this.value = value;
this.root = root;
}
}
/** @param {any} thing */
export function is_primitive(thing) {
return thing === null || (typeof thing !== 'object' && typeof thing !== 'function');
}
const object_proto_names = /* @__PURE__ */ Object.getOwnPropertyNames(Object.prototype)
.sort()
.join('\0');
/** @param {any} thing */
export function is_plain_object(thing) {
const proto = Object.getPrototypeOf(thing);
return (
proto === Object.prototype ||
proto === null ||
Object.getPrototypeOf(proto) === null ||
Object.getOwnPropertyNames(proto).sort().join('\0') === object_proto_names
);
}
/** @param {any} thing */
export function get_type(thing) {
return Object.prototype.toString.call(thing).slice(8, -1);
}
/** @param {string} char */
function get_escaped_char(char) {
switch (char) {
case '"':
return '\\"';
case '<':
return '\\u003C';
case '\\':
return '\\\\';
case '\n':
return '\\n';
case '\r':
return '\\r';
case '\t':
return '\\t';
case '\b':
return '\\b';
case '\f':
return '\\f';
case '\u2028':
return '\\u2028';
case '\u2029':
return '\\u2029';
default:
return char < ' ' ? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}` : '';
}
}
/** @param {string} str */
export function stringify_string(str) {
let result = '';
let last_pos = 0;
const len = str.length;
for (let i = 0; i < len; i += 1) {
const char = str[i];
const replacement = get_escaped_char(char);
if (replacement) {
result += str.slice(last_pos, i) + replacement;
last_pos = i + 1;
}
}
return `"${last_pos === 0 ? str : result + str.slice(last_pos)}"`;
}
/** @param {Record<string | symbol, any>} object */
export function enumerable_symbols(object) {
return Object.getOwnPropertySymbols(object).filter(
(symbol) => Object.getOwnPropertyDescriptor(object, symbol).enumerable
);
}
const is_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
/** @param {string} key */
export function stringify_key(key) {
return is_identifier.test(key) ? '.' + key : '[' + JSON.stringify(key) + ']';
}
/** @param {number} n */
export function is_valid_array_index(n) {
if (!Number.isInteger(n)) return false;
if (n < 0) return false;
if (n > MAX_ARRAY_INDEX) return false;
return true;
}
/** @param {number} n */
export function is_valid_array_len(n) {
if (!Number.isInteger(n)) return false;
if (n < 0) return false;
if (n > MAX_ARRAY_LEN) return false;
return true;
}
/** @param {string} s */
function is_valid_array_index_string(s) {
if (s.length === 0) return false;
if (s.length > 1 && s.charCodeAt(0) === 48) return false; // leading zero
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c < 48 || c > 57) return false;
}
// by this point we know it's a string of digits, but it has to be within
// the range of valid array indices
return is_valid_array_index(+s);
}
/**
* Returns the length of the leading run of valid array indices in `keys`.
* @param {readonly string[]} keys
*/
function array_index_cut(keys) {
for (var i = keys.length - 1; i >= 0; i--) {
if (is_valid_array_index_string(keys[i])) {
break;
}
}
return i + 1;
}
/**
* Finds the populated indices of an array.
* @param {unknown[]} array
*/
export function valid_array_indices(array) {
const keys = Object.keys(array);
keys.length = array_index_cut(keys);
return keys;
}
/**
* Given the own enumerable string keys of an array-like value, in property
* order, returns the leading run of them that are valid array indices.
*
* This is the filtering half of the `indicesOf` stringify operation,
* exposed so that custom operations — which typically already have the keys
* in hand, e.g. from a foreign runtime — don't have to reimplement it.
*
* Does not modify `keys`.
*
* @param {readonly string[]} keys
* @returns {string[]}
*/
export function filter_array_indices(keys) {
return keys.slice(0, array_index_cut(keys));
}
+63
View File
@@ -0,0 +1,63 @@
import * as assert from 'uvu/assert';
import { suite } from 'uvu';
import { valid_array_indices } from './utils.js';
const test = suite('valid_array_indices');
test('returns all indices for a normal dense array', () => {
const arr = ['a', 'b', 'c'];
assert.equal(valid_array_indices(arr), ['0', '1', '2']);
});
test('returns empty array for an empty array', () => {
assert.equal(valid_array_indices([]), []);
});
test('returns populated indices for a sparse array', () => {
const arr = [, 'b', ,];
assert.equal(valid_array_indices(arr), ['1']);
});
test('strips non-numeric properties from a dense array', () => {
const arr = ['a', 'b'];
arr.foo = 'x';
arr.bar = 42;
assert.equal(valid_array_indices(arr), ['0', '1']);
});
test('strips non-numeric properties from a very sparse array', () => {
const arr = [];
arr[1_000_000] = 'x';
arr.foo = 'should be ignored';
assert.equal(valid_array_indices(arr), ['1000000']);
});
test('returns empty array when only non-numeric properties exist', () => {
const arr = [];
arr.foo = 'x';
arr.bar = 42;
assert.equal(valid_array_indices(arr), []);
});
test('handles multiple non-numeric properties after indices', () => {
const arr = [1, 2, 3];
arr.a = 'x';
arr.b = 'y';
arr.c = 'z';
assert.equal(valid_array_indices(arr), ['0', '1', '2']);
});
test('handles a single-element array with non-numeric property', () => {
const arr = ['only'];
arr.extra = true;
assert.equal(valid_array_indices(arr), ['0']);
});
test('handles array properties pretending to be indices', () => {
const arr = ['a', 'b'];
arr[-1] = 'negative index';
arr[2 ** 32 - 1] = 'too large index';
assert.equal(valid_array_indices(arr), ['0', '1']);
});
test.run();
+515
View File
@@ -0,0 +1,515 @@
declare module 'devalue' {
export type StringValueTag = StringValueTag_1;
export type ViewTag = ViewTag_1;
export type StringifyOperations = StringifyOperations_1;
export type DefaultStringifyOperations = DefaultStringifyOperations_1;
export type StringifyOptions = StringifyOptions_1;
export type ParseOperations = ParseOperations_1;
export type DefaultParseOperations = DefaultParseOperations_1;
export type ParseOptions = ParseOptions_1;
/**
* Turn a value into the JavaScript that creates an equivalent value
*
*/
export function uneval(value: any, replacer?: (value: any, uneval: (value: any) => string) => string | void): string;
type StringValueTag_1 =
| 'URL'
| 'URLSearchParams'
| 'Temporal.Duration'
| 'Temporal.Instant'
| 'Temporal.PlainDate'
| 'Temporal.PlainTime'
| 'Temporal.PlainDateTime'
| 'Temporal.PlainMonthDay'
| 'Temporal.PlainYearMonth'
| 'Temporal.ZonedDateTime';
type ViewTag_1 =
| 'Int8Array'
| 'Uint8Array'
| 'Uint8ClampedArray'
| 'Int16Array'
| 'Uint16Array'
| 'Float16Array'
| 'Int32Array'
| 'Uint32Array'
| 'Float32Array'
| 'Float64Array'
| 'BigInt64Array'
| 'BigUint64Array'
| 'DataView';
type TypedArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Float16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| BigInt64Array
| BigUint64Array;
/**
* The introspection/extraction operations `stringify` performs on the value
* being serialized. Every dynamic operation — property reads, prototype
* method calls, iteration, type classification — goes through this
* interface, so overriding members lets you control exactly how values are
* inspected.
*
* Use cases:
* - **Side-effect-free serialization**: replace operations that can execute
* user code (getters, proxy traps, patched prototypes, `Symbol.toStringTag`
* accessors) with implementations based on captured intrinsics, internal
* slots, or property descriptors.
* - **Foreign-runtime serialization**: serialize values that live in another
* JavaScript runtime (a `node:vm` context, a WASM-hosted engine, a remote
* process) by implementing the operations over handle objects. The
* `stringify` algorithm never touches the value directly, so "value" can
* be any opaque token as long as the operations agree on what it means.
*
* All members are optional when passed to `stringify` — omitted members fall
* back to the defaults (native behavior, exported as
* `defaultStringifyOperations`).
*
* Members are named by what they do with the value:
* - `isXxx`/`hasXxx` — predicates returning booleans
* - `toXxx` — conversions whose whole result crosses into host JavaScript
* (`toPrimitive`, `toISOString`) or into a native container (`toPromise`)
* - `xxxOf` — queries returning host data *about* the value (`typeOf`,
* `tagOf`, `lengthOf`) or its constituents, which remain in value space
* (`valuesOf`, `entriesOf`)
* - `xxxInfo` — multi-field descriptors mixing host data and constituent
* values (`viewInfo`, `regExpInfo`)
* - bare verbs (`get`, `unbox`, `identify`) — accessors whose results remain
* in value space
*
* (`toStringValue` and `unbox` deliberately avoid the names `toString` and
* `valueOf`, which would shadow `Object.prototype` methods on the operations
* object.)
*/
interface StringifyOperations_1 {
/**
* Returns the key used for deduplication and cycle detection (compared
* with `Map` key semantics). Two values that represent the same logical
* object must return the same key. Default: the value itself.
*
* Override this when serializing through handles, where two distinct
* handle objects may refer to the same underlying value.
*
* Keys are compared across *every* value in the payload, including
* primitives, so an implementation that derives keys for objects must
* make sure they cannot collide with a primitive that appears in the
* same payload — returning e.g. the string `'42'` as an object's key
* would alias it to the string `'42'` elsewhere in the payload and emit
* a wrong back-reference. Prefer keys that are unforgeable, such as the
* underlying object itself, a symbol, or a wrapper object.
*/
identify(value: any): unknown;
/**
* Classifies a value. Same contract as the `typeof` operator, except
* `null` must be reported as `'null'` (not `'object'`).
*/
typeOf(value: any):
| 'undefined'
| 'null'
| 'boolean'
| 'number'
| 'bigint'
| 'string'
| 'symbol'
| 'function'
| 'object';
/**
* Extracts the host-JavaScript primitive from a value whose `typeOf` is
* `'null'`, `'boolean'`, `'number'`, `'bigint'` or `'string'`.
* Default: the value itself (it already is the primitive).
*/
toPrimitive(value: any): undefined | null | boolean | number | bigint | string;
/**
* Returns the brand of an object value — the strings produced by
* `Object.prototype.toString` without the wrapping (`'Date'`, `'Array'`,
* `'Map'`, `'Object'`, `'Temporal.Instant'`, …). This decides which
* serialization strategy is used, so hardened implementations should use
* engine-level brand checks rather than (spoofable, getter-invoking)
* `Symbol.toStringTag` lookups.
*/
tagOf(value: any): string;
/** Returns true if the object value should be treated as a thenable. */
isThenable(value: any): boolean;
/**
* Converts a thenable into a native promise, whose settled value is then
* serialized. The returned promise may reject, in which case
* `stringifyAsync` rejects. Only called from `stringifyAsync`, for values
* where `isThenable` returned true.
*/
toPromise(thenable: any): Promise<any>;
/**
* Extracts the inner value of a boxed primitive (`Number`, `String`,
* `Boolean`, `BigInt` objects). Equivalent to `boxed.valueOf()`. The
* result is serialized recursively, so it may be a foreign value/handle.
*/
unbox(boxed: any): any;
/**
* Returns the ISO string for a `Date` value, or `''` for an invalid
* date. Equivalent to `date.toISOString()`.
*/
toISOString(date: any): string;
/**
* Returns the string form of a `URL`, `URLSearchParams` or `Temporal.*`
* value. Equivalent to `value.toString()`.
*/
toStringValue(value: any): string;
/** Returns the source and flags of a `RegExp` value. */
regExpInfo(regexp: any): { source: string; flags: string };
/**
* Returns an iterable over the elements of a `Set` value. The iterable
* is consumed on the host; elements may be foreign values/handles.
*/
valuesOf(set: any): Iterable<any>;
/**
* Returns an iterable over the `[key, value]` entries of a `Map` value.
* The iterable is consumed on the host; keys/values may be foreign
* values/handles.
*/
entriesOf(map: any): Iterable<[any, any]>;
/**
* Returns the view metadata of a typed array or `DataView` value.
* `length` is only meaningful for typed arrays. `buffer` is serialized
* recursively, so it may be a foreign value/handle.
*/
viewInfo(view: any): {
buffer: any;
byteOffset: number;
byteLength: number;
length?: number;
bufferByteLength: number;
};
/**
* Returns a host `ArrayBuffer` with the bytes of an `ArrayBuffer` value.
* Default: the value itself. Foreign-runtime implementations should copy
* the bytes into a host buffer.
*/
toArrayBuffer(buffer: any): ArrayBuffer;
/** Returns the length of an `Array` value. */
lengthOf(array: any): number;
/**
* Returns true if a value has an own property at `key`. Same contract as
* `Object.hasOwn(value, key)`.
*/
hasOwn(value: any, key: string | number): boolean;
/**
* Returns the populated indices of a (sparse) `Array` value as strings,
* in ascending order.
*
* Implementations that already have the value's own enumerable string
* keys — as a foreign-runtime implementation typically does — should pass
* them through the exported `filterArrayIndices` helper rather than
* reimplementing the filtering, which encodes the sparse-array heuristic.
*
* Equivalent to `Object.keys(array)` filtered to
* valid array indices.
*/
indicesOf(array: any): string[];
/**
* Classifies a plain-object candidate:
* - `{ kind: 'plain' | 'null-proto', keys }` — a serializable POJO and
* its own enumerable string keys
* - `{ kind: 'not-plain' }` — a non-POJO (stringify throws)
* - `{ kind: 'symbol-keys' }` — a POJO with enumerable symbol keys
* (stringify throws)
*/
shapeOf(
value: any
):
| { kind: 'plain' | 'null-proto'; keys: string[] }
| { kind: 'not-plain' }
| { kind: 'symbol-keys' };
/**
* Reads a property from an `Array` or plain-object value. Equivalent to
* `value[key]`. Hardened implementations can read through property
* descriptors to control what happens for accessor properties.
*/
get(value: any, key: string | number): any;
}
/** The native JavaScript implementation exported as `defaultStringifyOperations`. */
interface DefaultStringifyOperations_1 extends StringifyOperations_1 {
identify(value: any): any;
toPrimitive(
value: undefined | null | boolean | number | bigint | string
): undefined | null | boolean | number | bigint | string;
toISOString(date: Date): string;
regExpInfo(regexp: RegExp): { source: string; flags: string };
valuesOf(set: Set<any>): Set<any>;
entriesOf(map: Map<any, any>): Map<any, any>;
viewInfo(view: any): {
buffer: ArrayBufferLike;
byteOffset: number;
byteLength: number;
length?: number;
bufferByteLength: number;
};
toArrayBuffer(buffer: ArrayBuffer): ArrayBuffer;
lengthOf(array: any[]): number;
indicesOf(array: any[]): string[];
}
/** Options for `stringify` and `stringifyAsync`. */
interface StringifyOptions_1 {
/**
* Overrides for the introspection/extraction operations used while
* serializing. Omitted members fall back to `defaultStringifyOperations`.
*/
operations?: Partial<StringifyOperations_1>;
}
/**
* The construction operations `parse` and `unflatten` perform while reviving
* a value. Every value the algorithm creates — primitives, built-in
* instances, containers — and every mutation it performs to populate those
* containers goes through this interface, so overriding members lets you
* control exactly what gets built.
*
* Use cases:
* - **Cross-realm revival**: construct values from the intrinsics of a
* different realm (e.g. a `node:vm` context) so that the result passes
* `instanceof` checks inside that realm.
* - **Foreign-runtime revival**: build values inside another JavaScript
* runtime (a WASM-hosted engine, a remote process) by implementing the
* operations over handle objects. The algorithm never inspects the values
* it creates — it only passes them back into other operations — so
* "value" can be any opaque token.
*
* The naming follows the same scheme as `StringifyOperations`, with the
* host/value-space boundary running the other way:
*
* - `fromXxx` — conversions whose input is entirely host data and whose
* result crosses into value space; each is the inverse of the
* corresponding `toXxx` (`fromPrimitive` / `toPrimitive`,
* `fromISOString` / `toISOString`, `fromStringValue` / `toStringValue`,
* `fromArrayBuffer` / `toArrayBuffer`).
* - `fromXxxInfo` — construction from a multi-field descriptor, the inverse
* of the corresponding `xxxInfo` (`fromRegExpInfo` / `regExpInfo`,
* `fromViewInfo` / `viewInfo`).
* - `createXxx` — empty value-space containers, populated afterwards by the
* mutators. That ordering is what makes cyclic values possible: the empty
* container is cached before its contents are revived.
* - bare verbs — value-space operations whose operands and results stay in
* value space (`box` inverts `unbox`, `set` inverts `get`, `addValue`
* inverts `valuesOf`, `addEntry` inverts `entriesOf`).
*
* All members are optional when passed to `parse`/`unflatten` — omitted
* members fall back to the defaults (native behavior, exported as
* `defaultParseOperations`).
*/
interface ParseOperations_1 {
/**
* Wraps a host primitive (`string`, `number`, `boolean`, `bigint`,
* `null`, `undefined`, and the special values `NaN`, `±Infinity`, `-0`)
* into the representation the other operations expect. The inverse of
* `toPrimitive`. Default: the value itself.
*/
fromPrimitive(
primitive: string | number | boolean | bigint | null | undefined
): any;
/**
* Creates a `Date` from an ISO string. The inverse of `toISOString`.
* An empty string represents an invalid date (as produced for
* `new Date(NaN)`).
*/
fromISOString(iso: string): any;
/**
* Creates a `URL`, `URLSearchParams` or `Temporal.*` value from its
* string form — the same tags `toStringValue` serializes, and its
* inverse. `tag` distinguishes them (e.g. `'URL'`,
* `'Temporal.Instant'`).
*/
fromStringValue(tag: StringValueTag_1, text: string): any;
/**
* Creates an `ArrayBuffer` from a host `ArrayBuffer` holding the decoded
* bytes. The inverse of `toArrayBuffer`. Default: the buffer itself.
* Foreign-runtime implementations should copy the bytes into the target
* runtime.
*/
fromArrayBuffer(buffer: ArrayBuffer): any;
/**
* Creates a `RegExp` from its source and flags. The inverse of
* `regExpInfo`. `flags` is `undefined` when the pattern had no flags.
*/
fromRegExpInfo(source: string, flags: string | undefined): any;
/**
* Creates a typed array or `DataView` over an already-revived buffer.
* The inverse of `viewInfo`. `tag` is the constructor name (e.g.
* `'Uint8Array'`, `'DataView'`). `byteOffset` and `length` are
* `undefined` when the view spans the whole buffer; otherwise `length`
* is the element count for typed arrays and the byte length for
* `DataView`, matching the constructor signatures.
*/
fromViewInfo(
tag: ViewTag_1,
buffer: any,
byteOffset: number | undefined,
length: number | undefined
): any;
/**
* Creates a boxed primitive object (`Number`, `String`, `Boolean`,
* `BigInt` wrapper) around an already-revived inner primitive. The
* inverse of `unbox`. Equivalent to `Object(value)`.
*/
box(value: any): any;
/**
* Creates an array of the given length, to be populated with `set`.
* The length is bounded by the size of the input, so it is safe to
* allocate eagerly. Indices that are never set must remain holes.
*/
createArray(length: number): any;
/**
* Creates a sparse array of the given length, to be populated with
* `set`. Unlike `createArray`, the length comes from the input rather
* than being bounded by it, so implementations must not allocate
* storage proportional to it.
*/
createSparseArray(length: number): any;
/** Creates an empty object, to be populated with `set`. */
createObject(): any;
/**
* Creates an empty null-prototype object, to be populated with `set`.
* Equivalent to `Object.create(null)`.
*/
createNullPrototypeObject(): any;
/** Creates an empty `Set`, to be populated with `addValue`. */
createSet(): any;
/** Creates an empty `Map`, to be populated with `addEntry`. */
createMap(): any;
/**
* Sets an element or property on a value created by `createArray`,
* `createSparseArray`, `createObject` or `createNullPrototypeObject`.
* The inverse of `get`, which likewise serves both arrays and objects.
*/
set(target: any, key: string | number, value: any): void;
/** Adds a value to a `Set` created by `createSet`. The inverse of `valuesOf`. */
addValue(set: any, value: any): void;
/** Adds an entry to a `Map` created by `createMap`. The inverse of `entriesOf`. */
addEntry(map: any, key: any, value: any): void;
}
/** The native JavaScript implementation exported as `defaultParseOperations`. */
interface DefaultParseOperations_1 extends ParseOperations_1 {
fromPrimitive(
primitive: string | number | boolean | bigint | null | undefined
): string | number | boolean | bigint | null | undefined;
fromISOString(iso: string): Date;
fromStringValue(tag: StringValueTag_1, text: string): URL | URLSearchParams | object;
fromArrayBuffer(buffer: ArrayBuffer): ArrayBuffer;
fromRegExpInfo(source: string, flags: string | undefined): RegExp;
fromViewInfo(
tag: ViewTag_1,
buffer: ArrayBufferLike,
byteOffset: number | undefined,
length: number | undefined
): TypedArray | DataView;
box(value: any): object;
createArray(length: number): any[];
createSparseArray(length: number): any[];
createObject(): Record<string, any>;
createNullPrototypeObject(): Record<string, any>;
createSet(): Set<any>;
createMap(): Map<any, any>;
addValue(set: Set<any>, value: any): void;
addEntry(map: Map<any, any>, key: any, value: any): void;
}
/** Options for `parse` and `unflatten`. */
interface ParseOptions_1 {
/**
* Overrides for the construction operations used while reviving.
* Omitted members fall back to `defaultParseOperations`.
*/
operations?: Partial<ParseOperations_1>;
}
/**
* Revive a value serialized with `devalue.stringify`
*
*/
export function parse(serialized: string, revivers?: Record<string, (value: any) => any>, options?: ParseOptions_1): any;
/**
* Revive a value flattened with `devalue.stringify`
*
*/
export function unflatten(parsed: number | any[], revivers?: Record<string, (value: any) => any>, options?: ParseOptions_1): any;
/**
* Turn a value into a JSON string that can be parsed with `devalue.parse`
*
*/
export function stringify(value: any, reducers?: Record<string, (value: any) => any>, options?: StringifyOptions_1): string;
/**
* Turn a value into a JSON string that can be parsed with `devalue.parse`
*
*/
export function stringifyAsync(value: any, reducers?: Record<string, (value: any) => any>, options?: StringifyOptions_1): Promise<string>;
export const defaultStringifyOperations: Readonly<DefaultStringifyOperations_1>;
export const defaultParseOperations: Readonly<DefaultParseOperations_1>;
/**
* Given the own enumerable string keys of an array-like value, in property
* order, returns the leading run of them that are valid array indices.
*
* This is the filtering half of the `indicesOf` stringify operation,
* exposed so that custom operations — which typically already have the keys
* in hand, e.g. from a foreign runtime — don't have to reimplement it.
*
* Does not modify `keys`.
*
* */
export function filterArrayIndices(keys: readonly string[]): string[];
export class DevalueError extends Error {
/**
* @param value - The value that failed to be serialized
* @param root - The root value being serialized
*/
constructor(message: string, keys: string[], value?: any, root?: any);
path: string;
value: any;
root: any;
}
export {};
}
//# sourceMappingURL=index.d.ts.map
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"file": "index.d.ts",
"names": [
"StringValueTag",
"ViewTag",
"StringifyOperations",
"DefaultStringifyOperations",
"StringifyOptions",
"ParseOperations",
"DefaultParseOperations",
"ParseOptions",
"uneval",
"TypedArray",
"parse",
"unflatten",
"stringify",
"stringifyAsync",
"DevalueError"
],
"sources": [
"../src/types.d.ts",
"../src/uneval.js",
"../src/parse.js",
"../src/stringify.js",
"../src/utils.js"
],
"sourcesContent": [
null,
null,
null,
null,
null
],
"mappings": ";aAAYA,cAAcA;aAYdC,OAAOA;aAmEFC,mBAAmBA;aAoKnBC,0BAA0BA;aAsB1BC,gBAAgBA;aA+ChBC,eAAeA;aA2GfC,sBAAsBA;aA0BtBC,YAAYA;;;;;iBCvabC,MAAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;MDKVC,UAAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBERNC,KAAKA;;;;;iBAULC,SAASA;;;;;iBCVTC,SAASA;;;;;iBAWHC,cAAcA;;;;;;;;;;;;;;;cCfvBC,YAAYA",
"ignoreList": []
}