@bytesell/slurp-compiler-wasm is the Slurp compiler built to WebAssembly, for
hosts that are not Rust: an editor, a preview pane, a language server, a build
script, a browser.
Install
.wasm with fs.readFileSync. It works from ESM through Node’s CJS interop, and
the entry point assigns each export individually rather than spreading, so
import { parse } from '@bytesell/slurp-compiler-wasm' resolves rather than
coming back undefined.
For a browser host with a bundler, build it yourself from the repository:
build.mjs accepts any wasm-pack target (nodejs, web, bundler,
no-modules, deno). It needs
wasm-pack on PATH; the
wasm32-unknown-unknown rustup target is installed for you if it is missing.
Output lands in compiler-wasm/pkg/, which is a build artifact and is not
committed.
A
--target web build is loaded the way every wasm-pack web build is, with a
default-exported init() that you must await before calling anything. The
seven named exports below are identical either way.Everything returns a JSON string
Every export serialises throughJsValue::from_str, so every function returns
a string you must JSON.parse. None of them returns an object.
JsonString<T>, a
branded string that will not typecheck as a T, and parseJson is the one way
to cross that boundary. parseJson is a plain JSON.parse wrapper that keeps
the fact visible at the call site.
The parsed shapes (ParseResult, RenderResult, SchemaResult, SlurpError)
are exported as types too.
The exports
Plus
parseJson<T>(json), which is not a WASM export but is what you call on
every result above.
There is no compile_wasm, no parse_wasm and no get_errors_wasm, whatever an
older integration may call.
parse
ast is null only when lexing failed outright; a parse
that recovers returns an AST alongside whatever diagnostics it collected, so
check ok rather than checking ast for null.
The AST comes back as JSON because that is what render takes. Parse once,
render many times with different contexts.
render and render_dev
parse returned, so it is
JSON.stringify(parseResult.ast) on the way back in.
Malformed input comes back as an InvalidAst or InvalidContext error rather
than a thrown exception. Nothing in this package throws.
Mode differences
render is production mode. render_dev is development mode, and it differs in
three ways:
{debug expr}nodes render as a visible<pre data-slurp-debug>element.- CSS-structural characters stripped from a
stylevalue are reported. - An un-annotated interpolation in a JavaScript-evaluated attribute is reported.
style="color: ${ c }" with c = "red; background: url(x)", both modes emit
style="color: red background: urlx"; only render_dev tells you the value lost
characters.
An editor or preview pane wants render_dev. A production SSR path wants
render.
The budget diagnostics are not among the development-only pair. Loop
truncation at 1,000 items, the output-byte budget and the render-depth budget are
reported in both modes, because each one silently removes content from the
page. See Limits.
validate and validate_full
- an unfiltered
${ }in a<script>body, - a
| jsslot in JavaScript statement position, where escaping the quote characters cannot contain the value, env.SLURP_SECRET_*, in any file,request.*outside middleware,{redirect}and{next}outside middleware.
validate tells an author
their file is clean and then slurp build rejects it:
validate_full for an authoring tool, a publish gate or a CI check.
The two stay separate rather than merged so pure editor-preview hosts are
unaffected.
Two things at the call site:
-
isMiddlewaremust match the value the eventual compile will use. The rules are inverted, not merely looser:request.*and{redirect}are legal only in middleware, and most other path roots are illegal there. A caller with no way to know passesfalse, which is what the CLI and both Rustcompileentry points do. -
The security walk returns on its first violation. It is a
Resultin Rust, not an accumulator, so at most one security diagnostic is appended per call, while parse diagnostics accumulate normally. Fix the reported one and re-run to see the next.
extract_schema
section { } block out of a template’s frontmatter as editor-facing
JSON. schema is null when the template declares none, which is not an error.
render_section
bogus above are dropped and
out-of-range numbers are clamped to the schema’s min and max, rather than
rejected, so saved state survives a theme update.
It renders in production mode and it does not take a block catalog, so
@theme-targeted blocks are dropped from the output while surviving in storage.
That is a host-side concern; see
Embedding with Rust.
Security
SLURP_SECRET_* access, middleware scope and the redirect restrictions are
server-side concerns enforced by the host, and a preview is not a deploy. A
template that violates all of them parses and renders cleanly through the other
six.
Do not use a WASM render as a production render path. Use the
Rust API server-side, where compile and its relatives
run the walk on every call.
What a WASM render does still apply is the renderer’s own script-context
backstops, because those live in the renderer rather than the walk. An unfiltered
${ } in a <script> body and a false | js claim both come back as
UnsafeScriptInterpolation errors with the offending slot emitted empty:
Diagnostics
Every export reports through the same shape:file is always the literal "wasm-input", since these functions take a source
string and know no path. Substitute your own when you surface a diagnostic to a
user.
code is machine-consumable, so match on it rather than pattern-matching the
message. See Error codes.
The browser runtime is a different package
Not to be confused with this one.@bytesell/slurp-runtime is the optional
client runtime that ships to a visitor’s browser; this package is the compiler. A
page using none of {fetch}, {$let}, {try} or client navigation does not
need the runtime at all.
The core bundle measures 2,570 bytes gzipped, and Alpine is an external import
rather than something bundled into it.
The runtime’s capability and the compiler’s output do not currently meet in the
middle:
- It registers zero Alpine magics and zero Alpine directives. It touches
Alpine in exactly three places:
Alpine.start(),Alpine.initTree()after a client navigation, andAlpine.data('slurpFetch', ...). {$let}has no hydrator. Nothing readsdata-slurp-let.- Several markers the runtime looks for have no producer in the compiler:
[data-slurp-page],<meta name="slurp-route">,x-data="slurpFetch(...)"and[data-slurp-sentinel].
experimental/ prefix (@bytesell/slurp-runtime/experimental/fetch and
.../experimental/infinite) rather than from the package root. Client navigation
needs a theme that supplies [data-slurp-page] and a navigate attribute on
<body> itself.
A host can supply any of the above.
Next
Rust API
The server-side path, where the security walk runs on every compile.
Error codes
What each
code means.Limits
The budgets a render reports in both modes.
Editor setup
The VS Code extension, which is a consumer of this package.