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

mat*_*tmb 6 typescript reactjs webpack next.js babel-loader

我有一个使用纱线工作区的 monorepo,其中有 2 个 Next.js 项目。

\n
apps\n \xe2\x94\xa3 app-1\n \xe2\x94\x97 app-2\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
app-1 package.json\n{\n  "name": "@apps/app-1",\n  "version": "0.1.0",\n  "private": true,\n  "dependencies": {\n    "@apps/app-2": "workspace:*",\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
app-1 tsconfig.json\n\n{\n  "compilerOptions": {\n    "baseUrl": "./src",\n    "paths": {\n      "@apps/app-2/*": ["../../app-2/src/*"],\n      "@apps/app-2": ["../../app-2/src"]\n    }\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这工作得很好,但是,当组件导入app-2其他组件(例如import Component from "components/Component".

\n

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

\n
app-1 tsconfig\n{\n  "compilerOptions": {\n    "baseUrl": "./src",\n    "paths": {\n      "components/*": ["../../app-2/src/components/*"], // new path resolves absolute urls from app-2\n      "@apps/app-2/*": ["../../app-2/src/*"],\n      "@apps/app-2": ["../../app-2/src"]\n    }\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

理想的解决方案是使用我们的配置或加载器更改某些内容,并按照您的预期解析这些路径。

\n

小智 5

next.js负载tsconfig.jsonwebpackConfig.resolve. 看:在此输入图像描述

当一个组件app-2导入其他组件时import Component from "components/Component",根据webpack解析。components/Componentapp-1/tsconfig.json

解决办法:添加一个resolve pluginfor app-2.

在此输入图像描述

  1. app-1/tsconfig.json:
{
  //...
  "compilerOptions":{
    //...
    "paths": {
      "@apps/*": ["../app-2/*"],
      "components/*": ["./components/*"]
    },
  }
}
Run Code Online (Sandbox Code Playgroud)
  1. app-2/tsconfig.json:
{
  //...
  "compilerOptions":{
    //...
    "paths": {
      "components/*": ["./components/*"]
    },
  }
}
Run Code Online (Sandbox Code Playgroud)
  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;
  },
};
Run Code Online (Sandbox Code Playgroud)
  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;
    }
  }
};

Run Code Online (Sandbox Code Playgroud)


mat*_*tmb 1

我已经尝试过提供的答案,不幸的是它们对我不起作用。在阅读了一些文档之后,最终修复它的是一个简单的 tsconfig 更改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'"
    }
  }
}

Run Code Online (Sandbox Code Playgroud)

请注意,由于这些都是 Next.js 项目,彼此共享代码,因此我必须使用next-transpile-modules并将每个项目包装next.config.jswithTM其文档中概述的函数中