Skip to content

Commit

Permalink
feat(api/test): make ts-jest's pathsToModuleNameMapper helper avail…
Browse files Browse the repository at this point in the history
…able

This copies in the module name mapper helper from `ts-jest` so we can use that
whether we're using ts-jest or not.
  • Loading branch information
jrolfs committed Jan 8, 2024
1 parent 9f3b26b commit d16d625
Show file tree
Hide file tree
Showing 9 changed files with 421 additions and 52 deletions.
3 changes: 2 additions & 1 deletion .babelrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"targets": {"node": "12"},
"modules": "commonjs"
}
]
],
"@babel/preset-typescript"
]
}
Binary file not shown.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"scripts": {
"build": "run-p 'build:*'",
"build:source": "babel --source-maps --out-dir dist --ignore '**/__tests__/**','**/__mocks__/**' --copy-files --no-copy-ignored src",
"build:source": "babel --source-maps --extensions '.ts' --out-dir dist --ignore '**/__tests__/**','**/__mocks__/**' --copy-files --no-copy-ignored src",
"build:types": "tsc -p src/",
"ci-after-success": "node src ci-after-success",
"commit": "node src commit",
Expand Down Expand Up @@ -132,6 +132,7 @@
"@babel/cli": "^7.23.0",
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@babel/preset-typescript": "^7.23.3",
"@types/cross-spawn": "^6.0.4",
"@types/lodash.merge": "^4",
"depcheck": "^1.4.7",
Expand Down
1 change: 1 addition & 0 deletions src/api/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './test/index'
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`pathsToModuleNameMapper should convert tsconfig mapping with given prefix: <rootDir>/ 1`] = `
Object {
"^@foo\\\\-bar/common$": "<rootDir>/../common/dist/library",
"^@pkg/(.*)$": "<rootDir>/packages/$1",
"^api/(.*)$": "<rootDir>/src/api/$1",
"^client$": Array [
"<rootDir>/src/client",
"<rootDir>/src/client/index",
],
"^log$": "<rootDir>/src/utils/log",
"^mocks/(.*)$": "<rootDir>/test/mocks/$1",
"^server$": "<rootDir>/src/server",
"^test/(.*)$": "<rootDir>/test/$1",
"^test/(.*)/mock$": Array [
"<rootDir>/test/mocks/$1",
"<rootDir>/test/__mocks__/$1",
],
"^util/(.*)$": "<rootDir>/src/utils/$1",
}
`;
exports[`pathsToModuleNameMapper should convert tsconfig mapping with given prefix: foo 1`] = `
Object {
"^@foo\\\\-bar/common$": "foo/../common/dist/library",
"^@pkg/(.*)$": "foo/packages/$1",
"^api/(.*)$": "foo/src/api/$1",
"^client$": Array [
"foo/src/client",
"foo/src/client/index",
],
"^log$": "foo/src/utils/log",
"^mocks/(.*)$": "foo/test/mocks/$1",
"^server$": "foo/src/server",
"^test/(.*)$": "foo/test/$1",
"^test/(.*)/mock$": Array [
"foo/test/mocks/$1",
"foo/test/__mocks__/$1",
],
"^util/(.*)$": "foo/src/utils/$1",
}
`;
104 changes: 104 additions & 0 deletions src/api/test/__tests__/paths-to-module-name-mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {pathsToModuleNameMapper} from '../paths-to-module-name-mapper'

const tsconfigMap = {
log: ['src/utils/log'],
server: ['src/server'],
client: ['src/client', 'src/client/index'],
'util/*': ['src/utils/*'],
'api/*': ['src/api/*'],
'test/*': ['test/*'],
'mocks/*': ['test/mocks/*'],
'test/*/mock': ['test/mocks/*', 'test/__mocks__/*'],
'@foo-bar/common': ['../common/dist/library'],
'@pkg/*': ['./packages/*'],
}

