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

glsl-modules

v0.7.0

Published

A GLSL preprocessor for JavaScript that adds modular import/export support and a flexible plugin system for custom syntax and functionality.

Readme

   glsl-modules

npm version

glsl-modules is a GLSL preprocessor for JavaScript/TypeScript that extends GLSL with client-side module import/export functionality and a plugin system for custom syntax. This means that the shaders are dynamically built and resolved at run time, not at build time.

Interested in seeing it in action and trying it out for yourself? Check out the playground: https://glsl-modules.vercel.app

The library is still a work in progress, and the API is subject to change.

Contents:

Installation

The library is available on npm:

npm install glsl-modules

Examples

Note: The GLSL code snippets below are simplified, missing version directives and so on.

Example shader

Here is a typical glsl-modules shader, showcasing both imports and custom syntax handled by a plugin:

const fragmentShader = /*glsl*/`
import { randomColor } from "random/color"

in vec2 uv;
out vec4 color;

vec3 baseColor = css-red; // requires css-color-plugin!

void main() {
  vec3 randomColor = randomColor(uv);
  vec3 gradientColor = mix(baseColor, randomColor, uv.x);
  color = vec4(gradientColor, 1);
}
`

It imports the function randomColor from the module color in the library random.

Resolving a shader

The fragmentShader defined above must be resolved to a valid GLSL shader before use. There are two ways to achieve this:

  1. with a GLSLRegistry (recommended) - can be reused to resolve multiple shaders, avoids repeating setup
  2. with resolveGLSL - for resolving a single shader

First, using resolveGLSL:

import { resolveGLSL } from "glsl-modules";
import { randomLibrary } from "./random-library";
import { cssColorsPlugin } from "glsl-modules/plugins";

const resolvedFragmentShader = resolveGLSL(fragmentShader, { 
  libraries: [randomLibrary],
  plugins: [cssColorsPlugin()]
});

With a GLSLRegistry:

const registry = new GLSLRegistry({
  libraries: [randomLibrary],
  plugins: [cssColorsPlugin()]
});

const resolvedVertexShader = registry.resolve(vertexShader);
const resolvedFragmentShader = registry.resolve(fragmentShader);

When resolving, glsl-modules constructs a new shader string that contains the imported content in the correct order, and applies the activated plugins. For fragmentShader, that might look something like this:

in vec2 uv;
out vec4 color;

vec3 baseColor = vec3(1, 0, 0);

float random(vec2 position, float seed) {
  // This is bad on purpose, just a placeholder
  return fract(dot(position, vec2(123, 987)) + seed);
}

vec3 randomColor(vec2 position) {
  return vec3(random(position, 0.0), random(position, 1.0), random(position, 2.0));
}

void main() {
  vec3 randomColor = randomColor(uv);
  vec3 gradientColor = mix(baseColor, randomColor, uv.x);
  color = vec4(gradientColor, 1);
}

The random function was not explicitly imported, but is included since randomColor depends on it.

Defining a library

A library is a collection of modules, where the content of each module is defined as a string of GLSL code. A module consists of entities like functions, variables and structs, which can be exported and made available outside the module.

A module can import content from other modules, and the shorthand @ is used when importing from a module inside the same library.

Here is an example library utilities, which has the library random as a dependency:

import { randomLibrary } from "./random-library";

const rotationDefinition = /*glsl*/`
export { rotate }

vec2 rotate(vec2 p, float rotation) {
  return mat2(cos(rotation), sin(rotation), -sin(rotation), cos(rotation))*p;
}`

const miscellaneousDefinition = /*glsl*/`
// internal = only available for modules in same library
export { PI, internal ColorData }

float PI = 3.14159265358;
struct ColorData {
  vec3 color;
  vec2 position;
};`

const grainDefinition = /*glsl*/`
import { ColorData as CD } from "@/miscellaneous" // From same library
import { random } from "random/1d" // From dependency

export { grain as addGrain } // Aliased export

vec3 grain(CD data, float amount) {
  return data.color + amount*(-1.0 + 2.0*random(data.position));
}`

const utilitiesLibrary = new GLSLLibrary({
  name: "utilities",
  definition: {
    // Module utilities/rotation
    "rotation": rotationDefinition,
    // Module utilities/miscellaneous
    "miscellaneous": miscellaneousDefinition,
    // Module utilities/grain
    "grain": grainDefinition,
  },
  dependencies: [randomLibrary]
});

More complex library structures can be achieved by nesting objects, like in this imagined noise library:

const noiseDefinition = {
  "@index": "",   // noise
  "1d": {
    "@index": ""  // noise/1d
    "value": "",  // noise/1d/value
    "perlin": "", // noise/1d/perlin
    "worley": "", // noise/1d/worley
  },
  "2d": {
    "value": "",  // noise/2d/value
    "perlin": "", // noise/2d/perlin
  },
  "3d": {
    "value": "",  // noise/3d/value
    "perlin": "", // noise/3d/perlin
  }
}

Defining a plugin

Plugins are used to modify the contents of library modules and shader code. There are three stages: preprocess, transform, and postprocess.

preprocess: modifies the raw string definition of an entire module/shader before parsing.

