diff --git a/e2e/browser-mode/fixtures/module-name-mapper/rstest.config.ts b/e2e/browser-mode/fixtures/module-name-mapper/rstest.config.ts new file mode 100644 index 000000000..dea52ceb9 --- /dev/null +++ b/e2e/browser-mode/fixtures/module-name-mapper/rstest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from '@rstest/core'; +import { BROWSER_PORTS } from '../ports'; + +export default defineConfig({ + browser: { + enabled: true, + provider: 'playwright', + headless: true, + port: BROWSER_PORTS['module-name-mapper'], + }, + include: ['tests/**/*.test.ts'], + testTimeout: 30000, + root: __dirname, + resolve: { + moduleNameMapper: { + // Map module-a to module-b using exact match + '^module-a$': '/src/moduleB.ts', + // Map @utils/* to ./src/utils/* using capture groups + '^@utils/(.*)$': '/src/utils/$1', + }, + }, +}); diff --git a/e2e/browser-mode/fixtures/module-name-mapper/src/moduleA.ts b/e2e/browser-mode/fixtures/module-name-mapper/src/moduleA.ts new file mode 100644 index 000000000..699e1d2a1 --- /dev/null +++ b/e2e/browser-mode/fixtures/module-name-mapper/src/moduleA.ts @@ -0,0 +1 @@ +export const value = 'module-a'; diff --git a/e2e/browser-mode/fixtures/module-name-mapper/src/moduleB.ts b/e2e/browser-mode/fixtures/module-name-mapper/src/moduleB.ts new file mode 100644 index 000000000..4bbc55717 --- /dev/null +++ b/e2e/browser-mode/fixtures/module-name-mapper/src/moduleB.ts @@ -0,0 +1 @@ +export const value = 'module-b'; diff --git a/e2e/browser-mode/fixtures/module-name-mapper/src/utils/helper.ts b/e2e/browser-mode/fixtures/module-name-mapper/src/utils/helper.ts new file mode 100644 index 000000000..db669e7e3 --- /dev/null +++ b/e2e/browser-mode/fixtures/module-name-mapper/src/utils/helper.ts @@ -0,0 +1 @@ +export const helper = 'helper-function'; diff --git a/e2e/browser-mode/fixtures/module-name-mapper/tests/mapper.test.ts b/e2e/browser-mode/fixtures/module-name-mapper/tests/mapper.test.ts new file mode 100644 index 000000000..819a02430 --- /dev/null +++ b/e2e/browser-mode/fixtures/module-name-mapper/tests/mapper.test.ts @@ -0,0 +1,15 @@ +import { expect, it } from '@rstest/core'; +// This should resolve to ./src/utils/helper.ts +// @ts-expect-error - moduleNameMapper redirect +import { helper } from '@utils/helper'; +// This should resolve to module-b due to moduleNameMapper +// @ts-expect-error - moduleNameMapper redirect +import { value } from 'module-a'; + +it('moduleNameMapper should redirect module-a to module-b', () => { + expect(value).toBe('module-b'); +}); + +it('moduleNameMapper should support capture groups', () => { + expect(helper).toBe('helper-function'); +}); diff --git a/e2e/browser-mode/fixtures/ports.ts b/e2e/browser-mode/fixtures/ports.ts index a08bfb217..7a65d300e 100644 --- a/e2e/browser-mode/fixtures/ports.ts +++ b/e2e/browser-mode/fixtures/ports.ts @@ -20,4 +20,5 @@ export const BROWSER_PORTS = { 'viewport-preset': 5216, reporter: 5220, 'reporter-watch': 5222, + 'module-name-mapper': 5224, } as const; diff --git a/e2e/browser-mode/moduleNameMapper.test.ts b/e2e/browser-mode/moduleNameMapper.test.ts new file mode 100644 index 000000000..6bfa99bcb --- /dev/null +++ b/e2e/browser-mode/moduleNameMapper.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from '@rstest/core'; +import { runBrowserCli } from './utils'; + +describe('browser mode - moduleNameMapper', () => { + it('should resolve modules using moduleNameMapper config', async () => { + const { expectExecSuccess, cli } = + await runBrowserCli('module-name-mapper'); + + await expectExecSuccess(); + expect(cli.stdout).toMatch(/Tests.*2 passed/); + }); +}); diff --git a/e2e/build/fixtures/moduleNameMapper/index.test.ts b/e2e/build/fixtures/moduleNameMapper/index.test.ts new file mode 100644 index 000000000..cf3856a00 --- /dev/null +++ b/e2e/build/fixtures/moduleNameMapper/index.test.ts @@ -0,0 +1,15 @@ +import { expect, it } from '@rstest/core'; +// This should resolve to ./src/utils/helper.ts +// @ts-expect-error +import { helper } from '@utils/helper'; +// This should resolve to module-b due to moduleNameMapper +// @ts-expect-error +import { value } from 'module-a'; + +it('moduleNameMapper should redirect module-a to module-b', () => { + expect(value).toBe('module-b'); +}); + +it('moduleNameMapper should support capture groups', () => { + expect(helper).toBe('helper-function'); +}); diff --git a/e2e/build/fixtures/moduleNameMapper/rstest.config.ts b/e2e/build/fixtures/moduleNameMapper/rstest.config.ts new file mode 100644 index 000000000..4980417ae --- /dev/null +++ b/e2e/build/fixtures/moduleNameMapper/rstest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + name: 'node', + root: __dirname, + resolve: { + moduleNameMapper: { + // Map module-a to module-b using exact match + '^module-a$': '/src/moduleB.ts', + // Map @utils/* to ./src/utils/* using capture groups + '^@utils/(.*)$': '/src/utils/$1', + }, + }, +}); diff --git a/e2e/build/fixtures/moduleNameMapper/src/moduleA.ts b/e2e/build/fixtures/moduleNameMapper/src/moduleA.ts new file mode 100644 index 000000000..699e1d2a1 --- /dev/null +++ b/e2e/build/fixtures/moduleNameMapper/src/moduleA.ts @@ -0,0 +1 @@ +export const value = 'module-a'; diff --git a/e2e/build/fixtures/moduleNameMapper/src/moduleB.ts b/e2e/build/fixtures/moduleNameMapper/src/moduleB.ts new file mode 100644 index 000000000..4bbc55717 --- /dev/null +++ b/e2e/build/fixtures/moduleNameMapper/src/moduleB.ts @@ -0,0 +1 @@ +export const value = 'module-b'; diff --git a/e2e/build/fixtures/moduleNameMapper/src/utils/helper.ts b/e2e/build/fixtures/moduleNameMapper/src/utils/helper.ts new file mode 100644 index 000000000..db669e7e3 --- /dev/null +++ b/e2e/build/fixtures/moduleNameMapper/src/utils/helper.ts @@ -0,0 +1 @@ +export const helper = 'helper-function'; diff --git a/e2e/build/index.test.ts b/e2e/build/index.test.ts index 5c5b93903..a8a12a877 100644 --- a/e2e/build/index.test.ts +++ b/e2e/build/index.test.ts @@ -13,16 +13,25 @@ describe('test build config', () => { { name: 'plugin' }, { name: 'tools/rspack' }, { name: 'decorators' }, - ])('$name config should work correctly', async ({ name }, { - onTestFinished, - }) => { + { name: 'moduleNameMapper' }, + { + name: 'moduleNameMapperHappyDom', + fixtureDir: 'moduleNameMapper', + testEnvironment: 'happy-dom', + }, + ])('$name config should work correctly', async ({ + name, + fixtureDir = name, + testEnvironment, + }, { onTestFinished }) => { const { expectExecSuccess } = await runRstestCli({ command: 'rstest', args: [ 'run', - `fixtures/${name}`, + `fixtures/${fixtureDir}`, '-c', - `fixtures/${name}/rstest.config.ts`, + `fixtures/${fixtureDir}/rstest.config.ts`, + ...(testEnvironment ? ['--testEnvironment', testEnvironment] : []), ], onTestFinished, options: { diff --git a/packages/browser/src/hostController.ts b/packages/browser/src/hostController.ts index ea28463ad..1dd7fa963 100644 --- a/packages/browser/src/hostController.ts +++ b/packages/browser/src/hostController.ts @@ -3,6 +3,7 @@ import fs from 'node:fs/promises'; import type { IncomingMessage, ServerResponse } from 'node:http'; import type { AddressInfo } from 'node:net'; import { fileURLToPath } from 'node:url'; +import type { RsbuildPlugin } from '@rstest/core'; import { type BrowserTestRunOptions, type BrowserTestRunResult, @@ -869,6 +870,7 @@ const createBrowserRuntime = async ({ containerDistPath, containerDevServer, forceHeadless, + builtinRsbuildPlugins, }: { context: Rstest; manifestPath: string; @@ -880,6 +882,9 @@ const createBrowserRuntime = async ({ containerDevServer?: string; /** Force headless mode regardless of user config (used for list command) */ forceHeadless?: boolean; + builtinRsbuildPlugins: { + pluginModuleNameMapper: RsbuildPlugin; + }; }): Promise => { const virtualManifestPlugin = new rspack.experiments.VirtualModulesPlugin({ [manifestPath]: manifestSource, @@ -951,6 +956,7 @@ const createBrowserRuntime = async ({ // Add plugin to merge user Rsbuild config with rstest required config rsbuildInstance.addPlugins([ + builtinRsbuildPlugins.pluginModuleNameMapper, { name: 'rstest:browser-user-config', setup(api) { @@ -1337,9 +1343,9 @@ async function resolveProjectEntries( export const runBrowserController = async ( context: Rstest, - options?: BrowserTestRunOptions, + options: BrowserTestRunOptions, ): Promise => { - const { skipOnTestRunEnd = false } = options ?? {}; + const { skipOnTestRunEnd = false, builtinRsbuildPlugins } = options; const buildStart = Date.now(); const browserProjects = getBrowserProjects(context); const useHeadlessDirect = browserProjects.every( @@ -1540,6 +1546,7 @@ export const runBrowserController = async ( : undefined, containerDistPath, containerDevServer, + builtinRsbuildPlugins, }); } catch (error) { return failWithError(error, async () => { @@ -2525,8 +2532,11 @@ export type ListBrowserTestsResult = { */ export const listBrowserTests = async ( context: Rstest, - options?: { + options: { shardedEntries?: Map }>; + builtinRsbuildPlugins: { + pluginModuleNameMapper: RsbuildPlugin; + }; }, ): Promise => { const projectEntries = await resolveProjectEntries( @@ -2570,6 +2580,7 @@ export const listBrowserTests = async ( containerDistPath: undefined, containerDevServer: undefined, forceHeadless: true, // Always use headless for list command + builtinRsbuildPlugins: options.builtinRsbuildPlugins, }); } catch (error) { logger.error( diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index b11c3e228..c8a780e57 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -19,15 +19,16 @@ export { export async function runBrowserTests( context: Rstest, - options?: BrowserTestRunOptions, + options: BrowserTestRunOptions, ): Promise { return runBrowserController(context, options); } export async function listBrowserTests( context: Rstest, + options: BrowserTestRunOptions, ): Promise { - return listBrowserTestsImpl(context); + return listBrowserTestsImpl(context, options); } export type { diff --git a/packages/core/src/core/browserLoader.ts b/packages/core/src/core/browserLoader.ts index 18b52462e..28a5b1df6 100644 --- a/packages/core/src/core/browserLoader.ts +++ b/packages/core/src/core/browserLoader.ts @@ -16,13 +16,11 @@ export interface BrowserModule { validateBrowserConfig: (context: unknown) => void; runBrowserTests: ( context: unknown, - options?: BrowserTestRunOptions, + options: BrowserTestRunOptions, ) => Promise; listBrowserTests: ( context: unknown, - options?: { - shardedEntries?: Map }>; - }, + options: BrowserTestRunOptions, ) => Promise<{ list: ListCommandResult[]; close: () => Promise; diff --git a/packages/core/src/core/listTests.ts b/packages/core/src/core/listTests.ts index cc2c45d7e..244256c4e 100644 --- a/packages/core/src/core/listTests.ts +++ b/packages/core/src/core/listTests.ts @@ -194,7 +194,14 @@ const collectBrowserTests = async ({ projectRoots, }); validateBrowserConfig(context); - return listBrowserTests(context, { shardedEntries }); + const { pluginModuleNameMapper } = await import('./plugins/moduleNameMapper'); + + return listBrowserTests(context, { + shardedEntries, + builtinRsbuildPlugins: { + pluginModuleNameMapper: pluginModuleNameMapper(context), + }, + }); }; const collectTestFiles = async ({ diff --git a/packages/core/src/core/plugins/external.ts b/packages/core/src/core/plugins/external.ts index 7058d7168..a99765c07 100644 --- a/packages/core/src/core/plugins/external.ts +++ b/packages/core/src/core/plugins/external.ts @@ -105,6 +105,7 @@ export const pluginExternal: (context: RstestContext) => RsbuildPlugin = ( normalizedConfig: { testEnvironment }, outputModule, } = context.projects.find((p) => p.environmentName === name)!; + return mergeEnvironmentConfig(config, { output: { externals: diff --git a/packages/core/src/core/plugins/moduleNameMapper.ts b/packages/core/src/core/plugins/moduleNameMapper.ts new file mode 100644 index 000000000..95630a443 --- /dev/null +++ b/packages/core/src/core/plugins/moduleNameMapper.ts @@ -0,0 +1,164 @@ +import { type RsbuildPlugin, type Rspack, rspack } from '@rsbuild/core'; +import type { RstestContext } from '../../types'; + +type ModuleNameMapperConfig = Record; + +/** + * Replace `` token in the replacement path with the actual root directory. + */ +function replaceRootDir(value: string, rootDir: string): string { + return value.replace(//g, rootDir); +} + +/** + * Create NormalModuleReplacementPlugin instances for moduleNameMapper configuration. + * This is a shared utility used by both node and browser modes. + * + * @returns Array of NormalModuleReplacementPlugin instances + */ +export function createModuleNameMapperPlugins(options: { + /** The moduleNameMapper configuration */ + moduleNameMapper: ModuleNameMapperConfig; + /** The root directory for `` token replacement */ + rootDir: string; + /** The rspack instance to use for creating plugins (defaults to imported rspack) */ + rspack?: typeof rspack; +}): Rspack.WebpackPluginInstance[] { + const { + moduleNameMapper, + rootDir, + rspack: rspackInstance = rspack, + } = options; + const plugins: Rspack.WebpackPluginInstance[] = []; + + for (const [pattern, replacement] of Object.entries(moduleNameMapper)) { + const resourceRegExp = new RegExp(pattern); + const replacements = Array.isArray(replacement) + ? replacement + : [replacement]; + + // Process each replacement with rootDir substitution + const processedReplacements = replacements.map((r) => + replaceRootDir(r, rootDir), + ); + + // Use the first replacement path + // Note: Unlike Jest, we don't try to resolve each path in order + // because NormalModuleReplacementPlugin doesn't support async resolution + // Users should ensure the first path exists or use a single path + const newResource = processedReplacements[0]!; + + plugins.push( + new rspackInstance.NormalModuleReplacementPlugin( + resourceRegExp, + (resource: Rspack.ResolveData) => { + // Apply capture group replacements ($1, $2, etc.) + const match = resource.request?.match(resourceRegExp); + if (match) { + let resolvedPath = newResource; + // Replace capture group references with matched values + for (let i = 1; i < match.length; i++) { + resolvedPath = resolvedPath.replace( + new RegExp(`\\$${i}`, 'g'), + match[i] || '', + ); + } + resource.request = resolvedPath; + } + }, + ), + ); + } + + return plugins; +} + +/** + * Check if a request matches any moduleNameMapper pattern. + * If it does, don't externalize it - let NormalModuleReplacementPlugin handle it. + */ +const matchesModuleNameMapper = ( + request: string, + moduleNameMapper: Record | undefined, +): boolean => { + if (!moduleNameMapper) return false; + + for (const pattern of Object.keys(moduleNameMapper)) { + if (new RegExp(pattern).test(request)) { + return true; + } + } + return false; +}; + +const excludeExternalize: ( + moduleNameMapper: Record | undefined, +) => ( + data: Rspack.ExternalItemFunctionData, + callback: ( + err?: Error, + result?: Rspack.ExternalItemValue, + type?: Rspack.ExternalsType, + ) => void, +) => void = + (moduleNameMapper) => + ({ request }, callback) => { + if (!request) { + return callback(); + } + + // If request matches moduleNameMapper, don't externalize - let it be transformed + if (matchesModuleNameMapper(request, moduleNameMapper)) { + return callback(undefined, false); + } + return callback(); + }; + +/** + * Apply module name mapper using rspack.NormalModuleReplacementPlugin. + * + * This is similar to Jest's moduleNameMapper configuration. + * - Keys are regex patterns to match the module request + * - Values are replacement paths (string or array of strings) + * - Capture groups ($1, $2, etc.) are supported + * - `` token is replaced with the directory containing the config file + */ +export const pluginModuleNameMapper: (context: RstestContext) => RsbuildPlugin = + (context) => ({ + name: 'rstest:module-name-mapper', + setup: (api) => { + api.modifyRspackConfig((config, { environment }) => { + const project = context.projects.find( + (p) => p.environmentName === environment.name, + ); + + if (!project) { + return config; + } + + const moduleNameMapper = + project.normalizedConfig.resolve?.moduleNameMapper; + + if (!moduleNameMapper || Object.keys(moduleNameMapper).length === 0) { + return config; + } + + config.plugins ??= []; + + const mapperPlugins = createModuleNameMapperPlugins({ + moduleNameMapper, + rootDir: project.rootPath, + }); + config.plugins.push(...mapperPlugins); + + // Make sure that externals configuration is not modified by users + config.externals = Array.isArray(config.externals) + ? config.externals + : []; + + config.externals.unshift(excludeExternalize(moduleNameMapper)); + + return config; + }); + }, + }); diff --git a/packages/core/src/core/rsbuild.ts b/packages/core/src/core/rsbuild.ts index 2970f9cce..f3c8ff179 100644 --- a/packages/core/src/core/rsbuild.ts +++ b/packages/core/src/core/rsbuild.ts @@ -23,6 +23,7 @@ import { pluginIgnoreResolveError } from './plugins/ignoreResolveError'; import { pluginInspect } from './plugins/inspect'; import { pluginMockRuntime } from './plugins/mockRuntime'; import { pluginCacheControl } from './plugins/moduleCacheControl'; +import { pluginModuleNameMapper } from './plugins/moduleNameMapper'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -122,6 +123,7 @@ export const prepareRsbuild = async ( isWatch: command === 'watch', }), pluginExternal(context), + pluginModuleNameMapper(context), !isolate ? pluginCacheControl( Object.values({ diff --git a/packages/core/src/core/runTests.ts b/packages/core/src/core/runTests.ts index 4fb983243..f3677e23e 100644 --- a/packages/core/src/core/runTests.ts +++ b/packages/core/src/core/runTests.ts @@ -25,14 +25,20 @@ import type { Rstest } from './rstest'; async function runBrowserModeTests( context: Rstest, browserProjects: typeof context.projects, - options: BrowserTestRunOptions, + options: Omit, ): Promise { const projectRoots = browserProjects.map((p) => p.rootPath); const { validateBrowserConfig, runBrowserTests } = await loadBrowserModule({ projectRoots, }); validateBrowserConfig(context); - return runBrowserTests(context, options); + const { pluginModuleNameMapper } = await import('./plugins/moduleNameMapper'); + return runBrowserTests(context, { + ...options, + builtinRsbuildPlugins: { + pluginModuleNameMapper: pluginModuleNameMapper(context), + }, + }); } export async function runTests(context: Rstest): Promise { diff --git a/packages/core/src/env.d.ts b/packages/core/src/env.d.ts index ec1bd7da3..9f43deb7d 100644 --- a/packages/core/src/env.d.ts +++ b/packages/core/src/env.d.ts @@ -9,11 +9,21 @@ declare const PLAYWRIGHT_VERSION: string; * The actual types come from the package when installed. */ declare module '@rstest/browser' { - import type { ListCommandResult, RstestContext } from './types'; + import type { + ListCommandResult, + RstestContext, + BrowserTestRunOptions, + } from './types'; export function validateBrowserConfig(context: RstestContext): void; - export function runBrowserTests(context: RstestContext): Promise; - export function listBrowserTests(context: RstestContext): Promise<{ + export function runBrowserTests( + context: RstestContext, + options: BrowserTestRunOptions, + ): Promise; + export function listBrowserTests( + context: RstestContext, + options: BrowserTestRunOptions, + ): Promise<{ list: ListCommandResult[]; close: () => Promise; }>; diff --git a/packages/core/src/types/browser.ts b/packages/core/src/types/browser.ts index 83e9d4ba1..02ea57ce5 100644 --- a/packages/core/src/types/browser.ts +++ b/packages/core/src/types/browser.ts @@ -1,3 +1,4 @@ +import type { RsbuildPlugin } from '@rsbuild/core'; import type { TestFileResult, TestResult } from './testSuite'; /** @@ -15,6 +16,9 @@ export interface BrowserTestRunOptions { * Key is project environmentName. */ shardedEntries?: Map }>; + builtinRsbuildPlugins: { + pluginModuleNameMapper: RsbuildPlugin; + }; } /** diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index ccc17d29c..ba4b5c4e1 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -439,7 +439,18 @@ export interface RstestConfig { 'cssModules' | 'externals' | 'cleanDistPath' | 'module' >; - resolve?: RsbuildConfig['resolve']; + resolve?: RsbuildConfig['resolve'] & { + /** + * A map from regular expressions to module names or to arrays of module names + * that allow to stub out resources with a single module. + * + * This is similar to Jest's `moduleNameMapper` configuration. + * Keys are regex patterns and values are replacement paths. + * Use `$1`, `$2`, etc. to reference capture groups. + * Use `` as a string token to reference the root directory. + */ + moduleNameMapper?: Record; + }; tools?: Pick< NonNullable, diff --git a/website/docs/en/config/build/resolve.mdx b/website/docs/en/config/build/resolve.mdx index 515621382..0aa77ec76 100644 --- a/website/docs/en/config/build/resolve.mdx +++ b/website/docs/en/config/build/resolve.mdx @@ -21,3 +21,31 @@ Force Rstest to resolve the specified packages from project root, which is usefu ## resolve.extensions Automatically resolve file extensions when importing modules. This means you can import files without explicitly writing their extensions. + +## resolve.moduleNameMapper + +- **Type:** `Record` +- **Default:** `undefined` + +A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module. This is similar to Jest's [moduleNameMapper](https://jestjs.io/docs/configuration#modulenamemapper-objectstring-string--arraystring) configuration. + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + resolve: { + moduleNameMapper: { + // Map exact module name + '^module-a$': '/mocks/module-a.ts', + // Map with capture groups + '^@utils/(.*)$': '/src/utils/$1', + // Stub CSS imports + '\\.(css|less|scss)$': 'identity-obj-proxy', + }, + }, +}); +``` + +:::tip +For simple path aliasing, consider using TypeScript's `paths` or [resolve.alias](#resolvealias) in `tsconfig.json` instead, which provides better performance and IDE support. +::: diff --git a/website/docs/en/guide/migration/jest.mdx b/website/docs/en/guide/migration/jest.mdx index 573ebb266..d44a55fa7 100644 --- a/website/docs/en/guide/migration/jest.mdx +++ b/website/docs/en/guide/migration/jest.mdx @@ -45,24 +45,24 @@ export default defineConfig({ Here are some common Jest configurations and their Rstest equivalents: -| Jest Configuration | Rstest Equivalent | -| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `testRegex` | [`include`](/config/test/include) | -| `testMatch` | [`include`](/config/test/include) | -| `testPathIgnorePatterns` | [`exclude`](/config/test/exclude) | -| `transformIgnorePatterns` | [`output.externals`](/config/build/output#outputexternals)、[`source.exclude`](/config/build/source#sourceexclude) | -| `displayName` | [`name`](/config/test/name) | -| `rootDir` | [`root`](/config/test/root) | -| `setupFilesAfterEnv` | [`setupFiles`](/config/test/setup-files) | -| `verbose` | [`verbose-reporter`](/config/test/reporters#verbose-reporter) | -| `injectGlobals` | [`globals`](/config/test/globals) | -| `moduleNameMapper` | [`resolve.alias`](/config/build/resolve#resolvealias) | -| `maxWorkers` | [`pool.maxWorkers`](/config/test/pool) | -| `collectCoverage` | [`coverage.enabled`](/config/test/coverage#enabled) | -| `coverageDirectory` | [`coverage.reportsDirectory`](/config/test/coverage#reportsdirectory) | -| `coverageProvider` | [`coverage.provider`](/config/test/coverage#provider) | -| `coveragePathIgnorePatterns` | [`coverage.exclude`](/config/test/coverage#exclude) | -| `coverageThreshold` | [`coverage.thresholds`](/config/test/coverage#thresholds) | +| Jest Configuration | Rstest Equivalent | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `testRegex` | [`include`](/config/test/include) | +| `testMatch` | [`include`](/config/test/include) | +| `testPathIgnorePatterns` | [`exclude`](/config/test/exclude) | +| `transformIgnorePatterns` | [`output.externals`](/config/build/output#outputexternals)、[`source.exclude`](/config/build/source#sourceexclude) | +| `displayName` | [`name`](/config/test/name) | +| `rootDir` | [`root`](/config/test/root) | +| `setupFilesAfterEnv` | [`setupFiles`](/config/test/setup-files) | +| `verbose` | [`verbose-reporter`](/config/test/reporters#verbose-reporter) | +| `injectGlobals` | [`globals`](/config/test/globals) | +| `moduleNameMapper` | [`resolve.alias`](/config/build/resolve#resolvealias)、[`resolve.moduleNameMapper`](/config/build/resolve#resolvemodulenamemapper) | +| `maxWorkers` | [`pool.maxWorkers`](/config/test/pool) | +| `collectCoverage` | [`coverage.enabled`](/config/test/coverage#enabled) | +| `coverageDirectory` | [`coverage.reportsDirectory`](/config/test/coverage#reportsdirectory) | +| `coverageProvider` | [`coverage.provider`](/config/test/coverage#provider) | +| `coveragePathIgnorePatterns` | [`coverage.exclude`](/config/test/coverage#exclude) | +| `coverageThreshold` | [`coverage.thresholds`](/config/test/coverage#thresholds) | For more details, please refer to the [Configuration](/config) section. diff --git a/website/docs/zh/config/build/resolve.mdx b/website/docs/zh/config/build/resolve.mdx index dfa85a2ae..b82f32317 100644 --- a/website/docs/zh/config/build/resolve.mdx +++ b/website/docs/zh/config/build/resolve.mdx @@ -21,3 +21,31 @@ import { RsbuildDocBadge } from '@components/RsbuildDocBadge'; ## resolve.extensions 自动添加导入文件的扩展名。这意味着你可以导入文件,而不需要显式地写它们的扩展名。 + +## resolve.moduleNameMapper + +- **类型:** `Record` +- **默认值:** `undefined` + +从正则表达式到模块名称或模块名称数组的映射,允许用单个模块替换多个模块。这类似于 Jest 的 [moduleNameMapper](https://jestjs.io/zh-Hans/docs/configuration#modulenamemapper-objectstring-string--arraystring) 配置。 + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + resolve: { + moduleNameMapper: { + // 精确匹配模块名 + '^module-a$': '/mocks/module-a.ts', + // 使用捕获组进行匹配 + '^@utils/(.*)$': '/src/utils/$1', + // 替换 CSS 导入 + '\\.(css|less|scss)$': 'identity-obj-proxy', + }, + }, +}); +``` + +:::tip +对于简单的路径别名,建议使用 TypeScript 的 `tsconfig.json` 中的 `paths` 或 [resolve.alias](#resolvealias) 配置,它们提供更好的性能和 IDE 支持。 +::: diff --git a/website/docs/zh/guide/migration/jest.mdx b/website/docs/zh/guide/migration/jest.mdx index 0b8ddb23b..53eb1f50a 100644 --- a/website/docs/zh/guide/migration/jest.mdx +++ b/website/docs/zh/guide/migration/jest.mdx @@ -45,24 +45,24 @@ export default defineConfig({ 以下是一些常见的 Jest 配置及其对应的 Rstest 配置: -| Jest 配置 | Rstest 对等配置 | -| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `testRegex` | [`include`](/config/test/include) | -| `testMatch` | [`include`](/config/test/include) | -| `testPathIgnorePatterns` | [`exclude`](/config/test/exclude) | -| `transformIgnorePatterns` | [`output.externals`](/config/build/output#outputexternals)、[`source.exclude`](/config/build/source#sourceexclude) | -| `displayName` | [`name`](/config/test/name) | -| `rootDir` | [`root`](/config/test/root) | -| `setupFilesAfterEnv` | [`setupFiles`](/config/test/setup-files) | -| `verbose` | [`verbose-reporter`](/config/test/reporters#verbose-reporter) | -| `injectGlobals` | [`globals`](/config/test/globals) | -| `moduleNameMapper` | [`resolve.alias`](/config/build/resolve#resolvealias) | -| `maxWorkers` | [`pool.maxWorkers`](/config/test/pool) | -| `collectCoverage` | [`coverage.enabled`](/config/test/coverage#enabled) | -| `coverageDirectory` | [`coverage.reportsDirectory`](/config/test/coverage#reportsdirectory) | -| `coverageProvider` | [`coverage.provider`](/config/test/coverage#provider) | -| `coveragePathIgnorePatterns` | [`coverage.exclude`](/config/test/coverage#exclude) | -| `coverageThreshold` | [`coverage.thresholds`](/config/test/coverage#thresholds) | +| Jest 配置 | Rstest 对等配置 | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `testRegex` | [`include`](/config/test/include) | +| `testMatch` | [`include`](/config/test/include) | +| `testPathIgnorePatterns` | [`exclude`](/config/test/exclude) | +| `transformIgnorePatterns` | [`output.externals`](/config/build/output#outputexternals)、[`source.exclude`](/config/build/source#sourceexclude) | +| `displayName` | [`name`](/config/test/name) | +| `rootDir` | [`root`](/config/test/root) | +| `setupFilesAfterEnv` | [`setupFiles`](/config/test/setup-files) | +| `verbose` | [`verbose-reporter`](/config/test/reporters#verbose-reporter) | +| `injectGlobals` | [`globals`](/config/test/globals) | +| `moduleNameMapper` | [`resolve.alias`](/config/build/resolve#resolvealias)、[`resolve.moduleNameMapper`](/config/build/resolve#resolvemodulenamemapper) | +| `maxWorkers` | [`pool.maxWorkers`](/config/test/pool) | +| `collectCoverage` | [`coverage.enabled`](/config/test/coverage#enabled) | +| `coverageDirectory` | [`coverage.reportsDirectory`](/config/test/coverage#reportsdirectory) | +| `coverageProvider` | [`coverage.provider`](/config/test/coverage#provider) | +| `coveragePathIgnorePatterns` | [`coverage.exclude`](/config/test/coverage#exclude) | +| `coverageThreshold` | [`coverage.thresholds`](/config/test/coverage#thresholds) | 更多详情,请参考 [配置文档](/config)。 diff --git a/website/rspress.config.ts b/website/rspress.config.ts index f71695b73..f49e29665 100644 --- a/website/rspress.config.ts +++ b/website/rspress.config.ts @@ -97,6 +97,7 @@ export default defineConfig({ }), ], performance: { + buildCache: false, printFileSize: { total: true, detail: false, diff --git a/website/theme/components/ConfigOverview.tsx b/website/theme/components/ConfigOverview.tsx index 09bc0543f..36f544680 100644 --- a/website/theme/components/ConfigOverview.tsx +++ b/website/theme/components/ConfigOverview.tsx @@ -143,6 +143,7 @@ const BUILD_OVERVIEW_GROUPS: BasicGroup[] = [ items: [ 'resolve.aliasStrategy', 'resolve.alias', + 'resolve.moduleNameMapper', 'resolve.dedupe', 'resolve.extensions', ],