Skip to content

Commit ab6f8cd

Browse files
committed
feat(api/test): make ts-jest's pathsToModuleNameMapper helper available
This copies in the module name mapper helper from `ts-jest` so we can use that whether we're using ts-jest or not.
1 parent 5fe61c1 commit ab6f8cd

9 files changed

+421
-52
lines changed

Diff for: .babelrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"targets": {"node": "12"},
77
"modules": "commonjs"
88
}
9-
]
9+
],
10+
"@babel/preset-typescript"
1011
]
1112
}

Diff for: package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
},
1414
"scripts": {
1515
"build": "run-p 'build:*'",
16-
"build:source": "babel --source-maps --out-dir dist --ignore '**/__tests__/**','**/__mocks__/**' --copy-files --no-copy-ignored src",
16+
"build:source": "babel --source-maps --extensions '.ts' --out-dir dist --ignore '**/__tests__/**','**/__mocks__/**' --copy-files --no-copy-ignored src",
1717
"build:types": "tsc -p src/",
1818
"ci-after-success": "node src ci-after-success",
1919
"commit": "node src commit",
@@ -132,6 +132,7 @@
132132
"@babel/cli": "^7.23.0",
133133
"@babel/core": "^7.23.2",
134134
"@babel/preset-env": "^7.23.2",
135+
"@babel/preset-typescript": "^7.23.3",
135136
"@types/cross-spawn": "^6.0.4",
136137
"@types/lodash.merge": "^4",
137138
"depcheck": "^1.4.7",

Diff for: src/api/test.js

Whitespace-only changes.

Diff for: src/api/test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './test/index'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`pathsToModuleNameMapper should convert tsconfig mapping with given prefix: <rootDir>/ 1`] = `
4+
Object {
5+
"^@foo\\\\-bar/common$": "<rootDir>/../common/dist/library",
6+
"^@pkg/(.*)$": "<rootDir>/packages/$1",
7+
"^api/(.*)$": "<rootDir>/src/api/$1",
8+
"^client$": Array [
9+
"<rootDir>/src/client",
10+
"<rootDir>/src/client/index",
11+
],
12+
"^log$": "<rootDir>/src/utils/log",
13+
"^mocks/(.*)$": "<rootDir>/test/mocks/$1",
14+
"^server$": "<rootDir>/src/server",
15+
"^test/(.*)$": "<rootDir>/test/$1",
16+
"^test/(.*)/mock$": Array [
17+
"<rootDir>/test/mocks/$1",
18+
"<rootDir>/test/__mocks__/$1",
19+
],
20+
"^util/(.*)$": "<rootDir>/src/utils/$1",
21+
}
22+
`;
23+
24+
exports[`pathsToModuleNameMapper should convert tsconfig mapping with given prefix: foo 1`] = `
25+
Object {
26+
"^@foo\\\\-bar/common$": "foo/../common/dist/library",
27+
"^@pkg/(.*)$": "foo/packages/$1",
28+
"^api/(.*)$": "foo/src/api/$1",
29+
"^client$": Array [
30+
"foo/src/client",
31+
"foo/src/client/index",
32+
],
33+
"^log$": "foo/src/utils/log",
34+
"^mocks/(.*)$": "foo/test/mocks/$1",
35+
"^server$": "foo/src/server",
36+
"^test/(.*)$": "foo/test/$1",
37+
"^test/(.*)/mock$": Array [
38+
"foo/test/mocks/$1",
39+
"foo/test/__mocks__/$1",
40+
],
41+
"^util/(.*)$": "foo/src/utils/$1",
42+
}
43+
`;
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {pathsToModuleNameMapper} from '../paths-to-module-name-mapper'
2+
3+
const tsconfigMap = {
4+
log: ['src/utils/log'],
5+
server: ['src/server'],
6+
client: ['src/client', 'src/client/index'],
7+
'util/*': ['src/utils/*'],
8+
'api/*': ['src/api/*'],
9+
'test/*': ['test/*'],
10+
'mocks/*': ['test/mocks/*'],
11+
'test/*/mock': ['test/mocks/*', 'test/__mocks__/*'],
12+
'@foo-bar/common': ['../common/dist/library'],
13+
'@pkg/*': ['./packages/*'],
14+
}
15+
16+
describe('pathsToModuleNameMapper', () => {
17+
test('should convert tsconfig mapping with no given prefix', () => {
18+
expect(pathsToModuleNameMapper(tsconfigMap)).toMatchInlineSnapshot(`
19+
Object {
20+
"^@foo\\\\-bar/common$": "../common/dist/library",
21+
"^@pkg/(.*)$": "./packages/$1",
22+
"^api/(.*)$": "src/api/$1",
23+
"^client$": Array [
24+
"src/client",
25+
"src/client/index",
26+
],
27+
"^log$": "src/utils/log",
28+
"^mocks/(.*)$": "test/mocks/$1",
29+
"^server$": "src/server",
30+
"^test/(.*)$": "test/$1",
31+
"^test/(.*)/mock$": Array [
32+
"test/mocks/$1",
33+
"test/__mocks__/$1",
34+
],
35+
"^util/(.*)$": "src/utils/$1",
36+
}
37+
`)
38+
})
39+
40+
test('should add `js` extension to resolved config with useESM: true', () => {
41+
expect(pathsToModuleNameMapper(tsconfigMap, {useESM: true})).toEqual({
42+
/**
43+
* Why not using snapshot here?
44+
* Because the snapshot does not keep the property order, which is important for jest.
45+
* A pattern ending with `\\.js` should appear before another pattern without the extension does.
46+
*/
47+
'^log$': 'src/utils/log',
48+
'^server$': 'src/server',
49+
'^client$': ['src/client', 'src/client/index'],
50+
'^util/(.*)\\.js$': 'src/utils/$1',
51+
'^util/(.*)$': 'src/utils/$1',
52+
'^api/(.*)\\.js$': 'src/api/$1',
53+
'^api/(.*)$': 'src/api/$1',
54+
'^test/(.*)\\.js$': 'test/$1',
55+
'^test/(.*)$': 'test/$1',
56+
'^mocks/(.*)\\.js$': 'test/mocks/$1',
57+
'^mocks/(.*)$': 'test/mocks/$1',
58+
'^test/(.*)/mock\\.js$': ['test/mocks/$1', 'test/__mocks__/$1'],
59+
'^test/(.*)/mock$': ['test/mocks/$1', 'test/__mocks__/$1'],
60+
'^@foo\\-bar/common$': '../common/dist/library',
61+
'^@pkg/(.*)\\.js$': './packages/$1',
62+
'^@pkg/(.*)$': './packages/$1',
63+
'^(\\.{1,2}/.*)\\.js$': '$1',
64+
})
65+
})
66+
67+
test.each(['<rootDir>/', 'foo'])(
68+
'should convert tsconfig mapping with given prefix',
69+
prefix => {
70+
expect(pathsToModuleNameMapper(tsconfigMap, {prefix})).toMatchSnapshot(
71+
prefix,
72+
)
73+
},
74+
)
75+
76+
describe('warnings', () => {
77+
beforeEach(() => {
78+
jest.spyOn(console, 'warn').mockImplementation()
79+
})
80+
81+
afterEach(() => jest.mocked(console.warn).mockRestore())
82+
83+
test('should warn about mapping it cannot handle', () => {
84+
expect(
85+
pathsToModuleNameMapper({
86+
kept: ['src/kept'],
87+
'no-target': [],
88+
'too/*/many/*/stars': ['to/*/many/*/stars'],
89+
}),
90+
).toMatchInlineSnapshot(`
91+
Object {
92+
"^kept$": "src/kept",
93+
}
94+
`)
95+
96+
expect(jest.mocked(console.warn)).toHaveBeenCalledWith(
97+
'Not mapping "no-target" because it has no target.',
98+
)
99+
expect(jest.mocked(console.warn)).toHaveBeenCalledWith(
100+
'Not mapping "too/*/many/*/stars" because it has more than one star (`*`).',
101+
)
102+
})
103+
})
104+
})

