diff --git a/e2e/__tests__/__snapshots__/logger.test.ts.snap b/e2e/__tests__/__snapshots__/logger.test.ts.snap
index 1e08c41d3b..6e75593b75 100644
--- a/e2e/__tests__/__snapshots__/logger.test.ts.snap
+++ b/e2e/__tests__/__snapshots__/logger.test.ts.snap
@@ -5,6 +5,7 @@ Array [
"[level:20] creating jest presets not handling JavaScript files",
"[level:20] creating jest presets not handling JavaScript files",
"[level:20] creating Importer singleton",
+ "[level:20] checking version of jest: OK",
"[level:20] created new transformer",
"[level:30] no matching config-set found, creating a new one",
"[level:20] loaded module typescript",
@@ -43,6 +44,7 @@ Array [
"[level:20] creating jest presets not handling JavaScript files",
"[level:20] creating jest presets not handling JavaScript files",
"[level:20] creating Importer singleton",
+ "[level:20] checking version of jest: OK",
"[level:20] created new transformer",
"[level:30] no matching config-set found, creating a new one",
"[level:20] loaded module typescript",
@@ -87,6 +89,7 @@ Array [
"[level:20] creating jest presets not handling JavaScript files",
"[level:20] creating jest presets not handling JavaScript files",
"[level:20] creating Importer singleton",
+ "[level:20] checking version of jest: OK",
"[level:20] created new transformer",
"[level:30] no matching config-set found, creating a new one",
"[level:20] loaded module typescript",
diff --git a/src/__snapshots__/index.spec.ts.snap b/src/__snapshots__/ts-jest-transformer.spec.ts.snap
similarity index 100%
rename from src/__snapshots__/index.spec.ts.snap
rename to src/__snapshots__/ts-jest-transformer.spec.ts.snap
diff --git a/src/index.spec.ts b/src/index.spec.ts
index f97f58ac46..dc026dd2c3 100644
--- a/src/index.spec.ts
+++ b/src/index.spec.ts
@@ -1,194 +1,12 @@
-import { LogLevels } from 'bs-logger'
-import { sep } from 'path'
-
-import { ConfigSet } from './config/config-set'
-import { SOURCE_MAPPING_PREFIX } from './compiler/instance'
-import { logTargetMock } from './__helpers__/mocks'
-
-const logTarget = logTargetMock()
-
-beforeEach(() => {
- logTarget.clear()
-})
-
-describe('TsJestTransformer', () => {
- describe('configFor', () => {
- test('should return the same config-set for same values with jest config string is not in configSetsIndex', () => {
- const obj1 = { cwd: '/foo/.', rootDir: '/bar//dummy/..', globals: {} }
- const cs3 = require('./').configsFor(obj1 as any)
-
- expect(cs3.cwd).toBe(`${sep}foo`)
- expect(cs3.rootDir).toBe(`${sep}bar`)
- })
-
- test('should return the same config-set for same values with jest config string in configSetsIndex', () => {
- const obj1 = { cwd: '/foo/.', rootDir: '/bar//dummy/..', globals: {} }
- const obj2 = { ...obj1 }
- const cs1 = require('./').configsFor(obj1 as any)
- const cs2 = require('./').configsFor(obj2 as any)
-
- expect(cs1.cwd).toBe(`${sep}foo`)
- expect(cs1.rootDir).toBe(`${sep}bar`)
- expect(cs2).toBe(cs1)
- })
- })
-
- describe('getCacheKey', () => {
- test('should be different for each argument value', () => {
- const tr = require('./')
- const input = {
- fileContent: 'export default "foo"',
- fileName: 'foo.ts',
- jestConfigStr: '{"foo": "bar"}',
- options: { config: { foo: 'bar' } as any, instrument: false, rootDir: '/foo' },
- }
- const keys = [
- tr.getCacheKey(input.fileContent, input.fileName, input.jestConfigStr, input.options),
- tr.getCacheKey(input.fileContent, 'bar.ts', input.jestConfigStr, input.options),
- tr.getCacheKey(input.fileContent, input.fileName, '{}', { ...input.options, instrument: true }),
- tr.getCacheKey(input.fileContent, input.fileName, '{}', { ...input.options, rootDir: '/bar' }),
- ]
-
- // each key should have correct length
- for (const key of keys) {
- expect(key).toHaveLength(40)
- }
- // unique array should have same length
- expect(keys.filter((k, i, all) => all.indexOf(k) === i)).toHaveLength(keys.length)
- })
- })
-
- describe('process', () => {
- let tr!: any
-
- beforeEach(() => {
- tr = require('./')
- })
-
- test('should process input as stringified content with content matching stringifyContentPathRegex option', () => {
- const fileContent = '
Hello World
'
- const filePath = 'foo.html'
- const jestCfg = {
- globals: {
- 'ts-jest': {
- stringifyContentPathRegex: '\\.html$',
- },
- },
- } as any
- tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
-
- const result = tr.process(fileContent, filePath, jestCfg)
-
- expect(result).toMatchInlineSnapshot(`"module.exports=\\"Hello World
\\""`)
- })
-
- test('should process type definition input', () => {
- const fileContent = 'type Foo = number'
- const filePath = 'foo.d.ts'
- const jestCfg = Object.create(null)
- tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
- const result = tr.process(fileContent, filePath, jestCfg)
-
- expect(result).toEqual('')
- })
-
- test('should process js file with allowJs false and show warning log', () => {
- const fileContent = 'const foo = 1'
- const filePath = 'foo.js'
- const jestCfg = {
- globals: {
- 'ts-jest': { tsconfig: { allowJs: false } },
- },
- } as any
- tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
- logTarget.clear()
-
- const result = tr.process(fileContent, filePath, jestCfg)
-
- expect(result).toEqual(fileContent)
- expect(logTarget.lines[1].substring(0)).toMatchInlineSnapshot(`
- "[level:40] Got a \`.js\` file to compile while \`allowJs\` option is not set to \`true\` (file: foo.js). To fix this:
- - if you want TypeScript to process JS files, set \`allowJs\` to \`true\` in your TypeScript config (usually tsconfig.json)
- - if you do not want TypeScript to process your \`.js\` files, in your Jest config change the \`transform\` key which value is \`ts-jest\` so that it does not match \`.js\` files anymore
- "
- `)
- })
-
- test.each(['foo.ts', 'foo.tsx'])('should process ts/tsx file', (filePath) => {
- const fileContent = 'const foo = 1'
- const output = 'var foo = 1'
- const jestCfg = Object.create(null)
- tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
- jest.spyOn(ConfigSet.prototype, 'tsCompiler', 'get').mockImplementationOnce(() => ({
- compile: () => output,
- cwd: '.',
- program: undefined,
- }))
-
- const result = tr.process(fileContent, filePath, jestCfg)
-
- expect(result).toEqual(output)
- })
-
- test.each(['foo.js', 'foo.jsx'])('should process js/jsx file with allowJs true', (filePath) => {
- const fileContent = 'const foo = 1'
- const output = 'var foo = 1'
- const jestCfg = {
- globals: {
- 'ts-jest': { tsconfig: { allowJs: true } },
- },
- } as any
- tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
- logTarget.clear()
- jest.spyOn(ConfigSet.prototype, 'tsCompiler', 'get').mockImplementationOnce(() => ({
- compile: () => output,
- cwd: '.',
- program: undefined,
- }))
-
- const result = tr.process(fileContent, filePath, jestCfg)
-
- expect(result).toEqual(output)
- })
-
- test('should process file with unknown extension and show warning message without babel-jest', () => {
- const fileContent = 'foo'
- const filePath = 'foo.bar'
- const jestCfg = {
- globals: {
- 'ts-jest': { tsconfig: { allowJs: true } },
- },
- } as any
- tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
- logTarget.clear()
-
- const result = tr.process(fileContent, filePath, jestCfg)
-
- expect(result).toEqual(fileContent)
- expect(logTarget.lines[1]).toMatchInlineSnapshot(`
- "[level:40] Got a unknown file type to compile (file: foo.bar). To fix this, in your Jest config change the \`transform\` key which value is \`ts-jest\` so that it does not match this kind of files anymore.
- "
- `)
- })
-
- test.each(['foo.bar', 'foo.js'])('should process file with babel-jest', (filePath) => {
- const fileContent = 'foo'
- const jestCfg = {
- globals: {
- 'ts-jest': { babelConfig: true },
- },
- } as any
- tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
- logTarget.clear()
-
- const result = tr.process('foo', filePath, jestCfg)
-
- if (typeof result !== 'string') {
- expect(result.code.substring(0, result.code.indexOf(SOURCE_MAPPING_PREFIX))).toMatchSnapshot()
- }
- if (filePath === 'foo.bar') {
- expect(logTarget.filteredLines(LogLevels.warn)[0]).toMatchSnapshot()
- }
- })
+import * as tsJest from '.'
+import { TsJestTransformer } from './ts-jest-transformer'
+
+describe('createTransformer', () => {
+ it('should create different instances', () => {
+ const tr1 = tsJest.createTransformer()
+ const tr2 = tsJest.createTransformer()
+ expect(tr1).toBeInstanceOf(TsJestTransformer)
+ expect(tr2).toBeInstanceOf(TsJestTransformer)
+ expect(tr1).not.toBe(tr2)
})
})
diff --git a/src/index.ts b/src/index.ts
index 3465307e8a..c9f6946e93 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,192 +1,8 @@
-import type { CacheKeyOptions, TransformedSource, Transformer, TransformOptions } from '@jest/transform'
-import type { Config } from '@jest/types'
-import type { Logger } from 'bs-logger'
+import { VersionCheckers } from './utils/version-checkers'
+import { TsJestTransformer } from './ts-jest-transformer'
-import { ConfigSet } from './config/config-set'
-import { DECLARATION_TYPE_EXT, JS_JSX_REGEX, TS_TSX_REGEX } from './constants'
-import { stringify } from './utils/json'
-import { JsonableValue } from './utils/jsonable-value'
-import { rootLogger } from './utils/logger'
-import { Errors, interpolate } from './utils/messages'
-import { sha1 } from './utils/sha1'
+export function createTransformer(): TsJestTransformer {
+ VersionCheckers.jest.warn()
-interface CachedConfigSet {
- configSet: ConfigSet
- jestConfig: JsonableValue
- transformerCfgStr: string
+ return new TsJestTransformer()
}
-
-class TsJestTransformer implements Transformer {
- /**
- * cache ConfigSet between test runs
- *
- * @internal
- */
- private static readonly _cachedConfigSets: CachedConfigSet[] = []
- protected readonly logger: Logger
- protected _transformCfgStr!: string
-
- constructor() {
- this.logger = rootLogger.child({ namespace: 'ts-jest-transformer' })
-
- this.logger.debug('created new transformer')
- }
-
- /**
- * @public
- */
- configsFor(jestConfig: Config.ProjectConfig): ConfigSet {
- const ccs: CachedConfigSet | undefined = TsJestTransformer._cachedConfigSets.find(
- (cs) => cs.jestConfig.value === jestConfig,
- )
- let configSet: ConfigSet
- if (ccs) {
- this._transformCfgStr = ccs.transformerCfgStr
- configSet = ccs.configSet
- } else {
- // try to look-it up by stringified version
- const serializedJestCfg = stringify(jestConfig)
- const serializedCcs = TsJestTransformer._cachedConfigSets.find(
- (cs) => cs.jestConfig.serialized === serializedJestCfg,
- )
- if (serializedCcs) {
- // update the object so that we can find it later
- // this happens because jest first calls getCacheKey with stringified version of
- // the config, and then it calls the transformer with the proper object
- serializedCcs.jestConfig.value = jestConfig
- this._transformCfgStr = serializedCcs.transformerCfgStr
- configSet = serializedCcs.configSet
- } else {
- // create the new record in the index
- this.logger.info('no matching config-set found, creating a new one')
-
- configSet = new ConfigSet(jestConfig)
- const jest = { ...jestConfig }
- const globals = (jest.globals = { ...jest.globals } as any)
- // we need to remove some stuff from jest config
- // this which does not depend on config
- jest.name = undefined as any
- jest.cacheDirectory = undefined as any
- // we do not need this since its normalized version is in tsJest
- delete globals['ts-jest']
- this._transformCfgStr = new JsonableValue({
- digest: configSet.tsJestDigest,
- babel: configSet.babelConfig,
- ...jest,
- tsconfig: {
- options: configSet.parsedTsConfig.options,
- raw: configSet.parsedTsConfig.raw,
- },
- }).serialized
- TsJestTransformer._cachedConfigSets.push({
- jestConfig: new JsonableValue(jestConfig),
- configSet,
- transformerCfgStr: this._transformCfgStr,
- })
- }
- }
-
- return configSet
- }
-
- /**
- * @public
- */
- process(
- input: string,
- filePath: Config.Path,
- jestConfig: Config.ProjectConfig,
- transformOptions?: TransformOptions,
- ): TransformedSource | string {
- this.logger.debug({ fileName: filePath, transformOptions }, 'processing', filePath)
-
- let result: string | TransformedSource
- const source: string = input
- const configs = this.configsFor(jestConfig)
- const { hooks } = configs
- const shouldStringifyContent = configs.shouldStringifyContent(filePath)
- const babelJest = shouldStringifyContent ? undefined : configs.babelJestTransformer
- const isDefinitionFile = filePath.endsWith(DECLARATION_TYPE_EXT)
- const isJsFile = JS_JSX_REGEX.test(filePath)
- const isTsFile = !isDefinitionFile && TS_TSX_REGEX.test(filePath)
- if (shouldStringifyContent) {
- // handles here what we should simply stringify
- result = `module.exports=${stringify(source)}`
- } else if (isDefinitionFile) {
- // do not try to compile declaration files
- result = ''
- } else if (!configs.parsedTsConfig.options.allowJs && isJsFile) {
- // we've got a '.js' but the compiler option `allowJs` is not set or set to false
- this.logger.warn({ fileName: filePath }, interpolate(Errors.GotJsFileButAllowJsFalse, { path: filePath }))
-
- result = source
- } else if (isJsFile || isTsFile) {
- // transpile TS code (source maps are included)
- /* istanbul ignore if */
- result = configs.tsCompiler.compile(source, filePath)
- } else {
- // we should not get called for files with other extension than js[x], ts[x] and d.ts,
- // TypeScript will bail if we try to compile, and if it was to call babel, users can
- // define the transform value with `babel-jest` for this extension instead
- const message = babelJest ? Errors.GotUnknownFileTypeWithBabel : Errors.GotUnknownFileTypeWithoutBabel
-
- this.logger.warn({ fileName: filePath }, interpolate(message, { path: filePath }))
-
- result = source
- }
- // calling babel-jest transformer
- if (babelJest) {
- this.logger.debug({ fileName: filePath }, 'calling babel-jest processor')
-
- // do not instrument here, jest will do it anyway afterwards
- result = babelJest.process(result, filePath, jestConfig, { ...transformOptions, instrument: false })
- }
- // allows hooks (useful for internal testing)
- /* istanbul ignore next (cover by e2e) */
- if (hooks.afterProcess) {
- this.logger.debug({ fileName: filePath, hookName: 'afterProcess' }, 'calling afterProcess hook')
-
- const newResult = hooks.afterProcess([input, filePath, jestConfig, transformOptions], result)
- if (newResult !== undefined) {
- return newResult
- }
- }
-
- return result
- }
-
- /**
- * Jest uses this to cache the compiled version of a file
- *
- * @see https://github.com/facebook/jest/blob/v23.5.0/packages/jest-runtime/src/script_transformer.js#L61-L90
- *
- * @public
- */
- getCacheKey(
- fileContent: string,
- filePath: string,
- _jestConfigStr: string,
- transformOptions: CacheKeyOptions,
- ): string {
- const configs = this.configsFor(transformOptions.config)
-
- this.logger.debug({ fileName: filePath, transformOptions }, 'computing cache key for', filePath)
-
- // we do not instrument, ensure it is false all the time
- const { instrument = false, rootDir = configs.rootDir } = transformOptions
-
- return sha1(
- this._transformCfgStr,
- '\x00',
- rootDir,
- '\x00',
- `instrument:${instrument ? 'on' : 'off'}`,
- '\x00',
- fileContent,
- '\x00',
- filePath,
- )
- }
-}
-
-export = new TsJestTransformer()
diff --git a/src/ts-jest-transformer.spec.ts b/src/ts-jest-transformer.spec.ts
new file mode 100644
index 0000000000..dc5b7b8b8a
--- /dev/null
+++ b/src/ts-jest-transformer.spec.ts
@@ -0,0 +1,195 @@
+import { LogLevels } from 'bs-logger'
+import { sep } from 'path'
+
+import { ConfigSet } from './config/config-set'
+import { SOURCE_MAPPING_PREFIX } from './compiler/instance'
+import { logTargetMock } from './__helpers__/mocks'
+import { TsJestTransformer } from './ts-jest-transformer'
+
+const logTarget = logTargetMock()
+
+beforeEach(() => {
+ logTarget.clear()
+})
+
+describe('TsJestTransformer', () => {
+ describe('configFor', () => {
+ test('should return the same config-set for same values with jest config string is not in configSetsIndex', () => {
+ const obj1 = { cwd: '/foo/.', rootDir: '/bar//dummy/..', globals: {} }
+ const cs3 = new TsJestTransformer().configsFor(obj1 as any)
+
+ expect(cs3.cwd).toBe(`${sep}foo`)
+ expect(cs3.rootDir).toBe(`${sep}bar`)
+ })
+
+ test('should return the same config-set for same values with jest config string in configSetsIndex', () => {
+ const obj1 = { cwd: '/foo/.', rootDir: '/bar//dummy/..', globals: {} }
+ const obj2 = { ...obj1 }
+ const cs1 = new TsJestTransformer().configsFor(obj1 as any)
+ const cs2 = new TsJestTransformer().configsFor(obj2 as any)
+
+ expect(cs1.cwd).toBe(`${sep}foo`)
+ expect(cs1.rootDir).toBe(`${sep}bar`)
+ expect(cs2).toBe(cs1)
+ })
+ })
+
+ describe('getCacheKey', () => {
+ test('should be different for each argument value', () => {
+ const tr = new TsJestTransformer()
+ const input = {
+ fileContent: 'export default "foo"',
+ fileName: 'foo.ts',
+ jestConfigStr: '{"foo": "bar"}',
+ options: { config: { foo: 'bar' } as any, instrument: false, rootDir: '/foo' },
+ }
+ const keys = [
+ tr.getCacheKey(input.fileContent, input.fileName, input.jestConfigStr, input.options),
+ tr.getCacheKey(input.fileContent, 'bar.ts', input.jestConfigStr, input.options),
+ tr.getCacheKey(input.fileContent, input.fileName, '{}', { ...input.options, instrument: true }),
+ tr.getCacheKey(input.fileContent, input.fileName, '{}', { ...input.options, rootDir: '/bar' }),
+ ]
+
+ // each key should have correct length
+ for (const key of keys) {
+ expect(key).toHaveLength(40)
+ }
+ // unique array should have same length
+ expect(keys.filter((k, i, all) => all.indexOf(k) === i)).toHaveLength(keys.length)
+ })
+ })
+
+ describe('process', () => {
+ let tr!: any
+
+ beforeEach(() => {
+ tr = new TsJestTransformer()
+ })
+
+ test('should process input as stringified content with content matching stringifyContentPathRegex option', () => {
+ const fileContent = 'Hello World
'
+ const filePath = 'foo.html'
+ const jestCfg = {
+ globals: {
+ 'ts-jest': {
+ stringifyContentPathRegex: '\\.html$',
+ },
+ },
+ } as any
+ tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
+
+ const result = tr.process(fileContent, filePath, jestCfg)
+
+ expect(result).toMatchInlineSnapshot(`"module.exports=\\"Hello World
\\""`)
+ })
+
+ test('should process type definition input', () => {
+ const fileContent = 'type Foo = number'
+ const filePath = 'foo.d.ts'
+ const jestCfg = Object.create(null)
+ tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
+ const result = tr.process(fileContent, filePath, jestCfg)
+
+ expect(result).toEqual('')
+ })
+
+ test('should process js file with allowJs false and show warning log', () => {
+ const fileContent = 'const foo = 1'
+ const filePath = 'foo.js'
+ const jestCfg = {
+ globals: {
+ 'ts-jest': { tsconfig: { allowJs: false } },
+ },
+ } as any
+ tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
+ logTarget.clear()
+
+ const result = tr.process(fileContent, filePath, jestCfg)
+
+ expect(result).toEqual(fileContent)
+ expect(logTarget.lines[1].substring(0)).toMatchInlineSnapshot(`
+ "[level:40] Got a \`.js\` file to compile while \`allowJs\` option is not set to \`true\` (file: foo.js). To fix this:
+ - if you want TypeScript to process JS files, set \`allowJs\` to \`true\` in your TypeScript config (usually tsconfig.json)
+ - if you do not want TypeScript to process your \`.js\` files, in your Jest config change the \`transform\` key which value is \`ts-jest\` so that it does not match \`.js\` files anymore
+ "
+ `)
+ })
+
+ test.each(['foo.ts', 'foo.tsx'])('should process ts/tsx file', (filePath) => {
+ const fileContent = 'const foo = 1'
+ const output = 'var foo = 1'
+ const jestCfg = Object.create(null)
+ tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
+ jest.spyOn(ConfigSet.prototype, 'tsCompiler', 'get').mockImplementationOnce(() => ({
+ compile: () => output,
+ cwd: '.',
+ program: undefined,
+ }))
+
+ const result = tr.process(fileContent, filePath, jestCfg)
+
+ expect(result).toEqual(output)
+ })
+
+ test.each(['foo.js', 'foo.jsx'])('should process js/jsx file with allowJs true', (filePath) => {
+ const fileContent = 'const foo = 1'
+ const output = 'var foo = 1'
+ const jestCfg = {
+ globals: {
+ 'ts-jest': { tsconfig: { allowJs: true } },
+ },
+ } as any
+ tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
+ logTarget.clear()
+ jest.spyOn(ConfigSet.prototype, 'tsCompiler', 'get').mockImplementationOnce(() => ({
+ compile: () => output,
+ cwd: '.',
+ program: undefined,
+ }))
+
+ const result = tr.process(fileContent, filePath, jestCfg)
+
+ expect(result).toEqual(output)
+ })
+
+ test('should process file with unknown extension and show warning message without babel-jest', () => {
+ const fileContent = 'foo'
+ const filePath = 'foo.bar'
+ const jestCfg = {
+ globals: {
+ 'ts-jest': { tsconfig: { allowJs: true } },
+ },
+ } as any
+ tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
+ logTarget.clear()
+
+ const result = tr.process(fileContent, filePath, jestCfg)
+
+ expect(result).toEqual(fileContent)
+ expect(logTarget.lines[1]).toMatchInlineSnapshot(`
+ "[level:40] Got a unknown file type to compile (file: foo.bar). To fix this, in your Jest config change the \`transform\` key which value is \`ts-jest\` so that it does not match this kind of files anymore.
+ "
+ `)
+ })
+
+ test.each(['foo.bar', 'foo.js'])('should process file with babel-jest', (filePath) => {
+ const fileContent = 'foo'
+ const jestCfg = {
+ globals: {
+ 'ts-jest': { babelConfig: true },
+ },
+ } as any
+ tr.getCacheKey(fileContent, filePath, JSON.stringify(jestCfg), { config: jestCfg } as any)
+ logTarget.clear()
+
+ const result = tr.process('foo', filePath, jestCfg)
+
+ if (typeof result !== 'string') {
+ expect(result.code.substring(0, result.code.indexOf(SOURCE_MAPPING_PREFIX))).toMatchSnapshot()
+ }
+ if (filePath === 'foo.bar') {
+ expect(logTarget.filteredLines(LogLevels.warn)[0]).toMatchSnapshot()
+ }
+ })
+ })
+})
diff --git a/src/ts-jest-transformer.ts b/src/ts-jest-transformer.ts
new file mode 100644
index 0000000000..da908f6e18
--- /dev/null
+++ b/src/ts-jest-transformer.ts
@@ -0,0 +1,190 @@
+import type { CacheKeyOptions, TransformedSource, Transformer, TransformOptions } from '@jest/transform'
+import type { Config } from '@jest/types'
+import type { Logger } from 'bs-logger'
+
+import { ConfigSet } from './config/config-set'
+import { DECLARATION_TYPE_EXT, JS_JSX_REGEX, TS_TSX_REGEX } from './constants'
+import { stringify } from './utils/json'
+import { JsonableValue } from './utils/jsonable-value'
+import { rootLogger } from './utils/logger'
+import { Errors, interpolate } from './utils/messages'
+import { sha1 } from './utils/sha1'
+
+interface CachedConfigSet {
+ configSet: ConfigSet
+ jestConfig: JsonableValue
+ transformerCfgStr: string
+}
+
+export class TsJestTransformer implements Transformer {
+ /**
+ * cache ConfigSet between test runs
+ *
+ * @internal
+ */
+ private static readonly _cachedConfigSets: CachedConfigSet[] = []
+ protected readonly logger: Logger
+ protected _transformCfgStr!: string
+
+ constructor() {
+ this.logger = rootLogger.child({ namespace: 'ts-jest-transformer' })
+
+ this.logger.debug('created new transformer')
+ }
+
+ /**
+ * @public
+ */
+ configsFor(jestConfig: Config.ProjectConfig): ConfigSet {
+ const ccs: CachedConfigSet | undefined = TsJestTransformer._cachedConfigSets.find(
+ (cs) => cs.jestConfig.value === jestConfig,
+ )
+ let configSet: ConfigSet
+ if (ccs) {
+ this._transformCfgStr = ccs.transformerCfgStr
+ configSet = ccs.configSet
+ } else {
+ // try to look-it up by stringified version
+ const serializedJestCfg = stringify(jestConfig)
+ const serializedCcs = TsJestTransformer._cachedConfigSets.find(
+ (cs) => cs.jestConfig.serialized === serializedJestCfg,
+ )
+ if (serializedCcs) {
+ // update the object so that we can find it later
+ // this happens because jest first calls getCacheKey with stringified version of
+ // the config, and then it calls the transformer with the proper object
+ serializedCcs.jestConfig.value = jestConfig
+ this._transformCfgStr = serializedCcs.transformerCfgStr
+ configSet = serializedCcs.configSet
+ } else {
+ // create the new record in the index
+ this.logger.info('no matching config-set found, creating a new one')
+
+ configSet = new ConfigSet(jestConfig)
+ const jest = { ...jestConfig }
+ const globals = (jest.globals = { ...jest.globals } as any)
+ // we need to remove some stuff from jest config
+ // this which does not depend on config
+ jest.name = undefined as any
+ jest.cacheDirectory = undefined as any
+ // we do not need this since its normalized version is in tsJest
+ delete globals['ts-jest']
+ this._transformCfgStr = new JsonableValue({
+ digest: configSet.tsJestDigest,
+ babel: configSet.babelConfig,
+ ...jest,
+ tsconfig: {
+ options: configSet.parsedTsConfig.options,
+ raw: configSet.parsedTsConfig.raw,
+ },
+ }).serialized
+ TsJestTransformer._cachedConfigSets.push({
+ jestConfig: new JsonableValue(jestConfig),
+ configSet,
+ transformerCfgStr: this._transformCfgStr,
+ })
+ }
+ }
+
+ return configSet
+ }
+
+ /**
+ * @public
+ */
+ process(
+ input: string,
+ filePath: Config.Path,
+ jestConfig: Config.ProjectConfig,
+ transformOptions?: TransformOptions,
+ ): TransformedSource | string {
+ this.logger.debug({ fileName: filePath, transformOptions }, 'processing', filePath)
+
+ let result: string | TransformedSource
+ const source: string = input
+ const configs = this.configsFor(jestConfig)
+ const { hooks } = configs
+ const shouldStringifyContent = configs.shouldStringifyContent(filePath)
+ const babelJest = shouldStringifyContent ? undefined : configs.babelJestTransformer
+ const isDefinitionFile = filePath.endsWith(DECLARATION_TYPE_EXT)
+ const isJsFile = JS_JSX_REGEX.test(filePath)
+ const isTsFile = !isDefinitionFile && TS_TSX_REGEX.test(filePath)
+ if (shouldStringifyContent) {
+ // handles here what we should simply stringify
+ result = `module.exports=${stringify(source)}`
+ } else if (isDefinitionFile) {
+ // do not try to compile declaration files
+ result = ''
+ } else if (!configs.parsedTsConfig.options.allowJs && isJsFile) {
+ // we've got a '.js' but the compiler option `allowJs` is not set or set to false
+ this.logger.warn({ fileName: filePath }, interpolate(Errors.GotJsFileButAllowJsFalse, { path: filePath }))
+
+ result = source
+ } else if (isJsFile || isTsFile) {
+ // transpile TS code (source maps are included)
+ /* istanbul ignore if */
+ result = configs.tsCompiler.compile(source, filePath)
+ } else {
+ // we should not get called for files with other extension than js[x], ts[x] and d.ts,
+ // TypeScript will bail if we try to compile, and if it was to call babel, users can
+ // define the transform value with `babel-jest` for this extension instead
+ const message = babelJest ? Errors.GotUnknownFileTypeWithBabel : Errors.GotUnknownFileTypeWithoutBabel
+
+ this.logger.warn({ fileName: filePath }, interpolate(message, { path: filePath }))
+
+ result = source
+ }
+ // calling babel-jest transformer
+ if (babelJest) {
+ this.logger.debug({ fileName: filePath }, 'calling babel-jest processor')
+
+ // do not instrument here, jest will do it anyway afterwards
+ result = babelJest.process(result, filePath, jestConfig, { ...transformOptions, instrument: false })
+ }
+ // allows hooks (useful for internal testing)
+ /* istanbul ignore next (cover by e2e) */
+ if (hooks.afterProcess) {
+ this.logger.debug({ fileName: filePath, hookName: 'afterProcess' }, 'calling afterProcess hook')
+
+ const newResult = hooks.afterProcess([input, filePath, jestConfig, transformOptions], result)
+ if (newResult !== undefined) {
+ return newResult
+ }
+ }
+
+ return result
+ }
+
+ /**
+ * Jest uses this to cache the compiled version of a file
+ *
+ * @see https://github.com/facebook/jest/blob/v23.5.0/packages/jest-runtime/src/script_transformer.js#L61-L90
+ *
+ * @public
+ */
+ getCacheKey(
+ fileContent: string,
+ filePath: string,
+ _jestConfigStr: string,
+ transformOptions: CacheKeyOptions,
+ ): string {
+ const configs = this.configsFor(transformOptions.config)
+
+ this.logger.debug({ fileName: filePath, transformOptions }, 'computing cache key for', filePath)
+
+ // we do not instrument, ensure it is false all the time
+ const { instrument = false, rootDir = configs.rootDir } = transformOptions
+
+ return sha1(
+ this._transformCfgStr,
+ '\x00',
+ rootDir,
+ '\x00',
+ `instrument:${instrument ? 'on' : 'off'}`,
+ '\x00',
+ fileContent,
+ '\x00',
+ filePath,
+ )
+ }
+}