模块未在带有 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.js
为 webpackConfig.resolve
加载 tsconfig.json
。参见:
当app-2
中的组件导入其他组件时,如import Component from "components/Component"
,webpack
根据app-1/tsconfig.json
解析components/Component
。
解决方案:为app-2
添加一个resolve plugin
。
app-1/tsconfig.json
:
{
//...
"compilerOptions":{
//...
"paths": {
"@apps/*": ["../app-2/*"],
"components/*": ["./components/*"]
},
}
}
app-2/tsconfig.json
:
{
//...
"compilerOptions":{
//...
"paths": {
"components/*": ["./components/*"]
},
}
}
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;
},
};
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
函数中,如下所述在他们的文档中
我有一个使用 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.js
为 webpackConfig.resolve
加载 tsconfig.json
。参见:
当app-2
中的组件导入其他组件时,如import Component from "components/Component"
,webpack
根据app-1/tsconfig.json
解析components/Component
。
解决方案:为app-2
添加一个resolve plugin
。
app-1/tsconfig.json
:
{
//...
"compilerOptions":{
//...
"paths": {
"@apps/*": ["../app-2/*"],
"components/*": ["./components/*"]
},
}
}
app-2/tsconfig.json
:
{
//...
"compilerOptions":{
//...
"paths": {
"components/*": ["./components/*"]
},
}
}
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;
},
};
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
:
{
"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
函数中,如下所述在他们的文档中