Safe extraction of static JavaScript data from ESTree expressions.
estree-util-to-static-value converts ESTree expression nodes into plain JavaScript values at build time without executing code. Dynamic or unsupported subtrees are treated as unresolved.
Status/Scope: This utility extracts only the statically resolvable subset of ESTree and never evaluates or executes runtime code.
Reference usage: currently used in project-millipede/recma-static-refiner.
- Deterministic static extraction from ESTree expressions without executing code.
- Explicit unresolved signaling via
SKIP_VALUE. - Strict array policy (any unresolved element =>
SKIP_VALUE) and partial object policy (drop only unresolved entries). - Preserved-key transport for dynamic subtrees using
onPreservedExpression(capture + placeholder return). - Root object adapter (
extractStaticProps) for object-only outputs, returningnullfor non-object or unresolved roots.
When extracting data from AST, executing runtime code is unsafe and non-deterministic. This utility gives you deterministic static decoding only.
| Aspect | Runtime Evaluation | Build-Time Static Extraction (This Utility) |
|---|---|---|
| Execution | Runs user code | Never executes user code |
| Determinism | Depends on runtime state | Determined from AST-only rules |
| Dynamic nodes | Produce runtime values | Return unresolved (SKIP_VALUE) |
| Mixed objects | Full runtime result | Static subset only |
At extraction time, the engine walks ESTree and applies three phases:
| Phase | Input | Output | Purpose |
|---|---|---|---|
| 1. Direct Resolution | Single expression node | Static value or unresolved | Resolve literals, constant identifiers, static templates |
| 2. Recursive Extraction | Arrays/objects | Nested static value or unresolved | Traverse nested structures without execution |
| 3. Policy Integration | Child results | Final container result | Apply strict array policy and partial object policy |
Policy summary:
ArrayExpression: any unresolved child collapses the whole array toSKIP_VALUE.ObjectExpression: unresolved key/value drops only that entry.- Array elisions are preserved as holes.
- Array spreads trigger strict bailout.
- Object spreads are skipped.
Static extraction is intentionally constrained:
- Identifier nodes expose names, not runtime values (
{ type: "Identifier", name: "x" }). - Runtime-dependent expressions cannot be safely evaluated during build.
- Spreads can shadow or reorder effective runtime values.
Because of this, preservation happens in two different ways:
- Implicit preservation (omission): unresolved subtrees are excluded from extracted data and remain untouched in source AST.
- Explicit preservation (transport): keys listed in
preservedKeysare captured via placeholder + side-channel callback for later re-inlining.
npm install estree-util-to-static-value
# or
pnpm add estree-util-to-static-valueInput:
{ a: 1, b: "x", c: [true, 2] }Output:
{ a: 1, b: "x", c: [true, 2] }Input:
{ a: 1, b: someVar }Output:
{ a: 1 }Input:
[1, someVar, 3]Output:
SKIP_VALUEInput root:
[1, 2]extractStaticProps(...) output:
nullimport { parse } from 'meriyah';
import { is, type types } from 'estree-toolkit';
function parseExpression(code: string): types.Expression {
const program = parse(`(${code})`) as unknown;
if (!is.program(program)) {
throw new Error('Expected parser output to be an ESTree Program node.');
}
const first = program.body.at(0);
if (!first || !is.expressionStatement(first)) {
throw new Error('Expected wrapped code to produce an ExpressionStatement.');
}
return first.expression;
}import {
extractStaticValueFromExpression,
SKIP_VALUE
} from 'estree-util-to-static-value';
const node = parseExpression('{ a: 1, b: someVar }');
const result = extractStaticValueFromExpression(
node,
{ preservedKeys: new Set() },
'root'
);
if (result === SKIP_VALUE) {
// unresolved subtree/root
} else {
// static result
// => { a: 1 }
}For object-only root expectations, use extractStaticProps(...).
import type { types } from 'estree-toolkit';
import { extractStaticProps } from 'estree-util-to-static-value';
const toPathKey = (path: ReadonlyArray<string | number>) =>
JSON.stringify(path);
const preservedExpressionsByPath = new Map<string, types.Expression>();
const extracted = extractStaticProps(
propsNode,
{
preservedKeys: new Set(['children']),
onPreservedExpression: ({ path, node, expression }) => {
// `node` is always present; `expression` can be null.
if (expression) {
preservedExpressionsByPath.set(toPathKey(path), expression);
}
return {
kind: 'preserved',
path
};
}
},
'Component.props'
);In recma-static-refiner integration, path keys are encoded with a canonical helper (stringifyPropertyPath).
Behavior summary:
- static fields are extracted as plain JS values
- unresolved fields are normally omitted in objects
- preserved keys (like
children) are emitted as placeholders and captured via callback - use a canonical path-key encoder to avoid collisions
If your downstream pipeline rebuilds AST from extracted/derived values, preserved placeholders must be resolved back to their original expressions.
Minimal flow:
- Capture preserved expressions in
onPreservedExpression. - Carry placeholders through validation/transforms.
- During AST rebuild, replace each placeholder with the side-channel expression stored for that path.
extractStaticValueFromExpression(
expressionNode: types.Node,
options: ExtractOptions,
pathLabel: string,
pathSegments?: PropertyPath
): unknown | typeof SKIP_VALUECore recursive extractor for any expression root.
extractStaticProps(
propsNode: types.Node,
options: ExtractOptions,
pathLabel: string
): Record<string, unknown> | nullObject-root adapter. Returns null for unresolved or non-object roots.
extractPropertyKey(propertyNode: types.Property): string | number | nullResolves a statically addressable object key.
tryResolveStaticValue(node: types.Node): StaticResultDirect resolver for literals, constant identifiers, and static templates.
formatPath(base: string, segment: string | number): stringFormats human-readable diagnostic paths.
const SKIP_VALUE: symbolSentinel used for unresolved/dynamic extraction.
ExtractOptions controls preservation behavior and callback integration.
| Option | Type | Required | Description |
|---|---|---|---|
preservedKeys |
ReadonlySet<string> |
Yes | Keys to preserve as dynamic subtrees (for example children) |
onPreservedExpression |
(info) => unknown |
Conditional | Called for each preserved key and used to create the placeholder value written into extracted data |
onPreservedExpression(info) payload:
path:(string | number)[]logical location in extracted data.node:types.Nodeoriginal ESTree subtree.expression:types.Expression | nullexpression form when available.
If a preserved key is encountered without onPreservedExpression, extraction throws.
When a key is listed in preservedKeys (for example children), the extractor:
- Calls
onPreservedExpression({ path, node, expression }). - Writes the callback return value into extracted data at the same
path. - Lets downstream AST rebuild code replace the placeholder with the captured expression.
This is the mechanism that lets dynamic runtime subtrees pass through static-data pipelines safely.
Note (documentation anchors):
PreservedSubtreeLifecycleandExpressionRefPlaceholderarenevermarker types used only to anchor architecture documentation inarchitecture.ts.- They are not runtime placeholders and are not part of the extractor's public behavior contract.
- Consumers should rely on
ExtractOptions+onPreservedExpressionbehavior.
Deep dive source:
src/architecture.ts
For end-to-end round-trip integration details (capture, patch planning, and re-inlining), see project-millipede/recma-static-refiner.
- Non-computed identifier keys resolve by label (
{ title: 1 } -> "title"). - Literal/computed keys go through static resolution.
- Accepted resolved key types:
stringandnumber. - Rejected key results:
null,undefined,bigint,symbol, and dynamic expressions.
- Literals:
string,number,boolean,bigint,null,RegExp - Identifier constants:
undefined,NaN,Infinity - Template literals where all interpolations are statically resolvable
- Variable identifiers (for example
someVar) - Member access (for example
obj.value) - Function calls and constructors
- Unary, binary, logical, conditional, and sequence expressions
- JSX and other runtime-only expression forms
- Static data is extracted and represented as plain JS values.
- Runtime code is never executed; unresolved expressions stay unresolved.
- Preserved keys provide explicit transport for runtime subtrees through data-oriented pipelines.
- Extraction never executes user code.
- Arrays preserve positional integrity via strict bailout.
- Objects maximize recoverable data via selective omission.
- Unresolved is always explicit (
SKIP_VALUE, ornullin object-only root adapter).