/** * Create an ESTree identifier node for a given name. * * @param name * The name of the identifier. * @returns * The identifier node. */ function identifier(name) { return { type: 'Identifier', name }; } /** * Create an ESTree literal node for a given value. * * @param value * The value for which to create a literal. * @returns * The literal node. */ function literal(value) { return { type: 'Literal', value }; } /** * Create an ESTree call expression on an object member. * * @param object * The object to call the method on. * @param name * The name of the method to call. * @param args * Arguments to pass to the function call * @returns * The call expression node. */ function methodCall(object, name, args) { return { type: 'CallExpression', optional: false, callee: { type: 'MemberExpression', computed: false, optional: false, object, property: identifier(name) }, arguments: args }; } /** * Turn a number or bigint into an ESTree expression. This handles positive and negative numbers and * bigints as well as special numbers. * * @param number * The value to turn into an ESTree expression. * @returns * An expression that represents the given value. */ function processNumber(number) { if (number < 0 || Object.is(number, -0)) { return { type: 'UnaryExpression', operator: '-', prefix: true, argument: processNumber(-number) }; } if (typeof number === 'bigint') { return { type: 'Literal', bigint: String(number) }; } if (number === Number.POSITIVE_INFINITY || Number.isNaN(number)) { return identifier(String(number)); } return literal(number); } /** * Process an array of numbers. This is a shortcut for iterables whose constructor takes an array of * numbers as input. * * @param numbers * The numbers to add to the array expression. * @returns * An ESTree array expression whose elements match the input numbers. */ function processNumberArray(numbers) { return { type: 'ArrayExpression', elements: Array.from(numbers, processNumber) }; } /** * Check whether a value can be constructed from its string representation. * * @param value * The value to check * @returns * Whether or not the value can be constructed from its string representation. */ function isStringReconstructable(value) { return value instanceof URL || value instanceof URLSearchParams; } /** * Check whether a value can be constructed from its `valueOf()` result. * * @param value * The value to check * @returns * Whether or not the value can be constructed from its `valueOf()` result. */ function isValueReconstructable(value) { return (value instanceof Boolean || value instanceof Date || value instanceof Number || value instanceof String); } const wellKnownSymbols = new Map(); for (const name of Reflect.ownKeys(Symbol)) { const value = Symbol[name]; if (typeof value === 'symbol') { wellKnownSymbols.set(value, name); } } /** * Check whether a value is a Temporal value. * * @param value * The value to check * @returns * Whether or not the value is a Temporal value. */ function isTemporal(value) { return (typeof Temporal !== 'undefined' && (value instanceof Temporal.Duration || value instanceof Temporal.Instant || value instanceof Temporal.PlainDate || value instanceof Temporal.PlainDateTime || value instanceof Temporal.PlainYearMonth || value instanceof Temporal.PlainMonthDay || value instanceof Temporal.PlainTime || value instanceof Temporal.ZonedDateTime)); } /** * Check whether a value is a typed array. * * @param value * The value to check * @returns * Whether or not the value is a typed array. */ function isTypedArray(value) { return (value instanceof BigInt64Array || value instanceof BigUint64Array || (typeof Float16Array !== 'undefined' && value instanceof Float16Array) || value instanceof Float32Array || value instanceof Float64Array || value instanceof Int8Array || value instanceof Int16Array || value instanceof Int32Array || value instanceof Uint8Array || value instanceof Uint8ClampedArray || value instanceof Uint16Array || value instanceof Uint32Array); } /** * Compare two value contexts for sorting them based on reference count. * * @param a * The first context to compare. * @param b * The second context to compare. * @returns * The count of context a minus the count of context b. */ function compareContexts(a, b) { const aReferencedByB = a.referencedBy.has(b.value); const bReferencedByA = b.referencedBy.has(a.value); if (aReferencedByB) { if (bReferencedByA) { return a.count - b.count; } return -1; } if (bReferencedByA) { return 1; } return a.count - b.count; } /** * Replace the assigned right hand expression with the new expression. * * If there is no assignment expression, the original expression is returned. Otherwise the * assignment is modified and returned. * * @param expression * The expression to use for the assignment. * @param assignment * The existing assignmentexpression * @returns * The new expression. */ function replaceAssignment(expression, assignment) { if (!assignment || assignment.type !== 'AssignmentExpression') { return expression; } let node = assignment; while (node.right.type === 'AssignmentExpression') { node = node.right; } node.right = expression; return assignment; } /** * Create an ESTree epxression to represent a symbol. Global and well-known symbols are supported. * * @param symbol * The symbol to represent. * @returns * An ESTree expression to represent the symbol. */ function symbolToEstree(symbol) { const name = wellKnownSymbols.get(symbol); if (name) { return { type: 'MemberExpression', computed: false, optional: false, object: identifier('Symbol'), property: identifier(name) }; } if (symbol.description && symbol === Symbol.for(symbol.description)) { return methodCall(identifier('Symbol'), 'for', [literal(symbol.description)]); } throw new TypeError(`Only global symbols are supported, got: ${String(symbol)}`, { cause: symbol }); } /** * Create an ESTree property from a key and a value expression. * * @param key * The property key value * @param value * The property value as an ESTree expression. * @returns * The ESTree properry node. */ function property(key, value) { const isString = typeof key === 'string'; return { type: 'Property', method: false, shorthand: false, computed: key === '__proto__' || !isString, kind: 'init', key: isString ? literal(key) : symbolToEstree(key), value }; } /** * Convert a value to an ESTree node. * * @param value * The value to convert. * @param options * Additional options to configure the output. * @returns * The ESTree node. */ export function valueToEstree(value, options = {}) { const stack = []; const collectedContexts = new Map(); const namedContexts = []; const customTrees = new Map(); /** * Analyze a value and collect all reference contexts. * * @param val * The value to analyze. */ function analyze(val) { if (typeof val !== 'object' && typeof val !== 'function') { return; } if (val == null) { return; } const context = collectedContexts.get(val); if (context) { if (options.preserveReferences) { context.count += 1; } for (const ancestor of stack) { context.referencedBy.add(ancestor); } if (stack.includes(val)) { if (!options.preserveReferences) { throw new Error(`Found circular reference: ${val}`, { cause: val }); } const parent = stack.at(-1); const parentContext = collectedContexts.get(parent); parentContext.recursive = true; context.recursive = true; } return; } collectedContexts.set(val, { count: 1, recursive: false, referencedBy: new Set(stack), value: val }); const estree = options?.replacer?.(val); if (estree) { customTrees.set(val, estree); return; } if (typeof val === 'function') { throw new TypeError(`Unsupported value: ${val}`, { cause: val }); } if (isTypedArray(val)) { return; } if (isStringReconstructable(val)) { return; } if (isValueReconstructable(val)) { return; } if (value instanceof RegExp) { return; } if (isTemporal(value)) { return; } stack.push(val); if (val instanceof Map) { for (const pair of val) { analyze(pair[0]); analyze(pair[1]); } } else if (Array.isArray(val) || val instanceof Set) { for (const entry of val) { analyze(entry); } } else { const proto = Object.getPrototypeOf(val); if (proto != null && proto !== Object.prototype && !options.instanceAsObject) { throw new TypeError(`Unsupported value: ${val}`, { cause: val }); } for (const key of Reflect.ownKeys(val)) { analyze(val[key]); } } stack.pop(); } /** * Recursively generate the ESTree expression needed to reconstruct the value. * * @param val * The value to process. * @param isDeclaration * Whether or not this is for a variable declaration. * @returns * The ESTree expression to reconstruct the value. */ function generate(val, isDeclaration) { if (val === undefined) { return identifier(String(val)); } if (val == null || typeof val === 'string' || typeof val === 'boolean') { return literal(val); } if (typeof val === 'bigint' || typeof val === 'number') { return processNumber(val); } if (typeof val === 'symbol') { return symbolToEstree(val); } const context = collectedContexts.get(val); if (!isDeclaration && context?.name) { return identifier(context.name); } const tree = customTrees.get(val); if (tree) { return tree; } if (isValueReconstructable(val)) { return { type: 'NewExpression', callee: identifier(val.constructor.name), arguments: [generate(val.valueOf())] }; } if (val instanceof RegExp) { return { type: 'Literal', regex: { pattern: val.source, flags: val.flags } }; } if (typeof Buffer !== 'undefined' && Buffer.isBuffer(val)) { return methodCall(identifier('Buffer'), 'from', [processNumberArray(val)]); } if (isTypedArray(val)) { return { type: 'NewExpression', callee: identifier(val.constructor.name), arguments: [processNumberArray(val)] }; } if (isStringReconstructable(val)) { return { type: 'NewExpression', callee: identifier(val.constructor.name), arguments: [literal(String(val))] }; } if (isTemporal(val)) { return methodCall({ type: 'MemberExpression', computed: false, optional: false, object: identifier('Temporal'), property: identifier(val.constructor.name) }, 'from', [literal(String(val))]); } if (Array.isArray(val)) { const elements = Array.from({ length: val.length }); let trimmable; for (let index = 0; index < val.length; index += 1) { if (!(index in val)) { elements[index] = null; trimmable = undefined; continue; } const child = val[index]; const childContext = collectedContexts.get(child); if (context && childContext && namedContexts.indexOf(childContext) >= namedContexts.indexOf(context)) { elements[index] = null; trimmable ||= index; childContext.assignment = { type: 'AssignmentExpression', operator: '=', left: { type: 'MemberExpression', computed: true, optional: false, object: identifier(context.name), property: literal(index) }, right: childContext.assignment || identifier(childContext.name) }; } else { elements[index] = generate(child); trimmable = undefined; } } if (trimmable != null) { elements.splice(trimmable); } return { type: 'ArrayExpression', elements }; } if (val instanceof Set) { const elements = []; let finalizer; for (const child of val) { if (finalizer) { finalizer = methodCall(finalizer, 'add', [generate(child)]); } else { const childContext = collectedContexts.get(child); if (context && childContext && namedContexts.indexOf(childContext) >= namedContexts.indexOf(context)) { finalizer = methodCall(identifier(context.name), 'add', [generate(child)]); } else { elements.push(generate(child)); } } } if (context && finalizer) { context.assignment = replaceAssignment(finalizer, context.assignment); } return { type: 'NewExpression', callee: identifier('Set'), arguments: elements.length ? [{ type: 'ArrayExpression', elements }] : [] }; } if (val instanceof Map) { const elements = []; let finalizer; for (const [key, item] of val) { if (finalizer) { finalizer = methodCall(finalizer, 'set', [generate(key), generate(item)]); } else { const keyContext = collectedContexts.get(key); const itemContext = collectedContexts.get(item); if (context && ((keyContext && namedContexts.indexOf(keyContext) >= namedContexts.indexOf(context)) || (itemContext && namedContexts.indexOf(itemContext) >= namedContexts.indexOf(context)))) { finalizer = methodCall(identifier(context.name), 'set', [ generate(key), generate(item) ]); } else { elements.push({ type: 'ArrayExpression', elements: [generate(key), generate(item)] }); } } } if (context && finalizer) { context.assignment = replaceAssignment(finalizer, context.assignment); } return { type: 'NewExpression', callee: identifier('Map'), arguments: elements.length ? [{ type: 'ArrayExpression', elements }] : [] }; } const properties = []; if (Object.getPrototypeOf(val) == null) { properties.push({ type: 'Property', method: false, shorthand: false, computed: false, kind: 'init', key: identifier('__proto__'), value: literal(null) }); } const object = val; const propertyDescriptors = []; for (const key of Reflect.ownKeys(val)) { // TODO [>=4] Throw an error for getters. const child = object[key]; const { configurable, enumerable, writable } = Object.getOwnPropertyDescriptor(val, key); const childContext = collectedContexts.get(child); if (!configurable || !enumerable || !writable) { const propertyDescriptor = [property('value', generate(child))]; if (configurable) { propertyDescriptor.push(property('configurable', literal(true))); } if (enumerable) { propertyDescriptor.push(property('enumerable', literal(true))); } if (writable) { propertyDescriptor.push(property('writable', literal(true))); } propertyDescriptors.push([ key, { type: 'ObjectExpression', properties: propertyDescriptor } ]); } else if (context && childContext && namedContexts.indexOf(childContext) >= namedContexts.indexOf(context)) { if (key === '__proto__') { propertyDescriptors.push([ key, { type: 'ObjectExpression', properties: [ property('value', generate(child)), property('configurable', literal(true)), property('enumerable', literal(true)), property('writable', literal(true)) ] } ]); } else { childContext.assignment = { type: 'AssignmentExpression', operator: '=', left: { type: 'MemberExpression', computed: true, optional: false, object: identifier(context.name), property: generate(key) }, right: childContext.assignment || generate(child) }; } } else { properties.push(property(key, generate(child))); } } const objectExpression = { type: 'ObjectExpression', properties }; if (propertyDescriptors.length) { let name; let args; if (propertyDescriptors.length === 1) { const [[key, expression]] = propertyDescriptors; name = 'defineProperty'; args = [typeof key === 'string' ? literal(key) : symbolToEstree(key), expression]; } else { name = 'defineProperties'; args = [ { type: 'ObjectExpression', properties: propertyDescriptors.map(([key, expression]) => property(key, expression)) } ]; } if (!context) { return methodCall(identifier('Object'), name, [objectExpression, ...args]); } context.assignment = replaceAssignment(methodCall(identifier('Object'), name, [identifier(context.name), ...args]), context.assignment); } return objectExpression; } analyze(value); for (const [val, context] of collectedContexts) { if (context.recursive || context.count > 1) { // Assign reused or recursive references to a variable. context.name = `$${namedContexts.length}`; namedContexts.push(context); } else { // Otherwise don’t treat it as a reference. collectedContexts.delete(val); } } if (!namedContexts.length) { return generate(value); } const params = namedContexts.sort(compareContexts).map((context) => ({ type: 'AssignmentPattern', left: identifier(context.name), right: generate(context.value, true) })); const rootContext = collectedContexts.get(value); const finalizers = []; for (const context of collectedContexts.values()) { if (context !== rootContext && context.assignment) { finalizers.push(context.assignment); } } finalizers.push(rootContext ? rootContext.assignment || identifier(rootContext.name) : generate(value)); return { type: 'CallExpression', optional: false, arguments: [], callee: { type: 'ArrowFunctionExpression', expression: false, params, body: { type: 'SequenceExpression', expressions: finalizers } } }; } //# sourceMappingURL=estree-util-value-to-estree.js.map