dividab / tsconfig-paths-webpack-plugin

Load modules according to tsconfig paths in webpack.
MIT License
596 stars 51 forks source link

Doesn't seem to work with Webpack 4 #9

Closed bushmango closed 6 years ago

bushmango commented 6 years ago

I cannot get paths to work with webpack 4. An example of it working would be nice

jonaskello commented 6 years ago

According to this resolver plugins should be compatible:

Resolver plugins are backward-compatible, but should be migrated to the new plugin system.

I have not tried it myself yet. I'll in look into it and see what is going on.

jonaskello commented 6 years ago

I made a webpack4 branch here:

https://github.com/dividab/tsconfig-paths-webpack-plugin/tree/webpack4

In this branch I updated the included example to webpack 4. It seems to work (webpack emits some warnings but it still runs). You can clone this branch and try it with yarn example.

@bushmango Perhaps you can post a more detailed description of the problems you are experiencing?

firsttris commented 6 years ago

hello, some webpack 4 / webpack-dev-server 3.1 feedback

adapted your example from the webpack4 branch

webpack 4.0.1, webpack-dev-server: 3.1.0

╰─>$ yarn start:web
yarn run v1.5.1
warning package.json: No license field
$ webpack-dev-server --mode=development --port 8080 --history-api-fallback --host 0.0.0.0 --env.config=dev --env.app=web --colors
tsconfig-paths-webpack-plugin: Using config file at /home/firsttris/Projects/timetracking-app/tsconfig.json
/home/firsttris/Projects/timetracking-app/node_modules/webpack-dev-server/bin/webpack-dev-server.js:385
    throw e;
    ^

Error: Plugin could not be registered at 'described-resolve'. Hook was not found.
BREAKING CHANGE: There need to exist a hook at 'this.hooks'. To create a compatiblity layer for this hook, hook into 'this._pluginCompat'.
    at Compiler.plugin (/home/firsttris/Projects/timetracking-app/node_modules/tapable/lib/Tapable.js:63:9)
    at Compiler.deprecated [as plugin] (internal/util.js:53:15)
    at TsconfigPathsPlugin.apply (/home/firsttris/Projects/timetracking-app/node_modules/tsconfig-paths-webpack-plugin/lib/plugin.js:40:18)
    at webpack (/home/firsttris/Projects/timetracking-app/node_modules/webpack/lib/webpack.js:37:12)
    at startDevServer (/home/firsttris/Projects/timetracking-app/node_modules/webpack-dev-server/bin/webpack-dev-server.js:379:16)
    at processOptions (/home/firsttris/Projects/timetracking-app/node_modules/webpack-dev-server/bin/webpack-dev-server.js:361:5)
    at Object.<anonymous> (/home/firsttris/Projects/timetracking-app/node_modules/webpack-dev-server/bin/webpack-dev-server.js:504:1)
    at Module._compile (module.js:662:30)
    at Object.Module._extensions..js (module.js:673:10)
    at Module.load (module.js:575:32)
error An unexpected error occurred: "Command failed.
Exit code: 1
Command: sh
Arguments: -c webpack-dev-server --mode=development --port 8080 --history-api-fallback --host 0.0.0.0 --env.config=dev --env.app=web --colors
Directory: /home/firsttris/Projects/timetracking-app/packages/app
bushmango commented 6 years ago

Installing the webpack4 branch locally and looking through the example I was able to get it working, thanks! My actual issue is I accidentally had the resolve section in the webpack config twice, sorry!

yujiaze commented 6 years ago

webpack: 4.1.0 tsconfig-paths-webpack-plugin: 2.0.0

