├── .gitignore ├── README.md ├── main.d.ts ├── main.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esbuild-plugin-glslx 2 | 3 | A plugin for [esbuild](https://github.com/evanw/esbuild) that adds support for `*.glslx` file imports including shader type checking at build time. [GLSLX](https://github.com/evanw/glslx) is a language extension for GLSL that lets you write multiple WebGL 1.0 shaders in the same file using the `export` keyword. It comes with a [GLSLX Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=evanw.glslx-vscode) that enables standard IDE features for GLSLX including type checking, go-to-definition, symbol renaming, and format-on-save. GLSLX code looks something like this: 4 | 5 | ```glsl 6 | uniform sampler2D tex; 7 | attribute vec3 pos; 8 | varying vec2 coord; 9 | 10 | export void yourVertexShader() { 11 | coord = pos.xy; 12 | gl_Position = vec4(pos, 1); 13 | } 14 | 15 | export void yourFragmentShader() { 16 | gl_FragColor = texture2D(tex, coord); 17 | } 18 | ``` 19 | 20 | ## Basic Usage 21 | 22 | 1. Install this plugin in your project: 23 | 24 | ```sh 25 | npm install --save-dev esbuild-plugin-glslx 26 | ``` 27 | 28 | 2. Add this plugin to your esbuild build script: 29 | 30 | ```diff 31 | +const glslxPlugin = require('esbuild-plugin-glslx') 32 | ... 33 | esbuild.build({ 34 | ... 35 | plugins: [ 36 | + glslxPlugin(), 37 | ], 38 | }) 39 | ``` 40 | 41 | 3. Import your `*.glslx` file from JavaScript: 42 | 43 | ```js 44 | import { yourVertexShader, yourFragmentShader } from './shaders.glslx' 45 | 46 | // Each shader is a string that you can pass to your WebGL 1.0 rendering engine of choice 47 | const material = THREE.RawShaderMaterial({ 48 | vertexShader: yourVertexShader, 49 | fragmentShader: yourFragmentShader, 50 | }) 51 | ``` 52 | 53 | ## Usage with TypeScript 54 | 55 | If you would like to use GLSLX with TypeScript, you'll have to tell the TypeScript compiler how to interpret imports from `*.glslx` files. You have two options: 56 | 57 | * One way to do this is to add a blanket type declaration for all `*.glslx` files such that every import is considered to be a string: 58 | 59 | ```ts 60 | // Save this as the file "glslx.d.ts" alongside your source code 61 | declare module "*.glslx" { 62 | var values: Record; 63 | export = values; 64 | } 65 | ``` 66 | 67 | This means you can't use named imports anymore, but you can still use namespace imports like this: 68 | 69 | ```js 70 | import * as shaders from './shaders.glslx' 71 | console.log(shaders.yourVertexShader, shaders.yourFragmentShader) 72 | ``` 73 | 74 | * Another way to do this is to generate a `*.glslx.d.ts` file for each `*.glslx` file with a type declaration for each exported shader. This plugin can do that for you if you enable the `writeTypeDeclarations` option: 75 | 76 | ```diff 77 | const glslxPlugin = require('esbuild-plugin-glslx') 78 | 79 | esbuild.build({ 80 | ... 81 | plugins: [ 82 | glslxPlugin({ 83 | + writeTypeDeclarations: true, 84 | }), 85 | ], 86 | }) 87 | ``` 88 | 89 | Then you can use any kind of imports in TypeScript including named imports. 90 | -------------------------------------------------------------------------------- /main.d.ts: -------------------------------------------------------------------------------- 1 | declare function glslxPlugin(options?: { 2 | // If true, write out "*.glslx.d.ts" files 3 | writeTypeDeclarations?: boolean; 4 | }): { 5 | name: string; 6 | setup(build: any): void; 7 | }; 8 | 9 | export = glslxPlugin; 10 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const glslx = require('glslx'); 2 | 3 | module.exports = (options = {}) => ({ 4 | name: 'glslx', 5 | setup(build) { 6 | const writeTypeDeclarations = !!(options && options.writeTypeDeclarations); 7 | const path = require('path'); 8 | const fs = require('fs'); 9 | 10 | build.onLoad({ filter: /\.glslx$/ }, async (args) => { 11 | const contents = await fs.promises.readFile(args.path, 'utf8'); 12 | const input = [{ name: args.path, contents }]; 13 | const errors = []; 14 | const warnings = []; 15 | const watchFiles = []; 16 | const cache = Object.create(null); 17 | cache[args.path] = contents; 18 | 19 | const fileAccess = (filePath, relativeTo) => { 20 | const name = path.join(path.dirname(relativeTo), filePath); 21 | let contents = cache[name]; 22 | if (contents === undefined) { 23 | watchFiles.push(name); 24 | try { 25 | contents = fs.readFileSync(name, 'utf8'); 26 | } catch { 27 | return null; 28 | } 29 | cache[name] = contents; 30 | } 31 | return { name, contents }; 32 | }; 33 | 34 | for (const { kind, text, range } of glslx.compileIDE(input, { fileAccess }).diagnostics) { 35 | const message = { text }; 36 | 37 | if (range) { 38 | const lineText = cache[range.source].split(/\r|\n|\r\n/g)[range.start.line]; 39 | message.location = { 40 | file: range.source, 41 | line: range.start.line + 1, 42 | column: range.start.column, 43 | length: range.end.line === range.start.line ? range.end.column - range.start.column : 0, 44 | lineText, 45 | }; 46 | } 47 | 48 | if (kind === 'error') errors.push(message); 49 | if (kind === 'warning') warnings.push(message); 50 | } 51 | 52 | if (errors.length > 0) return { errors, warnings, watchFiles }; 53 | 54 | const json = JSON.parse(glslx.compile(input, { 55 | format: 'json', 56 | fileAccess, 57 | prettyPrint: true, 58 | disableRewriting: true, 59 | renaming: 'none', 60 | }).output); 61 | 62 | let js = ''; 63 | let ts = ''; 64 | for (const shader of json.shaders) { 65 | js += `export var ${shader.name} = ${JSON.stringify(shader.contents)};\n`; 66 | ts += `export var ${shader.name}: string;\n`; 67 | } 68 | if (writeTypeDeclarations) { 69 | await fs.promises.writeFile(args.path + '.d.ts', ts); 70 | } 71 | return { contents: js, warnings, watchFiles }; 72 | }); 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esbuild-plugin-glslx", 3 | "version": "0.0.2", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "esbuild-plugin-glslx", 9 | "version": "0.0.2", 10 | "license": "MIT", 11 | "dependencies": { 12 | "glslx": "0.3.0" 13 | } 14 | }, 15 | "node_modules/glslx": { 16 | "version": "0.3.0", 17 | "resolved": "https://registry.npmjs.org/glslx/-/glslx-0.3.0.tgz", 18 | "integrity": "sha512-T6RUrpLr7fRXUFWc5nAUN42tEzjYDLEJC1HcjIL7tXAGfsjXXslTmFhsC8cJPnB22r1D1MaXt/JmY/ugrXRj/Q==", 19 | "bin": { 20 | "glslx": "glslx" 21 | } 22 | } 23 | }, 24 | "dependencies": { 25 | "glslx": { 26 | "version": "0.3.0", 27 | "resolved": "https://registry.npmjs.org/glslx/-/glslx-0.3.0.tgz", 28 | "integrity": "sha512-T6RUrpLr7fRXUFWc5nAUN42tEzjYDLEJC1HcjIL7tXAGfsjXXslTmFhsC8cJPnB22r1D1MaXt/JmY/ugrXRj/Q==" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esbuild-plugin-glslx", 3 | "description": "A plugin for esbuild that enables importing *.glslx files.", 4 | "version": "0.0.4", 5 | "license": "MIT", 6 | "repository": "https://github.com/evanw/esbuild-plugin-glslx", 7 | "main": "./main.js", 8 | "types": "./main.d.ts", 9 | "dependencies": { 10 | "glslx": "0.3.0" 11 | } 12 | } 13 | --------------------------------------------------------------------------------