├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── bun.lock ├── ext.d.ts ├── favicon.svg ├── index.html ├── package.json ├── src ├── index.d.ts ├── index.js ├── loadShader.d.ts ├── loadShader.js └── types.d.ts ├── test ├── glsl │ ├── chunk0.frag │ ├── chunk3.frag │ ├── main.frag │ └── utils │ │ ├── chunk1.glsl │ │ └── chunk2.frag ├── index.js └── wgsl │ ├── chunk0.wgsl │ ├── chunk3.wgsl │ ├── main.wgsl │ └── utils │ ├── chunk1.wgsl │ └── chunk2.wgsl ├── vite-plugin-glsl.code-workspace └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders: 2 | node_modules/ 3 | test/glsl/lygia 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Folders: 2 | test 3 | node_modules 4 | 5 | # Files: 6 | .gitignore 7 | index.html 8 | favicon.svg 9 | vite.config.js 10 | *.code-workspace 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @ustym:registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 - 2025 Ustym Ukhman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vite Plugin GLSL # 2 | 3 | > Import, inline (and minify) GLSL/WGSL shader files 4 | 5 | ![npm](https://img.shields.io/npm/dt/vite-plugin-glsl?style=flat-square) 6 | ![GitHub package.json version](https://img.shields.io/github/package-json/v/UstymUkhman/vite-plugin-glsl?color=brightgreen&style=flat-square) 7 | ![GitHub](https://img.shields.io/github/license/UstymUkhman/vite-plugin-glsl?color=brightgreen&style=flat-square) 8 | 9 | _Inspired by [threejs-glsl-loader](https://github.com/MONOGRID/threejs-glsl-loader) and [vite-plugin-string](https://github.com/aweikalee/vite-plugin-string), compatible with [Babylon.js](https://www.babylonjs.com/), [three.js](https://threejs.org/) and [lygia](https://lygia.xyz/)._ 10 | 11 | ## Installation ## 12 | 13 | ```sh 14 | npm i vite-plugin-glsl --save-dev 15 | # or 16 | yarn add vite-plugin-glsl --dev 17 | # or 18 | pnpm add -D vite-plugin-glsl 19 | # or 20 | bun add vite-plugin-glsl --dev 21 | ``` 22 | 23 | ## Usage ## 24 | 25 | ```js 26 | // vite.config.js 27 | import glsl from 'vite-plugin-glsl'; 28 | import { defineConfig } from 'vite'; 29 | 30 | export default defineConfig({ 31 | plugins: [glsl()] 32 | }); 33 | ``` 34 | 35 | ### With TypeScript ### 36 | 37 | Add extension declarations to your [`types`](https://www.typescriptlang.org/tsconfig#types) in `tsconfig.json`: 38 | 39 | ```json 40 | { 41 | "compilerOptions": { 42 | "types": [ 43 | "vite-plugin-glsl/ext" 44 | ] 45 | } 46 | } 47 | ``` 48 | 49 | or as a [package dependency directive](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-) to your global types: 50 | 51 | ```ts 52 | /// 53 | ``` 54 | 55 | ## Default Options ## 56 | 57 | ```js 58 | glsl({ 59 | include: [ // Glob pattern, or array of glob patterns to import 60 | '**/*.glsl', '**/*.wgsl', 61 | '**/*.vert', '**/*.frag', 62 | '**/*.vs', '**/*.fs' 63 | ], 64 | exclude: undefined, // Glob pattern, or array of glob patterns to ignore 65 | warnDuplicatedImports: true, // Warn if the same chunk was imported multiple times 66 | removeDuplicatedImports: false, // Automatically remove an already imported chunk 67 | defaultExtension: 'glsl', // Shader suffix when no extension is specified 68 | minify: false, // Minify/optimize output shader code 69 | watch: true, // Recompile shader on change 70 | root: '/' // Directory for root imports 71 | }) 72 | ``` 73 | 74 | ## Example ## 75 | 76 | ``` 77 | root 78 | ├── src/ 79 | │ ├── glsl/ 80 | │ │ ├── chunk0.frag 81 | │ │ ├── chunk3.frag 82 | │ │ ├── main.frag 83 | │ │ ├── main.vert 84 | │ │ └── utils/ 85 | │ │ ├── chunk1.glsl 86 | │ │ └── chunk2.frag 87 | │ └── main.js 88 | ├── vite.config.js 89 | └── package.json 90 | ``` 91 | 92 | ```js 93 | // main.js 94 | import fragment from './glsl/main.frag'; 95 | ``` 96 | 97 | ```glsl 98 | // main.frag 99 | #version 300 es 100 | 101 | #ifndef GL_FRAGMENT_PRECISION_HIGH 102 | precision mediump float; 103 | #else 104 | precision highp float; 105 | #endif 106 | 107 | out vec4 fragColor; 108 | 109 | #include chunk0.frag; 110 | 111 | void main (void) { 112 | fragColor = chunkFn(); 113 | } 114 | ``` 115 | 116 | ```glsl 117 | // chunk0.frag 118 | 119 | // ".glsl" extension will be added automatically: 120 | #include utils/chunk1; 121 | 122 | vec4 chunkFn () { 123 | return vec4(chunkRGB(), 1.0); 124 | } 125 | ``` 126 | 127 | ```glsl 128 | // utils/chunk1.glsl 129 | 130 | #include chunk2.frag; 131 | #include ../chunk3.frag; 132 | 133 | vec3 chunkRGB () { 134 | return vec3(chunkRed(), chunkGreen(), 0.0); 135 | } 136 | ``` 137 | 138 | ```glsl 139 | // utils/chunk2.frag 140 | 141 | float chunkRed () { 142 | return 0.0; 143 | } 144 | ``` 145 | 146 | ```glsl 147 | // chunk3.frag 148 | 149 | float chunkGreen () { 150 | return 0.8; 151 | } 152 | ``` 153 | 154 | Will result in: 155 | 156 | ```glsl 157 | // main.frag 158 | #version 300 es 159 | 160 | #ifndef GL_FRAGMENT_PRECISION_HIGH 161 | precision mediump float; 162 | #else 163 | precision highp float; 164 | #endif 165 | 166 | out vec4 fragColor; 167 | 168 | float chunkRed () { 169 | return 0.0; 170 | } 171 | 172 | float chunkGreen () { 173 | return 0.8; 174 | } 175 | 176 | vec3 chunkRGB () { 177 | return vec3(chunkRed(), chunkGreen(), 0.0); 178 | } 179 | 180 | vec4 chunkFn () { 181 | return vec4(chunkRGB(), 1.0); 182 | } 183 | 184 | void main (void) { 185 | fragColor = chunkFn(); 186 | } 187 | ``` 188 | 189 | ## Change Log ## 190 | 191 | - Starting from `v1.4.0` `compress` option was renamed to `minify` and now it allows a promise callback. 192 | 193 | - Starting from `v1.3.2` this plugin allows to automatically remove already imported chunks with the `removeDuplicatedImports` option set to `true`. 194 | 195 | - Starting from `v1.3.1` this plugin is fully compatible with `vite^6.0.0`. 196 | 197 | - Starting from `v1.3.0` this plugin will not remove comments starting with `///`, unless `compress` option is set to `true`. 198 | 199 | - Starting from `v1.2.0` this plugin is fully compatible with `vite^5.0.0`. 200 | 201 | - Starting from `v1.1.1` this plugin has a complete TypeScript support. Check "Usage" > "With TypeScript" for more info. 202 | 203 | - Starting from `v1.0.0` this plugin is fully compatible with `vite^4.0.0`. 204 | 205 | - Starting from `v0.5.4` this plugin supports custom `compress` callback function to optimize output shader length after all shader chunks have been included. 206 | 207 | - Starting from `v0.5.0` this plugin supports shaders hot reloading when `watch` option is set to `true`. 208 | 209 | - Starting from `v0.4.0` this plugin supports chunk imports from project root and `root` option to override the default root directory. 210 | 211 | - Starting from `v0.3.0` this plugin is pure [ESM](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). Consider updating your project to an ESM module by adding `"type": "module"` in your `package.json` or consult [this](https://github.com/UstymUkhman/vite-plugin-glsl/issues/16) issue for possible workarounds. 212 | 213 | - Starting from `v0.2.2` this plugin supports `compress` option to optimize output shader length. You might consider setting this to `true` in production environment. 214 | 215 | - Starting from `v0.2.0` this plugin uses a config object as a single argument to `glsl` function and allows to disable import warnings with the `warnDuplicatedImports` param set to `false`. 216 | 217 | - Starting from `v0.1.5` this plugin warns about duplicated chunks imports and throws an error when a recursive loop occurres. 218 | 219 | - Starting from `v0.1.2` this plugin generates sourcemaps using vite esbuild when the `sourcemap` [option](https://github.com/UstymUkhman/vite-plugin-glsl/blob/main/vite.config.js#L5) is set to `true`. 220 | 221 | - Starting from `v0.1.0` this plugin supports WebGPU shaders with `.wgsl` extension. 222 | 223 | - Starting from `v0.0.9` this plugin supports optional semicolons at the end of `#include` statements. 224 | 225 | - Starting from `v0.0.7` this plugin supports optional single and double quotation marks around file names. 226 | 227 | ### Note: ### 228 | 229 | When used with [three.js](https://github.com/mrdoob/three.js) r0.99 and higher, it's possible to include shader chunks as specified in the [documentation](https://threejs.org/docs/index.html?q=Shader#api/en/materials/ShaderMaterial), those imports will be ignored by `vite-plugin-glsl` since they are handled internally by the library itself: 230 | 231 | ```glsl 232 | #include 233 | 234 | vec3 randVec3 (const in vec2 uv) { 235 | return vec3( 236 | rand(uv * 0.1), rand(uv * 2.5), rand(uv) 237 | ); 238 | } 239 | ``` 240 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "vite-plugin-glsl", 6 | "dependencies": { 7 | "@rollup/pluginutils": "^5.1.4", 8 | }, 9 | "devDependencies": { 10 | "vite": "^6.1.0", 11 | }, 12 | "peerDependencies": { 13 | "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", 14 | }, 15 | }, 16 | }, 17 | "packages": { 18 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], 19 | 20 | "@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="], 21 | 22 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="], 23 | 24 | "@esbuild/android-x64": ["@esbuild/android-x64@0.24.2", "", { "os": "android", "cpu": "x64" }, "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="], 25 | 26 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="], 27 | 28 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="], 29 | 30 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="], 31 | 32 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="], 33 | 34 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="], 35 | 36 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="], 37 | 38 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="], 39 | 40 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="], 41 | 42 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="], 43 | 44 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="], 45 | 46 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="], 47 | 48 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="], 49 | 50 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="], 51 | 52 | "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="], 53 | 54 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.2", "", { "os": "none", "cpu": "x64" }, "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="], 55 | 56 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="], 57 | 58 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="], 59 | 60 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="], 61 | 62 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="], 63 | 64 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="], 65 | 66 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="], 67 | 68 | "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], 69 | 70 | "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.6", "", { "os": "android", "cpu": "arm" }, "sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg=="], 71 | 72 | "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.6", "", { "os": "android", "cpu": "arm64" }, "sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA=="], 73 | 74 | "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg=="], 75 | 76 | "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg=="], 77 | 78 | "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.6", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ=="], 79 | 80 | "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ=="], 81 | 82 | "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.6", "", { "os": "linux", "cpu": "arm" }, "sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg=="], 83 | 84 | "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.6", "", { "os": "linux", "cpu": "arm" }, "sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg=="], 85 | 86 | "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA=="], 87 | 88 | "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q=="], 89 | 90 | "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.6", "", { "os": "linux", "cpu": "none" }, "sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw=="], 91 | 92 | "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ=="], 93 | 94 | "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.6", "", { "os": "linux", "cpu": "none" }, "sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg=="], 95 | 96 | "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw=="], 97 | 98 | "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw=="], 99 | 100 | "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.6", "", { "os": "linux", "cpu": "x64" }, "sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A=="], 101 | 102 | "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA=="], 103 | 104 | "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA=="], 105 | 106 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.6", "", { "os": "win32", "cpu": "x64" }, "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w=="], 107 | 108 | "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], 109 | 110 | "esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="], 111 | 112 | "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], 113 | 114 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 115 | 116 | "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], 117 | 118 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 119 | 120 | "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], 121 | 122 | "postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="], 123 | 124 | "rollup": ["rollup@4.34.6", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.6", "@rollup/rollup-android-arm64": "4.34.6", "@rollup/rollup-darwin-arm64": "4.34.6", "@rollup/rollup-darwin-x64": "4.34.6", "@rollup/rollup-freebsd-arm64": "4.34.6", "@rollup/rollup-freebsd-x64": "4.34.6", "@rollup/rollup-linux-arm-gnueabihf": "4.34.6", "@rollup/rollup-linux-arm-musleabihf": "4.34.6", "@rollup/rollup-linux-arm64-gnu": "4.34.6", "@rollup/rollup-linux-arm64-musl": "4.34.6", "@rollup/rollup-linux-loongarch64-gnu": "4.34.6", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.6", "@rollup/rollup-linux-riscv64-gnu": "4.34.6", "@rollup/rollup-linux-s390x-gnu": "4.34.6", "@rollup/rollup-linux-x64-gnu": "4.34.6", "@rollup/rollup-linux-x64-musl": "4.34.6", "@rollup/rollup-win32-arm64-msvc": "4.34.6", "@rollup/rollup-win32-ia32-msvc": "4.34.6", "@rollup/rollup-win32-x64-msvc": "4.34.6", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ=="], 125 | 126 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 127 | 128 | "vite": ["vite@6.1.0", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.1", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ=="], 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /ext.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @const 3 | * @readonly 4 | * @kind module 5 | * @description Generic shaders 6 | */ 7 | declare module '*.glsl' { 8 | const shader: string; 9 | export default shader; 10 | } 11 | 12 | /** 13 | * @const 14 | * @readonly 15 | * @kind module 16 | * @description WebGPU shaders 17 | */ 18 | declare module '*.wgsl' { 19 | const shader: string; 20 | export default shader; 21 | } 22 | 23 | /** 24 | * @const 25 | * @readonly 26 | * @kind module 27 | * @description Vertex shaders 28 | */ 29 | declare module '*.vert' { 30 | const shader: string; 31 | export default shader; 32 | } 33 | 34 | /** 35 | * @const 36 | * @readonly 37 | * @kind module 38 | * @description Fragment shaders 39 | */ 40 | declare module '*.frag' { 41 | const shader: string; 42 | export default shader; 43 | } 44 | 45 | /** 46 | * @const 47 | * @readonly 48 | * @kind module 49 | * @description Vertex shaders 50 | */ 51 | declare module '*.vs' { 52 | const shader: string; 53 | export default shader; 54 | } 55 | 56 | /** 57 | * @const 58 | * @readonly 59 | * @kind module 60 | * @description Fragment shaders 61 | */ 62 | declare module '*.fs' { 63 | const shader: string; 64 | export default shader; 65 | } 66 | -------------------------------------------------------------------------------- /favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite Plugin GLSL 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-glsl", 3 | "description": "Import, inline (and minify) GLSL/WGSL shader files", 4 | "homepage": "https://github.com/UstymUkhman/vite-plugin-glsl#readme", 5 | "packageManager": "^npm@10.8.3", 6 | "types": "./src/index.d.ts", 7 | "module": "./src/index.js", 8 | "main": "./src/index.js", 9 | "version": "1.4.2", 10 | "private": false, 11 | "license": "MIT", 12 | "type": "module", 13 | "author": { 14 | "name": "Ustym Ukhman", 15 | "email": "ustym.ukhman@gmail.com", 16 | "url": "https://github.com/UstymUkhman/" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/UstymUkhman/vite-plugin-glsl.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/UstymUkhman/vite-plugin-glsl/issues", 24 | "email": "ustym.ukhman@gmail.com" 25 | }, 26 | "publishConfig": { 27 | "save-dev": true, 28 | "access": "public", 29 | "registry": "https://registry.npmjs.org/" 30 | }, 31 | "exports": { 32 | ".": "./src/index.js", 33 | "./ext": { 34 | "types": "./ext.d.ts" 35 | } 36 | }, 37 | "files": [ 38 | "package.json", 39 | "README.md", 40 | "bun.lock", 41 | "ext.d.ts", 42 | "LICENSE", 43 | "src" 44 | ], 45 | "keywords": [ 46 | "vite", 47 | "glsl", 48 | "wgsl", 49 | "lygia", 50 | "webgl", 51 | "webgpu", 52 | "vitejs", 53 | "plugin", 54 | "threejs", 55 | "shaders", 56 | "babylonjs", 57 | "vite-plugin", 58 | "glsl-shaders", 59 | "wgsl-shaders", 60 | "webgl-shaders", 61 | "webgpu-shaders" 62 | ], 63 | "scripts": { 64 | "test": "vite" 65 | }, 66 | "peerDependencies": { 67 | "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" 68 | }, 69 | "dependencies": { 70 | "@rollup/pluginutils": "^5.1.4" 71 | }, 72 | "devDependencies": { 73 | "vite": "^6.1.0" 74 | }, 75 | "engines": { 76 | "node": ">= 20.17.0", 77 | "npm": ">= 10.8.3" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOptions } from './types.d'; 2 | import type { Plugin } from 'vite'; 3 | export type { PluginOptions }; 4 | 5 | /** 6 | * @function 7 | * @name glsl 8 | * @description Plugin entry point to import, 9 | * inline, (and minify) GLSL/WGSL shader files 10 | * 11 | * @see {@link https://vitejs.dev/guide/api-plugin.html} 12 | * @link https://github.com/UstymUkhman/vite-plugin-glsl 13 | * 14 | * @param {PluginOptions} options Plugin config object 15 | * 16 | * @returns {Plugin} Vite plugin that converts shader code 17 | */ 18 | export default function ({ 19 | include, exclude, 20 | warnDuplicatedImports, 21 | removeDuplicatedImports, 22 | defaultExtension, 23 | minify, 24 | watch, 25 | root 26 | }?: PluginOptions): Plugin; 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module vite-plugin-glsl 3 | * @author Ustym Ukhman 4 | * @description Import, inline (and minify) GLSL/WGSL shader files 5 | * @version 1.4.2 6 | * @license MIT 7 | */ 8 | 9 | import { createFilter } from '@rollup/pluginutils'; 10 | import { transformWithEsbuild } from 'vite'; 11 | import loadShader from './loadShader.js'; 12 | 13 | /** 14 | * @const 15 | * @default 16 | * @readonly 17 | * @type {string} 18 | */ 19 | const DEFAULT_EXTENSION = 'glsl'; 20 | 21 | /** 22 | * @const 23 | * @default 24 | * @readonly 25 | * @type {readonly RegExp[]} 26 | */ 27 | const DEFAULT_SHADERS = Object.freeze([ 28 | '**/*.glsl', '**/*.wgsl', 29 | '**/*.vert', '**/*.frag', 30 | '**/*.vs', '**/*.fs' 31 | ]); 32 | 33 | /** 34 | * @function 35 | * @name glsl 36 | * @description Plugin entry point to import, 37 | * inline, (and minify) GLSL/WGSL shader files 38 | * 39 | * @see {@link https://vitejs.dev/guide/api-plugin.html} 40 | * @link https://github.com/UstymUkhman/vite-plugin-glsl 41 | * 42 | * @param {import('./types').PluginOptions} options Plugin config object 43 | * 44 | * @returns {import('vite').Plugin} Vite plugin that converts shader code 45 | */ 46 | export default function ({ 47 | include = DEFAULT_SHADERS, 48 | exclude = undefined, 49 | warnDuplicatedImports = true, 50 | removeDuplicatedImports = false, 51 | defaultExtension = DEFAULT_EXTENSION, 52 | minify = false, 53 | watch = true, 54 | root = '/' 55 | } = {} 56 | ) { 57 | let sourcemap = false; 58 | const filter = createFilter(include, exclude); 59 | const prod = process.env.NODE_ENV === 'production'; 60 | 61 | return { 62 | enforce: 'pre', 63 | name: 'vite-plugin-glsl', 64 | 65 | configResolved (resolvedConfig) { 66 | sourcemap = resolvedConfig.build.sourcemap; 67 | }, 68 | 69 | async transform (source, shader) { 70 | if (!filter(shader)) return; 71 | 72 | const { dependentChunks, outputShader } = await loadShader(source, shader, { 73 | removeDuplicatedImports, 74 | warnDuplicatedImports, 75 | defaultExtension, 76 | minify, 77 | root 78 | }); 79 | 80 | watch && !prod && Array.from(dependentChunks.values()) 81 | .flat().forEach(chunk => this.addWatchFile(chunk)); 82 | 83 | return await transformWithEsbuild(outputShader, shader, { 84 | sourcemap: sourcemap && 'external', 85 | loader: 'text', format: 'esm', 86 | minifyWhitespace: prod 87 | }); 88 | } 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /src/loadShader.d.ts: -------------------------------------------------------------------------------- 1 | import type { LoadingOptions, LoadingOutput } from './types.d'; 2 | 3 | /** 4 | * @function 5 | * @name loadShader 6 | * @description Iterates through all external chunks, includes them 7 | * into the shader's source code and optionally minifies the output 8 | * 9 | * @param {string} source Shader's source code 10 | * @param {string} shader Shader's absolute path 11 | * @param {LoadingOptions} options Configuration object to define: 12 | * 13 | * - Warn if the same chunk was imported multiple times 14 | * - Automatically remove an already imported chunk 15 | * - Shader suffix when no extension is specified 16 | * - Directory for root imports 17 | * - Minify output shader code 18 | * 19 | * @returns {Promise} Loaded, parsed (and minified) 20 | * shader output and Map of shaders that import other chunks 21 | */ 22 | export default async function ( 23 | source: string, shader: string, 24 | options: LoadingOptions 25 | ): Promise; 26 | -------------------------------------------------------------------------------- /src/loadShader.js: -------------------------------------------------------------------------------- 1 | import { dirname, resolve, extname, posix, sep } from 'path'; 2 | import { emitWarning, cwd } from 'process'; 3 | import { readFileSync } from 'fs'; 4 | import { platform } from 'os'; 5 | 6 | /** 7 | * @name recursiveChunk 8 | * @type {string} 9 | * 10 | * @description Shader chunk path 11 | * that caused a recursion error 12 | */ 13 | let recursiveChunk = ''; 14 | 15 | /** 16 | * @const 17 | * @name allChunks 18 | * @type {readonly Set} 19 | * 20 | * @description List of all shader chunks, 21 | * it's used to track included files 22 | */ 23 | const allChunks = new Set(); 24 | 25 | /** 26 | * @const 27 | * @name dependentChunks 28 | * @type {readonly Map} 29 | * 30 | * @description Map of shaders that import other chunks, it's 31 | * used to track included files in order to avoid recursion 32 | * - Key: shader path that uses other chunks as dependencies 33 | * - Value: list of chunk paths included within the shader 34 | */ 35 | const dependentChunks = new Map(); 36 | 37 | /** 38 | * @const 39 | * @name duplicatedChunks 40 | * @type {readonly Map} 41 | * 42 | * @description Map of duplicated shader 43 | * imports, used by warning messages 44 | */ 45 | const duplicatedChunks = new Map(); 46 | 47 | /** 48 | * @const 49 | * @name include 50 | * @type {readonly RegExp} 51 | * 52 | * @description RegEx to match GLSL 53 | * `#include` preprocessor instruction 54 | */ 55 | const include = /#include(\s+([^\s<>]+));?/gi; 56 | 57 | /** 58 | * @function 59 | * @name resetSavedChunks 60 | * @description Clears all lists of saved chunks 61 | * and resets "recursiveChunk" path to empty 62 | * 63 | * @returns {string} Copy of "recursiveChunk" path 64 | */ 65 | function resetSavedChunks () { 66 | const chunk = recursiveChunk; 67 | duplicatedChunks.clear(); 68 | dependentChunks.clear(); 69 | 70 | recursiveChunk = ''; 71 | allChunks.clear(); 72 | return chunk; 73 | } 74 | 75 | /** 76 | * @function 77 | * @name getRecursionCaller 78 | * @description Gets last chunk that caused a 79 | * recursion error from the "dependentChunks" list 80 | * 81 | * @returns {string} Chunk path that started a recursion 82 | */ 83 | function getRecursionCaller () { 84 | const dependencies = [...dependentChunks.keys()]; 85 | return dependencies[dependencies.length - 1]; 86 | } 87 | 88 | /** 89 | * @function 90 | * @name checkDuplicatedImports 91 | * @description Checks if shader chunk was already included 92 | * and adds it to the "duplicatedChunks" list if yes 93 | * 94 | * @param {string} path Shader's absolute path 95 | * 96 | * @throws {Warning} If shader chunk was already included 97 | */ 98 | function checkDuplicatedImports (path) { 99 | const caller = getRecursionCaller(); 100 | 101 | const chunks = duplicatedChunks.get(caller) ?? []; 102 | if (chunks.includes(path)) return; 103 | 104 | chunks.push(path); 105 | duplicatedChunks.set(caller, chunks); 106 | 107 | emitWarning(`'${path}' was included multiple times.`, { 108 | code: 'vite-plugin-glsl', 109 | detail: 'Please avoid multiple imports of the same chunk in order to avoid' + 110 | ` recursions and optimize your shader length.\nDuplicated import found in file '${caller}'.` 111 | }); 112 | } 113 | 114 | /** 115 | * @function 116 | * @name removeSourceComments 117 | * @description Removes comments from shader source 118 | * code in order to avoid including commented chunks 119 | * 120 | * @param {string} source Shader's source code 121 | * @param {boolean} triple Remove comments starting with `///` 122 | * 123 | * @returns {string} Shader's source code without comments 124 | */ 125 | function removeSourceComments (source, triple = false) { 126 | if (source.includes('/*') && source.includes('*/')) { 127 | source = source.slice(0, source.indexOf('/*')) + 128 | source.slice(source.indexOf('*/') + 2, source.length); 129 | } 130 | 131 | const lines = source.split('\n'); 132 | 133 | for (let l = lines.length; l--; ) { 134 | const index = lines[l].indexOf('//'); 135 | 136 | if (index > -1) { 137 | if (lines[l][index + 2] === '/' && !include.test(lines[l]) && !triple) continue; 138 | lines[l] = lines[l].slice(0, lines[l].indexOf('//')); 139 | } 140 | } 141 | 142 | return lines.join('\n'); 143 | } 144 | 145 | /** 146 | * @function 147 | * @name checkRecursiveImports 148 | * @description Checks if shader dependencies 149 | * have caused a recursion error or warning 150 | * ignoring duplicate chunks if required 151 | * 152 | * @param {string} path Shader's absolute path 153 | * @param {string} lowPath Shader's lowercase path 154 | * @param {boolean} warn Check already included chunks 155 | * @param {boolean} ignore Ignore already included chunks 156 | * 157 | * @returns {boolean | null} Import recursion has occurred 158 | * or chunk was ignored because of `ignore` argument 159 | */ 160 | function checkRecursiveImports (path, lowPath, warn, ignore) { 161 | if (allChunks.has(lowPath)) { 162 | if (ignore) return null; 163 | warn && checkDuplicatedImports(path); 164 | } 165 | 166 | return checkIncludedDependencies(path, path); 167 | } 168 | 169 | /** 170 | * @function 171 | * @name checkIncludedDependencies 172 | * @description Checks if included 173 | * chunks caused a recursion error 174 | * 175 | * @param {string} path Current chunk absolute path 176 | * @param {string} root Main shader path that imports chunks 177 | * 178 | * @returns {boolean} Included chunk started a recursion 179 | */ 180 | function checkIncludedDependencies (path, root) { 181 | const dependencies = dependentChunks.get(path); 182 | let recursiveDependency = false; 183 | 184 | if (dependencies?.includes(root)) { 185 | recursiveChunk = root; 186 | return true; 187 | } 188 | 189 | dependencies?.forEach(dependency => recursiveDependency ||= 190 | checkIncludedDependencies(dependency, root) 191 | ); 192 | 193 | return recursiveDependency; 194 | } 195 | 196 | /** 197 | * @function 198 | * @name minifyShader 199 | * @description Minifies shader source code by 200 | * removing unnecessary whitespace and empty lines 201 | * 202 | * @param {string} shader Shader code with included chunks 203 | * @param {boolean} newLine Flag to require a new line for the code 204 | * 205 | * @returns {string} Minified shader's source code 206 | */ 207 | function minifyShader (shader, newLine = false) { 208 | return shader.replace(/\\(?:\r\n|\n\r|\n|\r)|\/\*.*?\*\/|\/\/(?:\\(?:\r\n|\n\r|\n|\r)|[^\n\r])*/g, '') 209 | .split(/\n+/).reduce((result, line) => { 210 | line = line.trim().replace(/\s{2,}|\t/, ' '); 211 | 212 | if (/@(vertex|fragment|compute)/.test(line) || line.endsWith('return')) line += ' '; 213 | 214 | if (line[0] === '#') { 215 | newLine && result.push('\n'); 216 | result.push(line, '\n'); 217 | newLine = false; 218 | } 219 | 220 | else { 221 | !line.startsWith('{') && result.length && result[result.length - 1].endsWith('else') && result.push(' '); 222 | result.push(line.replace(/\s*({|}|=|\*|,|\+|\/|>|<|&|\||\[|\]|\(|\)|\-|!|;)\s*/g, '$1')); 223 | newLine = true; 224 | } 225 | 226 | return result; 227 | }, []).join('').replace(/\n+/g, '\n'); 228 | } 229 | 230 | /** 231 | * @function 232 | * @name loadChunks 233 | * @description Includes shader's dependencies 234 | * and removes comments from the source code 235 | * 236 | * @param {string} source Shader's source code 237 | * @param {string} path Shader's absolute path 238 | * @param {Options} options Shader loading config object 239 | * 240 | * @throws {Error} If shader chunks started a recursion loop 241 | * 242 | * @returns {string} Shader's source code without external chunks 243 | */ 244 | function loadChunks (source, path, options) { 245 | const { warnDuplicatedImports, removeDuplicatedImports } = options; 246 | const unixPath = path.split(sep).join(posix.sep); 247 | 248 | const chunkPath = platform() === 'win32' && 249 | unixPath.toLocaleLowerCase() || unixPath; 250 | 251 | const recursion = checkRecursiveImports( 252 | unixPath, chunkPath, 253 | warnDuplicatedImports, 254 | removeDuplicatedImports 255 | ); 256 | 257 | if (recursion) return recursiveChunk; 258 | else if (recursion === null) return ''; 259 | 260 | source = removeSourceComments(source); 261 | let directory = dirname(unixPath); 262 | allChunks.add(chunkPath); 263 | 264 | if (include.test(source)) { 265 | dependentChunks.set(unixPath, []); 266 | const currentDirectory = directory; 267 | const ext = options.defaultExtension; 268 | 269 | source = source.replace(include, (_, chunkPath) => { 270 | chunkPath = chunkPath.trim().replace(/^(?:"|')?|(?:"|')?;?$/gi, ''); 271 | 272 | if (!chunkPath.indexOf('/')) { 273 | const base = cwd().split(sep).join(posix.sep); 274 | chunkPath = base + options.root + chunkPath; 275 | } 276 | 277 | const directoryIndex = chunkPath.lastIndexOf('/'); 278 | directory = currentDirectory; 279 | 280 | if (directoryIndex !== -1) { 281 | directory = resolve(directory, chunkPath.slice(0, directoryIndex + 1)); 282 | chunkPath = chunkPath.slice(directoryIndex + 1, chunkPath.length); 283 | } 284 | 285 | let shader = resolve(directory, chunkPath); 286 | if (!extname(shader)) shader = `${shader}.${ext}`; 287 | 288 | const shaderPath = shader.split(sep).join(posix.sep); 289 | dependentChunks.get(unixPath)?.push(shaderPath); 290 | 291 | return loadChunks( 292 | readFileSync(shader, 'utf8'), 293 | shader, options 294 | ); 295 | }); 296 | } 297 | 298 | if (recursiveChunk) { 299 | const caller = getRecursionCaller(); 300 | const recursiveChunk = resetSavedChunks(); 301 | 302 | throw new Error( 303 | `Recursion detected when importing "${recursiveChunk}" in "${caller}".` 304 | ); 305 | } 306 | 307 | return source.trim().replace(/(\r\n|\r|\n){3,}/g, '$1\n'); 308 | } 309 | 310 | /** 311 | * @function 312 | * @name loadShader 313 | * @description Iterates through all external chunks, includes them 314 | * into the shader's source code and optionally minifies the output 315 | * 316 | * @typedef {import('./types').LoadingOptions} Options 317 | * @typedef {import('./types').LoadingOutput} Output 318 | * 319 | * @param {string} source Shader's source code 320 | * @param {string} shader Shader's absolute path 321 | * @param {Options} options Configuration object to define: 322 | * 323 | * - Warn if the same chunk was imported multiple times 324 | * - Automatically remove an already imported chunk 325 | * - Shader suffix when no extension is specified 326 | * - Directory for root imports 327 | * - Minify output shader code 328 | * 329 | * @returns {Promise} Loaded, parsed (and minified) 330 | * shader output and Map of shaders that import other chunks 331 | */ 332 | export default async function (source, shader, options) { 333 | const { minify, ...config } = options; 334 | 335 | resetSavedChunks(); 336 | 337 | let output = loadChunks(source, shader, config); 338 | output = minify ? removeSourceComments(output, true) : output; 339 | 340 | return { 341 | dependentChunks, 342 | outputShader: minify 343 | ? typeof minify !== 'function' 344 | ? minifyShader(output) 345 | : await minify(output) 346 | : output 347 | }; 348 | } 349 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | /** @typedef {string | string[]} GlobPattern */ 2 | export type GlobPattern = string | string[]; 3 | 4 | /** @typedef {string | string[]} Callback */ 5 | type Callback = (shader: string) => string; 6 | 7 | /** 8 | * @default false 9 | * @typedef {boolean | Callback | Promise} Minify 10 | * 11 | * @description Boolean value or custom callback 12 | * function/promise to optimize output shader length 13 | * 14 | * @param {string} shader Shader code with included chunks 15 | * 16 | * @returns {string} Minified shader's source code 17 | */ 18 | type Minify = boolean | Callback | Promise; 19 | 20 | /** 21 | * @typedef {Object} LoadingOptions 22 | * @description Shader loading config object 23 | * 24 | * @property {boolean} warnDuplicatedImports Warn if the same chunk was imported multiple times 25 | * @property {boolean} removeDuplicatedImports Automatically remove an already imported chunk 26 | * @property {string} defaultExtension Shader suffix when no extension is specified 27 | * @property {Minify} minify Minify output shader code 28 | * @property {string} root Directory for root imports 29 | */ 30 | export type LoadingOptions = { 31 | warnDuplicatedImports: boolean; 32 | removeDuplicatedImports: boolean; 33 | defaultExtension: string; 34 | minify: Minify; 35 | root: string; 36 | }; 37 | 38 | /** 39 | * @since 0.2.0 40 | * @extends LoadingOptions 41 | * @typedef {Object} PluginOptions 42 | * @description Plugin config object 43 | * 44 | * @property {GlobPattern} include Glob pattern(s array) to import 45 | * @property {GlobPattern} exclude Glob pattern(s array) to ignore 46 | * @property {boolean} watch Recompile shader on change 47 | * 48 | * @default { 49 | * exclude: undefined, 50 | * include: DEFAULT_SHADERS, 51 | * warnDuplicatedImports: true, 52 | * removeDuplicatedImports: false, 53 | * defaultExtension: DEFAULT_EXTENSION, 54 | * minify: false, 55 | * watch: true, 56 | * root: '/' 57 | * } 58 | */ 59 | export type PluginOptions = Partial & { 60 | include?: GlobPattern; 61 | exclude?: GlobPattern; 62 | watch?: boolean; 63 | }; 64 | 65 | /** 66 | * @since 1.1.2 67 | * @typedef {Object} LoadingOutput 68 | * 69 | * @returns {LoadingOutput} Loaded, parsed (and minified) 70 | * shader output and Map of shaders that import other chunks 71 | * 72 | * @property {Map} dependentChunks Map of shaders that import other chunks 73 | * @property {string} outputShader Shader file with included chunks 74 | */ 75 | export type LoadingOutput = { 76 | dependentChunks: Map; 77 | outputShader: string; 78 | }; 79 | -------------------------------------------------------------------------------- /test/glsl/chunk0.frag: -------------------------------------------------------------------------------- 1 | #include utils/chunk1; 2 | 3 | vec4 chunkFn () { 4 | return vec4(chunkRGB(), 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /test/glsl/chunk3.frag: -------------------------------------------------------------------------------- 1 | float chunkGreen () { 2 | return 0.8; 3 | } 4 | -------------------------------------------------------------------------------- /test/glsl/main.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | #ifndef GL_FRAGMENT_PRECISION_HIGH 4 | precision mediump float; 5 | #else 6 | precision highp float; 7 | #endif 8 | 9 | out vec4 fragColor; 10 | 11 | #include /test/glsl/chunk0.frag 12 | 13 | void main (void) { 14 | fragColor = chunkFn(); 15 | } 16 | -------------------------------------------------------------------------------- /test/glsl/utils/chunk1.glsl: -------------------------------------------------------------------------------- 1 | #include 'chunk2.frag'; 2 | #include "../chunk3.frag"; 3 | 4 | vec3 chunkRGB () { 5 | return vec3(chunkRed(), chunkGreen(), 0.0); 6 | } 7 | -------------------------------------------------------------------------------- /test/glsl/utils/chunk2.frag: -------------------------------------------------------------------------------- 1 | float chunkRed () { 2 | return 0.0; 3 | } 4 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import GLSL from './glsl/main.frag'; 2 | import WGSL from './wgsl/main.wgsl'; 3 | 4 | const app = document.getElementById('app'); 5 | 6 | app.style.backgroundColor = '#222222'; 7 | app.style.fontFamily = 'monospace'; 8 | app.style.whiteSpace = 'pre-wrap'; 9 | 10 | app.style.color = '#bbbbbb'; 11 | app.style.padding = '16px'; 12 | 13 | app.textContent += '----- GLSL: -----\n\n'; 14 | app.textContent += GLSL; 15 | 16 | app.textContent += '\n\n----- WGSL: -----\n\n'; 17 | app.textContent += WGSL; 18 | 19 | console.info(`GLSL Shader Length: ${GLSL.length} characters.`); 20 | console.info(`WGSL Shader Length: ${WGSL.length} characters.`); 21 | 22 | if (import.meta.hot) { 23 | import.meta.hot.accept('/test/glsl/main.frag', ({ default: glsl }) => { 24 | console.clear(); 25 | console.info('GLSL Shader Hot Module Replacement.'); 26 | console.info(`GLSL Shader Length: ${glsl.length} characters.`); 27 | 28 | app.textContent = '----- GLSL: -----\n\n'; 29 | app.textContent += glsl; 30 | }); 31 | 32 | import.meta.hot.accept('/test/wgsl/main.wgsl', ({ default: wgsl }) => { 33 | console.clear(); 34 | console.info('WGSL Shader Hot Module Replacement.'); 35 | console.info(`WGSL Shader Length: ${wgsl.length} characters.`); 36 | 37 | app.textContent = '----- WGSL: -----\n\n'; 38 | app.textContent += wgsl; 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/wgsl/chunk0.wgsl: -------------------------------------------------------------------------------- 1 | #include utils/chunk1.wgsl; 2 | 3 | fn chunkFn() -> vec4f { 4 | return vec4f(chunkRGB(), 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /test/wgsl/chunk3.wgsl: -------------------------------------------------------------------------------- 1 | fn chunkGreen() -> f32 { 2 | return 0.8; 3 | } 4 | -------------------------------------------------------------------------------- /test/wgsl/main.wgsl: -------------------------------------------------------------------------------- 1 | #include /test/wgsl/chunk0.wgsl; 2 | 3 | @vertex 4 | fn mainVert(@builtin(vertex_index) index: u32) -> 5 | @builtin(position) position: vec4f 6 | { 7 | let position = array( 8 | vec2f(0.0, 1.0), 9 | vec2f(1.0, 1.0), 10 | vec2f(0.0, 0.0), 11 | 12 | vec2f(0.0, 0.0), 13 | vec2f(1.0, 0.0), 14 | vec2f(1.0, 1.0) 15 | ); 16 | 17 | let coords = position[index]; 18 | return vec4f(coords * 2 - 1, 0, 1); 19 | } 20 | 21 | @fragment 22 | fn mainFrag() -> @location(0) vec4f 23 | { 24 | return chunkFn(); 25 | } 26 | -------------------------------------------------------------------------------- /test/wgsl/utils/chunk1.wgsl: -------------------------------------------------------------------------------- 1 | #include './chunk2.wgsl'; 2 | #include "../chunk3.wgsl"; 3 | 4 | fn chunkRGB() -> vec3f { 5 | return vec3f(chunkRed(), chunkGreen(), 0.0); 6 | } 7 | -------------------------------------------------------------------------------- /test/wgsl/utils/chunk2.wgsl: -------------------------------------------------------------------------------- 1 | fn chunkRed() -> f32 { 2 | return 0.0; 3 | } 4 | -------------------------------------------------------------------------------- /vite-plugin-glsl.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "editor.bracketPairColorization.enabled": false, 4 | "javascript.format.semicolons": "insert", 5 | "eslint.validate": ["javascript"] 6 | }, 7 | 8 | "folders": [{ 9 | "name": "Vite Plugin GLSL", 10 | "path": "." 11 | }] 12 | } 13 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import glsl from './src/index.js'; 3 | 4 | export default defineConfig({ 5 | build: { sourcemap: true }, 6 | plugins: [glsl()], 7 | 8 | server: { 9 | open: false, 10 | port: 8080 11 | } 12 | }); 13 | --------------------------------------------------------------------------------