Search code examples
javascriptwebpackbabeljsnext.jslerna

Webpack resolve alias and compile file under that alias


I have project which uses lerna ( monorepo, multiple packages ). Few of the packages are standalone apps.

What I want to achieve is having aliases on few of the packages to have something like dependency injection. So for example I have alias @package1/backendProvider/useCheckout and in webpack in my standalone app I resolve it as ../../API/REST/useCheckout . So when I change backend provider to something else I would only change it in webpack.

Problem

Problem appears when this alias is used by some other package ( not standalone app ). For example:

Directory structure looks like this:

Project
    packageA
           ComponentA
    packageB
           API
              REST
                  useCheckout
    standalone app

ComponentA is in packageA

useCheckout is in packageB under /API/REST/useCheckout path

ComponentA uses useCheckout with alias like import useCheckout from '@packageA/backendProvider/useCheckout

Standalone app uses componentA

The error I get is that Module not found: Can't resolve '@packageA/backendProvider/useCheckout

However when same alias is used in standalone app ( that has webpack with config provided below ) it is working. Problem occurs only for dependencies.

Potential solutions

I know that one solution would be to compile each package with webpack - but that doesn't really seem friendly. What I think is doable is to tell webpack to resolve those aliases to directory paths and then to recompile it. First part ( resolving aliases ) is done.

Current code

As I'm using NextJS my webpack config looks like this:

 webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
    // Fixes npm packages that depend on `fs` module
    config.node = {
      fs: "empty"
    };

    const aliases = {
    ...
      "@package1/backendProvider": "../../API/REST/"
    };

    Object.keys(aliases).forEach(alias => {
      config.module.rules.push({
        test: /\.(js|jsx)$/,
        include: [path.resolve(__dirname, aliases[alias])],
        use: [defaultLoaders.babel]
      });

      config.resolve.alias[alias] = path.resolve(__dirname, aliases[alias]);
    });

    return config;
  }

Solution

  • You don’t need to use aliases. I have a similar setup, just switch to yarn (v1) workspaces which does a pretty smart trick, it adds sym link to all of your packages in the root node_modules.

    This way, each package can import other packages without any issue.

    In order to apply yarn workspaces with lerna:

    // lerna.json
    {
      "npmClient": "yarn",
      "useWorkspaces": true,
      "packages": [
        "packages/**"
      ],
    }
    
    // package.json
    {
      ...
      "private": true,
      "workspaces": [
        "packages/*",
      ]
      ...
    }
    

    This will enable yarn workspace with lerna.

    The only think that remains to solve is to make consumer package to transpile the required package (since default configs of babel & webpack is to ignore node_module transpilation).

    In Next.js project it is easy, use next-transpile-modules.

    // next.config.js
    
    const withTM = require('next-transpile-modules')(['somemodule', 'and-another']); // pass the modules you would like to see transpiled
     
    module.exports = withTM();
    

    In other packages that are using webpack you will need to instruct webpack to transpile your consumed packages (lets assume that they are under npm scope of @somescope/).

    So for example, in order to transpile typescript, you can add additional module loader.

    // webpack.config.js
    {
      ...
      module: {
        rules: [
          {
            test: /\.ts$/,
            loader: 'ts-loader',
            include: /[\\/]node_modules[\\/]@somescope[\\/]/, // <-- instruct to transpile ts files from this path
            options: {
              allowTsInNodeModules: true, // <- this a specific option of ts-loader
              transpileOnly: isDevelopment,
              compilerOptions: {
                module: 'commonjs',
                noEmit: false,
              },
            },
          }
        ]
      }
      ...
      resolve: {
          symlinks: false, // <-- important
      }
    }
    

    If you have css, you will need add a section for css as well.

    Hope this helps.

    Bonus advantage, yarn workspaces will reduce your node_modules size since it will install duplicate packages (with the same semver version) once!