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

shellcheck-wasm

v0.3.3

Published

ShellCheck compiled to WebAssembly with TypeScript API

Downloads

322

Readme

shellcheck-wasm

ShellCheck 0.11 compiled to WebAssembly, with a typed TypeScript API for Node.js and browsers.

The module is built with GHC's WebAssembly backend (wasm32-wasi) as a WASI reactor exposing the async JSFFI export lintWithOptions (and getVersion for cache invalidation). Both runtimes load the same artifact; the only difference is how the WASI imports are provided.

Requirements

  • Node.js >= 20
  • The prebuilt dist/shellcheck.wasm + dist/shellcheck.js (see Building the WASM module), or build them yourself with the GHC WASM toolchain

Installation

npm install shellcheck-wasm

Publishing

One-step release:

npm run release-patch   # 0.1.0 -> 0.1.1
npm run release-minor   # 0.1.0 -> 0.2.0
npm run release-major   # 0.1.0 -> 1.0.0

This runs tests, bumps the version, rebuilds dist/, commits, pushes the tag, and publishes to npm.

For manual control:

npm run build
npm publish

Usage

Node.js

import { createShellCheck, lintWithOptions } from 'shellcheck-wasm';

// One-shot
const results = await lintWithOptions('echo $UNQUOTED_VAR', { severity: 'warning' });

// Reusable instance (module is instantiated once and reused)
const shellcheck = await createShellCheck();
const results2 = await shellcheck.lintWithOptions('echo $VAR', { severity: 'warning' });

By default the loader resolves dist/shellcheck.wasm relative to the package. Pass an explicit path when needed:

const shellcheck = await createShellCheck({ wasmUrl: '/path/to/shellcheck.wasm' });

Browser

Serve dist/shellcheck.wasm and dist/shellcheck.js from the same origin with correct MIME types (application/wasm for the .wasm file, text/javascript for .js), then:

import { createShellCheck } from 'shellcheck-wasm';

const shellcheck = await createShellCheck({ wasmUrl: '/shellcheck.wasm' });
const results = await shellcheck.lintWithOptions(editorValue, { severity: 'warning' });
console.log(results);

wasmUrl may be any absolute or relative URL. The companion shellcheck.js (post-link JSFFI glue) is resolved by replacing the .wasm suffix with .js.

Important: The .wasm file must be served with Content-Type: application/wasm, otherwise the browser will refuse to instantiate it. The test suite validates this requirement (see browser-serve.test.ts).

API reference

createShellCheck(options?)

Creates (or returns the cached) ShellCheck instance, instantiating the WASM module on first call. Uses version-based cache invalidation; pass forceNew: true to bypass.

await createShellCheck(options?: {
  wasmUrl?: string;              // path or URL of shellcheck.wasm
  runtime?: 'node' | 'browser' | 'auto';  // default 'auto'
  forceNew?: boolean;            // bypass version cache (default false)
});

lintWithOptions(script, options)

await lintWithOptions('echo $VAR', { severity: 'error' });   // filtered
await lintWithOptions('echo $VAR', { exclude: [2086] });     // suppress SC2086

lint(script, options?)

await lint('echo $VAR');                          // LintResult[] (deprecated)

Deprecated: Use lintWithOptions instead. This convenience wrapper will be removed in a future version.

resetShellCheck()

Terminates the cached instance. Mainly useful in tests.

LintOptions

interface LintOptions {
  shell?: 'bash' | 'sh' | 'dash' | 'ksh' | 'busybox';
  severity?: 'error' | 'warning' | 'info' | 'style';
  exclude?: number[];      // warning codes to suppress, e.g. [2086]
  include?: number[];      // if set, only these codes are reported
  files?: Record<string, string>;  // virtual files for `source` directives
}

Severity filtering matches ShellCheck semantics: setting severity: 'error' hides info-level findings such as SC2086.

LintResult

interface LintResult {
  file: string;
  line: number;
  column: number;
  endLine?: number;
  endColumn?: number;
  code: number;   // e.g. 2086
  severity: 'error' | 'warning' | 'info' | 'style';
  message: string;
  fix?: {
    replacements: Array<{
      startLine: number;
      startColumn: number;
      endLine: number;
      endColumn: number;
      text: string;
    }>;
  };
}

Example output for echo $VAR:

[
  {
    "code": 2148, "severity": "error",
    "message": "Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive."
  },
  {
    "code": 2086, "severity": "info",
    "message": "Double quote to prevent globbing and word splitting.",
    "fix": { "replacements": [
      { "startLine": 1, "startColumn": 6, "endLine": 1, "endColumn": 6, "text": "\"" },
      { "startLine": 1, "startColumn": 10, "endLine": 1, "endColumn": 10, "text": "\"" }
    ] }
  }
]

How it works

