Skip to content

Commit 9b77a98

Browse files
authored
fix(core): resolve require.resolve from source module (#1322)
1 parent c0d1aa6 commit 9b77a98

9 files changed

Lines changed: 291 additions & 13 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = 'helper';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export declare const resolveHelper: () => string;
2+
export declare const resolveWithPaths: (base: string) => string;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Without `injectRequireResolveOrigin`, rstest used to resolve these
2+
// `require.resolve()` calls against the test entry. This fixture lives in a
3+
// different directory, so relative specifiers must use this module as origin.
4+
export const resolveHelper = () => require.resolve('./exportHelper');
5+
6+
export const resolveWithPaths = (base) =>
7+
require.resolve('rstest-require-resolve-target', { paths: [base] });
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { mkdirSync, writeFileSync } from 'node:fs';
2+
import { dirname, join } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import { expect, it } from '@rstest/core';
5+
import {
6+
resolveHelper,
7+
resolveWithPaths,
8+
} from './require-resolve-origin/index.js';
9+
10+
const __filename = fileURLToPath(import.meta.url);
11+
const __dirname = dirname(__filename);
12+
const customPath = join(__dirname, 'require-resolve-origin/custom-path');
13+
const packageDir = join(
14+
customPath,
15+
'node_modules/rstest-require-resolve-target',
16+
);
17+
18+
mkdirSync(packageDir, { recursive: true });
19+
writeFileSync(
20+
join(packageDir, 'package.json'),
21+
JSON.stringify({ main: 'index.js', name: 'rstest-require-resolve-target' }),
22+
);
23+
writeFileSync(join(packageDir, 'index.js'), "module.exports = 'target';");
24+
25+
it('should resolve require.resolve relative to the source module', () => {
26+
expect(resolveHelper()).toMatch(
27+
/[\\/]require-resolve-origin[\\/]exportHelper\.js$/,
28+
);
29+
});
30+
31+
it('should preserve require.resolve paths option with injected origin', () => {
32+
expect(resolveWithPaths(customPath)).toMatch(
33+
/[\\/]custom-path[\\/]node_modules[\\/]rstest-require-resolve-target[\\/]index\.js$/,
34+
);
35+
});

packages/core/src/core/plugins/basic.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,27 @@ export const pluginBasic: (context: RstestContext) => RsbuildPlugin = (
128128
config.devtool = 'nosources-source-map';
129129
}
130130

131+
const rstestPluginOptions = {
132+
injectModulePathName: true,
133+
importMetaPathName: true,
134+
hoistMockModule: true,
135+
manualMockRoot: pathe.resolve(rootPath, '__mocks__'),
136+
// The runtime hook below resolves relative dynamic-import
137+
// specifiers against the source module that produced the
138+
// call, instead of the test entry, fixing #1207.
139+
injectDynamicImportOrigin: true,
140+
// The runtime hook below resolves relative require.resolve
141+
// specifiers against the source module that produced the
142+
// call, instead of the test entry, fixing #848.
143+
injectRequireResolveOrigin: {
144+
functionName: outputModule
145+
? 'import.meta.__rstest_require_resolve__'
146+
: '__rstest_require_resolve__',
147+
},
148+
};
149+
131150
config.plugins.push(
132-
new rspack.experiments.RstestPlugin({
133-
injectModulePathName: true,
134-
importMetaPathName: true,
135-
hoistMockModule: true,
136-
manualMockRoot: pathe.resolve(rootPath, '__mocks__'),
137-
// The runtime hook below resolves relative dynamic-import
138-
// specifiers against the source module that produced the
139-
// call, instead of the test entry, fixing #1207.
140-
injectDynamicImportOrigin: true,
141-
}),
151+
new rspack.experiments.RstestPlugin(rstestPluginOptions),
142152
);
143153

144154
config.module.rules ??= [];

packages/core/src/runtime/worker/loadEsModule.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { builtinModules, createRequire } from 'node:module';
1+
import {
2+
builtinModules,
3+
createRequire as createNativeRequire,
4+
} from 'node:module';
25
import { isAbsolute } from 'node:path';
36
import { fileURLToPath, pathToFileURL } from 'node:url';
47
import vm, { type SourceTextModule } from 'node:vm';
@@ -39,7 +42,8 @@ const resolveModule = (
3942
: pathToFileURL(resolveBase).href;
4043

4144
if (!importMetaResolve) {
42-
return pathToFileURL(createRequire(parentURL).resolve(specifier)).href;
45+
return pathToFileURL(createNativeRequire(parentURL).resolve(specifier))
46+
.href;
4347
}
4448

4549
// Node's loader hook worker clones the parent URL when native TypeScript
@@ -65,6 +69,42 @@ export const appendSourceURL = (
6569
: `${codeContent}\n${suffix}`;
6670
};
6771

72+
const defineRstestRequireResolve =
73+
({
74+
testPath,
75+
distPath,
76+
assetFiles,
77+
}: {
78+
testPath: string;
79+
distPath: string;
80+
assetFiles: Record<string, string>;
81+
}) =>
82+
(
83+
specifier: string,
84+
optionsOrOrigin?: string | { paths?: string[] },
85+
maybeOrigin?: string,
86+
): string => {
87+
const options =
88+
typeof optionsOrOrigin === 'string' ? undefined : optionsOrOrigin;
89+
const origin =
90+
typeof optionsOrOrigin === 'string' ? optionsOrOrigin : maybeOrigin;
91+
const resolveBase = origin ?? testPath;
92+
93+
const currentDirectory = path.dirname(origin ?? distPath);
94+
const joinedPath = isRelativePath(specifier)
95+
? path.join(currentDirectory, specifier)
96+
: specifier;
97+
const normalizedPath = path.normalize(
98+
joinedPath.startsWith('file://') ? fileURLToPath(joinedPath) : joinedPath,
99+
);
100+
101+
if (assetFiles[normalizedPath]) {
102+
return normalizedPath;
103+
}
104+
105+
return createNativeRequire(resolveBase).resolve(specifier, options);
106+
};
107+
68108
const defineRstestDynamicImport =
69109
({
70110
distPath,
@@ -235,6 +275,12 @@ export const loadModule = async ({
235275
esmMode: EsmMode.Unknown,
236276
});
237277
// @ts-expect-error
278+
meta.__rstest_require_resolve__ = defineRstestRequireResolve({
279+
assetFiles,
280+
testPath,
281+
distPath: distPath || testPath,
282+
});
283+
// @ts-expect-error
238284
meta.readWasmFile = (
239285
wasmPath: URL,
240286
callback: (err: Error | null, data?: Buffer) => void,

packages/core/src/runtime/worker/loadModule.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,44 @@ const resolveModule = (specifier: string, resolveBase: string): string | URL =>
2424
: pathToFileURL(resolveBase).href,
2525
);
2626

27+
const defineRstestRequireResolve =
28+
({
29+
testPath,
30+
distPath,
31+
assetFiles,
32+
}: {
33+
testPath: string;
34+
distPath: string;
35+
assetFiles: Record<string, string>;
36+
}) =>
37+
(
38+
specifier: string,
39+
optionsOrOrigin?: string | { paths?: string[] },
40+
maybeOrigin?: string,
41+
): string => {
42+
const options =
43+
typeof optionsOrOrigin === 'string' ? undefined : optionsOrOrigin;
44+
// `origin` is the absolute path of the source module that produced the
45+
// `require.resolve()` call, injected by rspack's `RstestPlugin` when
46+
// `injectRequireResolveOrigin` is enabled. Falling back keeps native
47+
// `require.resolve` semantics for un-rewritten calls.
48+
const origin =
49+
typeof optionsOrOrigin === 'string' ? optionsOrOrigin : maybeOrigin;
50+
const resolveBase = origin ?? testPath;
51+
52+
const currentDirectory = path.dirname(origin ?? distPath);
53+
const joinedPath = isRelativePath(specifier)
54+
? path.join(currentDirectory, specifier)
55+
: specifier;
56+
const normalizedPath = path.normalize(joinedPath);
57+
58+
if (assetFiles[normalizedPath]) {
59+
return normalizedPath;
60+
}
61+
62+
return createNativeRequire(resolveBase).resolve(specifier, options);
63+
};
64+
2765
const createRequire = (
2866
filename: string,
2967
distPath: string,
@@ -69,7 +107,13 @@ const createRequire = (
69107
const resolved = _require.resolve(id);
70108
return _require(resolved);
71109
}) as NodeJS.Require;
72-
require.resolve = _require.resolve;
110+
const requireResolve = defineRstestRequireResolve({
111+
testPath: filename,
112+
distPath,
113+
assetFiles,
114+
}) as NodeJS.RequireResolve;
115+
requireResolve.paths = _require.resolve.paths.bind(_require.resolve);
116+
require.resolve = requireResolve;
73117
require.main = _require.main;
74118
return require;
75119
};
@@ -225,6 +269,11 @@ export const loadModule = ({
225269
interopDefault,
226270
assetFiles,
227271
}),
272+
__rstest_require_resolve__: defineRstestRequireResolve({
273+
testPath,
274+
distPath,
275+
assetFiles,
276+
}),
228277
__dirname: fileDir,
229278
__filename: testPath,
230279
...rstestContext,

packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,9 @@ exports[`prepareRsbuild > should generate rspack config correctly (esm output) 1
514514
"importMetaPathName": true,
515515
"injectDynamicImportOrigin": true,
516516
"injectModulePathName": true,
517+
"injectRequireResolveOrigin": {
518+
"functionName": "import.meta.__rstest_require_resolve__",
519+
},
517520
"manualMockRoot": "<ROOT>/packages/core/__mocks__",
518521
},
519522
],
@@ -1098,6 +1101,9 @@ exports[`prepareRsbuild > should generate rspack config correctly (jsdom) 1`] =
10981101
"importMetaPathName": true,
10991102
"injectDynamicImportOrigin": true,
11001103
"injectModulePathName": true,
1104+
"injectRequireResolveOrigin": {
1105+
"functionName": "__rstest_require_resolve__",
1106+
},
11011107
"manualMockRoot": "<ROOT>/packages/core/__mocks__",
11021108
},
11031109
],
@@ -1668,6 +1674,9 @@ exports[`prepareRsbuild > should generate rspack config correctly (node) 1`] = `
16681674
"importMetaPathName": true,
16691675
"injectDynamicImportOrigin": true,
16701676
"injectModulePathName": true,
1677+
"injectRequireResolveOrigin": {
1678+
"functionName": "__rstest_require_resolve__",
1679+
},
16711680
"manualMockRoot": "<ROOT>/packages/core/__mocks__",
16721681
},
16731682
],
@@ -2240,6 +2249,9 @@ exports[`prepareRsbuild > should generate rspack config correctly in watch mode
22402249
"importMetaPathName": true,
22412250
"injectDynamicImportOrigin": true,
22422251
"injectModulePathName": true,
2252+
"injectRequireResolveOrigin": {
2253+
"functionName": "__rstest_require_resolve__",
2254+
},
22432255
"manualMockRoot": "<ROOT>/packages/core/__mocks__",
22442256
},
22452257
],
@@ -3147,6 +3159,9 @@ exports[`prepareRsbuild > should generate rspack config correctly with projects
31473159
"importMetaPathName": true,
31483160
"injectDynamicImportOrigin": true,
31493161
"injectModulePathName": true,
3162+
"injectRequireResolveOrigin": {
3163+
"functionName": "__rstest_require_resolve__",
3164+
},
31503165
"manualMockRoot": "<ROOT>/packages/core/__mocks__",
31513166
},
31523167
],
@@ -3718,6 +3733,9 @@ exports[`prepareRsbuild > should generate rspack config correctly with projects
37183733
"importMetaPathName": true,
37193734
"injectDynamicImportOrigin": true,
37203735
"injectModulePathName": true,
3736+
"injectRequireResolveOrigin": {
3737+
"functionName": "__rstest_require_resolve__",
3738+
},
37213739
"manualMockRoot": "<ROOT>/packages/core/__mocks__",
37223740
},
37233741
],

0 commit comments

Comments
 (0)