npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@live-codes/grain-wasm

v0.1.0

Published

Compile and run Grain with one API, on the Grain compiler running in JavaScript

Readme

@live-codes/grain-wasm

Run Grain with one API. The compiler is JavaScript (a js_of_ocaml build of grainc), the standard library is compiled by it inside the page, and the WebAssembly it produces executes under a WASI shim in the same thread — no server compiles anything, and nothing is sent anywhere.

import { createCompiler } from '@live-codes/grain-wasm';

const compiler = await createCompiler('grain'); // Node: reads the assets in this package
const { stdout, errors, exitCode } = await compiler.run(`
module Hello

print("hello from Grain")
`);
hello from Grain

Install

npm install @live-codes/grain-wasm

The package carries its own runtime — the compiler (assets/grainc.bc.mjs) and the standard library sources (assets/stdlib/) — so in Node there is nothing else to set up, and in a browser one command puts a copy where you serve from. It has no dependencies.

Node

Node can read the packaged assets off disk, so createCompiler('grain') needs no configuration:

import { createCompiler } from '@live-codes/grain-wasm';

const compiler = await createCompiler('grain');
await compiler.run('module M\n\nprint("hi")', 'stdin for the program');
compiler.dispose();

Browser

Host it in a worker. Compiling is synchronous — grainc does not yield, and the program it produces runs to completion in one call — so a program that never returns blocks its thread until the host terminates the worker. A worker is what makes a runaway program killable.

A page cannot read files out of node_modules, so publish the assets and point baseUrl at them:

npx grain-wasm-copy-assets public/grain
import { createCompiler } from '@live-codes/grain-wasm';

const compiler = await createCompiler('grain', {
	baseUrl: new URL('/grain/', location.href),
	onProgress: (fraction) => console.log(`${Math.round(fraction * 100)}%`)
});
const { stdout } = await compiler.run(code, stdinText);

baseUrl may be a URL, an absolute URL, or a path relative to the page; the loader fetches grainc.bc.mjs and stdlib/ from it, so a directory that works for the page also works as a CDN path. It must send Access-Control-Allow-Origin if cross-origin. grainc.bc.mjs is 19 MB — serve it compressed and let the browser cache it as a module; it is fetched once per baseUrl.

Once published, the compiler module and the standard library are loaded once and shared by every compiler created against the same baseUrl, so a host that makes a compiler per run still only pays for the load once.

Which worker

With a bundler, import the module entry in a module worker:

// worker.module.js
import { createCompiler } from '@live-codes/grain-wasm';

const compiler = await createCompiler('grain', { baseUrl });

If the worker cannot use ES modules — LiveCodes builds its language workers from a content string — importScripts() the IIFE build instead:

// worker.js — a classic worker: no { type: 'module' }, no imports
importScripts('grain-wasm.global.js'); // from @live-codes/grain-wasm/iife

const compiler = await self.grainWasm.createCompiler('grain', { baseUrl });

self.grainWasm is the same API as the module entry (createCompiler and LANGUAGE_IDS). The bundle is ~15 KB, committed in dist/, built from the browser entry so it carries no Node code, and pulls in no dependencies — the compiler and the standard library are fetched at run time from baseUrl. example/ runs both entries — the module entry in a module worker, the IIFE in a classic worker — and checks that they agree.

The classic-worker path loads grainc.bc.mjs with a dynamic import(), which current browsers support in classic workers; that is what makes the IIFE genuinely usable without module support, rather than just a smaller module entry.

API

createCompiler(language = 'grain', options)

Returns a promise for a compiler. The compiler module and the standard library are loaded here, so a bad URL or an unreachable server fails at this point rather than at the first run(). language is grain (gr is accepted).

| Option | Description | | --- | --- | | baseUrl | Where grainc.bc.mjs and stdlib/ are served from. Required outside Node. | | onProgress | (fraction) => void while the standard library downloads, 0 to 1. |

The returned object:

| Member | Description | | --- | --- | | language | The resolved language id, always grain. | | fileName | The name the compiler reports sources under (input.gr). | | run(code, input?) | Compile and run. input is stdin, string or Uint8Array. | | dispose() | Release this compiler's hold on the shared runtime. Further runs throw. |

There is no warm(): the expensive part of a first run is loading the compiler and seeding the standard library, which createCompiler already does.

run() result

| Field | Description | | --- | --- | | stdout | What the program wrote to fd 1. | | stderr | What it wrote to fd 2. | | output | stdout and stderr in the order the program wrote them. | | errors | The compiler's diagnostics, one string per line. Empty on a clean compile. | | exitCode | The program's exit status, or null if it never ran. | | wasmBytes | Size of the compiled module. | | compileMs | Time spent compiling. | | runMs | Time spent running, or null if the program never ran. |

A failed compile does not throw: it comes back with exitCode: null and errors filled in — the same shape @live-codes/clang-wasm uses, so errors.length is a reliable failure test. Only a load failure, a disposed compiler, or a bad argument throws.

LANGUAGE_IDS

['grain']. Grain is one language and one compiler here: grainc always targets WebAssembly and always links the standard library, so there is no target or standard to choose between.

What works

