evanw / esbuild

An extremely fast bundler for the web
https://esbuild.github.io/
MIT License
38.03k stars 1.14k forks source link

Feature request: Ability to provide an ignore / exclude regex for plugin hooks #1222

Open ggoodman opened 3 years ago

ggoodman commented 3 years ago

Description

I would like to propose the addition of an exclude or ignore option during plugin hook registration that would act as the inverse of the current filter option. Currently, it is impossible to say something like 'exclude node_modules' for this plugin. If I don't want a plugin hook to apply to a file, I must register the hook and return null when I observe an args.path that would normally be excluded. For large node_modules graphs, this is every expensive and could be avoided.

I think that this would be a very valuable addition considering the intended simplifications that the Go team made to their regular expression support. I suspect that with JavaScript's regular expression grammar, we could express a node_modules exclusion with something like a negative lookahead / lookbehind assertion but this would fail on the go side.

evanw commented 3 years ago

This sounds like a good idea to me. Thanks for the idea!

rosenfeld commented 3 years ago

Just to give a concrete example on situations where this makes all difference, we use the jquery.tree plugin, and its source is a mess in the sense that some of the files would use require("jquery") while others would assume jQuery is defined. To make things worse, it also bundles its own node_modules/jquery, which is used by esbuild instead of the one available in the project's node_modules. That means I must replace both set of files. In one set I must replace require("jquery") with require("${ require.resolve('jquery') }" while for the other set I must prepend import jQuery from "${ jQueryFullPath }".

All of those files live under the same jqtree module path, but I can't prepend the jQuery import to all of them, that's why an exclude/ignore param would be very helpful.

I've created a generic plugin to handle what I get from webpack loaders such as imports-loader, exports-loader and more.

It was simpler, but I had to complicate it a little bit in order to support those exclusions directly in the plugin, while the implementation would get much simpler if esbuild would support exclusions itself:

const fs = require('fs');
let id = 0;
// plugin to allow general source modification operations such as prepend, append and replacements
// this basically takes care of the imports and exports loader from Webpack and more.
module.exports = options => options.map(([filter,
    { prepend, append, replacements, loader, excludes }]) => ({
  name: `source-modifier-${ ++id }`,
  setup(build) {
    build.onLoad({ filter }, async args => {
      let contents = await fs.promises.readFile(args.path, 'utf8');
      let processed = excludes && processExcludes(contents, excludes, args.path);

      contents = processed || processContents(contents, replacements, prepend, append);

      const result = { contents };
      if (loader) result.loader = loader;
      return result;
    });
  }
}));

function processExcludes(contents, excludes, path) {
  for (let [filter, { replacements, append, prepend }] of excludes) {
    if (filter.test(path)) return processContents(contents, replacements, prepend, append);
  }
}

function processContents(contents, replacements, prepend, append) {
  if (replacements) for (const [a, b] of replacements)
    contents = contents.replace(a, b);
  if (prepend || append)
    contents = [prepend || '', contents, append || ''].join("\n");
  return contents;
}

// I use it like this:
const sourceModifierPlugin = require('./esbuild-plugins/source-modifier.js');
const jqueryFullPath = require.resolve('jquery');

const buildConfig = {
  // ...
  plugins: {
    //...
    ...sourceModifierPlugin([
      [ /jqtree.*\.js/, {
        prepend: `import jQuery from "${ jqueryFullPath }"`,
        excludes: [
          [/(tree\.jquery|dragAndDropHandler)\.js/, { replacements: [
            [ 'require("jquery")', `require("${ jqueryFullPath }");` ],
          ] } ]
        ],
      } ],

    ]),
  }
}

I'd rather keep the plugin simpler and provide a single exclude/ignore option that I'd forward to esbuild. Then I'd replace 'require("jquery")' for all sources under jqtree (it won't hurt for those files that assume jQuery is defined) and skip the jQuery import for those 2 files using var jQuery = require("jquery");

arcanis commented 2 years ago

Having a exclude filter in plugins would be really useful; I noticed a lot of our bundling time was spent inside our custom resolver, which we only use for third-party package resolution.

Excluding our own packages from this set would speed up the bundling significantly, but since Go doesn't support negative lookaheads the final regex to exclude a few paths becomes a monstrosity - and loses the speed benefits.