-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support
tsconfig.json
paths in Jest (#698)
Per #696
- Loading branch information
Showing
6 changed files
with
194 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
"skuba": minor | ||
--- | ||
|
||
jest: Support `tsconfig.json` paths | ||
|
||
Module aliases other than `src` are now supported in `skuba test`. Our Jest preset includes a dynamic `moduleNameMapper` that reads the `paths` compiler option from your `tsconfig.json`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
const path = require('path'); | ||
|
||
const { pathsToModuleNameMapper } = require('ts-jest/utils'); | ||
const { | ||
sys, | ||
findConfigFile, | ||
readConfigFile, | ||
parseJsonConfigFileContent, | ||
} = require('typescript'); | ||
|
||
/** | ||
* Set a default `src` module alias for backward compatibility. | ||
* | ||
* TODO: drop this default in skuba v4. | ||
*/ | ||
const DEFAULT_PATHS = { src: ['src'], 'src/*': ['src/*'] }; | ||
|
||
/** | ||
* @returns {unknown} | ||
*/ | ||
const getConfigFromDisk = () => { | ||
const filename = | ||
findConfigFile('.', sys.fileExists.bind(this)) || 'tsconfig.json'; | ||
|
||
return readConfigFile(filename, sys.readFile.bind(this)).config; | ||
}; | ||
|
||
module.exports.createModuleNameMapper = (getConfig = getConfigFromDisk) => { | ||
try { | ||
const json = getConfig(); | ||
|
||
const parsedConfig = parseJsonConfigFileContent(json, sys, '.'); | ||
|
||
const paths = Object.fromEntries( | ||
Object.entries(parsedConfig.options.paths ?? DEFAULT_PATHS).flatMap( | ||
([key, values]) => [ | ||
// Pass through the input path entry almost verbatim. | ||
// We trim a trailing slash because TypeScript allows `import 'src'` | ||
// to be resolved by the alias `src/`, but Jest's mapper does not. | ||
[ | ||
key.replace(/\/$/, ''), | ||
values.map((value) => value.replace(/\/$/, '')), | ||
], | ||
// Append a variant of the input path entry. | ||
// As TypeScript allows both `import 'src'` and `import 'src/nested'` | ||
// to be resolved by the alias `src/*` (and likewise for plain `src`), | ||
// we need to seed two Jest mappings per path. | ||
...(key.endsWith('/*') | ||
? [ | ||
[ | ||
// Given a path `src/*`, seed an extra `src`. | ||
key.replace(/\/\*$/, ''), | ||
values.map((value) => value.replace(/\/\*$/, '')), | ||
], | ||
] | ||
: [ | ||
[ | ||
// Given a path `src`, seed an extra `src/*`. | ||
path.join(key, '*'), | ||
values.map((value) => path.join(value, '*')), | ||
], | ||
]), | ||
], | ||
), | ||
); | ||
|
||
const prefix = path.join('<rootDir>', parsedConfig.options.baseUrl || '.'); | ||
|
||
const moduleNameMapper = pathsToModuleNameMapper(paths, { prefix }); | ||
|
||
// Normalise away any `..`s that may crop up from `baseUrl` usage. | ||
// For example, a `baseUrl` of `src` and a path of `../cli` will result in | ||
// `<rootDir>/src/../cli`, which can be normalised to `<rootDir>/cli`. | ||
return Object.fromEntries( | ||
Object.entries(moduleNameMapper).map(([key, values]) => [ | ||
key, | ||
Array.isArray(values) | ||
? values.map((value) => path.normalize(value)) | ||
: path.normalize(values), | ||
]), | ||
); | ||
} catch { | ||
// Bail out here to support zero-config mode. | ||
return pathsToModuleNameMapper(DEFAULT_PATHS, { prefix: '<rootDir>' }); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { createModuleNameMapper } from './moduleNameMapper'; | ||
|
||
describe('moduleNameMapper', () => { | ||
const act = (paths?: unknown, baseUrl?: string) => | ||
createModuleNameMapper(() => ({ | ||
compilerOptions: { | ||
baseUrl, | ||
paths, | ||
}, | ||
})); | ||
|
||
it('expands wildcard paths', () => | ||
expect(act({ 'src/*': ['src/*'], 'lib/wip/*': ['lib/wip/*'] })) | ||
.toMatchInlineSnapshot(` | ||
Object { | ||
"^lib/wip$": "<rootDir>/lib/wip", | ||
"^lib/wip/(.*)$": "<rootDir>/lib/wip/$1", | ||
"^src$": "<rootDir>/src", | ||
"^src/(.*)$": "<rootDir>/src/$1", | ||
} | ||
`)); | ||
|
||
it('expands non-wildcard paths', () => | ||
expect(act({ cli: ['cli'], 'src/': ['src/'] })).toMatchInlineSnapshot(` | ||
Object { | ||
"^cli$": "<rootDir>/cli", | ||
"^cli/(.*)$": "<rootDir>/cli/$1", | ||
"^src$": "<rootDir>/src", | ||
"^src/(.*)$": "<rootDir>/src/$1", | ||
} | ||
`)); | ||
|
||
it('expands duplicate asymmetric paths', () => | ||
expect( | ||
act({ | ||
jquery: ['node_modules/jquery/dist/jquery'], | ||
'jquery/*': ['node_modules/jquery/dist/jquery/*'], | ||
}), | ||
).toMatchInlineSnapshot(` | ||
Object { | ||
"^jquery$": "<rootDir>/node_modules/jquery/dist/jquery", | ||
"^jquery/(.*)$": "<rootDir>/node_modules/jquery/dist/jquery/$1", | ||
} | ||
`)); | ||
|
||
it('respects a base URL', () => | ||
expect(act({ cli: ['../cli'], 'app/*': ['app/*'] }, 'src')) | ||
.toMatchInlineSnapshot(` | ||
Object { | ||
"^app$": "<rootDir>/src/app", | ||
"^app/(.*)$": "<rootDir>/src/app/$1", | ||
"^cli$": "<rootDir>/cli", | ||
"^cli/(.*)$": "<rootDir>/cli/$1", | ||
} | ||
`)); | ||
|
||
it('respects no paths', () => | ||
expect(act({})).toMatchInlineSnapshot(`Object {}`)); | ||
|
||
it('falls back on undefined paths', () => | ||
expect(act(undefined)).toMatchInlineSnapshot(` | ||
Object { | ||
"^src$": "<rootDir>/src", | ||
"^src/(.*)$": "<rootDir>/src/$1", | ||
} | ||
`)); | ||
|
||
it('falls back on invalid config', () => | ||
expect(act('INVALID')).toMatchInlineSnapshot(` | ||
Object { | ||
"^src$": "<rootDir>/src", | ||
"^src/(.*)$": "<rootDir>/src/$1", | ||
} | ||
`)); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
const { defaults } = require('ts-jest/presets'); | ||
|
||
const TS_JEST_NAME = 'ts-jest'; | ||
|
||
/** | ||
* Resolved path of the `ts-jest` preset. | ||
* | ||
* This allows Jest to resolve the preset even if it is installed to a nested | ||
* `./node_modules/skuba/node_modules/ts-jest` directory. | ||
*/ | ||
const TS_JEST_PATH = require.resolve(TS_JEST_NAME); | ||
|
||
// Rewrite `ts-jest` transformations using our resolved `TS_JEST_PATH`. | ||
module.exports.transform = Object.fromEntries( | ||
Object.entries(defaults.transform).map(([key, value]) => [ | ||
key, | ||
value === TS_JEST_NAME ? TS_JEST_PATH : value, | ||
]), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters