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

tgcloud-vendor-wasm

v0.2.2

Published

Pack a WebAssembly library into deployable modules for Telegram Serverless

Readme

vendor-wasm

Pack a WebAssembly library into deployable modules for Telegram Serverless (V8 isolate: no npm, no filesystem, static imports only). Compatible with @tgcloud/cli 0.2.0: everything lives under tgcloud/, project modules import each other by relative path with the .js extension.

Why

The platform deploys only .js files (tgcloud/schema.js, tgcloud/lib/, tgcloud/handlers/, tgcloud/endpoints/) — verified: a real .wasm placed in lib/ is invisible to tgcloud status. And at runtime there is no filesystem to read it from anyway. This tool embeds the binary as base64 chunks inside .js modules and generates the wiring that the isolate actually supports:

  • embedded wasmBinary (no fetch of a .wasm URL — and URL doesn't exist there anyway)
  • fully synchronous instantiation (new WebAssembly.Module + new WebAssembly.Instance), because async WebAssembly.instantiate never resolves in the isolate (hangs ~32s)
  • no atob/Buffer (a dependency-free base64 decoder is generated)
  • static imports only (import() throws Not supported)

Requirements

Node.js 18+. A project scaffolded (or npx tgcloud upgraded) for @tgcloud/cli 0.2.0 — modules under tgcloud/.

Install

npm i -g tgcloud-vendor-wasm

Quick start

Run from the project root:

npm i @sqlite.org/sqlite-wasm
vendor-wasm --from @sqlite.org/sqlite-wasm --out tgcloud/lib

Finds the .wasm, scores glue candidates, pulls relative deps transitively (including package entries), auto-vendors clean ESM bare imports, prints the equivalent manual command. Cross-package API layers still need a hand — see Limitations.

Manual mode (* globs allowed in paths — handy for hashed build files like chunk-*.mjs):

vendor-wasm --name mylib --wasm mod.wasm --loader glue.mjs \
  --extra api.mjs:api.js \
  --map "@pkg/types:./ffi.js" \
  [--out tgcloud/lib] [--split 80000] [--mode emscripten|raw] [--modules node_modules]

| Flag | Meaning | | ---- | ------- | | --from pkg | Auto-discover inside installed package (repeatable) | | --name | Target folder: lib/<name>/ (required unless discovered) | | --wasm | The .wasm binary → wasmP1..N.js + wasm.js aggregator (required) | | --loader | ESM glue factory (Emscripten MODULARIZE) → loader.js (required in emscripten mode) | | --extra src[:dest] | Extra ESM files, imports rewritten (repeatable) | | --map from:to | Rewrite a bare import specifier; to is relative (./ffi.js). The pre-0.2 lib/<name>/ffi form is still accepted and normalized (repeatable) | | --out | Output root (default tgcloud/lib) | | --split | Base64 chars per chunk (default 80000) | | --mode | emscripten (default) or raw (prints the module's imports so you can write stubs) | | --modules | node_modules root (default node_modules) | | --deflate | Deflate the wasm before base64 (u32LE len + raw-deflate, decoded in-isolate by a 9 KB vendored decoder) — ~2x smaller modules |

The tool aborts on CommonJS, on zero/ambiguous .wasm or glue matches (it lists candidates instead of guessing), warns on require() / dynamic import() / .wasm path references / a missing default export in the loader, syntax-checks every generated file and verifies the base64 roundtrip.

How discovery scores glue

Content + filename heuristics: wasmBinary / WebAssembly.* / instantiate* / default export / module|loader|glue|esm|browser|wrapper|factory score up; CJS markers, node-ish names, worker (no Worker in the isolate), test/example files score down. Ties break toward the plainer filename. Run with --loader to override the pick.

Using the output (emscripten mode)

From a handler or endpoint (relative import with .js, as CLI 0.2.0 requires):

// tgcloud/handlers/message.js
import { getModule } from '../lib/mylib/index.js';

const M = await getModule(); // singleton, sync-instantiated module

There is no directory-index resolution — import the entry by its full relative path (../lib/mylib/index.js, not ../lib/mylib). Inside the vendored folder every cross-file import is likewise relative (./loader.js, ./wasm.js). With --deflate, index.js inflates the payload first (framedToBytes); b64ToBytes stays exported for hand wiring (e.g. passing wasmBinary to a factory that accepts it).

Isolate notes: relative imports with .js only; no URL, atob, setTimeout, performance, import(); give the guest a compute budget (interrupt handler) and a memory limit (see lib/code.js in this repo for a complete example with QuickJS).

Tested libraries

Shipped presets, all verified end-to-end on the pre-0.2 runtime (bare, extensionless imports); the 0.2.0 output emits the same bytes as relative ./x.js imports — import graph, base64 roundtrip and factory wiring re-verified locally, live-isolate re-check recommended after the platform update:

  • quickjs — QuickJS engine trio (503 KB wasm, 15 files)
  • sqlite — @sqlite.org/sqlite-wasm, --from just works (869 KB, 18 files)
  • tree-sitter — release build (210 KB, 7 files); no default export, so index.js needs hand wiring (warned)
  • wasqlite — wa-sqlite sync build + MemoryVFS/API extras (558 KB, 17 files; --deflate: 365 KB payload, verified live SQL in the isolate)
  • unrar — node-unrar-js ESM chain with a wasmBinary API (208 KB, 13 files)
  • sevenzip — 7z-wasm factory (--deflate: 988 KB total, instantiates live with working FS/callMain; raw 1.65 MB doesn't fit the ~1 MB cap)
  • qoi — @jsquash/qoi encode build (14 KB wasm, 6 files, ~57 KB; the --map is load-bearing: the wrapper imports the glue by its source path, remapped to ./loader.js). Encode-only; a decode twin is the same preset with enc→dec and encode.js→decode.js. Two consumer paths, both verified: getModule() then mod.encode(pixels, w, h), or the library's own init(new WebAssembly.Module(b64ToBytes(wasmB64))) + default encode({ data, width, height }) (no fetch on either path; real qoif output)
  • jpegdec + jpegenc — @jsquash/jpeg decode/encode pair, Emscripten builds that just work with the default entry (verified live pipeline: JPEG → RGBA → center-crop + grayscale in plain JS → JPEG q80 → decode again; ffd8 throughout). ~270 KB together with --deflate (always pack this pair deflated). Consumer path (raw module calls — the wrappers' init() needs URL, which the isolate lacks): getModule(), then dec.decode(jpegBytes, false) / enc.encode(data, w, h, { ...defaultOptions, quality }) with defaultOptions imported from the packed meta.js; shim ImageData first (the glue polyfills it only under Node/CF Workers, but decode constructs one)

Also checked, no preset: esbuild-wasm, @dqbd/tiktoken and wasm-flate pack but need --mode raw + hand stub (Go / wasm-bindgen runtimes); @jsquash/png also needs raw + stub (initSync + an ImageData shim — proven in Node, just not shipped); @cf-wasm/photon packs with --deflate (976 KB, initSync hand-wire, JPEG-out only — too close to the ~1 MB cap to ship); @imagemagick/magick-wasm packs but is oversized for deploy (15 MB), as is wasm-vips (5 MB wasm + pthreads, no sync path); hash-wasm and xxhash-wasm need no packing (self-contained ESM — deploy directly). Dead ends by cause: CJS/UMD-only glue (sql.js, argon2-browser, re2-wasm, jq-web, vscode-oniguruma, @squoosh/lib, wawoff2, @bokuweb/zstd-wasm); fetch/URL/atob/WASI at init or multi-asset (pglite, rapier2d-compat, automerge, lightningcss-wasm, loro-crdt, harper.js).

Rule of thumb: Emscripten MODULARIZE with a default-exported factory packs; CJS glue aborts; Go/wasm-bindgen need raw + stub.

Limitations

  • Total module space is capped at roughly 1 MB (measured pre-0.2 via run: ~1006 KB works, ~1026 KB fails with Invalid module name). Keep packed output well under it — that is what --deflate is for (wa-sqlite: 836 → 505 KB, 7z: 2.2 MB → 988 KB and runnable; sqlite's 642 KB loader alone eats most of the budget, so it still doesn't fit).
  • API-layer files from other packages (like QuickJS's core chunk) are out of scope for discovery — the tool lists shipped-but-unincluded files so you can add them via --extra.
  • Single-threaded, non-WASI builds only: no threads, no wasi_snapshot_preview1 shims. Prefer browser/worker-flavored ESM glue over node/debug variants.
  • Debug builds score high (they contain every marker) — override with --loader if you want the release file.
  • Verified live in the isolate: wa-sqlite runs real SQL (SQLite 3.44.0) and 7z instantiates with a working FS/callMain. Async WebAssembly.instantiate hangs there (use the generated sync hook); there is no atob/URL/setTimeout/ performance/crypto, and import.meta is an empty object. Keep handler modules free of top-level await.

Contributing presets

Shipped presets: quickjs, sqlite, tree-sitter, wasqlite, unrar, sevenzip, qoi, jpegdec, jpegenc.

Got a library that needs hand-written flags? Send a PR adding presets/<name>.json:

{
  "name": "qjs",
  "wasm": "@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.wasm",
  "loader": "@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.mjs",
  "extra": [
    "quickjs-emscripten-core/dist/chunk-*.mjs:core.js",
    "@jitl/quickjs-ffi-types/dist/index.mjs:ffi.js",
    "@jitl/quickjs-wasmfile-release-sync/dist/ffi.mjs:ffiWasm.js"
  ],
  "map": ["@jitl/quickjs-ffi-types:./ffi.js"]
}

Paths are relative to --modules. Explicit flags win. Use it via --preset <name> (or a direct JSON path). Please verify the output with the tool's own checks plus a real --from/manual run before submitting.

Upgrading from vendor-wasm 0.1.x (pre-0.2 serverless)

  • Run npx tgcloud upgrade once to move schema.js/lib//handlers/ into tgcloud/.
  • Re-run the tool with --out tgcloud/lib (the new default). Generated imports changed from bare (lib/<name>/loader, extensionless) to relative (./loader.js, with extension), per the October 2026 platform update.
  • In your handlers/endpoints, import the entry relatively: import { getModule } from '../lib/<name>/index.js'.
  • Old --map targets (lib/<name>/ffi) still work — they are normalized to ./ffi.js — but prefer writing the relative form.

License

MIT — see LICENSE. Replace the copyright holder before publishing.