describe('pathsToModuleNameMapper', () => {
test('should convert tsconfig mapping with no given prefix', () => {
expect(pathsToModuleNameMapper(tsconfigMap)).toMatchInlineSnapshot(`
Object {
"^@foo\\\\-bar/common$": "../common/dist/library",
"^@pkg/(.*)$": "./packages/$1",
"^api/(.*)$": "src/api/$1",
"^client$": Array [
"src/client",
"src/client/index",
],
"^log$": "src/utils/log",
"^mocks/(.*)$": "test/mocks/$1",
"^server$": "src/server",
"^test/(.*)$": "test/$1",
"^test/(.*)/mock$": Array [
"test/mocks/$1",
"test/__mocks__/$1",
],
"^util/(.*)$": "src/utils/$1",
}
`)
})

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(['<rootDir>/', 'foo'])(
'should convert tsconfig mapping with given prefix',
prefix => {
expect(pathsToModuleNameMapper(tsconfigMap, {prefix})).toMatchSnapshot(
prefix,
)
},
)

describe('warnings', () => {
beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation()
})

afterEach(() => jest.mocked(console.warn).mockRestore())

test('should warn about mapping it cannot handle', () => {
expect(
pathsToModuleNameMapper({
kept: ['src/kept'],
'no-target': [],
'too/*/many/*/stars': ['to/*/many/*/stars'],
}),
).toMatchInlineSnapshot(`
Object {
"^kept$": "src/kept",
}
`)

expect(jest.mocked(console.warn)).toHaveBeenCalledWith(
'Not mapping "no-target" because it has no target.',
)
expect(jest.mocked(console.warn)).toHaveBeenCalledWith(
'Not mapping "too/*/many/*/stars" because it has more than one star (`*`).',
)
})
})
})
1 change: 1 addition & 0 deletions src/api/test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {pathsToModuleNameMapper} from './paths-to-module-name-mapper'
76 changes: 76 additions & 0 deletions src/api/test/paths-to-module-name-mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* NOTE: this was copy pasta'ed from `ts-jest` so that we can support path
* aliases in `tsconfig.json` without necessarily relying on `ts-jest`
*
* @see {@link https://github.com/kulshekhar/ts-jest/blob/dd3523cb7571714f06f1ea2ed1e3cf11970fbfce/src/config/paths-to-module-name-mapper.ts}
*/

import type {Config} from '@jest/types'
import type {CompilerOptions} from 'typescript'

type TsPathMapping = Exclude<CompilerOptions['paths'], undefined>
type JestPathMapping = Config.InitialOptions['moduleNameMapper']

// we don't need to escape all chars, so commented out is the real one
// const escapeRegex = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
const escapeRegex = (str: string) => str.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&')

export const pathsToModuleNameMapper = (
mapping: TsPathMapping,
{prefix = '', useESM = false}: {prefix?: string; useESM?: boolean} = {},
): JestPathMapping => {
const jestMap: JestPathMapping = {}
for (const fromPath of Object.keys(mapping)) {
const toPaths = mapping[fromPath]
// check that we have only one target path
if (toPaths.length === 0) {
console.warn(`Not mapping "${fromPath}" because it has no target.`)

continue
}

// split with '*'
const segments = fromPath.split(/\*/g)
if (segments.length === 1) {
const paths = toPaths.map(target => {
const enrichedPrefix =
prefix !== '' && !prefix.endsWith('/') ? `${prefix}/` : prefix

return `${enrichedPrefix}${target}`
})
const cjsPattern = `^${escapeRegex(fromPath)}$`
jestMap[cjsPattern] = paths.length === 1 ? paths[0] : paths
} else if (segments.length === 2) {
const paths = toPaths.map(target => {
const enrichedTarget =
target.startsWith('./') && prefix !== ''
? target.substring(target.indexOf('/') + 1)
: target
const enrichedPrefix =
prefix !== '' && !prefix.endsWith('/') ? `${prefix}/` : prefix

return `${enrichedPrefix}${enrichedTarget.replace(/\*/g, '$1')}`
})
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 {
console.warn(
`Not mapping "${fromPath}" because it has more than one star (\`*\`).`,
)
}
}

if (useESM) {
jestMap['^(\\.{1,2}/.*)\\.js$'] = '$1'
}

return jestMap
}
Loading

0 comments on commit d16d625

Please sign in to comment.