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

@levyer/vite-wasm

v0.0.2

Published

Vite plugin that compiles server entries into WebAssembly components

Readme

@levyer/vite-wasm

Compiles a Vite server entry into a WASI 0.3 WebAssembly component that exports wasi:http/[email protected], using componentize-qjs (QuickJS) with the component-model async ABI.

// vite.config.ts
import { defineConfig } from 'vite';
import wasm from '@levyer/vite-wasm';

export default defineConfig({
  plugins: [wasm({ entry: 'src/server.ts' })],
});
// src/server.ts — any fetch-style handler (Hono, itty-router, plain function, ...)
export default {
  fetch(request: Request) {
    return new Response('hello');
  },
};
  • vite dev serves the entry from Vite's SSR environment in Node.
  • vite build bundles the entry plus the runtime layer into dist/wasm/server.js, then componentizes it into dist/server.wasm.
  • Run it with wasmtime serve -Scli dist/server.wasm (wasmtime ≥ 46).

Frameworks

For frameworks with their own SSR build, point entry at the built SSR entry and let the plugin build that environment as a web-standard bundle. Example: SolidStart v2, whose server entry is an H3 app with fetch:

// vite.config.ts
export default defineConfig({
  plugins: [
    solidStart(),
    wasm({ entry: 'dist/server/entry-server.js', ssrEnvironment: 'ssr', dev: false }),
  ],
});
  • ssrEnvironment (build only) gives that environment worker-style export conditions, bundles all dependencies, uses rolldown's neutral platform, and fails the build on Node built-ins other than the shimmed async_hooks, naming the importing module.
  • assets embeds static files into the component and serves them (GET/HEAD) before the app. It defaults to the client environment's output when one was built (dist/client); hashed file names get immutable caching.
  • dev: false leaves vite dev to the framework.
  • httpClient: false leaves wasi:http/client out of the component's world (world levyer:vite-wasm/service-without-client), for hosts without outgoing HTTP. fetch then rejects with a TypeError for anything but blob: URLs, importing wasi:http/client from app code fails the build, and so do wit worlds or imports that bring it back.

Additional WIT interfaces

wit adds interfaces to the component's world without replacing the plugin's own:

wasm({
  entry: 'src/server.ts',
  wit: { path: 'wit', imports: ['wasi:keyvalue/store'] },
  devImplementations: { 'wasi:keyvalue/store': './dev/keyvalue.ts' },
});
import { open } from 'wasi:keyvalue/[email protected]';
  • path is a WIT directory: deps/ (e.g. from wkg fetch) and optionally a root package (*.wit). It is merged with the plugin's WIT into node_modules/.vite-wasm/wit, where a generated world includes levyer:vite-wasm/service plus worlds and imports. A package present in both trees is taken from yours.
  • worlds defaults to the root package's world when it has exactly one and imports isn't set. Worlds with exports are rejected: the component only exports wasi:http/handler.
  • Imports of any interface from the merged packages stay external in the bundle (also in a framework's ssrEnvironment build). When a framework imports its SSR output in Node after building it (SvelteKit's route analysis), those imports resolve to stubs.
  • In vite dev, devImplementations maps interfaces (with or without version) to modules implementing them. Interfaces without one get a stub whose functions and resource constructors throw when called.
  • TypeScript declarations for worlds and imports (not for the plugin's own WIT) are generated into wit.types (default node_modules/.vite-wasm/types; false disables them) whenever the config is loaded. The plugin's own type entry references the default directory, so any TypeScript program that includes vite.config.ts knows the WIT imports without further setup. Otherwise (or with a custom wit.types), reference it from an ambient declaration file: /// <reference types=".vite-wasm/types" />. The plugin owns that directory and deletes stale .d.ts files in it.
  • vite dev watches wit.path: changes regenerate the merged WIT and the declarations, and reload the stubs and the page.

node:async_hooks (AsyncLocalStorage, used e.g. by solid-js/web/storage for getRequestEvent()) is shimmed. QuickJS cannot propagate async context, so when the shim is bundled, the component handles one request at a time per instance, each until its body is fully written. Apps that don't use it (e.g. Hono) stay concurrent.

Runtime layer

QuickJS has no web APIs. src/runtime provides the ECMA-429 (WinterTC Minimum Common Web API) set on top of WASI 0.3. Per-API conformance against web-platform-tests is tracked in conformance/WEB-APIS.md.

| API | Backed by | | --- | --- | | fetch, Request, Response, Headers | wasi:http/client / wasi:http/types (bodies are stream<u8>); blob: URLs | | console.* | wasi:cli/stdout, wasi:cli/stderr | | setTimeout / setInterval / setImmediate, queueMicrotask | wasi:clocks/monotonic-clock wait-for | | performance (now, timeOrigin, marks/measures) | wasi:clocks/monotonic-clock, wasi:clocks/system-clock | | crypto.getRandomValues, crypto.randomUUID | wasi:random/random | | crypto.subtle | @noble/hashes, @noble/ciphers, @noble/curves: SHA-1/2, HMAC, AES-CTR/CBC/GCM/KW, PBKDF2, HKDF, ECDSA/ECDH (P-256/384/521), Ed25519, X25519 | | process.env | wasi:cli/environment (read on the first request) | | CompressionStream / DecompressionStream | fflate (gzip, deflate, deflate-raw) | | URLPattern | urlpattern-polyfill | | Event, EventTarget (incl. the global scope), AbortController, MessageChannel, reportError, onerror | pure JS | | TextEncoder/TextDecoder (+ streams), Blob, File, FormData, URL.createObjectURL | pure JS | | ReadableStream/WritableStream/TransformStream | web-streams-polyfill | | URL, URLSearchParams | core-js | | structuredClone, atob/btoa | QuickJS-ng built-ins |

Only what you use

The core APIs above are always bundled. The heavy optional ones — crypto.subtle (SubtleCrypto, CryptoKey), CompressionStream/DecompressionStream, URLPattern, MessageChannel/MessagePort — are bundled only when code in the bundle references them: the plugin configures rolldown's inject, so a reference such as crypto.subtle.digest(...), typeof CompressionStream or globalThis.URLPattern becomes an import of the module that installs that API. References in code that tree-shaking removes don't count.

Indirect access is not detected (const { subtle } = crypto, globalThis['URL' + 'Pattern']). Accessing crypto.subtle when it wasn't bundled throws an error naming the fix:

wasm({ entry: 'src/server.ts', include: ['crypto.subtle'] }); // or 'compression', 'urlpattern', 'messaging'
wasm({ entry: 'src/server.ts', globals: 'all' });              // always bundle everything

Module top-level code runs while the component is snapshotted at build time, so host calls there (timers, logs, env) are deferred until the first request.

ctx.waitUntil(promise) is supported: the response's trailers resolve only after those promises settle, which keeps the request task alive.

Known gaps:

  • WebAssembly — QuickJS has no wasm engine.
  • RSA algorithms in crypto.subtle; brotli compression; TextDecoder encodings other than UTF-8 and windows-1252.
  • Pure-JS crypto is slow on QuickJS: PBKDF2 with 100k iterations takes about 2 s.
  • DecompressionStream doesn't detect trailing junk in the same chunk as the end of the stream.
  • unhandledrejection events aren't fired (QuickJS exposes no rejection tracker to JS).
  • node:* built-ins, WebSockets.

Development

npm run wit:fetch   # wkg fetch → wit/deps
npm run wit:types   # jco guest-types → src/runtime/wasi (type declarations)
npm run build