docd27/rollup-plugin-glsl-optimize

View on GitHub
src/index.js

Summary

Maintainability
A
2 hrs
Test Coverage
import {createFilter} from '@rollup/pluginutils';
import {glslProcessSource} from './lib/glslProcess.js';
import {glslifyInit, glslifyProcessSource} from './lib/glslify.js';
import * as fsSync from 'fs';

/**
 * @typedef {import('./lib/glslProcess').GLSLStageName} GLSLStageName
 * @typedef {{[P in GLSLStageName]: string[]}} GLSLStageDefs */
/** @type {GLSLStageDefs} */
const stageDefs = {
  'vert': ['.vs', '.vert', '.vs.glsl', '.vert.glsl'],
  'frag': ['.fs', '.frag', '.fs.glsl', '.frag.glsl'],
  // The following are untested:
  'geom': ['.geom', '.geom.glsl'],
  'comp': ['.comp', '.comp.glsl'],
  'tesc': ['.tesc', '.tesc.glsl'],
  'tese': ['.tese', '.tese.glsl'],
};

const extsIncludeDefault = [...Object.values(stageDefs).flatMap(
    (exts) => exts.map((ext) => `**/*${ext}`)),
'**/*.glsl',
  // Additionally include all *.glsl by default so we throw an error
  // if the user includes a file extension without a stage
];

/** @type {[GLSLStageName, RegExp][]} */
const stageRegexes = (
  /** @type {[GLSLStageName, string[]][]} */(Object.entries(stageDefs))
      .map(([st, exts]) => [st,
        new RegExp(`(?:${exts.map((ext) => ext.replace('.', '\\.')).join('|')})$`, 'i'),
      ]));

function generateCode(source) {
  return `export default ${JSON.stringify(source)}; // eslint-disable-line`;
}

/**
 * @typedef {Array<string | RegExp> | string | RegExp | null} PathFilter
 * @typedef {Object} GLSLPluginGlobalOptions
 * @property {PathFilter} include
 *   File extensions within rollup to include.
 * @property {PathFilter} exclude
 *   File extensions within rollup to exclude.
 * @property {boolean} glslify
 *   Process sources using glslify prior to all preprocessing, validation and optimization.
 * @property {Partial<import('./lib/glslify.js').GlslifyOptions>} glslifyOptions
 *   When glslify enabled, pass these additional options to glslify.compile()
 * @typedef {GLSLPluginGlobalOptions & Partial<import('./lib/glslProcess.js').GLSLToolOptions>} GLSLPluginOptions
 */
/**
 * @param {Partial<GLSLPluginOptions>} userOptions
 * @return {import('rollup').Plugin}
 */
export default function glslOptimize(userOptions = {}) {
  /** @type {GLSLPluginOptions} */
  const pluginOptions = {
    include: extsIncludeDefault,
    exclude: [],
    glslify: false,
    glslifyOptions: {},
    ...userOptions,
  };

  const filter = createFilter(pluginOptions.include, pluginOptions.exclude);

  return {
    name: 'glsl-optimize',

    async options(options) {
      if (pluginOptions.glslify) { // Try to dynamically load glslify if installed
        await glslifyInit();
      }
      return options;
    },

    /*
      We use a load hook instead of transform because we want sourcemaps
      to reflect the optimized shader source.
    */
    async load(id) {
      if (!id || !filter(id) || !fsSync.existsSync(id)) return;

      let source;
      try {
        source = fsSync.readFileSync(id, {encoding: 'utf8'});
      } catch (err) {
        this.warn(`Failed to load file '${id}' : ${err.message}`);
        return;
      }

      /** @type {GLSLStageName} */
      const stage = stageRegexes.find(([, regex]) => id.match(regex))?.[0];
      if (!stage) {
        this.error({message: `File '${id}' : extension did not match a shader stage.`});
      }

      if (pluginOptions.glslify) {
        try {
          source = await glslifyProcessSource(id, source, pluginOptions.glslifyOptions,
              (message) => this.error({message}));
        } catch (err) {
          this.error({message: `Error processing GLSL source with glslify:\n${err.message}`});
        }
      }

      try {
        const result = await glslProcessSource(id, source, stage, pluginOptions);
        result.code = generateCode(result.code);
        return result;
      } catch (err) {
        this.error({message: `Error processing GLSL source:\n${err.message}`});
      }
    },
  };
}