shellcheck/ (v0.11.0 submodule, unmodified)
  └─ wasm/Main.hs + wasm/ShellCheck/Wasm/*
       │  wasm32-wasi-ghc 9.10 (ghc-wasm-meta, FLAVOUR=9.10)
       │  -no-hs-main -optl-mexec-model=reactor
       ▼
dist/shellcheck.wasm  (~17 MB, WASI reactor)
dist/shellcheck.js    (post-link.mjs JSFFI glue)
       │  src/runtime/{node,browser}.ts
       ▼
  lint(script, options?) → Promise<LintResult[]>

Key design points:

  • Single entry point. The Haskell layer calls ShellCheck.Checker.checkScript directly and maps CheckResult to JSON. No CLI parsing, no formatters, no filesystem access inside the module.
  • In-memory filesystem. source directives resolve against the files option map; there is no host filesystem access from WASM. Each lint call builds a fresh interface, so concurrent or repeated calls share no state.
  • Reactor lifecycle. Callers must _initialize once, then hs_init(0, 0), then await the async exports. Both runtimes implement exactly this sequence; see the GHC user's guide section on JavaScript FFI in the wasm backend.
  • No regex shim. regex-tdfa is pure Haskell and compiles to wasm32-wasi unmodified.
  • One WASI implementation. Both runtimes use @bjorn3/browser_wasi_shim. Node's builtin node:wasi was tried and aborts reactor initialization with an opaque exit-code throw; the shim initializes cleanly in both environments.

Building the WASM module

Requires the prebuilt GHC WASM toolchain (a stock ghcup GHC is single-target and cannot emit wasm32-wasi):

# One-time toolchain install (Linux x86_64, no compilation, ~1 GB)
curl https://gitlab.haskell.org/haskell-wasm/ghc-wasm-meta/-/raw/master/bootstrap.sh | sh
source ~/.ghc-wasm/env

npm run build:wasm

scripts/build-wasm.sh builds exe:shellcheck-wasm with the wasm32-wasi-cabal wrapper, then runs GHC's post-link.mjs to generate the JSFFI glue. Output lands in dist/. Use cabal <= 3.14 with the wrapper (3.16 has a known regression with this toolchain).

Development

npm install          # install JS dependencies
npm run build        # builds WASM + TypeScript (via unbuild)
npm test             # Node suites: unit + integration + browser-path-over-HTTP
npm run test:browser # real headless Chromium via @vitest/browser + Playwright
npm run lint         # Biome check
npm run lint:fix     # Biome check --write

Git hooks (Husky + lint-staged) run Biome on staged JS/TS files at commit and the test suite on push.

Test matrix

| Suite | Environment | What it covers | |---|---|---| | api.test.ts | Node | Type shapes, response envelopes | | node.test.ts | Node + built .wasm | SC2086/SC2164/SC3043 detection, severity/include/exclude filters, shell override, virtual source files, fix data | | browser-serve.test.ts | Node + local HTTP | BrowserShellCheck fetching .wasm over HTTP | | browser.test.ts | Real headless Chromium | Full browser path: fetch, instantiate, lint, options |

Integration suites skip automatically when dist/shellcheck.wasm is absent.

Project structure

shellcheck-wasm/
├── src/
│   ├── index.ts              # public entry point
│   ├── api.ts                # createShellCheck / lintWithOptions / resetShellCheck
│   ├── types.ts              # LintOptions, LintResult
│   ├── vitest-provided.d.ts  # ProvidedContext augmentation for browser tests
│   ├── runtime/
│   │   ├── node.ts           # Node loader (reads dist/ from disk)
│   │   ├── browser.ts        # browser loader (fetch + WASI shim)
│   │   └── utils.ts          # shared parseLintResponse
│   └── __tests__/            # Vitest suites
├── wasm/
│   ├── Main.hs               # stub (exports live in ShellCheck.Wasm.API)
│   └── ShellCheck/Wasm/
│       ├── API.hs            # foreign export javascript lintWithOptions/getVersion
│       ├── SystemInterface.hs# in-memory SystemInterface
│       └── Types.hs          # JSON types mirroring src/types.ts
├── shellcheck/               # upstream ShellCheck v0.11.0 (submodule)
├── test/
│   ├── fixtures/             # .sh fixtures
│   └── serve-dist.ts         # globalSetup: serves dist/ to browser tests
├── scripts/                  # build-wasm.sh
├── build.config.ts           # unbuild configuration
├── shellcheck-wasm.cabal
├── cabal.project
├── vitest.config.ts          # Node suites
└── vitest.browser.config.ts  # Chromium suite

dist/ (compiled JS, .wasm, glue, generated .d.ts) and dist-newstyle/ are gitignored build outputs.

Known limitations

  • The 17 MB module takes a few seconds to fetch and instantiate on first use; reuse the instance returned by createShellCheck.
  • The module has no host filesystem access; only virtual files can be resolved for source directives.
  • Exceptions inside the checker surface as rejected promises; validate options client-side.
  • Browser testing covers headless Chromium. Other engines should work (the module only needs post-MVP features all modern browsers ship), but they are not in the matrix.

License

GPL-3.0-or-later, same as ShellCheck. ShellCheck is copyright Vidar Holen and contributors; this wrapper is a derivative work.