Diff for: src/api/test/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {pathsToModuleNameMapper} from './paths-to-module-name-mapper'

Diff for: src/api/test/paths-to-module-name-mapper.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* NOTE: this was copy pasta'ed from `ts-jest` so that we can support path
3+
* aliases in `tsconfig.json` without necessarily relying on `ts-jest`
4+
*
5+
* @see {@link https://github.com/kulshekhar/ts-jest/blob/dd3523cb7571714f06f1ea2ed1e3cf11970fbfce/src/config/paths-to-module-name-mapper.ts}
6+
*/
7+
8+
import type {Config} from '@jest/types'
9+
import type {CompilerOptions} from 'typescript'
10+
11+
type TsPathMapping = Exclude<CompilerOptions['paths'], undefined>
12+
type JestPathMapping = Config.InitialOptions['moduleNameMapper']
13+
14+
// we don't need to escape all chars, so commented out is the real one
15+
// const escapeRegex = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
16+
const escapeRegex = (str: string) => str.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&')
17+
18+
export const pathsToModuleNameMapper = (
19+
mapping: TsPathMapping,
20+
{prefix = '', useESM = false}: {prefix?: string; useESM?: boolean} = {},
21+
): JestPathMapping => {
22+
const jestMap: JestPathMapping = {}
23+
for (const fromPath of Object.keys(mapping)) {
24+
const toPaths = mapping[fromPath]
25+
// check that we have only one target path
26+
if (toPaths.length === 0) {
27+
console.warn(`Not mapping "${fromPath}" because it has no target.`)
28+
29+
continue
30+
}
31+
32+
// split with '*'
33+
const segments = fromPath.split(/\*/g)
34+
if (segments.length === 1) {
35+
const paths = toPaths.map(target => {
36+
const enrichedPrefix =
37+
prefix !== '' && !prefix.endsWith('/') ? `${prefix}/` : prefix
38+
39+
return `${enrichedPrefix}${target}`
40+
})
41+
const cjsPattern = `^${escapeRegex(fromPath)}$`
42+
jestMap[cjsPattern] = paths.length === 1 ? paths[0] : paths
43+
} else if (segments.length === 2) {
44+
const paths = toPaths.map(target => {
45+
const enrichedTarget =
46+
target.startsWith('./') && prefix !== ''
47+
? target.substring(target.indexOf('/') + 1)
48+
: target
49+
const enrichedPrefix =
50+
prefix !== '' && !prefix.endsWith('/') ? `${prefix}/` : prefix
51+
52+
return `${enrichedPrefix}${enrichedTarget.replace(/\*/g, '$1')}`
53+
})
54+
if (useESM) {
55+
const esmPattern = `^${escapeRegex(segments[0])}(.*)${escapeRegex(
56+
segments[1],
57+
)}\\.js$`
58+
jestMap[esmPattern] = paths.length === 1 ? paths[0] : paths
59+
}
60+
const cjsPattern = `^${escapeRegex(segments[0])}(.*)${escapeRegex(
61+
segments[1],
62+
)}$`
63+
jestMap[cjsPattern] = paths.length === 1 ? paths[0] : paths
64+
} else {
65+
console.warn(
66+
`Not mapping "${fromPath}" because it has more than one star (\`*\`).`,
67+
)
68+
}
69+
}
70+
71+
if (useESM) {
72+
jestMap['^(\\.{1,2}/.*)\\.js$'] = '$1'
73+
}
74+
75+
return jestMap
76+
}

0 commit comments

Comments
 (0)