Skip to content

Commit eabe906

Browse files
authoredSep 25, 2022
feat: add useESM option to pathsToModuleNameMapper options (#3792)
1 parent 12a90d3 commit eabe906

11 files changed

+88
-22
lines changed
 

‎e2e/__tests__/native-esm-ts.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ onNodeVersions('>=12.16.0', () => {
88
})
99

1010
expect(exitCode).toBe(0)
11-
expect(json.numTotalTests).toBe(3)
12-
expect(json.numPassedTests).toBe(3)
11+
expect(json.numTotalTests).toBe(4)
12+
expect(json.numPassedTests).toBe(4)
1313
})
1414
})

‎e2e/native-esm-ts/__tests__/native-esm-ts.spec.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { test, expect } from '@jest/globals'
22

3-
import { double } from '../double'
3+
import { double } from '../double.js'
4+
import { quadruple } from '../quadruple/index.js'
45
import { triple } from '../triple.mjs'
56

67
test('double', () => {
@@ -11,6 +12,10 @@ test('triple', () => {
1112
expect(triple(2)).toBe(6)
1213
})
1314

15+
test('quadruple', () => {
16+
expect(quadruple(2)).toBe(8)
17+
})
18+
1419
test('import.meta', () => {
1520
expect(typeof import.meta.url).toBe('string')
1621
})

‎e2e/native-esm-ts/jest-isolated.config.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import config from './jest.config.js'
2+
13
/** @type {import('../../dist').JestConfigWithTsJest} */
2-
module.exports = {
3-
extensionsToTreatAsEsm: ['.ts'],
4-
resolver: '<rootDir>/mjs-resolver.ts',
4+
export default {
5+
...config,
56
transform: {
67
'^.+\\.m?tsx?$': [
78
'<rootDir>/../../legacy.js',

‎e2e/native-esm-ts/jest.config.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { pathsToModuleNameMapper } from '../../dist/index.js'
2+
import { createRequire } from 'module'
3+
4+
const require = createRequire(import.meta.url)
5+
const tsConfig = require('./tsconfig.json')
6+
7+
/** @type {import('../../dist').JestConfigWithTsJest} */
8+
export default {
9+
extensionsToTreatAsEsm: ['.ts'],
10+
resolver: '<rootDir>/mjs-resolver.ts',
11+
moduleNameMapper: pathsToModuleNameMapper(tsConfig.compilerOptions.paths, {
12+
prefix: '<rootDir>',
13+
useESM: true,
14+
}),
15+
transform: {
16+
'^.+\\.m?tsx?$': [
17+
'<rootDir>/../../legacy.js',
18+
{
19+
useESM: true,
20+
},
21+
],
22+
},
23+
}

‎e2e/native-esm-ts/package.json

-9
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,5 @@
22
"type": "module",
33
"devDependencies": {
44
"@jest/globals": "^29.0.3"
5-
},
6-
"jest": {
7-
"extensionsToTreatAsEsm": [".ts"],
8-
"resolver": "<rootDir>/mjs-resolver.ts",
9-
"transform": {
10-
"^.+\\.m?tsx?$": ["<rootDir>/../../legacy.js", {
11-
"useESM": true
12-
}]
13-
}
145
}
156
}
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const calculate = (x: number) => x * 4
2+
export default calculate

‎e2e/native-esm-ts/quadruple/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as quadruple } from '@quadruple/calculate.js'

‎e2e/native-esm-ts/tsconfig.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"module": "Node16",
44
"target": "ESNext",
55
"moduleResolution": "Node16",
6-
"esModuleInterop": true
6+
"esModuleInterop": true,
7+
"paths": {
8+
"@quadruple/*": ["quadruple/*"]
9+
}
710
}
811
}

‎src/config/paths-to-module-name-mapper.spec.ts

+27
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,33 @@ describe('pathsToModuleNameMapper', () => {
3939
`)
4040
})
4141

42+
test('should add `js` extension to resolved config with useESM: true', () => {
43+
expect(pathsToModuleNameMapper(tsconfigMap, { useESM: true })).toEqual({
44+
/**
45+
* Why not using snapshot here?
46+
* Because the snapshot does not keep the property order, which is important for jest.
47+
* A pattern ending with `\\.js` should appear before another pattern without the extension does.
48+
*/
49+
'^log$': 'src/utils/log',
50+
'^server$': 'src/server',
51+
'^client$': ['src/client', 'src/client/index'],
52+
'^util/(.*)\\.js$': 'src/utils/$1',
53+
'^util/(.*)$': 'src/utils/$1',
54+
'^api/(.*)\\.js$': 'src/api/$1',
55+
'^api/(.*)$': 'src/api/$1',
56+
'^test/(.*)\\.js$': 'test/$1',
57+
'^test/(.*)$': 'test/$1',
58+
'^mocks/(.*)\\.js$': 'test/mocks/$1',
59+
'^mocks/(.*)$': 'test/mocks/$1',
60+
'^test/(.*)/mock\\.js$': ['test/mocks/$1', 'test/__mocks__/$1'],
61+
'^test/(.*)/mock$': ['test/mocks/$1', 'test/__mocks__/$1'],
62+
'^@foo\\-bar/common$': '../common/dist/library',
63+
'^@pkg/(.*)\\.js$': './packages/$1',
64+
'^@pkg/(.*)$': './packages/$1',
65+
'^(\\.{1,2}/.*)\\.js$': '$1',
66+
})
67+
})
68+
4269
test.each(['<rootDir>/', 'foo'])('should convert tsconfig mapping with given prefix', (prefix) => {
4370
expect(pathsToModuleNameMapper(tsconfigMap, { prefix })).toMatchSnapshot(prefix)
4471
})

‎src/config/paths-to-module-name-mapper.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@ const logger = rootLogger.child({ [LogContexts.namespace]: 'path-mapper' })
1616

1717
export const pathsToModuleNameMapper = (
1818
mapping: TsPathMapping,
19-
{ prefix = '' }: { prefix: string } = Object.create(null),
19+
{ prefix = '', useESM = false }: { prefix?: string; useESM?: boolean } = {},
2020
): JestPathMapping => {
2121
const jestMap: JestPathMapping = {}
2222
for (const fromPath of Object.keys(mapping)) {
23-
let pattern: string
2423
const toPaths = mapping[fromPath]
2524
// check that we have only one target path
2625
if (toPaths.length === 0) {
@@ -37,8 +36,8 @@ export const pathsToModuleNameMapper = (
3736

3837
return `${enrichedPrefix}${target}`
3938
})
40-
pattern = `^${escapeRegex(fromPath)}$`
41-
jestMap[pattern] = paths.length === 1 ? paths[0] : paths
39+
const cjsPattern = `^${escapeRegex(fromPath)}$`
40+
jestMap[cjsPattern] = paths.length === 1 ? paths[0] : paths
4241
} else if (segments.length === 2) {
4342
const paths = toPaths.map((target) => {
4443
const enrichedTarget =
@@ -47,12 +46,20 @@ export const pathsToModuleNameMapper = (
4746

4847
return `${enrichedPrefix}${enrichedTarget.replace(/\*/g, '$1')}`
4948
})
50-
pattern = `^${escapeRegex(segments[0])}(.*)${escapeRegex(segments[1])}$`
51-
jestMap[pattern] = paths.length === 1 ? paths[0] : paths
49+
if (useESM) {
50+
const esmPattern = `^${escapeRegex(segments[0])}(.*)${escapeRegex(segments[1])}\\.js$`
51+
jestMap[esmPattern] = paths.length === 1 ? paths[0] : paths
52+
}
53+
const cjsPattern = `^${escapeRegex(segments[0])}(.*)${escapeRegex(segments[1])}$`
54+
jestMap[cjsPattern] = paths.length === 1 ? paths[0] : paths
5255
} else {
5356
logger.warn(interpolate(Errors.NotMappingMultiStarPath, { path: fromPath }))
5457
}
5558
}
5659

60+
if (useESM) {
61+
jestMap['^(\\.{1,2}/.*)\\.js$'] = '$1'
62+
}
63+
5764
return jestMap
5865
}

‎website/docs/getting-started/paths-mapping.md

+6
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,9 @@ const jestConfig: JestConfigWithTsJest = {
9494

9595
export default jestConfig
9696
```
97+
98+
With extra options as 2nd argument:
99+
100+
- `prefix`: append prefix to each of mapped config in the result
101+
- `useESM`: when using `type: module` in `package.json`, TypeScript enforces users to have explicit `js` extension when importing
102+
a `ts` file. This option is to help `pathsToModuleNameMapper` to create a config to suit with this scenario.

0 commit comments

Comments
 (0)
Please sign in to comment.