mattmb
mattmb

Reputation: 79

Module's not resolving in typescript monorepo with Next.js projects

I have a monorepo using yarn workspaces that has 2 Next.js projects.

apps
 ┣ app-1
 ┗ app-2

app-1 needs to import components from app-2. To do this, I add the app-2 project as a dependency and set the path in our app-1 tsconfig like so:

app-1 package.json
{
  "name": "@apps/app-1",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@apps/app-2": "workspace:*",
  }
}
app-1 tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@apps/app-2/*": ["../../app-2/src/*"],
      "@apps/app-2": ["../../app-2/src"]
    }
  }
}

This works just fine, however, the problem happens when a component in app-2 imports other components like import Component from "components/Component".

app-1 doesn't know how to resolve it and is looking for components/Components inside it's own src folder which does not exist. If that same component imported like this import Component from ../../Component it would resolve properly. To fix this, I set another path inside of app-1's tsconfig file to manually resolve. Now my tsconfig looks like

app-1 tsconfig
{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "components/*": ["../../app-2/src/components/*"], // new path resolves absolute urls from app-2
      "@apps/app-2/*": ["../../app-2/src/*"],
      "@apps/app-2": ["../../app-2/src"]
    }
  }
}

Without that line of text, trying to dev or build the app-1 project renders Type error: Cannot find module 'components/Component' or its corresponding type declarations. I don't want to manually resolve it this way because app-1 might want it's own components folder one day and would erroneously resolve to app-2's components folder.

It looks like a typescript issue based on the error, but I can't tell if maybe it has something to do with webpack/babel or from symlinks in our node_modules

The ideal solution is to change something with our config or loaders and have these path resolve as you'd expect.

Upvotes: 6

Views: 8495

Answers (3)

mattmb
mattmb

Reputation: 79

I had tried the provided answers and unfortunately they didn't work for me. What did end up fixing it, after reading through some documentation, was a simple tsconfig change in app-1:

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "*": ["*", "../../app-2/src/*"], // try to resolve in the current baseUrl, if not use the fallback.
      "@apps/app-2/*": ["../../app-2/src/*"], // reference app-2 imports inside app-1 like "import X from '@apps/app-2/components'"
    }
  }
}

Note that since these are both Next.js projects sharing code with each other, I had to use next-transpile-modules and wrapped each next.config.js in the withTM function as outlined in their docs

Upvotes: 1

Pankaj Vaghela
Pankaj Vaghela

Reputation: 53

you can use babel config as follows.

Use the module-resolver plugin.

To install: yarn add -D babel-plugin-module-resolver

and follow this config file.


module.exports = {
  presets: [], //Keep your preset as it is
  plugins: [
    [
      'module-resolver',
      {
        root: ['./src'],
        extensions: ['.js', '.jsx', '.json', '.svg', '.png', '.tsx'],
        // Note: you do not need to provide aliases for same-name paths immediately under root
        alias: {
          "@apps/app-2": '../../app-2/src',
        },
      },
    ],
    
  ],
};

Upvotes: 0

小聪聪到此一游
小聪聪到此一游

Reputation: 1294

next.js loads tsconfig.json for webpackConfig.resolve. See: enter image description here

When a component in app-2 imports other components like import Component from "components/Component", webpack resolve components/Component according to app-1/tsconfig.json.

Solution: add a resolve plugin for app-2.

enter image description here

  1. app-1/tsconfig.json:
{
  //...
  "compilerOptions":{
    //...
    "paths": {
      "@apps/*": ["../app-2/*"],
      "components/*": ["./components/*"]
    },
  }
}
  1. app-2/tsconfig.json:
{
  //...
  "compilerOptions":{
    //...
    "paths": {
      "components/*": ["./components/*"]
    },
  }
}
  1. app-1/next.config.js:
const path = require("path");

// fork from `@craco/craco/lib/loaders.js`
function getLoaderRecursively(rules, matcher) {
  let loader;

  rules.some((rule) => {
    if (rule) {
      if (matcher(rule)) {
        loader = rule;
      } else if (rule.use) {
        loader = getLoaderRecursively(rule.use, matcher);
      } else if (rule.oneOf) {
        loader = getLoaderRecursively(rule.oneOf, matcher);
      } else if (isArray(rule.loader)) {
        loader = getLoaderRecursively(rule.loader, matcher);
      }
    }

    return loader !== undefined;
  });

  return loader;
}


const MyJsConfigPathsPlugin = require("./MyJsConfigPathsPlugin");
const projectBBasePath = path.resolve("../app-2");
const projectBTsConfig = require(path.resolve(
  projectBBasePath,
  "tsconfig.json"
));

module.exports = {
  webpack(config) {
    const projectBJsConfigPathsPlugin = new MyJsConfigPathsPlugin(
      projectBTsConfig.compilerOptions.paths,
      projectBBasePath
    );

    config.resolve.plugins.unshift({
      apply(resolver) {
        resolver
          .getHook("described-resolve")
          .tapPromise(
            "ProjectBJsConfigPathsPlugin",
            async (request, resolveContext) => {
              if (request.descriptionFileRoot === projectBBasePath) {
                return await projectBJsConfigPathsPlugin.apply(
                  resolver,
                  request,
                  resolveContext
                );
              }
            }
          );
      },
    });

    // get babel-loader
    const tsLoader = getLoaderRecursively(config.module.rules, (rule) => {
      return rule.test?.source === "\\.(tsx|ts|js|mjs|jsx)$";
    });

    tsLoader.include.push(projectBBasePath);

    return config;
  },
};
  1. MyJsConfigPathsPlugin.js:
// fork from `packages/next/build/webpack/plugins/jsconfig-paths-plugin.ts`

const path = require("path");

const {
  // JsConfigPathsPlugin,
  pathIsRelative,
  matchPatternOrExact,
  isString,
  matchedText,
  patternText,
} = require("next/dist/build/webpack/plugins/jsconfig-paths-plugin");
const NODE_MODULES_REGEX = /node_modules/;

module.exports = class MyJsConfigPathsPlugin {
  constructor(paths, resolvedBaseUrl) {
    this.paths = paths;
    this.resolvedBaseUrl = resolvedBaseUrl;
  }

  async apply(resolver, request, resolveContext) {
    const paths = this.paths;
    const pathsKeys = Object.keys(paths);

    // If no aliases are added bail out
    if (pathsKeys.length === 0) {
      return;
    }

    const baseDirectory = this.resolvedBaseUrl;
    const target = resolver.ensureHook("resolve");

    const moduleName = request.request;

    // Exclude node_modules from paths support (speeds up resolving)
    if (request.path.match(NODE_MODULES_REGEX)) {
      return;
    }

    if (
      path.posix.isAbsolute(moduleName) ||
      (process.platform === "win32" && path.win32.isAbsolute(moduleName))
    ) {
      return;
    }

    if (pathIsRelative(moduleName)) {
      return;
    }

    // If the module name does not match any of the patterns in `paths` we hand off resolving to webpack
    const matchedPattern = matchPatternOrExact(pathsKeys, moduleName);
    if (!matchedPattern) {
      return;
    }

    const matchedStar = isString(matchedPattern)
      ? undefined
      : matchedText(matchedPattern, moduleName);
    const matchedPatternText = isString(matchedPattern)
      ? matchedPattern
      : patternText(matchedPattern);

    let triedPaths = [];

    for (const subst of paths[matchedPatternText]) {
      const curPath = matchedStar ? subst.replace("*", matchedStar) : subst;

      // Ensure .d.ts is not matched
      if (curPath.endsWith(".d.ts")) {
        continue;
      }

      const candidate = path.join(baseDirectory, curPath);
      const [err, result] = await new Promise((resolve) => {
        const obj = Object.assign({}, request, {
          request: candidate,
        });
        resolver.doResolve(
          target,
          obj,
          `Aliased with tsconfig.json or jsconfig.json ${matchedPatternText} to ${candidate}`,
          resolveContext,
          (resolverErr, resolverResult) => {
            resolve([resolverErr, resolverResult]);
          }
        );
      });

      // There's multiple paths values possible, so we first have to iterate them all first before throwing an error
      if (err || result === undefined) {
        triedPaths.push(candidate);
        continue;
      }

      return result;
    }
  }
};

Upvotes: 5

Related Questions