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

wesl-debug

v0.6.15

Published

Utilities for testing WESL/WGSL shaders in Node.js environments.

Readme

wesl-debug

Utilities for testing WESL/WGSL shaders in Node.js environments.

Overview

wesl-debug provides simple test harnesses for quickly running fragment and compute shaders GPU tests.

  • Write shader code in either WESL or WGSL.
  • Shader library imports in WESL are resolved automatically from node_modules.
  • Shaders are run using Dawn, the WebGPU engine used inside the Chrome browser.

Installation

npm install wesl-debug

Testing Compute Shaders

Use testComputeShader() to test compute shader behavior. A storage buffer is provided for writing test results.

Basic Example

import { testComputeShader } from "wesl-debug";

const gpu = /* initialize GPU */;

const src = `
  import test;  // provides test::results storage buffer

  @compute @workgroup_size(1)
  fn main() {
    test::results[0] = 42u;
    test::results[1] = 100u;
  }
`;

const result = await testComputeShader(import.meta.url, gpu, src, "u32");
// result = [42, 100, -999, -999]  // (unwritten values are filled with -999)

Storage Buffer

The test virtual module provides a storage buffer for results in WESL code:

  • Default buffer size: 16 bytes (4 × 4-byte elements for u32 or f32)
  • Custom size: Use the size parameter to specify buffer size in bytes
  • Access via test::results[index]
// Example with custom buffer size
const result = await testComputeShader({
  projectDir: import.meta.url,
  device,
  src: `
    import test;
    @compute @workgroup_size(1)
    fn main() {
      for (var i = 0u; i < 8u; i++) {
        test::results[i] = i * 10u;
      }
    }
  `,
  resultFormat: "u32",
  size: 32  // 32 bytes = 8 × 4-byte u32 elements
});
// result = [0, 10, 20, 30, 40, 50, 60, 70]

Testing Fragment Shaders

Use testFragmentShader() to test fragment shader behavior. The function renders using a fullscreen triangle and returns pixel values for validation.

Basic Example

import { testFragmentShader, getGPUDevice } from "wesl-debug";

const projectDir = import.meta.url;
const device = await getGPUDevice();

const src = `
  @fragment
  fn fs_main() -> @location(0) vec4f {
    return vec4f(0.5, 0.25, 0.75, 1.0);
  }
`;
const textureFormat = "rgba32float";
const r = await testFragmentShader({ projectDir, device, src, textureFormat });
// r = [0.5, 0.25, 0.75, 1.0]

Testing using Derivatives

Derivative functions like dpdx, dpdy, and fwidth require at least a 2×2 pixel quad. Use the size parameter to create a 2×2 texture:

// derivative of x coordinate
const src = `
  @fragment
  fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
    let dx = dpdx(pos.x);
    return vec4f(pos.x, dx, 0.0, 1.0);
  }
`;
const result = await testFragmentShader({
  projectDir: import.meta.url,
  device,
  src,
  textureFormat: "rg32float",
  size: [2, 2],
});
const [x, dx] = result; // result at pixel (0, 0)
// x = .5  dx = 1

Note: The test function always samples pixel (0,0) from the rendered texture.

Testing with Input Textures

Use inputTextures to test fragment shaders that sample from textures. Helper functions provide common test patterns:

import {
  testFragmentShader,
  getGPUDevice,
  createSolidTexture,
  createGradientTexture,
  createCheckerboardTexture,
  createSampler
} from "wesl-debug";

const device = await getGPUDevice();
const inputTex = createSolidTexture(device, [0.5, 0.5, 0.5, 1.0], 256, 256);
const sampler = createSampler(device);

const src = `
  @group(0) @binding(0) var input_tex: texture_2d<f32>;
  @group(0) @binding(1) var input_samp: sampler;

  @fragment
  fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
    let uv = pos.xy / 256.0;
    return textureSample(input_tex, input_samp, uv);
  }
`;

const result = await testFragmentShader({
  projectDir: import.meta.url,
  device,
  src,
  inputTextures: [{ texture: inputTex, sampler }]
});
// result = [0.5, 0.5, 0.5, 1.0]

Texture Helper Functions:

  • createSolidTexture(device, color, width, height) - uniform color
  • createGradientTexture(device, width, height, direction?) - gradient ('horizontal' or 'vertical')
  • createCheckerboardTexture(device, width, height, cellSize?) - checkerboard pattern
  • createSampler(device, options?) - texture sampler (linear filtering, clamp-to-edge by default)

Binding Convention: Textures bind sequentially starting at binding 0:

  • inputTextures[0] → texture at @binding(0), sampler at @binding(1)
  • inputTextures[1] → texture at @binding(2), sampler at @binding(3)

