UNPKG

bun-plugin-glsl

Version:

Import, inline (and minify) GLSL/WGSL shader files

196 lines (194 loc) 6.94 kB
// @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 };