diff --git a/e2e/__tests__/native-esm-ts.test.ts b/e2e/__tests__/native-esm-ts.test.ts index 50efbe8a41..7fdc21753d 100644 --- a/e2e/__tests__/native-esm-ts.test.ts +++ b/e2e/__tests__/native-esm-ts.test.ts @@ -8,7 +8,7 @@ onNodeVersions('>=12.16.0', () => { }) expect(exitCode).toBe(0) - expect(json.numTotalTests).toBe(3) - expect(json.numPassedTests).toBe(3) + expect(json.numTotalTests).toBe(4) + expect(json.numPassedTests).toBe(4) }) }) diff --git a/e2e/native-esm-ts/__tests__/native-esm-ts.spec.ts b/e2e/native-esm-ts/__tests__/native-esm-ts.spec.ts index 22e0000bd2..d54f4f0056 100644 --- a/e2e/native-esm-ts/__tests__/native-esm-ts.spec.ts +++ b/e2e/native-esm-ts/__tests__/native-esm-ts.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from '@jest/globals' -import { double } from '../double' +import { double } from '../double.js' +import { quadruple } from '../quadruple/index.js' import { triple } from '../triple.mjs' test('double', () => { @@ -11,6 +12,10 @@ test('triple', () => { expect(triple(2)).toBe(6) }) +test('quadruple', () => { + expect(quadruple(2)).toBe(8) +}) + test('import.meta', () => { expect(typeof import.meta.url).toBe('string') }) diff --git a/e2e/native-esm-ts/jest-isolated.config.js b/e2e/native-esm-ts/jest-isolated.config.js index e3368224bd..a5a4bd13c1 100644 --- a/e2e/native-esm-ts/jest-isolated.config.js +++ b/e2e/native-esm-ts/jest-isolated.config.js @@ -1,7 +1,8 @@ +import config from './jest.config.js' + /** @type {import('../../dist').JestConfigWithTsJest} */ -module.exports = { - extensionsToTreatAsEsm: ['.ts'], - resolver: '/mjs-resolver.ts', +export default { + ...config, transform: { '^.+\\.m?tsx?$': [ '/../../legacy.js', diff --git a/e2e/native-esm-ts/jest.config.js b/e2e/native-esm-ts/jest.config.js new file mode 100644 index 0000000000..0b3339a149 --- /dev/null +++ b/e2e/native-esm-ts/jest.config.js @@ -0,0 +1,23 @@ +import { pathsToModuleNameMapper } from '../../dist/index.js' +import { createRequire } from 'module' + +const require = createRequire(import.meta.url) +const tsConfig = require('./tsconfig.json') + +/** @type {import('../../dist').JestConfigWithTsJest} */ +export default { + extensionsToTreatAsEsm: ['.ts'], + resolver: '/mjs-resolver.ts', + moduleNameMapper: pathsToModuleNameMapper(tsConfig.compilerOptions.paths, { + prefix: '', + useESM: true, + }), + transform: { + '^.+\\.m?tsx?$': [ + '/../../legacy.js', + { + useESM: true, + }, + ], + }, +} diff --git a/e2e/native-esm-ts/package.json b/e2e/native-esm-ts/package.json index d9c963fb06..52da452be2 100644 --- a/e2e/native-esm-ts/package.json +++ b/e2e/native-esm-ts/package.json @@ -2,14 +2,5 @@ "type": "module", "devDependencies": { "@jest/globals": "^29.0.3" - }, - "jest": { - "extensionsToTreatAsEsm": [".ts"], - "resolver": "/mjs-resolver.ts", - "transform": { - "^.+\\.m?tsx?$": ["/../../legacy.js", { - "useESM": true - }] - } } } diff --git a/e2e/native-esm-ts/quadruple/calculate.ts b/e2e/native-esm-ts/quadruple/calculate.ts new file mode 100644 index 0000000000..4f3afc5b44 --- /dev/null +++ b/e2e/native-esm-ts/quadruple/calculate.ts @@ -0,0 +1,2 @@ +const calculate = (x: number) => x * 4 +export default calculate diff --git a/e2e/native-esm-ts/quadruple/index.ts b/e2e/native-esm-ts/quadruple/index.ts new file mode 100644 index 0000000000..78919a5fe9 --- /dev/null +++ b/e2e/native-esm-ts/quadruple/index.ts @@ -0,0 +1 @@ +export { default as quadruple } from '@quadruple/calculate.js' diff --git a/e2e/native-esm-ts/tsconfig.json b/e2e/native-esm-ts/tsconfig.json index a33e7ef827..3ef020cec9 100644 --- a/e2e/native-esm-ts/tsconfig.json +++ b/e2e/native-esm-ts/tsconfig.json @@ -3,6 +3,9 @@ "module": "Node16", "target": "ESNext", "moduleResolution": "Node16", - "esModuleInterop": true + "esModuleInterop": true, + "paths": { + "@quadruple/*": ["quadruple/*"] + } } } diff --git a/src/config/paths-to-module-name-mapper.spec.ts b/src/config/paths-to-module-name-mapper.spec.ts index d465ac5ba0..167adb2aa2 100644 --- a/src/config/paths-to-module-name-mapper.spec.ts +++ b/src/config/paths-to-module-name-mapper.spec.ts @@ -39,6 +39,33 @@ describe('pathsToModuleNameMapper', () => { `) }) + test('should add `js` extension to resolved config with useESM: true', () => { + expect(pathsToModuleNameMapper(tsconfigMap, { useESM: true })).toEqual({ + /** + * Why not using snapshot here? + * Because the snapshot does not keep the property order, which is important for jest. + * A pattern ending with `\\.js` should appear before another pattern without the extension does. + */ + '^log$': 'src/utils/log', + '^server$': 'src/server', + '^client$': ['src/client', 'src/client/index'], + '^util/(.*)\\.js$': 'src/utils/$1', + '^util/(.*)$': 'src/utils/$1', + '^api/(.*)\\.js$': 'src/api/$1', + '^api/(.*)$': 'src/api/$1', + '^test/(.*)\\.js$': 'test/$1', + '^test/(.*)$': 'test/$1', + '^mocks/(.*)\\.js$': 'test/mocks/$1', + '^mocks/(.*)$': 'test/mocks/$1', + '^test/(.*)/mock\\.js$': ['test/mocks/$1', 'test/__mocks__/$1'], + '^test/(.*)/mock$': ['test/mocks/$1', 'test/__mocks__/$1'], + '^@foo\\-bar/common$': '../common/dist/library', + '^@pkg/(.*)\\.js$': './packages/$1', + '^@pkg/(.*)$': './packages/$1', + '^(\\.{1,2}/.*)\\.js$': '$1', + }) + }) + test.each(['/', 'foo'])('should convert tsconfig mapping with given prefix', (prefix) => { expect(pathsToModuleNameMapper(tsconfigMap, { prefix })).toMatchSnapshot(prefix) }) diff --git a/src/config/paths-to-module-name-mapper.ts b/src/config/paths-to-module-name-mapper.ts index e3a34b6feb..6efb088dd5 100644 --- a/src/config/paths-to-module-name-mapper.ts +++ b/src/config/paths-to-module-name-mapper.ts @@ -16,11 +16,10 @@ const logger = rootLogger.child({ [LogContexts.namespace]: 'path-mapper' }) export const pathsToModuleNameMapper = ( mapping: TsPathMapping, - { prefix = '' }: { prefix: string } = Object.create(null), + { prefix = '', useESM = false }: { prefix?: string; useESM?: boolean } = {}, ): JestPathMapping => { const jestMap: JestPathMapping = {} for (const fromPath of Object.keys(mapping)) { - let pattern: string const toPaths = mapping[fromPath] // check that we have only one target path if (toPaths.length === 0) { @@ -37,8 +36,8 @@ export const pathsToModuleNameMapper = ( return `${enrichedPrefix}${target}` }) - pattern = `^${escapeRegex(fromPath)}$` - jestMap[pattern] = paths.length === 1 ? paths[0] : paths + const cjsPattern = `^${escapeRegex(fromPath)}$` + jestMap[cjsPattern] = paths.length === 1 ? paths[0] : paths } else if (segments.length === 2) { const paths = toPaths.map((target) => { const enrichedTarget = @@ -47,12 +46,20 @@ export const pathsToModuleNameMapper = ( return `${enrichedPrefix}${enrichedTarget.replace(/\*/g, '$1')}` }) - pattern = `^${escapeRegex(segments[0])}(.*)${escapeRegex(segments[1])}$` - jestMap[pattern] = paths.length === 1 ? paths[0] : paths + if (useESM) { + const esmPattern = `^${escapeRegex(segments[0])}(.*)${escapeRegex(segments[1])}\\.js$` + jestMap[esmPattern] = paths.length === 1 ? paths[0] : paths + } + const cjsPattern = `^${escapeRegex(segments[0])}(.*)${escapeRegex(segments[1])}$` + jestMap[cjsPattern] = paths.length === 1 ? paths[0] : paths } else { logger.warn(interpolate(Errors.NotMappingMultiStarPath, { path: fromPath })) } } + if (useESM) { + jestMap['^(\\.{1,2}/.*)\\.js$'] = '$1' + } + return jestMap } diff --git a/website/docs/getting-started/paths-mapping.md b/website/docs/getting-started/paths-mapping.md index 3f37fa7e7a..fd1a3b3105 100644 --- a/website/docs/getting-started/paths-mapping.md +++ b/website/docs/getting-started/paths-mapping.md @@ -94,3 +94,9 @@ const jestConfig: JestConfigWithTsJest = { export default jestConfig ``` + +With extra options as 2nd argument: + +- `prefix`: append prefix to each of mapped config in the result +- `useESM`: when using `type: module` in `package.json`, TypeScript enforces users to have explicit `js` extension when importing + a `ts` file. This option is to help `pathsToModuleNameMapper` to create a config to suit with this scenario.