6

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.

3 Answers 3

5

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;
    }
  }
};

Sign up to request clarification or add additional context in comments.

1 Comment

The core step is add paths in compilerOptions
1

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

3 Comments

where do you install next-transpile-modules? In both apps?
It's been a while since I had this problem, but from memory I had to add it to each next app that would be sharing code with my package (including adding it to my shared package). Were you able to get it working for you?
Yes. I don't know if it's the same but I added the option transpilePackages: ["shared-package"] in the next.config.js file of each next app and it worked for me. Wasn't necessary add that option to the next.config.js file of the shared package. Previously I configured the workspaces with yarn classic.yarnpkg.com/lang/en/docs/workspaces
0

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',
        },
      },
    ],
    
  ],
};

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.