3

I'm planning on using a set of a little bit more sophisticated conventions to import assets in my webpack project. So I'm trying to write a plugin that should rewrite parts of requested module locators and then pass that down the resolver waterfall.


Let's assume we just want to

  • check if a requested module starts with the # character and
  • if so, replace that with ./lib/. The new module locator should now be looked up by the default resolver.

This means when a file /var/www/source.js does require("#example"), it should then actually get /var/www/lib/example.js.


So far I've figured out I'm apparently supposed to use the module event hook for this purpose. That's also the way chosen by other answers which unfortunately did not help me too much.

So this is my take on the custom resolve plugin, it's pretty straightforward:

function MyResolver () {}
MyResolver.prototype.apply = function (compiler) {

  compiler.plugin('module', function (init, callback) {
    // Check if rewrite is necessary
    if (init.request.startsWith('#')) {

      // Create a new payload
      const modified = Object.assign({}, init, {
        request: './lib/' + init.request.slice(1)
      })

      // Continue the waterfall with modified payload
      callback(null, modified)
    } else {

      // Continue the waterfall with original payload
      callback(null, init)
    }
  })

}

However, using this (in resolve.plugins) doesn't work. Running webpack, I get the following error:

ERROR in .
Module build failed: Error: EISDIR: illegal operation on a directory, read
 @ ./source.js 1:0-30

Apparently, this is not the way to do things. But since I couldn't find much example material out there on the matter, I'm a little bit out of ideas.


To make this easier to reproduce, I've put this exact configuration into a GitHub repo. So if you're interested in helping, you may just fetch it:

git clone https://github.com/Loilo/webpack-custom-resolver.git

Then just run npm install and npm run webpack to see the error.

Community
  • 1
  • 1
Loilo
  • 13,466
  • 8
  • 37
  • 47

1 Answers1

3

Update: Note that the plugin architecture changed significantly in webpack 4. The code below will no longer work on current webpack versions.

If you're interested in a webpack 4 compliant version, leave a comment and I'll add it to this answer.

I've found the solution, it was mainly triggered by reading the small doResolve() line in the docs.

The solution was a multiple-step process:

1. Running callback() is not sufficient to continue the waterfall.

To pass the resolving task back to webpack, I needed to replace

callback(null, modified)

with

this.doResolve(
  'resolve',
  modified,
  `Looking up ${modified.request}`,
  callback
)

(2. Fix the webpack documentation)

The docs were missing the third parameter (message) of the doResolve() method, resulting in an error when using the code as shown there. That's why I had given up on the doResolve() method when I found it before putting the question up on SO.

I've made a pull request, the docs should be fixed shortly.

3. Don't use Object.assign()

It seems that the original request object (named init in the question) must not be duplicated via Object.assign() to be passed on to the resolver.

Apparently it contains internal information that trick the resolver into looking up the wrong paths.

So this line

const modified = Object.assign({}, init, {
  request: './lib/' + init.request.slice(1)
})

needs to be replaced by this:

const modified = {
  path: init.path,
  request: './lib/' + init.request.slice(1),
  query: init.query,
  directory: init.directory
}

That's it. To see it a bit clearer, here's the whole MyResolver plugin from above now working with the mentioned modifications:

function MyResolver () {}
MyResolver.prototype.apply = function (compiler) {

  compiler.plugin('module', function (init, callback) {
    // Check if rewrite is necessary
    if (init.request.startsWith('#')) {

      // Create a new payload
      const modified = {
        path: init.path,
        request: './lib/' + init.request.slice(1),
        query: init.query,
        directory: init.directory
      }

      // Continue the waterfall with modified payload
      this.doResolve(
        // "resolve" just re-runs the whole resolving of this module,
        // but this time with our modified request.
        'resolve',
        modified,
        `Looking up ${modified.request}`,
        callback
      )
    } else {
      this.doResolve(
        // Using "resolve" here would cause an infinite recursion,
        // use an array of the possibilities instead.
        [ 'module', 'file', 'directory' ],
        modified,
        `Looking up ${init.request}`,
        callback
      )
    }
  })

}
Loilo
  • 13,466
  • 8
  • 37
  • 47
  • This seems to process only files that do not have relative paths. As soon as I remove the # char the path is interpreted as relative and the resolver is bypassed. Is there any way to intercept relative paths and modify them? – Adrian Moisa Jun 12 '18 at 13:33
  • Sure, just hop into the `else` branch, check the `modified.path` and adjust it as needed. – Loilo Jun 12 '18 at 14:48
  • 1
    Meanwhile I found [NormalModuleReplacementPlugin](https://webpack.js.org/plugins/normal-module-replacement-plugin/). This one does the trick for me. I can access any path (relative or alias). Thanks for the quick answer! – Adrian Moisa Jun 12 '18 at 14:59