eval/inject improved; testing won t fail on non existing javascript engine

This commit is contained in:
2026-05-28 00:07:21 +02:00
parent 8fe47a7ed4
commit e89ab88670
6 changed files with 185 additions and 66 deletions
+57 -34
View File
@@ -22,10 +22,13 @@
@bold{js-maker} is a small, syntax-driven JavaScript generator for writing a
practical JavaScript subset in Racket notation. It provides two macros,
@racket[js] and @racket[js/expression]. Both macros run at expansion time and
return JavaScript source code as a string. The generated JavaScript can then be
embedded in a page, written to a file, tested with Node or another JavaScript
engine, or used as part of a larger code-generation workflow.
@racket[js] and @racket[js/expression]. In the ordinary case the macros
expand to a string containing JavaScript source code. When the source contains
@racket[(inject racket-expr)] or its historical alias @racket[(eval racket-expr)],
the macros instead expand to a Racket expression that computes the JavaScript
source string at run time. The generated JavaScript can then be embedded in a
page, written to a file, tested with Node or another JavaScript engine, or used
as part of a larger code-generation workflow.
The package is deliberately not a full Racket compiler. It recognizes a
well-defined set of Racket-like forms and maps them to JavaScript while trying
@@ -60,9 +63,12 @@ Inside function bodies, the last expression is returned automatically unless it
is already a statement form such as @racket[return], @racket[define],
@racket[set!], @racket[while], or @racket[for]. At the top level of a
@racket[js] form, js-maker also returns the value of a final value-producing
form, such as @racket[let], @racket[begin], @racket[if], or
@racket[with-handlers]. This makes @racket[js] output suitable as the body of a
WebView @tt{runJavaScript} wrapper function.
form, such as @racket[let], @racket[begin], or @racket[if]. This makes
@racket[js] output suitable as the body of a WebView @tt{runJavaScript}
wrapper function. A top-level @racket[with-handlers] is treated as a statement
so that a catch handler used for side effects does not prematurely return from
the surrounding wrapper. Use @racket[js/expression] when the value of a
@racket[with-handlers] form itself is needed.
For example:
@@ -71,7 +77,7 @@ For example:
(displayln
(js
(let ([el (send document getElementById 'test)])
(set! (js-dot el innerHTML) (eval html))
(set! (js-dot el innerHTML) (inject html))
#t))))
]
@@ -107,12 +113,14 @@ self-call.
@section{Mental model}
The generator is best understood as a source-to-source translator over syntax.
The input is converted to datum form and matched against the supported surface
Most input is converted to datum form and matched against the supported surface
language. The Racket code is normally not evaluated. The deliberate exception
is @racket[(eval racket-expr)], which interpolates a Racket value into the
JavaScript source text. The expression is evaluated in the lexical context of
the @racket[js] or @racket[js/expression] use and its value is emitted as a
JavaScript literal. This has a few important consequences:
is @racket[(inject racket-expr)], and the compatible older spelling
@racket[(eval racket-expr)]. These forms mark a Racket expression whose value
must be serialized as a JavaScript literal while the generated source string is
being constructed. The expression is evaluated in the lexical context of the
@racket[js] or @racket[js/expression] use. This has a few important
consequences:
@compact-items[
@item{Only forms known to js-maker are translated specially. Unknown calls are
@@ -128,19 +136,24 @@ JavaScript literal. This has a few important consequences:
helper functions/IIFEs to preserve semantics.}
]
@subsection{Racket value interpolation with eval}
@subsection{Racket value interpolation with inject}
The form @racket[(eval racket-expr)] is an interpolation escape hatch inherited
from the original transformer. It evaluates @racket[racket-expr] as Racket in
the use-site lexical context and then splices the resulting value into the
JavaScript source as a literal. This makes surrounding Racket bindings visible
to the interpolation expression.
The form @racket[(inject racket-expr)] is the preferred interpolation escape
hatch. It evaluates @racket[racket-expr] as Racket in the use-site lexical
context and then splices the resulting value into the JavaScript source as a
literal. This makes surrounding Racket bindings visible to the interpolation
expression.
The older spelling @racket[(eval racket-expr)] is still accepted as an alias for
compatibility with existing code. New code should prefer @racket[inject],
because the form is not JavaScript @tt{eval} and does not evaluate JavaScript
source text.
@(rkt+js
#<<RKT
(let ([x 10]
[y 20])
(js (let ([a (eval (* x y))])
(js (let ([a (inject (* x y))])
(return (* a a)))))
RKT
#<<JS
@@ -153,20 +166,26 @@ JS
@(rkt+js
#<<RKT
(js/expression (array (eval (+ 1 2))
(eval (string-append "a" "b"))))
(js/expression (array (inject (+ 1 2))
(inject (string-append "a" "b"))))
RKT
#<<JS
[3, "ab"]
JS
)
This is not JavaScript @tt{eval}. To call JavaScript @tt{eval}, call the
JavaScript function explicitly, for example @racket[(send window eval "1 + 2")].
Racket-side @racket[eval] is best used for constants, generated literal data,
and small configuration values that are known while the JavaScript source is
being constructed. It should not be used for run-time browser state, DOM access,
or user input.
Neither @racket[inject] nor its alias @racket[eval] is JavaScript @tt{eval}.
To call JavaScript @tt{eval}, call the JavaScript function explicitly, for
example @racket[(send window eval "1 + 2")]. Racket-side interpolation is best
used for constants, generated literal data, and small configuration values that
are known while the JavaScript source is being constructed. It should not be
used for run-time browser state, DOM access, or untrusted user input.
The name @racket[inject] was chosen over a name such as @racket[subst] because
it is not raw textual substitution. js-maker serializes the Racket value to a
JavaScript literal, including string escaping, booleans, numbers, lists, vectors,
hashes, symbols, keywords, characters, @racket[#f], @racket[#t], @racket[null],
and @racket[(void)].
@section{Supported expression and statement forms}
@@ -184,7 +203,9 @@ both, depending on context:
@item{@racket[when], @racket[unless], @racket[while], @racket[for],
@racket[for/list], @racket[for/vector], and @racket[for/fold].}
@item{@racket[with-handlers] for the generic @racket[exn?] case.}
@item{@racket[(eval racket-expr)] for Racket-side value interpolation into JavaScript literals.}
@item{@racket[(inject racket-expr)] for Racket-side value interpolation into
JavaScript literals; @racket[(eval racket-expr)] is accepted as a
compatibility alias.}
@item{Interop forms such as @racket[send], @racket[new], @racket[js-ref],
@racket[js-dot], @racket[set-prop!], @racket[delete-prop!],
@racket[js-delete], @racket[array], @racket[object],
@@ -728,11 +749,13 @@ with a JavaScript executor. The executor module searches for engines such as
Node, Deno, Bun, QuickJS, V8 @tt{d8}, JavaScriptCore @tt{jsc}, SpiderMonkey
@tt{js}, and an optional Chromium fallback. Node is the preferred default.
If no JavaScript engine is available, tests are generated but execution is
skipped with clear warnings and a successful exit status. This is intentional
so package tests do not fail merely because a JavaScript runtime is missing.
Set @tt{JSMAKER_REQUIRE_ENGINE=1} or @tt{JSMAKER_REQUIRE_NODE=1} to make a
missing engine a hard failure.
If no JavaScript engine is available, tests are generated and the framework
uses an explicit @tt{non-failing-javascript-stub}. The stub prints notes to
stdout, does not execute the generated JavaScript, and exits successfully.
This is intentional so package tests do not fail merely because a JavaScript
runtime is missing, especially in package-server environments. Set
@tt{JSMAKER_REQUIRE_ENGINE=1} or @tt{JSMAKER_REQUIRE_NODE=1} to make a missing
engine a hard failure.
Useful commands are: