bun-plugin-glsl
Version:
Import, inline (and minify) GLSL/WGSL shader files
196 lines (194 loc) • 6.94 kB
JavaScript
// @bun
// plugin/src/loadShader.js
import { dirname, resolve, extname, posix, sep } from "path";
import { emitWarning, cwd } from "process";
import { readFileSync } from "fs";
import { platform } from "os";
var recursiveChunk = "";
var allChunks = new Set;
var dependentChunks = new Map;
var duplicatedChunks = new Map;
function resetSavedChunks() {
const chunk = recursiveChunk;
duplicatedChunks.clear();
dependentChunks.clear();
recursiveChunk = "";
allChunks.clear();
return chunk;
}
function getRecursionCaller() {
const dependencies = [...dependentChunks.keys()];
return dependencies[dependencies.length - 1];
}
function checkDuplicatedImports(path) {
const caller = getRecursionCaller();
const chunks = duplicatedChunks.get(caller) ?? [];
if (chunks.includes(path))
return;
chunks.push(path);
duplicatedChunks.set(caller, chunks);
emitWarning(`'${path}' was included multiple times.`, {
code: "vite-plugin-glsl",
detail: "Please avoid multiple imports of the same chunk in order to avoid" + ` recursions and optimize your shader length.
Duplicated import found in file '${caller}'.`
});
}
function removeSourceComments(source, keyword, triple = false) {
const pattern = new RegExp(String.raw`${keyword}(\s+([^\s<>]+));?`, "gi");
if (source.includes("/*") && source.includes("*/")) {
source = source.slice(0, source.indexOf("/*")) + source.slice(source.indexOf("*/") + 2, source.length);
}
const lines = source.split(`
`);
for (let l = lines.length;l--; ) {
const index = lines[l].indexOf("//");
if (index > -1) {
if (lines[l][index + 2] === "/" && !pattern.test(lines[l]) && !triple)
continue;
lines[l] = lines[l].slice(0, lines[l].indexOf("//"));
}
}
return lines.join(`
`);
}
function checkRecursiveImports(path, lowPath, warn, ignore) {
if (allChunks.has(lowPath)) {
if (ignore)
return null;
warn && checkDuplicatedImports(path);
}
return checkIncludedDependencies(path, path);
}
function checkIncludedDependencies(path, root) {
const dependencies = dependentChunks.get(path);
let recursiveDependency = false;
if (dependencies?.includes(root)) {
recursiveChunk = root;
return true;
}
dependencies?.forEach((dependency) => recursiveDependency ||= checkIncludedDependencies(dependency, root));
return recursiveDependency;
}
function minifyShader(shader, newLine = false) {
const getAllCharIndexes = (line, char = "-", start = 0) => {
const indexes = [];
while ((start = line.indexOf(char, start)) !== -1)
indexes.push(start++);
return indexes;
};
return shader.replace(/\\(?:\r\n|\n\r|\n|\r)|\/\*.*?\*\/|\/\/(?:\\(?:\r\n|\n\r|\n|\r)|[^\n\r])*/g, "").split(/\n+/).reduce((result, line) => {
line = line.trim().replace(/\s{2,}|\t/, " ");
if (/@(vertex|fragment|compute)/.test(line) || line.endsWith("return"))
line += " ";
if (line[0] === "#") {
newLine && result.push(`
`);
result.push(line, `
`);
newLine = false;
} else {
!line.startsWith("{") && result.length && result[result.length - 1].endsWith("else") && result.push(" ");
line = line.replace(/\s*({|}|=|\*|,|\+|\/|>|<|&|\||\[|\]|\(|\)|!|;)\s*/g, "$1");
const indexes = getAllCharIndexes(line);
indexes.forEach((index) => {
if (line[index - 1] === " " && line[index - 2] !== "-")
line = `${line.slice(0, index - 1)}${line.slice(index--)}`;
if (line[index + 1] === " " && line[index + 2] !== "-")
line = `${line.slice(0, index + 1)}${line.slice(index + 2)}`;
});
result.push(line);
newLine = true;
}
return result;
}, []).join("").replace(/\n+/g, `
`);
}
function loadChunks(source, path, options) {
const { importKeyword, warnDuplicatedImports, removeDuplicatedImports } = options;
const pattern = new RegExp(String.raw`${importKeyword}(\s+([^\s<>]+));?`, "gi");
const unixPath = path.split(sep).join(posix.sep);
const chunkPath = platform() === "win32" && unixPath.toLocaleLowerCase() || unixPath;
const recursion = checkRecursiveImports(unixPath, chunkPath, warnDuplicatedImports, removeDuplicatedImports);
if (recursion)
return recursiveChunk;
else if (recursion === null)
return "";
source = removeSourceComments(source);
let directory = dirname(unixPath);
allChunks.add(chunkPath);
if (pattern.test(source)) {
dependentChunks.set(unixPath, []);
const currentDirectory = directory;
const ext = options.defaultExtension;
source = source.replace(pattern, (_, chunkPath2) => {
chunkPath2 = chunkPath2.trim().replace(/^(?:"|')?|(?:"|')?;?$/gi, "");
if (!chunkPath2.indexOf("/")) {
const base = cwd().split(sep).join(posix.sep);
chunkPath2 = base + options.root + chunkPath2;
}
const directoryIndex = chunkPath2.lastIndexOf("/");
directory = currentDirectory;
if (directoryIndex !== -1) {
directory = resolve(directory, chunkPath2.slice(0, directoryIndex + 1));
chunkPath2 = chunkPath2.slice(directoryIndex + 1, chunkPath2.length);
}
let shader = resolve(directory, chunkPath2);
if (!extname(shader))
shader = `${shader}.${ext}`;
const shaderPath = shader.split(sep).join(posix.sep);
dependentChunks.get(unixPath)?.push(shaderPath);
return loadChunks(readFileSync(shader, "utf8"), shader, options);
});
}
if (recursiveChunk) {
const caller = getRecursionCaller();
const recursiveChunk2 = resetSavedChunks();
throw new Error(`Recursion detected when importing "${recursiveChunk2}" in "${caller}".`);
}
return source.trim().replace(/(\r\n|\r|\n){3,}/g, `$1
`);
}
async function loadShader_default(source, shader, options) {
const { minify, ...config } = options;
resetSavedChunks();
let output = loadChunks(source, shader, config);
output = minify ? removeSourceComments(output, options.importKeyword, true) : output;
return {
dependentChunks,
outputShader: minify ? typeof minify !== "function" ? minifyShader(output) : await minify(output) : output
};
}
// src/index.ts
function src_default({
include = /\.(glsl|wgsl|vert|frag|vs|fs)$/,
removeDuplicatedImports = false,
warnDuplicatedImports = true,
defaultExtension = "glsl",
importKeyword = "#include",
minify = false,
root = "/"
} = {}) {
return {
name: "bun-plugin-glsl",
setup(build) {
build.onLoad({ filter: include }, async (args) => {
const source = await Bun.file(args.path).text();
const { outputShader } = await loadShader_default(source, args.path, {
removeDuplicatedImports,
warnDuplicatedImports,
defaultExtension,
importKeyword,
minify,
root
});
return {
exports: { default: outputShader },
loader: "object"
};
});
}
};
}
export {
src_default as default
};