From 5bbfd06a0c114dbecd75b763bcfa76d4a6203ab1 Mon Sep 17 00:00:00 2001 From: Ahn Date: Wed, 11 Nov 2020 06:36:28 +0100 Subject: [PATCH] refactor: move jest transformer class to package entry (#2122) BREAKING CHANGE: - One currently refers type in `jest.config.js` ``` /** @typedef {import('ts-jest')} */ module.exports = { //... } ``` should change to ``` /** @typedef {import('ts-jest/dist/types')} */ module.exports = { //... } ``` - Remove possibilities to import `mocked`, `createJestPreset`, `pathsToModuleNameMapper` from package entry. One should change to ``` import { mocked, createJestPreset, pathsToModuleNameMapper` } from 'ts-jest/utils' ``` --- docs/user/config/index.md | 2 +- e2e/__cases__/test-helpers/deprecated.spec.ts | 9 - .../__snapshots__/logger.test.ts.snap | 3 - .../__snapshots__/test-helpers.test.ts.snap | 14 +- ...former.spec.ts.snap => index.spec.ts.snap} | 0 src/index.spec.ts | 258 ++++++++++++------ src/index.ts | 211 ++++++++++++-- src/ts-jest-transformer.spec.ts | 195 ------------- src/ts-jest-transformer.ts | 190 ------------- src/types.ts | 9 + src/utils/messages.ts | 1 - 11 files changed, 371 insertions(+), 521 deletions(-) delete mode 100644 e2e/__cases__/test-helpers/deprecated.spec.ts rename src/__snapshots__/{ts-jest-transformer.spec.ts.snap => index.spec.ts.snap} (100%) delete mode 100644 src/ts-jest-transformer.spec.ts delete mode 100644 src/ts-jest-transformer.ts diff --git a/docs/user/config/index.md b/docs/user/config/index.md index f1e2bd9586..d30ab39a0f 100644 --- a/docs/user/config/index.md +++ b/docs/user/config/index.md @@ -186,7 +186,7 @@ module.exports = { To utilize IDE suggestions, you can use `JSDoc` comments to provide suggested `ts-jest` configs for your Jest config: ```js -/** @typedef {import('ts-jest')} */ +/** @typedef {import('ts-jest/dist/types')} */ /** @type {import('@jest/types').Config.InitialOptions} */ const config = { // [...] diff --git a/e2e/__cases__/test-helpers/deprecated.spec.ts b/e2e/__cases__/test-helpers/deprecated.spec.ts deleted file mode 100644 index fd89b8235b..0000000000 --- a/e2e/__cases__/test-helpers/deprecated.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { mocked } from 'ts-jest' -import { foo } from './pass-to-mock' -jest.mock('./pass-to-mock') - -test('foo', () => { - foo() - // it should log that the helper moved - expect(mocked(foo).mock.calls.length).toBe(1) -}) diff --git a/e2e/__tests__/__snapshots__/logger.test.ts.snap b/e2e/__tests__/__snapshots__/logger.test.ts.snap index c1d261c16d..c4060d043f 100644 --- a/e2e/__tests__/__snapshots__/logger.test.ts.snap +++ b/e2e/__tests__/__snapshots__/logger.test.ts.snap @@ -5,7 +5,6 @@ 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", @@ -44,7 +43,6 @@ 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", @@ -89,7 +87,6 @@ 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/e2e/__tests__/__snapshots__/test-helpers.test.ts.snap b/e2e/__tests__/__snapshots__/test-helpers.test.ts.snap index 9a0179c804..2aec4eba29 100644 --- a/e2e/__tests__/__snapshots__/test-helpers.test.ts.snap +++ b/e2e/__tests__/__snapshots__/test-helpers.test.ts.snap @@ -19,11 +19,8 @@ exports[`test-helpers 1`] = ` 9 expect(mocked(bar, true).dummy.deep.deeper(42)).toBeUndefined() ~~ - ts-jest[root] (WARN) The \`mocked\` helper has been moved to \`ts-jest/utils\`. Use \`import { mocked } from 'ts-jest/utils'\` instead. - PASS ./deprecated.spec.ts - - Test Suites: 1 failed, 2 passed, 3 total - Tests: 4 passed, 4 total + Test Suites: 1 failed, 1 passed, 2 total + Tests: 3 passed, 3 total Snapshots: 0 total Time: XXs Ran all test suites. @@ -49,11 +46,8 @@ exports[`with esModuleInterop set to false 1`] = ` 9 expect(mocked(bar, true).dummy.deep.deeper(42)).toBeUndefined() ~~ - ts-jest[root] (WARN) The \`mocked\` helper has been moved to \`ts-jest/utils\`. Use \`import { mocked } from 'ts-jest/utils'\` instead. - PASS ./deprecated.spec.ts - - Test Suites: 1 failed, 2 passed, 3 total - Tests: 4 passed, 4 total + Test Suites: 1 failed, 1 passed, 2 total + Tests: 3 passed, 3 total Snapshots: 0 total Time: XXs Ran all test suites. diff --git a/src/__snapshots__/ts-jest-transformer.spec.ts.snap b/src/__snapshots__/index.spec.ts.snap similarity index 100% rename from src/__snapshots__/ts-jest-transformer.spec.ts.snap rename to src/__snapshots__/index.spec.ts.snap diff --git a/src/index.spec.ts b/src/index.spec.ts index 69199c80a8..f97f58ac46 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,104 +1,194 @@ -import type { testing } from 'bs-logger' +import { LogLevels } from 'bs-logger' +import { sep } from 'path' -import * as tsJest from '.' +import { ConfigSet } from './config/config-set' +import { SOURCE_MAPPING_PREFIX } from './compiler/instance' import { logTargetMock } from './__helpers__/mocks' -import { TsJestTransformer } from './ts-jest-transformer' -jest.mock('./ts-jest-transformer', () => { - class TsJestTransformer { - process: jest.Mock = jest.fn() - getCacheKey: jest.Mock = jest.fn() - constructor(public opt?: any) {} - } +const logTarget = logTargetMock() - return { TsJestTransformer } +beforeEach(() => { + logTarget.clear() }) -jest.mock('./presets/create-jest-preset', () => ({ - createJestPreset: () => ({ jestPreset: true }), -})) -describe('ts-jest', () => { - it('should export a `createTransformer` function', () => { - expect(typeof tsJest.createTransformer).toBe('function') - }) +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) - it('should export a `createJestPreset` function', () => { - expect(typeof tsJest.createJestPreset).toBe('function') - }) + expect(cs3.cwd).toBe(`${sep}foo`) + expect(cs3.rootDir).toBe(`${sep}bar`) + }) - it('should export a `mocked` function', () => { - expect(typeof tsJest.mocked).toBe('function') - }) + 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) - it('should export a `pathsToModuleNameMapper` function', () => { - expect(typeof tsJest.pathsToModuleNameMapper).toBe('function') + expect(cs1.cwd).toBe(`${sep}foo`) + expect(cs1.rootDir).toBe(`${sep}bar`) + expect(cs2).toBe(cs1) + }) }) -}) -describe('old entry point', () => { - const MANIFEST = { tsJestIndex: true } - const spy = jest.spyOn(console, 'warn') - spy.mockImplementation(() => undefined) - afterAll(() => { - spy.mockRestore() - }) + 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' }), + ] - it('should warn when using old path to ts-jest', () => { - jest.mock('../dist/index', () => MANIFEST) - expect(require('../preprocessor.js')).toBe(MANIFEST) - expect(spy).toHaveBeenCalledTimes(1) - expect(spy.mock.calls[0]).toMatchInlineSnapshot(` -Array [ - "ts-jest[main] (WARN) Replace any occurrences of \\"ts-jest/dist/preprocessor.js\\" or \\"/node_modules/ts-jest/preprocessor.js\\" in the 'transform' section of your Jest config with just \\"ts-jest\\".", -] -`) + // 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('moved helpers', () => { - let target: testing.LogTargetMock - beforeEach(() => { - target = logTargetMock() - target.clear() - }) + describe('process', () => { + let tr!: any - it('should warn when using mocked', () => { - tsJest.mocked(42) - expect(target.lines.warn).toMatchInlineSnapshot(` -Array [ - "[level:40] The \`mocked\` helper has been moved to \`ts-jest/utils\`. Use \`import { mocked } from 'ts-jest/utils'\` instead. -", -] -`) - }) + beforeEach(() => { + tr = require('./') + }) - it('should warn when using createJestPreset', () => { - tsJest.createJestPreset() - expect(target.lines.warn).toMatchInlineSnapshot(` -Array [ - "[level:40] The \`createJestPreset\` helper has been moved to \`ts-jest/utils\`. Use \`import { createJestPreset } from 'ts-jest/utils'\` instead. -", -] -`) - }) + 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) - it('should warn when using pathsToModuleNameMapper', () => { - tsJest.pathsToModuleNameMapper({}) - expect(target.lines.warn).toMatchInlineSnapshot(` -Array [ - "[level:40] The \`pathsToModuleNameMapper\` helper has been moved to \`ts-jest/utils\`. Use \`import { pathsToModuleNameMapper } from 'ts-jest/utils'\` instead. -", -] -`) - }) -}) + 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) -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) + 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/index.ts b/src/index.ts index 295ad0310b..3465307e8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,37 +1,192 @@ -import { LogContexts, LogLevels } from 'bs-logger' +import type { CacheKeyOptions, TransformedSource, Transformer, TransformOptions } from '@jest/transform' +import type { Config } from '@jest/types' +import type { Logger } from 'bs-logger' -import { createJestPreset as createJestPresetCore } from './presets/create-jest-preset' -import { pathsToModuleNameMapper as pathsToModuleNameMapperCore } from './config/paths-to-module-name-mapper' -import { TsJestTransformer } from './ts-jest-transformer' -import type { TsJestGlobalOptions } from './types' +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 { Deprecations, interpolate } from './utils/messages' -import { mocked as mockedCore } from './utils/testing' -import { VersionCheckers } from './utils/version-checkers' - -declare module '@jest/types' { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Config { - interface ConfigGlobals { - 'ts-jest': TsJestGlobalOptions +import { Errors, interpolate } from './utils/messages' +import { sha1 } from './utils/sha1' + +interface CachedConfigSet { + configSet: ConfigSet + jestConfig: JsonableValue + transformerCfgStr: string +} + +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 } -} -// deprecate helpers -const warn = rootLogger.child({ [LogContexts.logLevel]: LogLevels.warn }) -const helperMoved = any>(name: string, helper: T) => - warn.wrap(interpolate(Deprecations.HelperMovedToUtils, { helper: name }), helper) + /** + * @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 -/** @deprecated */ -export const mocked = helperMoved('mocked', mockedCore) -/** @deprecated */ -export const createJestPreset = helperMoved('createJestPreset', createJestPresetCore) -/** @deprecated */ -export const pathsToModuleNameMapper = helperMoved('pathsToModuleNameMapper', pathsToModuleNameMapperCore) + this.logger.warn({ fileName: filePath }, interpolate(message, { path: filePath })) -export function createTransformer(): TsJestTransformer { - VersionCheckers.jest.warn() + 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) - return new TsJestTransformer() + // 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 deleted file mode 100644 index b8ba3bdf48..0000000000 --- a/src/ts-jest-transformer.spec.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { LogLevels } from 'bs-logger' -import { sep } from 'path' - -import { TsJestTransformer } from './ts-jest-transformer' -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', () => { - it('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`) - }) - - it('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', () => { - it('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: TsJestTransformer - - beforeEach(() => { - tr = new TsJestTransformer() - }) - - it('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

\\""`) - }) - - it('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('') - }) - - it('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 - " - `) - }) - - it.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) - }) - - it.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) - }) - - it('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. - " - `) - }) - - it.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 deleted file mode 100644 index da908f6e18..0000000000 --- a/src/ts-jest-transformer.ts +++ /dev/null @@ -1,190 +0,0 @@ -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, - ) - } -} diff --git a/src/types.ts b/src/types.ts index 60d1247ffc..82c67133d7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,15 @@ import type * as _ts from 'typescript' import type { ConfigSet } from './config/config-set' +declare module '@jest/types' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Config { + interface ConfigGlobals { + 'ts-jest': TsJestGlobalOptions + } + } +} + /** * @internal */ diff --git a/src/utils/messages.ts b/src/utils/messages.ts index ac1cd87adf..36dae04c9d 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -35,7 +35,6 @@ export const enum Deprecations { ConfigOption = '"[jest-config].{{oldPath}}" is deprecated, use "[jest-config].{{newPath}}" instead.', ConfigOptionWithNote = '"[jest-config].{{oldPath}}" is deprecated, use "[jest-config].{{newPath}}" instead.\n ↳ {{note}}', ConfigOptionUseBabelRcNote = 'See `babel-jest` related issue: https://github.com/facebook/jest/issues/3845', - HelperMovedToUtils = "The `{{helper}}` helper has been moved to `ts-jest/utils`. Use `import { {{helper}} } from 'ts-jest/utils'` instead.", AstTransformerArrayConfig = 'The configuration for astTransformers as string[] is deprecated and will be removed in ts-jest 27. Please define your custom AST transformers in a form of an object. More information you can check online documentation https://kulshekhar.github.io/ts-jest/user/config/astTransformers', PackageJson = 'The option `packageJson` is deprecated and will be removed in ts-jest 27. This option is not used by internal `ts-jest`', }