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

garfish-wasm-esm-plugin

v0.4.0

Published

Build-time compiler and Garfish runtime backed by an OXC WebAssembly transformer.

Readme

garfish-wasm-esm-plugin

codecov

Garfish plugin for running <script type="module"> resources through a browser WebAssembly transformer. The wasm core uses OXC to parse ESM syntax and rewrites imports/exports into the Garfish runtime helpers.

Usage

import Garfish from 'garfish';
import { GarfishEsModule } from 'garfish-wasm-esm-plugin';

Garfish.run({
  plugins: [
    GarfishEsModule(),
  ],
});

The plugin only handles scripts that Garfish already marks as module scripts. For Vite-style sub applications, keep using an HTML entry with <script type="module">.

Module load concurrency

// Default: at most 24 concurrent loads per Runtime.
GarfishEsModule({ limitConcurrency: true });

// Start loads without the Runtime concurrency queue delaying them.
GarfishEsModule({ limitConcurrency: false });

limitConcurrency defaults to true and also works with new Runtime(...). The limit applies to concurrent Loader.load() calls per subapp Runtime in both batch and streaming scheduling modes, including cache reads. It does not limit compilation or evaluation concurrency. Disabling it removes only this Runtime limit; dependency scheduling and browser/Loader behavior still apply.

Build-time compilation

The compiler entry can turn an emitted ESM module into a JavaScript artifact that the runtime consumes without loading the wasm transformer:

import { compileGarfishModule } from 'garfish-wasm-esm-plugin/compiler';

const artifact = await compileGarfishModule(
  emittedChunk.code,
  emittedChunk.fileName,
);

// Emit `artifact` into the Garfish build using the same relative file name.

The artifact is a JavaScript file whose leading comment carries the static import and export metadata produced by OXC. The transformed module body still runs through the existing Garfish sandbox execution path, so no separate manifest is required.

For dual output, keep matching directory structures so relative module specifiers resolve without a runtime URL convention:

dist/esm/assets/main.js
dist/esm/assets/dependency.js
dist/garfish/assets/main.js
dist/garfish/assets/dependency.js

Import the browser runtime from the runtime-only entry and disable fallback compilation after the Garfish tree is complete:

import Garfish from 'garfish';
import { GarfishEsModule } from 'garfish-wasm-esm-plugin/runtime';

Garfish.run({
  plugins: [
    GarfishEsModule({
      runtimeCompile: false,
    }),
  ],
});

runtimeCompile defaults to true for compatibility. Precompiled artifacts always bypass wasm transformation. When the option is true, a plain ESM module can still fall back to browser compilation; when it is false, loading a plain ESM module fails with an explicit error.

Module evaluation failures are retained for the lifetime of a Runtime. Repeated imports rethrow the original value, including its stack and cause, and failed module instances and their static importers are invalidated. Successful independent dependencies remain reusable. Download and compilation failures retain their existing retry behavior; evaluation can be retried in a new Runtime. Clearing the shared compilation cache does not reset evaluation state, and cached Garfish apps keep the state of their existing runtime.

Vite plugin

The Vite entry keeps the original ESM output and emits a precompiled Garfish mirror during the same build:

import { defineConfig } from 'vite';
import { garfishPrecompile } from 'garfish-wasm-esm-plugin/vite';

export default defineConfig({
  plugins: [
    garfishPrecompile({
      outDir: 'garfish',
      htmlEntries: ['subapp.html'],
    }),
  ],
});

For every JavaScript chunk, the plugin emits a compiled asset under the same relative path below garfish/. Non-HTML assets are mirrored by default so relative asset URLs keep working. Listed HTML entries are also mirrored and their absolute Vite asset URLs are redirected to the Garfish tree. Original ESM chunks and HTML remain unchanged.

dist/assets/main.js
dist/subapp.html
dist/garfish/assets/main.js
dist/garfish/subapp.html

The generated Garfish HTML keeps its module script tags, so load it with GarfishEsModule({ runtimeCompile: false }). Generated sourcemaps are not supported yet because the precompiled code needs a new mapping rather than a copy of Vite's ESM map.

Supported Resolution

This version supports both HTML import maps and Garfish externals at runtime.

HTML import maps are read from the sub application's HTML entry:

<script type="importmap">
{
  "imports": {
    "@scope/shared": "https://cdn.example.com/shared/index.js"
  }
}
</script>

Bare imports that are not provided by Garfish externals are resolved with @jspm/import-map against the current module URL.

Garfish externals are read from Garfish.externals. The matching rule follows import map semantics:

  • keys without a trailing slash match only the exact module id;
  • keys with a trailing slash match that full prefix, so @abc/def/ externalizes imports such as @abc/def/test.js;
  • the longest matching external prefix wins when multiple prefix keys match. Every matching subpath reads from the external module value registered under that prefix key.