[nodemon] starting `node dev/index.js`
/Users/root/code/projects/nestjs/node_modules/tapable/lib/Tapable.js:63
                throw new Error(`Plugin could not be registered at '${name}'. Hook was not found.\n` +
                ^

Error: Plugin could not be registered at 'described-resolve'. Hook was not found.
BREAKING CHANGE: There need to exist a hook at 'this.hooks'. To create a compatiblity layer for this hook, hook into 'this._pluginCompat'.
jonaskello commented 6 years ago

@firsttris @yujiaze Thanks for posting the errors. I upgraded the webpack 4 branch to 4.1.0 but I did not get the error you got. Although, it seems likely your errors are related to this plugin because it uses the described-resolve hook.

I'm not sure why you get the error and I don't. Could you perhaps try to clone the webpack4 branch in this repo and check if that works for you? If it does, check what is different between that config and yours. Or you can post your full config and I'll try to reproduce using that.

TatsuUkraine commented 6 years ago

same issue in my project after I tried to use webpack@4.1.1

/home/denis/projects/pu-search/node_modules/webpack/node_modules/tapable/lib/Tapable.js:63
        throw new Error(`Plugin could not be registered at '${name}'. Hook was not found.\n` +
        ^

Error: Plugin could not be registered at 'described-resolve'. Hook was not found.
BREAKING CHANGE: There need to exist a hook at 'this.hooks'. To create a compatiblity layer for this hook, hook into 'this._pluginCompat'.
    at Compiler.plugin (/home/denis/projects/pu-search/node_modules/webpack/node_modules/tapable/lib/Tapable.js:63:9)
    at Compiler.deprecated [as plugin] (internal/util.js:52:15)
    at TsconfigPathsPlugin.apply (/home/denis/projects/pu-search/node_modules/tsconfig-paths-webpack-plugin/lib/plugin.js:40:18)
    at webpack (/home/denis/projects/pu-search/node_modules/webpack/lib/webpack.js:37:12)
    at setupDevServer (/home/denis/projects/pu-search/build/setup-dev-server.js:49:28)
    at Object.<anonymous> (/home/denis/projects/pu-search/server.js:53:55)
    at Module._compile (module.js:624:30)
    at Object.Module._extensions..js (module.js:635:10)
    at Module.load (module.js:545:32)
    at tryModuleLoad (module.js:508:12)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! pu-search@1.0.0 dev: `node server`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the pu-search@1.0.0 dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/denis/.npm/_logs/2018-03-08T13_11_17_455Z-debug.log
jonaskello commented 6 years ago

@TatsuUkraine Could you compare your config to the working webpack 4 example from this repo? Or provide a reporudction config?

To run the working example, clone this repo, then

$ git checkout webpack4
$ yarn example
TatsuUkraine commented 6 years ago

@jonaskello it will be quite hard to provide my full webpack config, since I have dynamic configuration build. But here is how I register your plugin in my config

client build:

plugins: [
        new ForkTsCheckerWebpackPlugin({tsconfig: path.resolve(__dirname, '../tsconfig.json'), vue: true}),
        new TsconfigPathsPlugin({configFile: path.resolve(__dirname, '../tsconfig.json')}),
   // strip dev-only code in Vue source
        new webpack.DefinePlugin({
            'process.env': config
        }),
        // extract vendor chunks for better caching
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: function (module) {
                // a module is extracted into the vendor chunk if...
                return (
                    // it's inside node_modules
                    /node_modules/.test(module.context) &&
                    // and not a CSS file (due to extract-text-webpack-plugin limitation)
                    !/\.css$/.test(module.request)
                )
            }
        }),
        // extract webpack runtime & manifest to avoid vendor chunk hash changing
        // on every build.
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest'
        }),
        new VueSSRClientPlugin()
]

server build

plugins: [
        new ForkTsCheckerWebpackPlugin({tsconfig: path.resolve(__dirname, '../tsconfig.json'), vue: true}),
        new TsconfigPathsPlugin({configFile: path.resolve(__dirname, '../tsconfig.json')}),
        new webpack.DefinePlugin({
            'process.env': config
        }),
        new VueSSRServerPlugin()
]
jonaskello commented 6 years ago

