An open API service indexing awesome lists of open source software.

https://github.com/sebastienrousseau/noyalib-wasm

WebAssembly (wasm-bindgen) bindings for the noyalib YAML library — browser-ready parse / serialise / lossless edit.
https://github.com/sebastienrousseau/noyalib-wasm

browser noyalib parser rust serde serialization wasm wasm-bindgen webassembly yaml zero-unsafe

Last synced: about 13 hours ago
JSON representation

WebAssembly (wasm-bindgen) bindings for the noyalib YAML library — browser-ready parse / serialise / lossless edit.

Awesome Lists containing this project

README

          


Noyalib logo

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.


Build
npm
Docs.rs
Bundle size
OpenSSF Scorecard

---

## Contents

- [Install](#install) — npm, build from source
- [Quick Start](#quick-start) — parse, edit, validate
- [Why this approach?](#why-this-approach) — vs `js-yaml`
- [Surface](#surface) — exported APIs
- [Bundle size](#bundle-size) — what you ship to users
- [Targets](#targets) — every wasm-pack flavour
- [Provenance](#provenance) — npm + cosign
- [Examples](#examples) — Node + browser demos
- [When not to use noyalib-wasm](#when-not-to-use-noyalib-wasm)
- [Documentation](#documentation)
- [License](#license)

---

## Install

```sh
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:

```sh
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 onward `noyalib-wasm` lives here as its own crate,
> released in strict lockstep with the parent
> [`noyalib`](https://github.com/sebastienrousseau/noyalib) at
> the same version. See
> [ADR-0005](https://github.com/sebastienrousseau/noyalib/blob/main/doc/adr/0005-workspace-split.md)
> for the rationale and rollback recipe.

---

## Quick Start

```js
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`](https://github.com/nodeca/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:

1. **`js-yaml` discards comments by spec.** It implements the
YAML data model, which excludes comments. Round-tripping a
document through `parse` → `dump` strips every `#` line.
noyalib's `Document` API runs through a lossless CST that
reproduces the source byte-for-byte; only the surgically
touched span changes on a `set`.

2. **`js-yaml` follows YAML 1.1 by default.** That's the
"Norway problem": `country: NO` parses as `country: false`,
silently rewriting the country code. noyalib defaults to
YAML 1.2 strict semantics; only `true` / `false` are
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:

```js
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 `noyavalidate` CLI 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-yaml`
minified + 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:

```sh
wasm-pack build --target bundler # webpack, rollup, esbuild
wasm-pack build --target web # native ES module via
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](https://docs.npmjs.com/generating-provenance-statements)
linking the published bundle to the GitHub Actions run that
produced it. Verify via:

```sh
npm view @sebastienrousseau/noyalib-wasm provenance
```

The underlying `.wasm` is also signed with cosign keyless
alongside every release; the verify command is identical to
the source crate's:

```sh
cosign verify-blob \
--certificate-identity-regexp 'https://github.com/sebastienrousseau/noyalib/' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
--certificate noyalib_wasm_bg.wasm.pem \
--signature noyalib_wasm_bg.wasm.sig \
noyalib_wasm_bg.wasm
```

Full cookbook: [`pkg/VERIFY.md`](https://github.com/sebastienrousseau/noyalib/blob/main/pkg/VERIFY.md).

---

## Examples

Browser + Node demos under
[`crates/noyalib-wasm/examples/`](examples/):

| Path | Target | What it shows |
|---|---|---|
| [`node-stringify.js`](examples/node-stringify.js) | Node | `parse` + `stringify` round-trip. |
| [`cst-edit.js`](examples/cst-edit.js) | Node | Lossless CST edit; comments + whitespace preserved. |
| [`schema-validate.js`](examples/schema-validate.js) | Node | JSON Schema 2020-12 validation, good and bad cases. |
| [`browser/index.html`](examples/browser/index.html) | Browser | Live in-page YAML editor with a parsed-JSON pane. |

```bash
# Node:
wasm-pack build --release --target nodejs crates/noyalib-wasm
node crates/noyalib-wasm/examples/cst-edit.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-yaml`
is 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.85.0** stable. The `wasm-bindgen` 0.2 ecosystem
floors the toolchain at 1.85; the core `noyalib` library
itself stays at 1.75. CI verifies the floor on every PR via
the `Per-crate MSRV` workflow job. The bump policy lives in
[`doc/POLICIES.md`](https://github.com/sebastienrousseau/noyalib/blob/main/doc/POLICIES.md#1-msrv-minimum-supported-rust-version).

**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

- **Engineering policies** (MSRV, SemVer, security, performance, concurrency, platform support, feature flags):
[`doc/POLICIES.md`](https://github.com/sebastienrousseau/noyalib/blob/main/doc/POLICIES.md)
- **Security policy**:
[`SECURITY.md`](https://github.com/sebastienrousseau/noyalib/blob/main/SECURITY.md)
- **JS API reference**:
[`doc/js-api.md`](https://github.com/sebastienrousseau/noyalib/blob/main/crates/noyalib-wasm/doc/js-api.md)
- **Bundling (Vite, Webpack, Next.js, Cloudflare Workers, Deno, Bun)**:
[`doc/bundling.md`](https://github.com/sebastienrousseau/noyalib/blob/main/crates/noyalib-wasm/doc/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>

---

## License

Dual-licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
or [MIT](https://opensource.org/licenses/MIT), at your option.