Skip to main content
@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.
Slurp is 0.1.0, pre-1.0. This surface will change without a deprecation cycle until 1.0. Pin an exact version.

Install

The published package is built for the Node target: CommonJS, loading the .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 through JsValue::from_str, so every function returns a string you must JSON.parse. None of them returns an object.
The TypeScript declarations encode it. An export returns 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

Lexes and parses. 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

Both take the AST as a JSON string, which is what 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:
  1. {debug expr} nodes render as a visible <pre data-slurp-debug> element.
  2. CSS-structural characters stripped from a style value are reported.
  3. An un-annotated interpolation in a JavaScript-evaluated attribute is reported.
Points 2 and 3 are advisories recorded in development mode only. In production both things still happen, silently. Given 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

Both return a flat array of errors and warnings, not the split shape the other exports use. An empty array means clean.
These two do not report the same thing, and the difference is security-relevant.validate parses. validate_full parses and runs the compiler’s compile-time security walk, which is what slurp build and slurp validate enforce.
Rules that live in the walk and not in the parser:
  • an unfiltered ${ } in a <script> body,
  • a | js slot 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.
None of those are parse errors, so a tool calling only validate tells an author their file is clean and then slurp build rejects it:
Prefer 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:
  • isMiddleware must 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 passes false, which is what the CLI and both Rust compile entry points do.
  • The security walk returns on its first violation. It is a Result in 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

Reads the 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.
This is also the fastest way to debug frontmatter, because unknown frontmatter directives are skipped silently. If a directive did nothing, this shows you what the compiler actually read.

render_section

Renders a section template with saved editor state merged over its schema defaults. An editor uses it for an instant preview of a fetch-free section, with no server round trip.
The merge is tolerant: unknown keys such as 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

parse, render, render_dev, validate, extract_schema and render_section do not run the security walk. validate_full is the only one that does.
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:
So the escaping guarantees hold in WASM. The policy rules do not.

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, and Alpine.data('slurpFetch', ...).
  • {$let} has no hydrator. Nothing reads data-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].
In practice that means the fetch and infinite-scroll bundles are not reachable from compiled output today, which is why they are published behind an 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.