Module: lib/compiler/codegen/actions

Compiles an <action> or <resource> body into a real function.

What this replaces

An action body is arbitrary JavaScript, so it could not take the expression generator's path — that implements the expression language templates use, and an action legitimately contains if, for, try, const, await and everything else. Anything with real statement syntax therefore fell through to:

new Function(`with(this) { ${source} }`)

Two things followed from that line. The application needed 'unsafe-eval', which is what made "Avenx runs under a strict CSP" true of templates and false of most non-trivial actions. And the body ran against AvenxSandbox, the source-text sandbox the expression evaluator was written to replace.

What happens instead

The body is parsed at build time and the only thing rewritten is how free identifiers resolve. Everything else — control flow, declarations, destructuring, template literals, regular expressions, await — is copied through verbatim, because the engine that will run it understands it perfectly well and this compiler does not need to.

if (!text) { return; }              if (!text) { return; }
items.push({ label: text });   ->   axGet($s,"items").push({ label: axGet($s,"text") });
text = '';                          $s.text = '';

A read becomes axGet, which resolves scope-first and routes an allowed global through the tracer's substitution point, so Date.now() in an action is still recorded and replayed. A write becomes a plain property assignment on the scope, which is exactly what the interpreter's writeIdentifier did and what routes the value into reactive state.

What this changes about security, precisely

Nothing gets weaker, and one thing gets stronger.

Action bodies never had the expression evaluator's guarantees: they went to new Function with a source-text sandbox around them, and a source-text check for constructor falls to string concatenation. That is unchanged here — a body's member accesses are the developer's own JavaScript, as they always were.

What is new is that naming a restricted global (window, fetch, localStorage) or writing a forbidden static key (__proto__, constructor, prototype) is refused at build time, with the file and the offending name, rather than at the moment that branch happens to run. And there is no eval, no new Function and no with on this path at all.

Why a real parser, and why only at build time

Everything else in this compiler reads source with a purpose-built scanner, and for template expressions that is right: the language is small, closed and defined by Avenx. An action body is not — it is whatever JavaScript the developer wrote, including every feature the language gains next year. A hand-written parser would have permanent gaps, and each gap is either a silent miscompile or a body pushed back onto new Function, which is the thing being removed.

So this uses acorn: MIT, no dependencies of its own, and the parser most of the ecosystem's tooling already trusts. It is a dependency of the compiler. Nothing under lib/core imports it, so the browser runtime keeps its zero dependencies, which was always the property worth protecting.

Source:

Classes

Scope

Members

(inner, constant) BLOCK_NODES :Set:.<string:>

Node types that introduce a block scope.

Type:
  • Set:.<string:>
Source:

(inner, constant) FUNCTION_NODES :Set:.<string:>

Node types that introduce a function scope.

Type:
  • Set:.<string:>
Source:

Methods

(static) compileActionToSource(source) → {string}

Compiles an action or resource body into a scope-taking function.

Parameters:
Name Type Description
source string

The body as written.

Source:
Throws:

When the body cannot be compiled.

Type
ExpressionCodegenError
Returns:

JavaScript source for the function.

Type
string

(static) tryCompileAction(source) → {Object|Object}

Compiles an action body, returning the reason rather than throwing.

Parameters:
Name Type Description
source string

The body as written.

Source:
Returns:

The generated source, or the reason.

Type
Object | Object

(inner) analyse(program, source) → {Object}

Walks a parsed body, recording how each free identifier is used.

Parameters:
Name Type Description
program object

The acorn Program node.

source string

The original source, for error messages.

Source:
Returns:

The identifier references to rewrite.

Type
Object

(inner) applyEdits(source, edits) → {string}

Applies a set of source edits, right to left.

Parameters:
Name Type Description
source string

The original source.

edits Array:.<{start:: number:, end:: number:, text:: string:}>

The edits.

Source:
Returns:

The rewritten source.

Type
string

(inner) declaredByPattern(node, into)

Collects the names a binding pattern declares.

Parameters:
Name Type Description
node object

A pattern node.

into Set:.<string:>

Where to collect them.

Source:

(inner) hoist(body, scope, functionLevel)

Hoists the declarations a scope's statements introduce.

Run on entering a scope rather than as the walk reaches each statement, so a reference that appears before its declaration still resolves to the local — count in count; let count = 1; is a local in a temporal dead zone, not a free name to be routed to component state.

Parameters:
Name Type Description
body Array:.<object:>

Statements in the scope.

scope Scope

The scope to populate.

functionLevel boolean

Whether to also take var from nested blocks.

Source:

(inner) hoistVarsDeep(node, scope)

Finds var and function declarations nested inside blocks.

Parameters:
Name Type Description
node object

The statement to search.

scope Scope

The function scope to populate.

Source: