estree-util-to-static-value
v1.0.0
Published
> Safe extraction of static JavaScript data from ESTree expressions.
Maintainers
Readme
estree-util-to-static-value
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.
Features
- 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.
Why Build-Time Processing?
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 |
Static Data Processing Pipeline
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.
Dynamic Analysis Limits and Preservation Modes
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.
Installation
npm install estree-util-to-static-value
# or
pnpm add estree-util-to-static-valueExamples
Static Object
Input:
{ a: 1, b: "x", c: [true, 2] }Output:
{ a: 1, b: "x", c: [true, 2] }Partial Object Extraction
Input:
{ a: 1, b: someVar }Output:
{ a: 1 }Strict Array Bailout
Input:
[1, someVar, 3]Output:
SKIP_VALUERoot Object Adapter
Input root:
[1, 2]extractStaticProps(...) output:
nullHow to Use
Step 1: Parse a Single Expression into ESTree
import { 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;
}Step 2: Extract Static Value and Handle Unresolved
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(...).
Step 3: Preserve Dynamic Subtrees (for example children)
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
Step 4: Re-inline Preserved Expressions (Advanced)
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.
Configuration Reference
ExtractOptions
export type ExtractOptions = {
preservedKeys: ReadonlySet<string>;
onPreservedExpression?: (info: PreservedExpressionInfo) => unknown;
};ExtractOptions controls how dynamic/unresolved subtrees are handled during extraction.
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| preservedKeys | ReadonlySet<string> | Yes | Keys that bypass normal static evaluation and are emitted as expression placeholders (for example children) |
| onPreservedExpression | (info) => unknown | Conditional | Called for each preserved key; callback return is written as the placeholder. Required at runtime when a preserved key is encountered |
PreservedExpressionInfo shape:
type PreservedExpressionInfo = {
path: (string | number)[];
node: types.Node;
expression: types.Expression | null;
};Preserved Placeholder Model
When a key is listed in preservedKeys (for example children), the extractor does this:
- Calls
onPreservedExpression({ path, node, expression }). - Writes the callback return value into extracted data at the same
path. - Lets downstream AST rebuild code resolve that placeholder back to the stored expression.
This is the mechanism that lets dynamic runtime subtrees pass through static-data pipelines safely.
Note:
PreservedSubtreeLifecycleandExpressionRefPlaceholderare documentation marker types (never) used to describe this design.- Consumers should rely on
ExtractOptions+ callback behavior, not those marker types. - If a preserved key is encountered and
onPreservedExpressionis missing, extraction throws.
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.
API Surface
| API | Returns | Notes |
| --- | --- | --- |
| extractStaticValueFromExpression(node, options, pathLabel, pathSegments?) | unknown | typeof SKIP_VALUE | Core recursive extractor |
| extractStaticProps(propsNode, options, pathLabel) | Record<string, unknown> | null | Root adapter that accepts only plain-object results |
| formatPath(base, segment) | string | Utility to format human-readable property paths for diagnostics |
| extractPropertyKey(propertyNode) | string | number | null | Resolves object/property keys from ESTree Property / PropertyDefinition nodes |
| tryResolveStaticValue(node) | StaticResult | Direct static resolver for supported expression forms |
| SKIP_VALUE | symbol | Sentinel for unresolved/dynamic extraction |
Exported Types
| Type | Description |
| --- | --- |
| ExtractOptions | Extractor runtime options (preservedKeys, onPreservedExpression) |
| PreservedExpressionInfo | Preserved callback payload (path, node, expression) |
| PropertyPath | Logical nested path as (string | number)[] |
| StaticSuccess | Successful direct static resolution shape |
| StaticFailure | Failed direct static resolution shape |
| StaticResult | Union of StaticSuccess and StaticFailure |
Property Key Rules
- 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.
Capabilities
Supported Direct Resolution
- Literals:
string,number,boolean,bigint,null,RegExp - Identifier constants:
undefined,NaN,Infinity - Template literals where all interpolations are statically resolvable
Currently Unresolved by Default
- 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
Runtime vs Static Semantics
- 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.
Invariants
- 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).