Verified in Node and in Chromium; npm test compiles and runs all of it (13 tests).

  • Compiling and running client-side — source → WebAssembly → execution, no server involved.
  • The standard library, compiled from source in the browser like any other code: List, Bytes, Wasi.File and the rest of what the pervasives prelude pulls in.
  • Recursion, loops, enums and pattern matching.
  • stdin — a program reads the text you pass in on fd 0 (File.fdRead), and a repeat run reads it again from the start.
  • Diagnostics with line/character ranges, e.g. Error on line 4, characters 8-13:.
  • Exit codes — a program that exits cleanly reports 0.

Measured on this machine: the first compile of a program is ~5–10 s (Node), and Chromium similar; most of that is the compiler walking the standard library the first time. A hello-world module is ~220 KB of WebAssembly.

Limitations

  • Output is batched, not streamed. The program runs to completion inside one call, so stdout arrives when it finishes. For a program that prints forever, nothing appears until it is killed. This is the main gap against LiveCodes' other wasm languages.
  • One source string. The standard library is on the virtual filesystem, but a program cannot yet import extra modules or files it brings with it.
  • No argv, no environment, no real filesystem. A program gets stdin and the standard library and nothing else; filesystem calls outside the seeded paths fail.
  • A compile takes a few seconds. Fine for a Run button; too slow to drive from a keystroke debounce.
  • Pinned to Grain 0.6.0. The compiler build and the standard library version must move together; there is no version negotiation. See THIRD-PARTY-NOTICES.md.
  • It installs globalThis.require. The compiler resolves a bare require("fs") against the global scope (see below), so this package defines globalThis.require while loaded, chained to whatever was there before. It does not touch globalThis.process.

Things worth knowing

Each of these cost real debugging time and shapes the code. They were established by running the compiler, not by reading its docs.

1. The compiler is a Node program that thinks it is under Node. grainc.bc.mjs is js_of_ocaml output whose Unix runtime does require("fs") / require("constants") and inspects a process. It must believe it is Node (process.versions.node, process.platform === "posix", process.cwd() → "/") or path handling silently changes. src/engine.js supplies all of that.

2. It needs an fs — and only a small one. src/fs.js is a ~300-line in-memory filesystem covering exactly the synchronous Node surface the bundle reaches for, found by running it against a logged proxy:

openSync closeSync readSync writeSync ftruncateSync fstatSync statSync lstatSync
readdirSync existsSync mkdirSync unlinkSync rmdirSync renameSync readlinkSync
symlinkSync opendirSync    (+ a constants table)

Upstream the same role is played by memfs, which is CommonJS-only; a hand-written fs is why this package needs no bundler and no dependencies.

3. It does not need Buffer — but a WASI runtime would have. The only Buffer in the compiler bundle is Grain's own Stdlib.Buffer. Choosing to hand-roll the WASI shim (src/wasi.js) rather than depending on @wasmer/wasi also removed that runtime's Buffer.from(base64) need, so this package ships no Buffer polyfill at all.

4. grainc(d) takes the global object as a parameter. It reads d.process, so passing a sandbox avoids clobbering the host's real process (which would be fatal under Node) — but the bare require(...) inside the bundle resolves against the real global scope, so that one value has to live on globalThis. The runner stashes the active filesystem for the duration of the call, so require("fs") can answer per compile.

5. Diagnostics live on fd 2 and end with a sentinel. A failed compile writes File "input.gr", line N, characters A-B:\nError: … to fd 2 and then aborts by throwing a js_of_ocaml error. The engine catches the throw and returns everything before the literal grain: internal error, rewriting the file prefix to Error on.

6. Tail calls are feature-detected, not assumed. The compiler can emit return_call and memory.copy; src/features.js probes for both with WebAssembly.validate and passes --no-wasm-tail-call / --no-bulk-memory when they are missing. Tail calls are not universal (Safari before 18), so emitting them unconditionally would produce modules that fail to compile on real browsers.

LiveCodes integration

  • Create one compiler per sandbox worker, lazily on the first Grain run, and reuse it: compiles are cheap once the compiler module and stdlib are loaded.
  • Hosting the assets is a copy and a URL. grain-wasm-copy-assets publishes grainc.bc.mjs and stdlib/; pass the directory as baseUrl. Serve grainc.bc.mjs gzipped and cacheable — it is 19 MB.
  • Keep one compiler per worker. The compiled compiler module and the standard library are shared by every compiler created against the same baseUrl.
  • run() collects output and returns it rather than streaming it; a runaway program can only be stopped by terminating the worker.
  • The compiler always reports the source as input.gr, so diagnostics carry input.gr(<line>). A host with its own file name should map that name when showing errors.

Development

npm install --include=dev     # note: `npm config get omit` is `dev` on some machines
npm test                      # node --test — real compiles and runs, no mocks
npm run build:iife            # rewrites dist/grain-wasm.global.js (committed)
npm run asset-receipts        # after changing anything under assets/

dist/ is committed because a classic worker can only importScripts() a URL, and a consumer should not need a bundler to get one. bin/copy-assets.mjs verifies the assets against src/asset-receipts.js before copying, so a stale pairing fails there rather than on someone's page.

Licence

MIT for this package — see LICENSE. The vendored compiler is Grain under LGPL-3.0, and the standard library comes from @grain/stdlib (MIT declared, LGPL-3.0 in the Grain repository) — read THIRD-PARTY-NOTICES.md before redistributing them.