@TatsuUkraine Thanks! Since the minimal example that I have is working I cannot troubleshoot from that. So I'm trying to get to a minimal reproduction config. You don't have to provide the full config. A small (but runnable) config with just enough to reproduce the error you are getting would be perfect. So if you could make a small config where you bascially strip everything but the tsconfig-plugin, and you still get the error that would be very helpful.

Did the example in this repo's webpack4 branch work for you?

jonaskello commented 6 years ago

Btw, I think CommonsChunkPlugin should not be used in webpack4? See here for more info.

Nayni commented 6 years ago

Hello,

I found this issue while I was running into the same sorts of issues during my Webpack 4 upgrade/usage.

Just like many of the above reporters I also had the same issue where Webpack 4 was crashing with Error: Plugin could not be registered at 'described-resolve'. Hook was not found.

After an afternoon of digging I eventually came up with 2 observations:

Which plugin array you use is very important

This also took me a while to figure out, but Webpack actually has 2 places where you can define a plugins array. The first place is the in the root object of the config, this is described here and it's where I also initially placed the usage of this plugin (and I believe everyone who is having problems are probably doing the same thing).

There is however a second place where you are allowed to specify a plugins array, which is in the resolve property of the config. This scopes your plugin to run specifically during resolve.

What I noticed is that if you place the plugin definition in this last type of array (the resolve one) the plugin works perfectly fine (note that I cloned the webpack4 branch here).

However, if your plugin was defined in the first array, the root of the config, this used to work perfectly fine, but with webpack 4 this has changed.

So the easy fix for users is to just move the plugin definition into the resolve plugins array. This solved my problems. It's also the reason your branch "just works" because your example places the plugin in this array.

We can solve this and be Webpack 4 and backwards compatible.

I took the some time to hack together a solution that actually makes the plugin work in both cases. I am by no means a Webpack expert, so some things might not be 100% legit, but I tested all my cases and this causes the plugin to work for webpack 3 but also makes it work for webpack 4 regardless of which array the plugin was specified in.

I'll just paste the code in here. Please note that I didn't make the effort to refactor this properly and it really is what I described, it's a hack, it needs major clean up.

export class TsconfigPathsPlugin implements ResolverPlugin {

  // ...

  apply(resolver: Resolver): void {
    const { baseUrl } = this;

    if (!baseUrl) {
      // Nothing to do if there is no baseUrl
      this.log.logWarning(
        "Found no baseUrl in tsconfig.json, not applying tsconfig-paths-webpack-plugin"
      );
      return;
    }

    // tslint:disable:no-any
    // Hack starts here...
    const compiler = resolver as any;

    console.log("hooks ==", compiler.hooks);
    console.log("describedResolve ==", compiler.hooks.describedResolve);
    console.log("afterResolvers ==", compiler.hooks.afterResolvers);

    // Check for Webpack 4 hooks
    if (compiler.hooks) {
      // afterResolvers will only exist, if the plugin was places in the root plugins
      // There is no hook to describedResolve. Instead we have to first hook into afterResolvers
      // When they get called, we can get back to calling the describedResolve Hook.
      if (compiler.hooks.afterResolvers) {
        console.log("Plugin placed in Root Plugins!");
        compiler.hooks.afterResolvers.tap("TsconfigPathsPlugin", () => {
          // TODO: This is ugly as hell, please refactor.
          compiler.resolverFactory.hooks.resolver
            .for("normal")
            .tap("TsconfigPathsPlugin", (res: any) => {
              const target = res.ensureHook(this.target);
              res
                .getHook(this.source)
                .tapAsync(
                  "TsconfigPathsPlugin",
                  (request: any, _: any, callback: any) => {
                    const func = createPlugin(
                      this.matchPath,
                      res,
                      this.absoluteBaseUrl,
                      target,
                      this.extensions
                    );
                    func(request, callback);
                  }
                );
            });
        });
      } else {
        console.log("Plugin placed in Resolve.Plugins!");
        // We are in Webpack 4, and the plugin was placed in the resolve.plugin array,
        // we have an instant hook to describedResolve, using the new Tapable API.
        // TODO: Clean up createPluginWp4 hack.
        compiler.hooks.describedResolve.tapAsync(
          "TsconfigPathsPlugin",
          createPluginWp4(
            this.matchPath,
            resolver,
            this.absoluteBaseUrl,
            this.target,
            this.extensions
          )
        );
      }
    } else {
      console.log("Legacy Webpack!");
      // It's Webpack < 4.0.0, default the old plugin system.
      resolver.plugin(
        this.source,
        createPlugin(
          this.matchPath,
          resolver,
          this.absoluteBaseUrl,
          this.target,
          this.extensions
        )
      );
    }
  }
}

