TypeScript icon, indicating that this package has built-in type declarations

    0.3.0 • Public • Published


    npm install --save @use-gpu/shader
    yarn add @use-gpu/shader

    GLSL Linker and Tree Shaker

    @use-gpu/shader is a Typescript library to link together snippets of shader code, while removing dead code, very quickly.

    It enables two kinds of imports to be used:

    • Static - ES Style
    #pragma import { getColor } from 'path/to/color'  // functions, declarations and types
    • Dynamic - Function Prototype
    vec4 getColor(); // linked at run-time

    This allows you to split up and organize your GLSL code as you see fit, as well as create dynamic shader permutations. It also lets you bind shaders at run-time without immediate linking, thus providing an equivalent of GLSL closures.

    @use-gpu/shader supports GLSL 4.5 and uses a Lezer grammar for the parsing.


    When combined with @use-gpu/glsl-loader (webpack or node), you can import a tree of .glsl modules directly in JS/TS as a pre-packaged bundle:

    import mainShader from 'path/to/main.glsl';
    import { linkBundle } from '@use-gpu/shader/glsl';
    const glslCode = linkBundle(mainShader);

    All dependencies will be parsed at build-time and deduplicated, using the normal import mechanism. They are packed with their symbol table and a sparse token list, so that code generation can happen immediately without re-parsing.


    You can also bind shaders to each other using bindBundle. This returns a new module instead of immediately producing the linked shader code. The result acts as a GLSL closure that you can use as a first-class value in your program:

    const bound = bindBundle(bundle, {moduleA, moduleB});

    The bound module can be passed around, and used as a new link to bind to another module recursively. This is highly useful to e.g. abstract over data sources or decorate shaders with new behavior.


    You can also skip the bundler and work with raw strings. In this case it is up to you to gather all the associated module code:

    import { linkCode } from '@use-gpu/shader/glsl';
    const moduleA = "...";
    const moduleB = "...";
    const moduleC = "...";
    const linked = linkCode(moduleC, {moduleA, moduleB});

    Shaders parsed at run-time will be cached on a least-recently-used basis, based on content hash.


    // Import symbols from a .glsl file
    #pragma import { symbol, … } from "path/to/file"
    #pragma import { symbol as symbol, … } from "path/to/file"
    // Mark next declaration as exported
    #pragma export
    // Mark next function prototype as optional (e.g. inside an `#ifdef`)
    #pragma optional
    // Mark next declaration as global (don't namespace it)
    #pragma global


    Static Import

    Imports from other files are declared using an ES-style directive referencing the filesystem:


    #pragma import { getColor } from 'path/to/color'
    void main() {
      gl_FragColor = getColor();

    Only exported symbols may be imported:


    #pragma export
    vec4 getColor() {
      return vec4(used(), 0.5, 0.0, 1.0);
    float used() {
      return 1.0;
    void unused() {
      // ...

    When passed to linkBundle, the result is:

    Linked result

    #version 450
    vec4 _u4_getColor() {
      return vec4(_u4_used(), 0.5, 0.0, 1.0);
    float _u4_used() {
      return 1.0;
    void main() {
      gl_FragColor = _u4_getColor();

    All top-level symbols outside the main module are namespaced with a prefix like _u4_ to avoid collisions, unless marked as global.


    For dynamic linking at run-time, you link up with a function prototype instead:


    vec4 getColor();
    void main() {
      gl_FragColor = getColor();

    You can import named symbols from .glsl files in JS/TS, and use them directly as links:

    import mainShader from 'path/to/main.glsl';
    import { getColor } from 'path/to/color.glsl';
    const glslCode = linkBundle(mainShader, {getColor});

    The linking mechanism works the same.


    Does this interpret pre-processor directives?

    No. It ignores and passes through all other #directives. This is done to avoid having to re-parse when definitions change.

    This means the linker sees all top-level declarations regardless of #ifs, and resolves all imports.

    You can mark prototypes as #pragma optional if it is ok to leave them unlinked.

    Does this work for WGSL?

    Not right now. Though plugging in a WGSL grammar and doing the same trick should be feasible.

    Isn't it silly to ship and work with strings instead of byte code?

    Processing pre-parsed GLSL bundles is very fast and simple, even with tree shaking. Rewriting a SPIR-V program the same way is much more fiddly.



    Returns linked GLSL code by assembling:

    • code / module / bundle: Main module.
    • modules: Dictionary of modules to import manually from. { [path]: T }
    • links: Dictionary of modules to link specific prototypes to. { [name]: T }
    • defines: Dictionary of key/values to #define at the start.
    • cache: Override the internal cache or disable it.

    Use from:to as the link name to link two differently named functions. This is equivalent to a static import { $to as $from } ....


    Link direct source code.

      code: string,
      modules: Record<string, string> = {},
      links: Record<string, string> = {},
      defines: Record<string, string | number | boolean | null | undefined> = {},
      cache?: LRU | null,
    ) => string;


    Link parsed modules.

      module: ParsedModule,
      modules: Record<string, ParsedModule> = {},
      links: Record<string, ParsedModule> = {},
      defines: Record<string, string | number | boolean | null | undefined> = {},
    ) => string;


    Link packaged bundle of module + libs.

      bundle: ParsedBundle,
      links: Record<string, ParsedBundle> = {},
      defines: Record<string, string | number | boolean | null | undefined> = {},
    ) => string;


    (s: string) => void

    Replace the global #version 450 preamble with another string.


    Link modules/bundles together into a new bundle at run-time. i.e.:

    linkBundle(bindBundle(bundle, {links})) is equivalent to linkBundle(bundle, {links}).

    This is a fast operation which only affects the top-level module in a bundle.

    Provide a unique key to ensure deterministic output.


      bundle: ShaderModule,
      links: Record<string, ParsedModule | ParsedBundle> = {},
      defines?: Record<string, ShaderDefine> | null,
      key?: string | number,
    ) => ParsedBundle;


      main: ParsedModule,
      libs: Record<string, ParsedModule | ParsedBundle> = {},
      links: Record<string, ParsedModule | ParsedBundle> = {},
      defines?: Record<string, ShaderDefine> | null,
      key?: string | number,
    ) => ParsedBundle;


    Specify entry to point to a specific symbol as entry point.


    Parse a code module into its in-memory representation (AST + symbol/shake table).

      code: string,
      name: string,
      entry?: string,
      compressed: boolean = false,
    ) => ParsedModule;


    Load a module from the given cache, or parse it if missing.

      code: string,
      name: string,
      entry?: string,
      cache: LRU | null = null,
    ) => ParsedModule;


    Wrapper around npm LRU.

    Type Summary

    export type ParsedModule = {
      name: string,
      code: string,
      table: SymbolTable,
      tree?: Tree,
      shake?: ShakeTable,
      entry?: string,
    export type ParsedBundle = {
      module: ParsedModule,
      libs: Record<string, ParsedBundle>,
      entry?: string,
    export type ParsedModuleCache = LRU<string, ParsedModule>;


    Made by Steven Wittens. Part of @use-gpu.




    npm i @use-gpu/shader

    DownloadsWeekly Downloads






    Unpacked Size

    121 kB

    Total Files


    Last publish


    • unconed