import React from 'react';
import * as sharedWidgets from '@abc/def';
import Garfish from 'garfish';
import { GarfishEsModule } from 'garfish-wasm-esm-plugin';

Garfish.externals = {
  react: React,
  '@abc/def/': sharedWidgets,
};

Garfish.run({
  plugins: [
    GarfishEsModule({
      garfishExternals: ['react', '@abc/def/'],
    }),
  ],
});

With the config above, import React from 'react' is exact-matched, while import { Button } from '@abc/def/button.js' is treated as external because it matches the @abc/def/ prefix. @abc/defx/button.js does not match that prefix.

Runtime-generated namespace modules stay live: exported getters read the current value from the backing module object instead of capturing an initial snapshot.

Wasm Size

The generated transformer artifact is pkg/garfish_wasm_esm_plugin_bg.wasm.

| Artifact | Size | | --- | ---: | | Raw wasm | 857,617 bytes (837.5 KiB) | | Gzip | 326,597 bytes (318.9 KiB) |

The size comes from bundling OXC parser and semantic analysis into the browser runtime. The semantic pass is intentional because imported bindings need symbol aware rewriting to preserve ESM live binding behavior after the code is lowered to Garfish runtime helpers.

Why This Plugin Exists

Garfish already knows when an HTML entry contains <script type="module">, but the module graph still needs browser-runtime compilation before it can run inside Garfish's sandboxed execution model. A build-time transform is not enough for dynamically loaded sub applications because the host may only see the module source after Garfish has fetched the HTML entry and its scripts.

This package keeps that work in a Garfish plugin boundary:

  • wasm runs in the browser and parses the fetched module source on demand;
  • OXC AST and semantic data are used instead of regex or text-only rewriting;
  • imports, exports, import.meta, dynamic import(), import maps, and Garfish externals are handled by the same runtime;
  • live bindings survive the CommonJS-like helper lowering used by the plugin.

Build

pnpm install
pnpm build

pnpm build first runs wasm-pack build --target web --out-dir pkg, then builds the TypeScript Garfish wrapper into dist.

Test

pnpm test
pnpm test:coverage

pnpm test runs the Rust unit tests, builds the wasm transformer, and runs Vitest in Node. pnpm test:coverage writes coverage/coverage-summary.json and coverage/lcov.info; CI uploads that report to Codecov.

On pull requests, GitHub Actions uploads the coverage report with codecov/codecov-action; Codecov owns the coverage PR comment and status checks. The workflow also updates a repository PR comment with Codecov report links and the latest benchmark table.

Benchmark

Run pnpm benchmark:browser to compare native ESM execution with this plugin's precompiled artifacts in an existing browser. It measures import-to-ready and instrumented module-body time with paired trials, warmups, correctness checks, median/P75 summaries and JSON export. Network preparation and precompilation are outside the timed interval. See the browser benchmark guide.

pnpm benchmark
pnpm benchmark:update

pnpm benchmark measures the wasm transform path against fixed ESM fixtures. pnpm benchmark:update refreshes both benchmarks/transform.md and the table below.

| Fixture | Source bytes | Mean | p75 | p99 | Throughput | Samples | | --- | ---: | ---: | ---: | ---: | ---: | ---: | | small-live-bindings | 255 | 0.011 ms | 0.011 ms | 0.014 ms | 90,876 ops/sec | 90,215 | | medium-dashboard | 1,247 | 0.064 ms | 0.063 ms | 0.078 ms | 15,850 ops/sec | 15,707 | | large-re-export | 5,314 | 0.250 ms | 0.250 ms | 0.348 ms | 4,010 ops/sec | 3,993 |

Measured on Node v22.23.1 with BENCH_TIME_MS=1000 and BENCH_WARMUP_MS=250.

Vite Example

pnpm example:dev

The example starts a Garfish host page that loads subapp.html as an HTML entry with a <script type="module"> sub application. It imports this package through the local source alias so changes in src/ can be exercised without publishing.

Release

This package uses Changesets. Add a changeset for user-facing package changes:

pnpm changeset

GitHub Actions expects an NPM_TOKEN repository secret with permission to publish garfish-wasm-esm-plugin to npm.

When a same-repository PR includes a releasable changeset, CI consumes that changeset in the runner workspace and publishes a beta package with the npm beta dist tag. The beta version format is:

<next-version>-pr-<pr-number>-<utc-YYYYMMDDHHMMSS>

When the PR is merged into master, CI consumes the changeset, commits the version/changelog metadata, publishes the formal npm package with the default dist tag, then pushes the release commit and tags back to master.