【问题标题】:Module's not resolving in typescript monorepo with Next.js projects模块没有在带有 Next.js 项目的 typescript monorepo 中解析
【发布时间】:2022-03-30 23:48:04
【问题描述】:

我有一个使用 yarn 工作区的 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 不知道如何解决它,并在它自己的不存在的src 文件夹中寻找components/Components。如果像 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 中的符号链接有关。

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

【问题讨论】:

    标签: reactjs typescript webpack next.js babel-loader


    【解决方案1】:

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

    【讨论】:

      【解决方案2】:

      你可以使用 babel 配置如下。

      使用模块解析器插件。

      要安装: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',
              },
            },
          ],
          
        ],
      };
      

      【讨论】:

        【解决方案3】:

        我已经尝试了提供的答案,但不幸的是它们对我不起作用。在阅读了一些 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 函数中,如其文档中所述

        【讨论】:

          猜你喜欢
          • 2017-06-14
          • 2011-05-12
          • 2022-06-13
          • 2017-06-17
          • 2020-08-15
          • 2020-10-18
          • 2020-06-30
          • 1970-01-01
          • 2020-02-22
          相关资源
          最近更新 更多