Skip to content

project-millipede/estree-util-to-static-value

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

Table of Contents

  1. Overview
  2. Getting Started
  3. API and Configuration
  4. Behavior and Limits

Overview

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, returning null for 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 to SKIP_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 preservedKeys are captured via placeholder + side-channel callback for later re-inlining.

Getting Started

Installation

npm install estree-util-to-static-value
# or
pnpm add estree-util-to-static-value

Examples

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_VALUE

Root Object Adapter

Input root:

[1, 2]

extractStaticProps(...) output:

null

How 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:

  1. Capture preserved expressions in onPreservedExpression.
  2. Carry placeholders through validation/transforms.
  3. During AST rebuild, replace each placeholder with the side-channel expression stored for that path.

API and Configuration

API Reference

extractStaticValueFromExpression
extractStaticValueFromExpression(
  expressionNode: types.Node,
  options: ExtractOptions,
  pathLabel: string,
  pathSegments?: PropertyPath
): unknown | typeof SKIP_VALUE

Core recursive extractor for any expression root.

extractStaticProps
extractStaticProps(
  propsNode: types.Node,
  options: ExtractOptions,
  pathLabel: string
): Record<string, unknown> | null

Object-root adapter. Returns null for unresolved or non-object roots.

extractPropertyKey
extractPropertyKey(propertyNode: types.Property): string | number | null

Resolves a statically addressable object key.

tryResolveStaticValue
tryResolveStaticValue(node: types.Node): StaticResult

Direct resolver for literals, constant identifiers, and static templates.

formatPath
formatPath(base: string, segment: string | number): string

Formats human-readable diagnostic paths.

SKIP_VALUE
const SKIP_VALUE: symbol

Sentinel used for unresolved/dynamic extraction.

Options

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.Node original ESTree subtree.
  • expression: types.Expression | null expression form when available.

If a preserved key is encountered without onPreservedExpression, extraction throws.

Preserved Placeholder Model

When a key is listed in preservedKeys (for example children), the extractor:

  1. Calls onPreservedExpression({ path, node, expression }).
  2. Writes the callback return value into extracted data at the same path.
  3. 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):

  • PreservedSubtreeLifecycle and ExpressionRefPlaceholder are never marker types used only to anchor architecture documentation in architecture.ts.
  • They are not runtime placeholders and are not part of the extractor's public behavior contract.
  • Consumers should rely on ExtractOptions + onPreservedExpression behavior.

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.

Property Key Rules

  • Non-computed identifier keys resolve by label ({ title: 1 } -> "title").
  • Literal/computed keys go through static resolution.
  • Accepted resolved key types: string and number.
  • Rejected key results: null, undefined, bigint, symbol, and dynamic expressions.

Behavior and Limits

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, or null in object-only root adapter).

About

Safely converts ESTree expression nodes into plain JavaScript data and returns SKIP_VALUE whenever any part of the input is dynamic or unresolved

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors