模块未在带有 Next.js 个项目的 typescript monorepo 中解析

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

我有一个使用 yarn workspaces 的 monorepo,它有 2 个 Next.js 项目。

apps
 ┣ app-1
 ┗ app-2

app-1 需要从 app-2 导入组件。为此,我将 app-2 项目添加为依赖项,并在我们的 app-1 tsconfig 中设置路径,如下所示:

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"]
    }
  }
}

这工作得很好,但是,当 app-2 中的组件导入其他组件时会出现问题,例如 import Component from "components/Component"

app-1 不知道如何解决它,正在寻找 components/Components 在它自己的 src 文件夹中,该文件夹不存在。如果像这样 import Component from ../../Component 导入相同的组件,它将正确解析。为了解决这个问题,我在 app-1 的 tsconfig 文件中设置了另一个路径来手动解析。现在我的 tsconfig 看起来像

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"]
    }
  }
}

如果没有那行文本,尝试开发或构建 app-1 项目呈现 Type error: Cannot find module 'components/Component' or its corresponding type declarations. 我不想以这种方式手动解决它,因为 app-1 可能想要它自己的components 文件夹有一天会错误地解析到 app-2 的组件文件夹。

它看起来像是基于错误的打字稿问题,但我无法判断它是否与 webpack/babel 或我们 node_modules

中的符号链接有关

理想的解决方案是使用我们的配置或加载程序进行一些更改,并按照您的预期解析这些路径。

next.jswebpackConfig.resolve 加载 tsconfig.json。参见:

app-2中的组件导入其他组件时,如import Component from "components/Component"webpack根据app-1/tsconfig.json解析components/Component

解决方案:为app-2添加一个resolve plugin

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

你可以使用babel配置如下。

使用 module-resolver 插件。

安装:yarn add -D babel-plugin-module-resolver

并遵循此配置文件。


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

我已经尝试了所提供的答案,不幸的是它们对我不起作用。在通读 documentation 之后,最终修复它的是 app-1:

中的一个简单的 tsconfig 更改
{
  "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'"
    }
  }
}

请注意,由于这两个 Next.js 项目彼此共享代码,我不得不使用 next-transpile-modules 并将每个 next.config.js 包装在 withTM 函数中,如下所述在他们的文档中