The noyalib-wasm Manual
noyalib for JavaScript: parse, serialise, and lossless YAML editing through wasm-bindgen. noyalib-wasm is one of the six crates of the noyalib family and
releases in lockstep with the core library at the identical =0.0.X.
This manual is the rendered form of the Markdown in the repository’s
docs/
directory; the repository copy is canonical. The family manual for the
core library, with the user guide, migrations, and policies, is at
https://sebastienrousseau.github.io/noyalib/manual/.
Pages
Install, quick start, and the full surface are in the README. The CHANGELOG is the complete release record.
Bundling noyalib-wasm
Wiring noyalib-wasm into the major JavaScript bundler / framework
toolchains. Each section is a known-working setup; copy the
relevant snippet, install the package, ship.
For the JavaScript API surface itself see js-api.md.
Quick reference
| Toolchain | Default just works? | Special config? |
|---|---|---|
Vite (@vitejs/plugin-react, etc.) | yes | top-level await + vite-plugin-wasm |
| Webpack 5 | yes | experiments.asyncWebAssembly |
| esbuild | yes | --loader:.wasm=binary |
| Rollup | yes | @rollup/plugin-wasm |
| Next.js (App Router 13+) | yes | enable WASM in next.config.js |
| Remix | yes | uses Vite under the hood |
| SvelteKit | yes | uses Vite under the hood |
| Astro | yes | uses Vite under the hood |
| Node.js (raw) | yes | dynamic import or --experimental-wasm-modules |
| Deno | yes | await import |
| Bun | yes | native WASM support |
| Cloudflare Workers | yes | binding via wrangler.toml |
| Browser ESM via CDN | yes | Skypack / esm.sh |
Vite
vite.config.ts:
import { defineConfig } from "vite";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
export default defineConfig({
plugins: [wasm(), topLevelAwait()],
});
Then in your code:
import init, { WasmDocument } from "noyalib-wasm";
await init();
const doc = new WasmDocument("name: noyalib\n");
Webpack 5
webpack.config.js:
module.exports = {
experiments: {
asyncWebAssembly: true,
},
};
If you’re targeting older browsers and need the synchronous
loader, switch to syncWebAssembly: true and import via the
web target build (specify ?init query when using the package).
esbuild
build.mjs:
import * as esbuild from "esbuild";
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
outfile: "dist/bundle.js",
format: "esm",
loader: { ".wasm": "binary" },
});
Rollup
rollup.config.js:
import wasm from "@rollup/plugin-wasm";
import resolve from "@rollup/plugin-node-resolve";
export default {
input: "src/index.ts",
output: { file: "dist/bundle.js", format: "esm" },
plugins: [
resolve(),
wasm({ targetEnv: "auto-inline" }),
],
};
Next.js (App Router)
next.config.js:
/** @type {import('next').NextConfig} */
module.exports = {
webpack: (config, { isServer }) => {
config.experiments = {
...config.experiments,
asyncWebAssembly: true,
layers: true,
};
return config;
},
};
Then in a server component or route handler:
// app/api/parse/route.ts
import { NextResponse } from "next/server";
import init, { parse } from "noyalib-wasm";
let ready: Promise<void> | null = null;
function ensureInit() {
if (!ready) ready = init().then(() => undefined);
return ready;
}
export async function POST(req: Request) {
await ensureInit();
const yaml = await req.text();
return NextResponse.json(parse(yaml));
}
Node.js (raw, no bundler)
// ESM: works in Node 20+ with --experimental-wasm-modules
import init, { parse } from "noyalib-wasm";
await init();
console.log(parse("name: noyalib\nport: 8080\n"));
For CommonJS, use the nodejs target build:
const { parse } = require("noyalib-wasm/nodejs");
console.log(parse("name: noyalib\n"));
Deno
import init, { WasmDocument } from "npm:noyalib-wasm";
await init();
const doc = new WasmDocument("name: noyalib\n");
console.log(doc.toString());
Bun
import init, { parse } from "noyalib-wasm";
await init();
console.log(parse("port: 8080\n"));
Bun’s native WASM loader picks up the package without config.
Cloudflare Workers
wrangler.toml:
name = "yaml-edge"
main = "src/index.ts"
compatibility_date = "2026-05-06"
# noyalib-wasm bundles its own .wasm asset; wrangler picks it up.
src/index.ts:
import init, { parse } from "noyalib-wasm/web";
let ready: Promise<void> | null = null;
export default {
async fetch(req: Request): Promise<Response> {
if (!ready) ready = init();
await ready;
const yaml = await req.text();
return Response.json(parse(yaml));
},
};
Browser ESM via CDN (no bundler)
<script type="module">
import init, { parse } from "https://esm.sh/noyalib-wasm";
await init();
console.log(parse("name: noyalib\n"));
</script>
Or via Skypack:
<script type="module">
import init, { parse } from "https://cdn.skypack.dev/noyalib-wasm";
await init();
console.log(parse("port: 8080\n"));
</script>
Build target variants
wasm-pack build produces three target variants; the package
ships all three behind one entry point:
| Target | Use when |
|---|---|
bundler (default) | Vite, Webpack, Rollup, esbuild — the bundler handles the WASM |
web | Direct browser <script type="module"> use |
nodejs | Node.js without ESM (require()) |
Modern bundlers and runtimes pick the right variant via the
package’s exports field. Only override (noyalib-wasm/web,
noyalib-wasm/nodejs) if the auto-detection fails for your
toolchain.
Bundle size
The compiled .wasm blob is roughly 180 KB uncompressed,
~70 KB brotli-compressed. The wasm-opt pass (enabled in the
release profile) shaves another ~5–10%. Tree-shaking does not
apply to WASM — the whole module loads even if you only use
parse().
For size-sensitive deployments:
- Pre-warm
init()on page load so the WASM cost is paid once and reused - Self-host the
.wasmfile from your CDN withCache-Control: immutable, max-age=31536000headers - Don’t try to dynamic-import per-call — the per-call overhead swamps the actual parse cost
Initialisation pattern
The package’s default export is an init() function. Call it
once before the first API call; subsequent calls to init() are
no-ops.
import init, { parse, stringify } from "noyalib-wasm";
// Pattern 1: top-level await (works in Vite, Bun, modern Node)
await init();
// Pattern 2: lazy initialise once
let ready: Promise<void> | null = null;
async function ensure() {
if (!ready) ready = init();
await ready;
}
await ensure();
parse("name: noyalib\n");
Troubleshooting
“WebAssembly module is not initialised”
You forgot to await init() before the first API call. Add it.
“Cannot find module ‘noyalib-wasm/web’”
Some bundlers don’t honour the package’s exports field. Either
upgrade the bundler, or import the entry point directly
(noyalib-wasm/dist/web/noyalib_wasm.js).
Bundle includes the WASM blob inline (huge bundle)
Some bundlers default to inlining .wasm as base64. Configure
the bundler to emit it as a separate asset and serve from a
static path. See the per-bundler sections above.
Bundle has a node: import error in the browser
You’re probably importing from noyalib-wasm/nodejs. Switch to
the default export or noyalib-wasm/web.
TypeScript can’t find the types
The package ships .d.ts files in the same directory as the
JS bundle. If your TypeScript can’t find them, ensure
"moduleResolution": "bundler" (or "node16" / "nodenext")
in tsconfig.json.
Related
- JS API reference — what the package exports
- Crate README — install + crate-level overview
crates/noyalib-wasm/examples/— runnable browser + Node demos
JavaScript API reference
The complete JavaScript / TypeScript surface that
noyalib-wasm exposes after a wasm-pack build. Two surfaces:
the simple API (functional, parse / stringify / merge)
and the document API (WasmDocument, lossless edits with
byte-faithful round-trip).
For build tool / bundler integration see
bundling.md.
Install
npm install noyalib-wasm
The package ships ESM, CJS, and web targets in one bundle;
modern bundlers pick the right one automatically.
Simple API
parse(yaml: string): unknown
Parse YAML to a plain JS object / array / scalar. Throws on parse error.
import { parse } from "noyalib-wasm";
const cfg = parse(`
name: noyalib
port: 8080
features: [a, b, c]
`);
// cfg.name === "noyalib"
// cfg.port === 8080
// cfg.features === ["a", "b", "c"]
stringify(value: unknown): string
Serialise a JS value back to a YAML string. The output is
canonical noyalib YAML — block style for non-leaf collections,
auto-quoted scalars where ambiguity would otherwise surface.
Throws if the value contains JS-specific shapes the YAML data
model doesn’t support (functions, symbols), if it contains itself
(a cycle), if it nests arrays, objects or Maps deeper than 128
levels (the parser’s default depth limit), or if it has more than
1,000,000 elements counting shared references once per use.
import { stringify } from "noyalib-wasm";
const yaml = stringify({ name: "noyalib", port: 8080 });
// "name: noyalib\nport: 8080\n"
validateJson(yaml: string): boolean
Returns true if the YAML parses successfully and contains only
JSON-compatible values (no tags, no anchors, no non-string
mapping keys). Used for “is this YAML safe to round-trip through
JSON.stringify?” checks.
parseJson(yaml: string): unknown
Like parse, but the result is the JSON data model: every tag is
stripped recursively, so !!binary R0lG... is the string and
!local [1] is the array. This is what the YAML test suite’s expected
JSON is, and what a JSON consumer expects; parse keeps tags as
{ "!tag": value } for callers that need them. Null values are JS
null in both.
getPath(yaml: string, path: string): unknown | null
Fetch a single value from a YAML string by dotted path. Returns
null if the path doesn’t exist.
import { getPath } from "noyalib-wasm";
const host = getPath(yamlString, "server.host");
const firstItem = getPath(yamlString, "items[0].name");
merge(base: string, override: string): string
Deep-merge two YAML documents — the override values win, but both documents’ comments and structure are preserved where possible.
import { merge } from "noyalib-wasm";
const merged = merge(
"name: app\nport: 8080\n",
"port: 9090\nworkers: 4\n"
);
// "name: app\nport: 9090\nworkers: 4\n"
Document API
class WasmDocument
A YAML document with byte-faithful source preservation and
path-targeted edits. The class wraps noyalib::cst::Document
from the Rust core, so to_string() after no edits returns the
input bytes exactly. Edits rewrite only the touched span.
new WasmDocument(yaml: string): WasmDocument
Parse a YAML string into a Document. Throws on parse error.
import { WasmDocument } from "noyalib-wasm";
const doc = new WasmDocument(`
# Production config
server:
host: api.example.com # public-facing
port: 8080
`);
doc.toString(): string
Re-emit the document as a string. Byte-identical to the input if no edits were made — including comments, whitespace, and trailing newlines.
doc.get(path: string): unknown | null
Read the parsed value at a dotted path. Returns null if the
path doesn’t exist.
const port = doc.get("server.port"); // 8080
const host = doc.get("server.host"); // "api.example.com"
const missing = doc.get("server.missing"); // null
doc.getSource(path: string): string | null
Read the raw source slice for the value at a dotted path — no re-quoting, no canonicalisation. Useful when you need to know exactly what the user typed.
const raw = doc.getSource("server.host");
// "api.example.com" (the bytes from the source, including any quoting)
doc.spanAt(path: string): { start: number; end: number } | null
Return the byte range [start, end) of the value at a dotted
path within the source string. Useful for editor integrations
that need to highlight the value the cursor is over.
const span = doc.spanAt("server.host");
// { start: 38, end: 53 }
doc.set(path: string, fragment: string): void
Set the value at a dotted path using a YAML fragment string. The fragment must parse as valid YAML in the target position; the document is left unchanged on parse error. A fragment nested deeper than 128 levels is refused before it is parsed into the document.
doc.set("server.port", "9090");
doc.set("server.host", '"new-api.example.com"');
doc.set("features", "[parse, emit, validate]");
After doc.toString(), only the bytes for the changed value
have moved — comments, blank lines, indent style, sibling
entries are byte-identical.
doc.setValue(path: string, value: unknown): void
Set the value at a dotted path using a JS object. Internally
serialises the JS value through noyalib::Value and applies the
result. Equivalent to doc.set(path, stringify(value)) but
slightly more direct, with the same limits as stringify.
doc.setValue("server", {
host: "api.example.com",
port: 9090,
tls: { enabled: true }
});
doc.replaceSpan(start: number, end: number, replacement: string): void
Replace the bytes in [start, end) with replacement. The
lower-level escape hatch — used when you have a span from
spanAt and want to do something the structured API doesn’t
cover. The edited document must still parse: an edit that leaves
invalid YAML, or YAML nested deeper than 128 levels (for example a
# replaced so a commented-out run of brackets becomes structure),
throws and leaves the document unchanged.
doc.commentsAt(path: string): { before: string[]; inline: string | null }
Read the YAML comments associated with the node at path.
Returns the leading-comments array (the # ... lines
immediately above the node) and the trailing inline comment
(the # ... on the same line as the value), if any. This is
the demo that motivates the entire CST architecture — comments
survive the round-trip and are queryable.
const { before, inline } = doc.commentsAt("server.host");
// before: ["# public-facing"] (depending on layout)
// inline: " public-facing" (if there's a same-line comment)
TypeScript types
The package ships full .d.ts declarations generated from the
wasm-bindgen surface. Editor autocompletion works out of the box
in any TypeScript project.
import type { WasmDocument } from "noyalib-wasm";
function bumpVersion(yamlSource: string, newVersion: string): string {
const doc: WasmDocument = new WasmDocument(yamlSource);
doc.set("version", newVersion);
return doc.toString();
}
Errors
All functions that can fail throw a JS Error with a .message
matching the Rust error’s Display output (e.g.
"YAML parse error at line 3, column 7: …"). The
noyalib::Error variant taxonomy from
the core errors reference is
preserved in the message; structured access to the variant kind
is not currently exposed (file an issue if you need it).
When to use which API
| Use case | API |
|---|---|
| Parse YAML → JS object → done | parse() |
| JS object → YAML string | stringify() |
| Quick value lookup, no edit | getPath(yaml, path) |
| Merge two configs | merge(base, override) |
| Edit a value, preserve comments / formatting | WasmDocument.set |
| Cursor-aware editor integration | WasmDocument.spanAt + replaceSpan |
| Surface comments to the user | WasmDocument.commentsAt |
| Round-trip with byte-faithfulness | new WasmDocument(s).toString() (no edits → identity) |
Related
- Bundling — Vite, Webpack, esbuild, Next.js integration
- Crate README — install + crate-level overview
crates/noyalib-wasm/examples/— runnable JS examples (browser + Node)
JavaScript YAML comparison
| Project | YAML 1.2 | Preserves untouched source | Browser-ready | JSON Schema |
|---|---|---|---|---|
noyalib-wasm | Yes | Yes, through WasmDocument | Yes | No |
js-yaml | Configurable | No | Yes | No |
Native noyalib | Yes | Yes | Requires a Rust host | Optional feature |
noyalib-wasm prioritizes one parser across browser and native noyalib tools,
plus lossless editing from JavaScript. A JavaScript-only parser can be smaller
when that consistency and source preservation are unnecessary. Validate package
size and runtime support against the current upstream releases before choosing.
Benchmarks
Build the Node target and run the JavaScript harness so the measurement includes the actual JavaScript-to-WebAssembly boundary:
wasm-pack build --release --target nodejs
node benches/bench.js
Record the CPU, operating system, runtime version, Rust version, commit, input size, bundle mode, and command. Measure raw and compressed bundle sizes from the same generated package. CI verifies that the harness and targets work, but does not enforce shared-runner timing thresholds.
Architecture
noyalib-wasm is a thin wasm-bindgen shell over the noyalib library.
The design rule is that everything which can fail in a non-trivial way
lives in pure Rust and is reachable from cargo test on the native
target; the JavaScript-facing layer only converts values.
Two layers
src/core.rsholds the logic. Every function takes&stror native Rust types and returnsResult<T, noyalib::Error>:parse_yaml_to_value,value_to_yaml,yaml_round_trip,validate_yaml_json,yaml_get_path,merge_yaml, and theDocumenthelpersdocument_span_at,document_get_value,document_get_source, anddocument_comments_at. Native unit tests pattern-match on the error variants here without anywasm-bindgentype in scope.src/lib.rsexports the JavaScript surface with#[wasm_bindgen]: the free functionsparse,stringify,validateJson,getPath, andmerge, plusWasmDocument, the lossless editing handle wrappingnoyalib::cst::Document(new,to_string,replace_span,get,get_source,span_at,set_value,set,comments_at). Each method is aJsValueconversion around onecorefunction and mapsnoyalib::ErrortoJsError.
Data flow
JavaScript string in, core function, serde_wasm_bindgen conversion
out. Values cross the boundary as plain JavaScript objects; documents
stay on the Rust side behind WasmDocument so edits keep every
untouched byte, which is the point of the CST.
Build
wasm-pack builds the bundle; the optional wasm-opt Cargo feature
enables a build-script post-pass that re-runs Binaryen’s wasm-opt
on the produced .wasm. It pulls in no Rust dependencies and is off
by default so development builds stay fast. The README’s bundle-size
section records the measured sizes.
Testing
cargo test covers core natively; the wasm-test CI job runs the
binding layer under wasm-bindgen-test. Two libFuzzer targets in
fuzz/ exercise core with arbitrary input, and CI replays the seed
corpus on every push.
Lockstep
The crate pins noyalib at the identical =0.0.X and releases with
it (core ADR-0005). There is no behaviour of its own to version; a
change in the core’s parser or emitter is visible here unchanged.
Engineering policies
Version and dependency policy
noyalib-wasm releases in strict lockstep with noyalib. Version 0.0.58 pins
the core at exactly =0.0.58. Release work uses feat/v0.0.58; each subsequent
iteration increments exactly 0.0.1.
Minimum Rust version
The minimum supported Rust version is 1.89.0, declared in the manifest and verified by CI. A floor increase is a breaking-axis change and requires a changelog entry.
Compatibility
Exported JavaScript names, argument and return shapes, error behaviour, npm
package layout, and lossless edit guarantees are public. Generated wasm-bindgen
output is never edited directly. During 0.0.x, the patch component is the
breaking axis.
Family-wide policies live in the core
POLICIES.md.
Repository standard compliance grade
Assessment date: 2026-09-20. Source rubric:
/Users/seb/Code/REPO-STANDARD.md. This is a conservative static assessment of
checked-in evidence, not a substitute for successful CI runs.
Result
13/24 signals (54%). README template: pass. Strict candidate tier: L1.
| Category | Signals | Summary |
|---|---|---|
| Identity and README | 2/3 | Canonical structure passes; complete L3 install verification is open |
| Documentation | 2/3 | Manual, architecture, and JavaScript API docs present |
| Build and install UX | 1/3 | Cargo and wasm-pack build paths present |
| Releases and binaries | 2/3 | Registry automation present; complete L3 evidence remains open |
| Packaging | 1/3 | npm distribution present; wider tracking and reproducibility are open |
| CI quality gates | 2/3 | Matrix and coverage signals present |
| Supply chain | 2/3 | Foundation controls present; advisory-audit detection remains open |
| Community | 1/3 | Foundation governance present; L2 template and docs-lint signals are open |
The cumulative tier remains L1. Priority work is CI-verified install snippets, explicit advisory audit, distribution tracking, reproducible WASM artifacts, and audit-visible community templates and markdown lint enforcement.
Detailed README reference
noyalib-wasm
wasm-bindgen wrapper around noyalib —
pure-Rust YAML 1.2, zero unsafe, ~338 KB after
LTO. Runs in browsers, Node, Cloudflare Workers, Deno, and
any other WASM-capable host.
Contents
- Install — npm, build from source
- Requirements — toolchain floor, platforms, the core pin
- Quick Start — parse, edit, validate
- Why this approach? — vs
js-yaml - Surface — exported APIs
- Bundle size — what you ship to users
- Targets — every wasm-pack flavour
- Provenance — npm + cosign
- Examples — Node + browser demos
- When not to use noyalib-wasm
- Documentation
- License
Install
npm install @sebastienrousseau/noyalib-wasm
# or
pnpm add @sebastienrousseau/noyalib-wasm
# or
yarn add @sebastienrousseau/noyalib-wasm
Or build from source against any wasm-pack target:
git clone https://github.com/sebastienrousseau/noyalib-wasm
cd noyalib-wasm
wasm-pack build --release --target bundler
Split from the monorepo since v0.0.12. Prior versions shipped from
sebastienrousseau/noyalib/crates/noyalib-wasm/. From v0.0.12 onwardnoyalib-wasmlives here as its own crate, released in strict lockstep with the parentnoyalibat the same version. See ADR-0005 for the rationale and rollback recipe.
Requirements
- Rust 1.89.0 or newer to build from source:
rust-versionin the manifest, enforced by themsrv-coreCI job on every push. - Any tier-1 platform. CI runs the tests on Linux, macOS, and Windows with the stable, beta, and nightly toolchains; stable is the gate, beta and nightly are early warning.
- The matching core. This crate pins
noyalibat the identical=0.0.Xand releases in lockstep with it; Cargo resolves that pin for you. - For the bundle:
wasm-packand a JavaScript runtime that loads WebAssembly (browsers, Node.js 18+, Deno, Bun, Cloudflare Workers).cargo testneeds neither; the pure-Rust core is tested natively.
Quick Start
import init, {
parse,
stringify,
validateJson,
getPath,
merge,
WasmDocument,
} from "@sebastienrousseau/noyalib-wasm";
await init(); // load the WASM blob
// Plain parse / stringify — like js-yaml. Mappings come back as
// plain JS Objects (not `Map`), so dotted property access works.
const obj = parse("host: api.example.com\nport: 8080\n");
console.log(obj.host); // "api.example.com"
const yaml = stringify(obj);
// Indexed read without going through `parse`.
const port = getPath("host: api.example.com\nport: 8080\n", "port"); // 8080
// JSON-compatible YAML 1.2 schema check.
validateJson("a: 1\nb: [2, 3]\n"); // true
// Lossless CST edit — comments + indentation preserved.
const doc = new WasmDocument(source);
doc.set("server.port", "9090");
fs.writeFileSync("config.yaml", doc.toString());
Why this approach?
js-yaml is the de-facto
JS YAML parser, and it’s good — but it makes two tradeoffs that
hurt for editor and tooling workloads:
-
js-yamldiscards comments by spec. It implements the YAML data model, which excludes comments. Round-tripping a document throughparse→dumpstrips every#line. noyalib’sDocumentAPI runs through a lossless CST that reproduces the source byte-for-byte; only the surgically touched span changes on aset. -
js-yamlfollows YAML 1.1 by default. That’s the “Norway problem”:country: NOparses ascountry: false, silently rewriting the country code. noyalib defaults to YAML 1.2 strict semantics; onlytrue/falseare booleans.
Custom YAML tags
parse(yaml) surfaces YAML tags as plain JS object keys:
!Color '#ff8800' deserialises into { "!Color": "#ff8800" }.
This matches the serde-bridge convention every other
serde-wasm-bindgen consumer uses (the Value::Tagged variant
serialises as a single-entry map for cross-format interop).
Round-tripping via stringify does not restore the
YAML-tag prefix — the JS object’s tag-as-key shape becomes a
quoted mapping key in the emitted YAML.
For editor / tooling workloads where the YAML-tag wire form
must survive a parse → emit cycle, use the WasmDocument
class instead. Its set / setValue are surgical edits
through the CST, so untouched tag prefixes round-trip
verbatim:
const doc = new WasmDocument("color: !Color '#ff8800'\n");
doc.set("color", "!Color '#00aaff'"); // tag survives
console.log(doc.toString()); // "color: !Color '#00aaff'\n"
Other differences worth knowing about:
- JSON Schema 2020-12 validation built in. Same engine as
the
noyavalidateCLI ships. - Pure-Rust, zero
unsafe. Every byte of the parser, scanner, formatter, and CST is checked at compile time by the workspace#![forbid(unsafe_code)]lint. - ~338 KB bundle. That’s roughly the same size as
js-yamlminified + gzipped, with the lossless-CST surface and YAML 1.2 semantics baked in.
Surface
All exports are camelCase, matching JS conventions.
Free functions
| Export | What it does |
|---|---|
parse(yaml: string): any | Parse a YAML document into a JS value. Mappings become plain Objects; sequences become Arrays; scalars become numbers / strings / booleans / null. Mirrors js-yaml’s load. |
stringify(value: any): string | Serialise a JS value back to YAML. |
validateJson(yaml: string): boolean | Validate that the document conforms to the YAML 1.2 JSON-compatible schema (only types JSON allows: null / bool / number / string / array / object). Returns true / false; structural JSON Schema 2020-12 validation is on the noyavalidate CLI roadmap. |
getPath(yaml: string, path: string): any | Indexed read without going through parse. Dotted paths ("server.host"); returns null if missing. |
merge(base: string, override: string): string | Deep-merge two YAML documents. Delegates to noyalib::Value::merge. |
WasmDocument class — lossless CST
Construct with new WasmDocument(yaml). Every method preserves
comments and formatting around untouched spans byte-faithfully.
| Method | What it does |
|---|---|
toString(): string | Re-emit. Byte-identical to the parsed source if no edits were made. |
get(path: string): any | Parsed value at a dotted path. Returns null if missing. |
getSource(path: string): string | null | Raw source fragment at a dotted path (no re-quoting / canonicalisation). |
set(path: string, fragment: string): void | Surgically rewrite a value at a dotted path. The fragment is a YAML-shaped string ("9090", "[1,2,3]", …). |
setValue(path: string, value: any): void | Same as set but accepts a JS value instead of a YAML fragment. |
spanAt(path: string): { start: number, end: number } | null | Byte range of the value at a dotted path. |
commentsAt(path: string): { before: string[], inline: string | null } | Comments associated with the node at a path. |
replaceSpan(start: number, end: number, replacement: string): void | Primitive byte replacement. |
Every function is async only via init() — once the WASM
blob is loaded, individual calls are synchronous.
Bundle size
| Build | Size (raw) | Size (gzip) |
|---|---|---|
Default (wasm-pack build --release --target bundler) | ~338 KB | ~140 KB |
--features wasm-opt (post-build pass) | ~280 KB | ~115 KB |
Tree-shaking-friendly — the Document API and the plain
parse / stringify API are independent modules; bundlers
drop whichever your code does not import.
For comparison: js-yaml 4.x lands around ~50 KB minified +
~12 KB gzipped, but does not provide lossless-CST or schema
validation.
Targets
wasm-pack build supports every target wasm-bindgen does:
wasm-pack build --target bundler # webpack, rollup, esbuild
wasm-pack build --target web # native ES module via <script type="module">
wasm-pack build --target nodejs # commonjs Node import
wasm-pack build --target deno # Deno-native module
wasm-pack build --target no-modules # plain global, no module loader
Cloudflare Workers and edge runtimes generally consume the
bundler target via their packaging step.
Provenance
Every release on npm carries an npm provenance attestation linking the published bundle to the GitHub Actions run that produced it. Verify via:
npm view @sebastienrousseau/noyalib-wasm provenance
GitHub Releases ship the crate archive and a CycloneDX SBOM,
each with a sigstore bundle and checksums. A standalone signed
.wasm artefact is not attached to releases; the npm package
(with its provenance attestation, above) is the distribution
channel for the compiled bundle. Verify a release artefact:
cosign verify-blob \
--certificate-identity-regexp '^https://github\.com/sebastienrousseau/noyalib-wasm/\.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
--bundle <artefact>.bundle \
<artefact>
Full cookbook: pkg/VERIFY.md.
Examples
Browser + Node demos under
examples/:
| Path | Target | What it shows |
|---|---|---|
node-stringify.js | Node | parse + stringify round-trip. |
cst-edit.js | Node | Lossless CST edit; comments + whitespace preserved. |
paths-and-spans.js | Node | getPath, merge, getSource, spanAt, replaceSpan, setValue, commentsAt. |
json-compat.js | Node | validateJson — refuse YAML that JSON cannot represent (NaN / Infinity). |
browser/index.html | Browser | Live in-page YAML editor with a parsed-JSON pane. |
benches/bench.js | Node | Throughput measured across the real JS↔wasm boundary. |
The wasm build ships no JSON Schema engine.
validateJsononly checks JSON round-trip safety. For JSON Schema 2020-12 usenoyavalidate --schema, or thenoyalibcrate’svalidate-schemafeature.
# Node:
wasm-pack build --release --target nodejs crates/noyalib-wasm
node crates/noyalib-wasm/examples/cst-edit.js
node crates/noyalib-wasm/examples/json-compat.js
# Browser:
wasm-pack build --release --target web crates/noyalib-wasm
cd crates/noyalib-wasm/examples/browser
python3 -m http.server # or any static-file server
When not to use noyalib-wasm
- You only ever consume YAML in Node and don’t care about
comment-preserving edits or YAML 1.2 strictness.
js-yamlis smaller (~50 KB minified) and the de-facto standard; reach for it first. - You need a streaming parser for multi-GB documents. The WASM bindings always read the full document into memory. For TB-scale streaming workloads, drive the noyalib library directly from a Rust process and pipe results out.
Compatibility
MSRV: Rust 1.89.0 stable, the floor shared across the noyalib family and the
maintainer’s other Rust crates (noyalib ADR-0014). No current
dependency requires it. The whole lockstep set, including the core
noyalib library, shares this floor. CI verifies the floor on every PR via
the Per-crate MSRV workflow job. The bump policy lives in
docs/POLICIES.md.
Tier-1 WASM targets (CI-verified each PR via
wasm-pack test --node): wasm32-unknown-unknown produced
under every wasm-pack mode — bundler (Webpack, Rollup,
esbuild, Vite), web (native ES module), nodejs (CJS),
deno, no-modules. Cloudflare Workers, Deno, and Bun
consume the bundler target.
Documentation
The four entry points, identical across every repo in the family:
-
User Manual — this crate’s rendered book: its guides, architecture, and release notes; the family manual for the core library is at https://sebastienrousseau.github.io/noyalib/manual/
-
API reference — rustdoc on docs.rs
-
Developer docs — this repo’s dev entry point, pointing at the family guide
-
Ecosystem map — the six crates, the lockstep model, the scorecard
-
Engineering policies (MSRV, SemVer, security, performance, concurrency, platform support, feature flags):
docs/POLICIES.md -
Security policy:
SECURITY.md -
JS API reference:
docs/js-api.md -
Bundling (Vite, Webpack, Next.js, Cloudflare Workers, Deno, Bun):
docs/bundling.md -
npm package: https://www.npmjs.com/package/@sebastienrousseau/noyalib-wasm
-
API reference (rustdoc): https://docs.rs/noyalib-wasm
-
Workspace README: https://github.com/sebastienrousseau/noyalib#readme
Conformance
Every push runs the official yaml-test-suite
through this crate’s own JSON model, from the same vendored suite and the same
core commit as the noyalib core: 382 of 382 single-document cases (the 24
multi-document cases are outside parse, which is single-document). The core
itself passes 406 of 406 with no skip list. A two-document configuration that
uses most of YAML at once (tests/fixtures/ultra-complex/) is also parsed to
exactly its expected JSON, natively and in Node. Details and the family table:
noyalib.com/conformance.
License
Dual-licensed under Apache 2.0 or MIT, at your option.
noyalib-wasm v0.0.17 Release Notes
Lockstep release with noyalib v0.0.17 (ADR-0005: publishes =0.0.17
pinned to the core).
What changed
- No crate-content change beyond the lockstep bump.
noyalibpin=0.0.16->=0.0.17; crate version bumped in lockstep.
Repository hardening (CI/docs only)
Coverage, MSRV, CodeQL, and OpenSSF Scorecard gates plus upstream cargo-vet audit imports were added across the satellites in this cycle; branch protection now makes commit signing unskippable. No user-facing change.
Upgrading
Requires Rust 1.86.0+.
noyalib-wasm v0.0.16 Release Notes
Lockstep release with noyalib v0.0.16 (ADR-0005 strict-lockstep: the
WASM bindings publish =X.Y.Z pinned to the core). No change to the JS
API surface (WasmDocument, parse, stringify, validateJson,
getPath, merge).
What changed
noyalibpin=0.0.15→=0.0.16and the crate version bumped in lockstep.- MSRV raised 1.85.0 → 1.86.0, matching the single lockstep floor adopted in v0.0.16.
- Release workflow publish steps made idempotent for clean re-runs.
Engineering / CI (post-release, no user-facing change)
- Signed-history enforcement, upstream audit imports, and a
dependabot-vetauto-refresh workflow. - The
wasm-bindgen-testsuite now runs in CI (wasm-pack test --node) — previously the JS binding surface had a test harness but no gate. The suite was expanded from 5 to 18 tests, covering every export (WasmDocument::{new, toString, get, getSource, spanAt, setValue, set, replaceSpan, commentsAt}and the free functionsparse,stringify,validateJson,getPath,merge) plus their error paths. - New CI gates: the wasm-test gate above, a coverage gate on the
pure-Rust core (
src/core.rs, ~100 %; the#[wasm_bindgen]FFI layer is gated by wasm-test instead), an MSRV gate, CodeQL, and OpenSSF Scorecard.
What did not change
- The JS/TS API surface and the published npm package shape.
#![forbid(unsafe_code)]— intact.- WASM bundle size characteristics.
Upgrading
npm install @sebastienrousseau/noyalib-wasm@0.0.16