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
Members
(inner, constant) BLOCK_NODES :Set:.<string:>
Node types that introduce a block scope.
Type:
- Source:
(inner, constant) FUNCTION_NODES :Set:.<string:>
Node types that introduce a function scope.
Type:
- 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 |
- 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: