feat: Phase 1 — extract the client (web SPA + desktop) to dtoro/oikos-web
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Problem: the hexagonal refactor churns the backend tree for nine more
phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper,
compose/web image) must move to its own repo first so doc/layout
rewrites land once on a backend-only tree.

Change:
- New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/
  (updateURL repointed to oikos-web releases), compose/, own CI (web +
  desktop jobs), own deploy script (CI-green gate, TOCTOU guard,
  version-tagged images, prune-to-3), own webhook receiver on :9798 +
  launchd unit, own compose project publishing the same 8091:80.
- Cutover executed on mac-mini in order: oikos stack's web service
  stopped+removed, oikos-web project brought up on 8091; outer Caddy
  untouched (targets the published port) — serving + Authentik flow +
  /wails 404 quirk verified post-cutover.
- Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI
  workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install
  targets, the compose web service, oikos-web from deploy.sh's fallback
  prune list; wails + go-keyring dropped from go.mod, vendor synced.
- README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now
  point at the new repo; mbse + mascot design docs carry a path note.

Risk: production SPA serving depends on the new pipeline now; rollback
is versioned-image re-up of the old web service from a pre-split
checkout (port 8091). Desktop builds installed before the split still
check dtoro/oikos releases — one manual reinstall, noted in the
oikos-web release notes.

Verification: go vet, make test (race), make generate-check, golangci
(no new findings; baseline down 400→365); post-cutover curls —
localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
This commit is contained in:
2026-08-15 22:27:52 +02:00
parent e074f04bdf
commit d4d99a7473
18854 changed files with 2615729 additions and 173735 deletions

60
web/node_modules/devalue/src/base64.js generated vendored Normal file
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
web/node_modules/devalue/src/base64.test.js generated vendored Normal file
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
web/node_modules/devalue/src/constants.js generated vendored Normal file
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
web/node_modules/devalue/src/operations.js generated vendored Normal file
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
web/node_modules/devalue/src/parse.js generated vendored Normal file
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
web/node_modules/devalue/src/stringify.js generated vendored Normal file
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
web/node_modules/devalue/src/types.d.ts generated vendored Normal file
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
web/node_modules/devalue/src/uneval.js generated vendored Normal file
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
web/node_modules/devalue/src/utils.js generated vendored Normal file
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
web/node_modules/devalue/src/utils.test.js generated vendored Normal file
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();