Example: A plugin that ensures that the code string starts with #version 300 es, which is required in WebGL2. However, this check is only necessary for shader output, not module definitions. The parameter isShader can be used to check this:

import { GLSLPlugin } from "glsl-modules";

function versionStringPlugin(): GLSLPlugin {
  const versionString = "#version 300 es";
  return {
    id: "version-string",
    preprocess: (code, isShader) => {
      if (isShader && !code.startsWith(versionString)) {
        return versionString + "\n" + code;
      } else {
        return code;
      }
    }
  }
}

transform: modifies the content of entities after parsing into the internal representation

postprocess: modifies the raw string definition of entities after the previous stages

The following plugin adds a comment containing the unique key (path + entity name + arguments for functions) of the entity to the start of the definition (e.g. /* libraryName/moduleName/functionName */):

import { definePlugin } from "glsl-modules";

const includeKeyOfEntityPlugin = definePlugin(() => ({
  id: "include-key-of-entity",
  postprocess: (code, entity) => {
    return `/* ${entity.key} */\n` + code;
  }
}));

Available plugins

The following plugins have been implemented and are included in the library.

arrow-functions

Adds support for JS-like arrow functions, which can be defined both globally and inside another function. In the latter case, the arrow function is only available inside its parent function. Note that the arrow function does not have access to other data defined in the same scope.

For short, single-line functions the syntax is returnType name = (arguments) => expression;, and the return keyword is omitted:

float sumOfCubes(float a, float b) {
  float cube = (float x) => x*x*x;

  float aCubed = cube(a);
  float bCubed = cube(b);

  return aCubed + bCubed;
}

For larger, multi-line functions the syntax is returnType name = (arguments) => { functionBody }:

void main = () => {
  vec3 cubedColor = pow(uv.xyx, vec3(3.0));
  color = vec4(cubedColor, 1);
}

In situations where an anonymous arrow function is needed, the syntax is returnType (arguments) => expression, e.g. float (float x) => exp(x).

function-as-argument

Implements higher-order functions, i.e. functions that take other functions as arguments. For example, a function that estimates the derivative of f:

float derivative(float f(float), float x, float h) {
  return (f(x + h) - f(x - h))/(2.0*h);
}

which can be used with some predefined function:

float df = derivative(someFunction, 0.5, 1e-4);

or, when combined with arrow-functions, an anonymous arrow function:

float df = derivative(float (float x) => x*x, 0.5, 1e-4);

As of now, function overloads are not supported. Only one higher-order function can have a given name.

named-arguments

Makes it possible to specify arguments in a JS object-like fashion, both when calling functions and creating structs. Useful when a function has many arguments, to avoid constantly looking up its definition.

The arguments can be specified in any order:

float foo(vec3 position, float size, int octaves, bool includeStart, float window) {
  // insert definition
}

float size = 0.2;
float result = foo({
  position: vec3(1, 0, 2),
  size,
  window: 0.4,
  octaves: 4,
  includeStart: true,
});

Note: For now, only user-defined functions are supported. Something like step({ x: 0.5, edge: 1.0 }) will not work.

namespaced-imports

Import and make an entire path available as a namespace:

import "utilities/math" as m;

vec3 foo(vec3 position) {
  // Function axisAngle in module utilities/math/rotation
  return m.rotation.axisAngle(position, vec3(1), 0.4);
}

css-colors

Write CSS colors directly in code. Should support any format, here are some examples:

vec3 namedColor = css-rebeccaPurple; // Must be prefixed with css-

vec3 hexColor = #FF0FAB;
vec4 hexAlphaColor = #0F04;

vec3 rgbColor = rgb(31 120 50);
vec4 rgbaColor = rgb(0 0 255 / 50%);

vec3 hslColor = hsl(from red calc(h + 90) s l);
vec4 hslaColor =  hsl(0.3turn 60% 45% / 0.7);

vec3 hwbColor = hwb(200 0% 0%);
vec4 hwbAlphaColor = hwb(200 0% 0% / 0.5);

vec3 lchColor = lch(52% 40 120);
vec4 lchAlphaColor = lch(52% 40 120 / 0.8);

vec3 oklchColor = oklch(0.7 0.15 120);
vec4 oklchAlphaColor = oklch(0.7 0.15 120 / 0.6);

vec3 labColor = lab(60% 20 -30);
vec4 labAlphaColor = lab(60% 20 -30 / 0.9);

vec3 oklabColor = oklab(0.65 0.1 -0.05);
vec4 oklabAlphaColor = oklab(0.65 0.1 -0.05 / 0.7);

vec3 colorSpaceColor = color(display-p3 1 0.5 0.2);
vec4 colorSpaceAlphaColor = color(srgb 0.2 0.4 0.6 / 0.3);

Current known limitations

Global #if, #else and #endif directives

glsl-modules currently does not handle global #if-#else-#endif blocks (that is, not inside a function definition). This, for example, will not work:

#if SOME_VALUE
float foo() { return 1.0; }
#else
float foo() { return 2.0; }
#endif

Library name collisions

It is currently not possible to add multiple libraries with the same name to a registry, or as dependencies to a library. This may be eventually solved by supporting aliases for library dependencies.