Image Testing & Visual Regression

Test complete rendered images and automate visual regression testing using snapshot comparison.

Full Image Retrieval

Use testFragmentShaderImage() to get the complete rendered image instead of just pixel (0,0):

import { testFragmentShaderImage, saveImageDataToPNG } from "wesl-debug";

const result = await testFragmentShaderImage({
  projectDir: import.meta.url,
  device,
  src: blurShaderSource,
  size: [256, 256],
  inputTextures: [{ texture: inputTex, sampler }]
});

// Save for visual inspection
await saveImageDataToPNG(result, "__image_dev__/blur-result.png");

Advanced Test Textures

Additional texture generators for image processing tests:

import {
  createRadialGradientTexture,   // White center → black edge
  createEdgePatternTexture,       // Sharp lines for edge detection
  createColorBarsTexture,         // RGB primaries/secondaries
  createNoiseTexture,             // Deterministic seeded noise
  createPhotoSampleTexture        // Load from PNG file
} from "wesl-debug";

const radial = createRadialGradientTexture(device, 256);
const edges = createEdgePatternTexture(device, 256);
const colors = createColorBarsTexture(device, 256);
const noise = createNoiseTexture(device, 256, 42); // seed = 42

// Use the bundled test photo (512x512 lemur image)
import { getLemurImagePath } from "wesl-debug";
const photo = createPhotoSampleTexture(device, getLemurImagePath());

Visual Regression Testing

Use snapshot comparison to catch unintended visual changes:

import { imageMatcher } from "wesl-debug";

// In test setup file or at top of test
imageMatcher();

test("blur filter produces expected result", async () => {
  const result = await testFragmentShaderImage({
    projectDir: import.meta.url,
    device,
    src: blurShaderSource,
    size: [256, 256],
    inputTextures: [{ texture: inputTex, sampler }]
  });

  // Compare against reference snapshot
  await expect(result).toMatchImage("blur-filter");
});

Snapshot Workflow:

# Run tests - creates reference snapshots on first run
pnpm vitest

# Review generated snapshots in __image_snapshots__/
# Commit if they look correct
git add __image_snapshots__/
git commit -m "Add visual regression tests"

# After code changes, tests fail if output changed
pnpm vitest  # Shows diffs in __image_diffs__/

# If changes are intentional, update snapshots
pnpm vitest -- -u

Directory Structure:

  • __image_snapshots__/ - Reference images (committed to git)
  • __image_actual__/ - Current test outputs (gitignored, saved on every run)
  • __image_diffs__/ - Diff visualizations (gitignored, only on failure)
  • __image_diff_report__/ - HTML report (gitignored, self-contained)
  • __image_dev__/ - Dev experiments (gitignored)

Comparison Options

Fine-tune snapshot comparison thresholds:

await expect(result).toMatchImage("edge-detection", {
  threshold: 0.1,                      // Color difference threshold (0-1)
  allowedPixelRatio: 0.01,  // Allow 1% of pixels to differ
  allowedPixels: 100         // Or allow 100 pixels to differ
});

HTML Diff Report

When snapshot tests fail, an HTML report is automatically generated showing all failures side-by-side:

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    setupFiles: ["./test/setup.ts"],
    reporters: [
      "default",
      ["vitest-image-snapshot/reporter"]
    ]
  }
});

If any tests fail, a report is saved to __image_diff_report__/index.html and shows:

  • Side-by-side comparison (Expected | Actual | Diff)
  • Mismatch statistics per test
  • Clickable images for full-size viewing

Complete Test Example

import { afterAll, beforeAll, expect, test } from "vitest";
import { testFragmentShader, testComputeShader } from "wesl-debug";
import { destroySharedDevice, getGPUDevice } from "wesl-debug";

const projectDir = import.meta.url;
let device: GPUDevice;

beforeAll(async () => {
  device = await getGPUDevice();
});

afterAll(() => {
  destroySharedDevice();
});

test("fragment shader renders color", async () => {
  const src = `
    @fragment
    fn fs_main() -> @location(0) vec4f {
      return vec4f(1.0, 0.0, 0.0, 1.0);
    }
  `;
  const textureFormat = "rgba32float";
  const r = await testFragmentShader({ projectDir, device, src, textureFormat });
  expect(r).toEqual([1.0, 0.0, 0.0, 1.0]);
});

test("compute shader writes results", async () => {
  const src = `
    import test;

    @compute @workgroup_size(1)
    fn main() {
      test::results[0] = 123u;
    }
  `;
  const result = await testComputeShader(projectDir, device, src, "u32");
  expect(result[0]).toBe(123);
});