// tslint:disable:no-any
// This is just a copy of the old method, but it returns the Webpack 4 syntax.
// The only difference here is that the function returned has an additional parameter, which we currently don't use.
// It's their new RequestContext.
function createPluginWp4(
  matchPath: TsconfigPaths.MatchPathAsync,
  resolver: Resolver,
  absoluteBaseUrl: string,
  target: string,
  extensions: ReadonlyArray<string>
): any {
  const fileExistAsync = createFileExistAsync(resolver.fileSystem);
  const readJsonAsync = createReadJsonAsync(resolver.fileSystem);
  return (request: any, _: any, callback: any) => {
    const innerRequest = getInnerRequest(resolver, request);

    if (
      !innerRequest ||
      (innerRequest.startsWith(".") || innerRequest.startsWith(".."))
    ) {
      return callback();
    }

    matchPath(
      innerRequest,
      readJsonAsync,
      fileExistAsync,
      extensions,
      (err, foundMatch) => {
        if (err) {
          callback(err);
        }

        if (!foundMatch) {
          return callback();
        }

        const newRequest = {
          ...request,
          request: foundMatch,
          path: absoluteBaseUrl
        };

        return resolver.doResolve(
          target,
          newRequest,
          `Resolved request '${innerRequest}' to '${foundMatch}' using tsconfig.json paths mapping`,
          createInnerCallback((err2: Error, result2: string): void => {
            if (arguments.length > 0) {
              console.log("here?");
              return callback(err2, result2);
            }
            // don't allow other aliasing or raw request
            callback(null, null);
          }, callback)
        );
      }
    );
  };
}

This still causes 2 deprecation warnings, but the plugin is 100% functional for my own project.

I hope this is a good place to start @jonaskello

EDIT: I've pushed the code to my own fork: https://github.com/Nayni/tsconfig-paths-webpack-plugin/blob/nayni-webpack4/src/plugin.ts

EDIT2: Actually missed the use-cases in comments and swapped them around while cleaning it up. All good now!

jonaskello commented 6 years ago

@Nayni Thanks, that is a great find :-). I have never tried to put resolver plugins in the regular plugins array so I would not have found that myself!

I'm not sure webpack actaully supports resolver plugins in the regular plugins array. I guess it could work anyway, but if webpack does not support it, I think it may break again later and then we have more trouble. So instead maybe we could be even more clear in the readme that this is a resolver plugin and it should be in the resolver section of the config? And perhaps we can throw an error if we can detect that it is registred in the wrong section?

@Nayni If you want to send a PR for your changes I would be happy to review it!

Nayni commented 6 years ago

I think the best option would be to be more clear on where this plugin is supposed to be placed. Because I still consider the code I posted above to be a hack.

I guess it should be possible to check where the plugin was placed, at least in Webpack 4 this would be as easy as checking if we have resolver hooks, for Webpack 3 I have no clue though. I'm not sure if it's possible to inspect the original config in a plugin, but it would be a great help for people who are just starting with Webpack or this plugin.

I'll see what I can do with my changes in terms of a PR, probably somewhere later this week.

TatsuUkraine commented 6 years ago

@jonaskello yep, you are right about chunks, I just posted my current config, since the way how I use your plugin did change)

jonaskello commented 6 years ago

This is now fixed in 3.0.0 thanks to the PR by @Nayni!