diff --git a/.gitignore b/.gitignore index 51b54f49dd..b4c5a4644a 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,8 @@ jspm_packages tests/simple-long-path/long-src-path # is linked to the temp dir of the os e2e/__workdir_synlink__ +# while refactoring... +old/ # binaries *.tgz diff --git a/.npmignore b/.npmignore index 47251ba739..13278b872c 100644 --- a/.npmignore +++ b/.npmignore @@ -44,5 +44,7 @@ jspm_packages # Optional REPL history .node_repl_history +# while refactoring... +old/ *.tgz \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 80275219a2..5037968b95 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,15 +1,16 @@ module.exports = { rootDir: '.', transform: { - '\\.ts$': '/dist/index.js', + '\\.ts$': '/dist/index.js' }, testMatch: ['/src/**/?(*.)+(spec|test).ts?(x)'], collectCoverageFrom: [ '/src/**/*.ts', + '!/src/**/*.d.ts', '!/src/**/*.spec.ts', '!/src/**/*.test.ts', - '!/src/**/__*__/*', + '!/src/**/__*__/*' ], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], - testEnvironment: 'node', -}; + testEnvironment: 'node' +} diff --git a/package-lock.json b/package-lock.json index 4644df68d7..fd60283848 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,12 @@ "any-observable": "^0.3.0" } }, + "@types/arrify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/arrify/-/arrify-1.0.4.tgz", + "integrity": "sha512-63nK8r8jvEVJ1r0ENaY9neB1wDzPHFYAzKiIxPawuzcijEX8XeOywwPL8fkSCwiTIYop9MSh+TKy9L2ZzTXV6g==", + "dev": true + }, "@types/babel__core": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.0.1.tgz", @@ -53,6 +59,15 @@ "@babel/types": "^7.0.0-beta.54" } }, + "@types/buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-BLFpLBcN+RPKUsFxqRkMiwqTOOdi+TrKr5OpLJ9qCnUdSxS6S80+QRX/mIhfR66u0Ykc4QTkReaejOM2ILh+9Q==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/fs-extra": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.4.tgz", @@ -73,6 +88,12 @@ "integrity": "sha512-/UMY+2GkOZ27Vrc51pqC5J8SPd39FKt7kkoGAtWJ8s4msj0b15KehDWIiJpWY3/7tLxBQLLzJhIBhnEsXdzpgw==", "dev": true }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/lodash": { "version": "4.14.116", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.116.tgz", @@ -88,6 +109,15 @@ "@types/lodash": "*" } }, + "@types/mkdirp": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.5.2.tgz", + "integrity": "sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "10.5.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.5.8.tgz", @@ -303,8 +333,7 @@ "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" }, "asn1": { "version": "0.2.4", @@ -444,6 +473,14 @@ "private": "^0.1.8", "slash": "^1.0.0", "source-map": "^0.5.7" + }, + "dependencies": { + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + } } }, "babel-generator": { @@ -761,8 +798,7 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "builtin-modules": { "version": "1.1.1", @@ -1744,8 +1780,7 @@ "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fast-levenshtein": { "version": "2.0.6", @@ -3624,10 +3659,12 @@ "dev": true }, "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } }, "jsonfile": { "version": "4.0.0", @@ -4124,6 +4161,11 @@ "yallist": "^2.1.2" } }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==" + }, "makeerror": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", @@ -4317,8 +4359,7 @@ "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "mixin-deep": { "version": "1.3.1", @@ -4345,7 +4386,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" }, @@ -4353,8 +4393,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, @@ -6641,6 +6680,11 @@ "dev": true } } + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=" } } } diff --git a/package.json b/package.json index 3087e38783..b9998e6bb3 100644 --- a/package.json +++ b/package.json @@ -37,18 +37,30 @@ "url": "https://github.com/kulshekhar/ts-jest/issues" }, "homepage": "https://github.com/kulshekhar/ts-jest#readme", - "dependencies": {}, + "dependencies": { + "arrify": "^1.0.1", + "buffer-from": "^1.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json5": "^1.0.1", + "make-error": "^1.3.4", + "mkdirp": "^0.5.1", + "yn": "^2.0.0" + }, "peerDependencies": { "babel-jest": ">=22.0.0 <24.0.0", "jest": ">=22.0.0 <24.0.0", "typescript": ">=2.7.0 <4.0.0" }, "devDependencies": { + "@types/arrify": "^1.0.4", "@types/babel__core": "^7.0.1", + "@types/buffer-from": "^1.1.0", "@types/fs-extra": "5.0.4", "@types/gist-package-json": "git+https://gist.github.com/5c1cc527fe6b5b7dba41fec7fe54bf6e.git", "@types/jest": "^23.3.1", + "@types/json5": "0.0.29", "@types/lodash.set": "^4.3.4", + "@types/mkdirp": "^0.5.2", "@types/node": "^10.5.8", "@types/semver": "^5.5.0", "closest-file-data": "^0.1.4", diff --git a/src/__helpers__/fakers.ts b/src/__helpers__/fakers.ts index 004ef47e13..449e88b298 100644 --- a/src/__helpers__/fakers.ts +++ b/src/__helpers__/fakers.ts @@ -1,6 +1,6 @@ -import { TsJestGlobalOptions, BabelConfig, TsJestConfig } from '../types' +import { TsJestGlobalOptions, BabelConfig, TsJestConfig } from '../lib/types' import { resolve } from 'path' -import { ImportReasons } from '../utils/messages' +import { ImportReasons } from '../lib/messages' export function filePath(relPath: string): string { return resolve(__dirname, '..', '..', relPath) @@ -54,10 +54,13 @@ describe('hello', () => { export function tsJestConfig(options?: Partial): TsJestConfig { return { - babelJest: undefined, + version: '0.0.0-mock0', + typeCheck: false, + compiler: 'typescript', + babelConfig: undefined, tsConfig: undefined, - diagnostics: [], stringifyContentPathRegex: undefined, + diagnostics: { ignoreCodes: [], pretty: false }, ...options, } } @@ -85,6 +88,6 @@ export function babelConfig(options?: BabelConfig): T { } as any } -export function importReason(text: string = 'because'): ImportReasons { +export function importReason(text: string = '[[BECAUSE]]'): ImportReasons { return text as any } diff --git a/src/__helpers__/mock-there.ts b/src/__helpers__/mock-there.ts index 264d13fbc1..f3f0c8a94c 100644 --- a/src/__helpers__/mock-there.ts +++ b/src/__helpers__/mock-there.ts @@ -2,7 +2,7 @@ export default function mockThese(map: string[] | { [k: string]: () => any }) { const isArray = Array.isArray(map) const items: string[] = isArray ? (map as string[]) : Object.keys(map) items.forEach(item => { - const val = isArray ? () => item : map[item] + const val = isArray ? () => item : (map as any)[item] jest.doMock(item, val, { virtual: true }) }) } diff --git a/src/__mocks__/ts-program.ts b/src/__mocks__/ts-program.ts deleted file mode 100644 index cf7475fd82..0000000000 --- a/src/__mocks__/ts-program.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as fakers from '../__helpers__/fakers' -import { TsJestConfig, TsJestProgram } from '../types' -import { ParsedCommandLine } from 'typescript' - -// tslint:disable-next-line:variable-name -export let __tsConfig: any = {} - -export default class TsProgramMock implements TsJestProgram { - get parsedConfig(): ParsedCommandLine { - return __tsConfig - } - - constructor( - public rootDir: string = fakers.filePath(''), - public tsJestConfig: TsJestConfig = fakers.tsJestConfig(), - ) {} - - transpileModule(_: string, source: string) { - return source - } -} diff --git a/src/__snapshots__/ts-jest-transformer.spec.ts.snap b/src/__snapshots__/ts-jest-transformer.spec.ts.snap deleted file mode 100644 index beddb44117..0000000000 --- a/src/__snapshots__/ts-jest-transformer.spec.ts.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`process hoisting should hoist jest.mock calls using babel 1`] = ` -" -\\"use strict\\"; - -jest.mock('./upper', function () { - return function (s) { - return s.toUpperCase(); - }; -}); -var __importDefault = this && this.__importDefault || function (mod) { - return mod && mod.__esModule ? mod : { \\"default\\": mod }; -}; -var upper_1 = __importDefault(require(\\"./upper\\")); -var lower_1 = __importDefault(require(\\"./lower\\"));describe('hello', function () { - test('my test', function () { - jest.mock('./lower', function () { - return function (s) { - return s.toLowerCase(); - }; - }); - - expect(upper_1.default('hello')).toBe('HELLO'); - expect(lower_1.default('HELLO')).toBe('hello'); - }); -}); -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZpbGUudHMiXSwibmFtZXMiOlsiamVzdCIsIm1vY2siLCJzIiwidG9VcHBlckNhc2UiLCJfX2ltcG9ydERlZmF1bHQiLCJtb2QiLCJfX2VzTW9kdWxlIiwidXBwZXJfMSIsInJlcXVpcmUiLCJsb3dlcl8xIiwiZGVzY3JpYmUiLCJ0ZXN0IiwidG9Mb3dlckNhc2UiLCJleHBlY3QiLCJkZWZhdWx0IiwidG9CZSJdLCJtYXBwaW5ncyI6IjtBQUNBOztBQU1BQSxLQUFLQyxJQUFMLENBQVUsU0FBVixFQUFxQixZQUFZO0FBQUUsV0FBTyxVQUFVQyxDQUFWLEVBQWE7QUFBRSxlQUFPQSxFQUFFQyxXQUFGLEVBQVA7QUFBeUIsS0FBL0M7QUFBa0QsQ0FBckY7QUFMQSxJQUFJQyxrQkFBbUIsUUFBUSxLQUFLQSxlQUFkLElBQWtDLFVBQVVDLEdBQVYsRUFBZTtBQUNuRSxXQUFRQSxPQUFPQSxJQUFJQyxVQUFaLEdBQTBCRCxHQUExQixHQUFnQyxFQUFFLFdBQVdBLEdBQWIsRUFBdkM7QUFDSCxDQUZEO0FBR0EsSUFBSUUsVUFBVUgsZ0JBQWdCSSxRQUFRLFNBQVIsQ0FBaEIsQ0FBZDtBQUNBLElBQUlDLFVBQVVMLGdCQUFnQkksUUFBUSxTQUFSLENBQWhCLENBQWQsQ0FFQUUsU0FBUyxPQUFULEVBQWtCLFlBQVk7QUFDMUJDLFNBQUssU0FBTCxFQUFnQixZQUFZO0FBR3hCWCxhQUFLQyxJQUFMLENBQVUsU0FBVixFQUFxQixZQUFZO0FBQUUsbUJBQU8sVUFBVUMsQ0FBVixFQUFhO0FBQUUsdUJBQU9BLEVBQUVVLFdBQUYsRUFBUDtBQUF5QixhQUEvQztBQUFrRCxTQUFyRjs7QUFGQUMsZUFBT04sUUFBUU8sT0FBUixDQUFnQixPQUFoQixDQUFQLEVBQWlDQyxJQUFqQyxDQUFzQyxPQUF0QztBQUNBRixlQUFPSixRQUFRSyxPQUFSLENBQWdCLE9BQWhCLENBQVAsRUFBaUNDLElBQWpDLENBQXNDLE9BQXRDO0FBRUgsS0FKRDtBQUtILENBTkQiLCJmaWxlIjoiZmlsZS50cyIsInNvdXJjZXNDb250ZW50IjpbIlxuXCJ1c2Ugc3RyaWN0XCI7XG52YXIgX19pbXBvcnREZWZhdWx0ID0gKHRoaXMgJiYgdGhpcy5fX2ltcG9ydERlZmF1bHQpIHx8IGZ1bmN0aW9uIChtb2QpIHtcbiAgICByZXR1cm4gKG1vZCAmJiBtb2QuX19lc01vZHVsZSkgPyBtb2QgOiB7IFwiZGVmYXVsdFwiOiBtb2QgfTtcbn07XG52YXIgdXBwZXJfMSA9IF9faW1wb3J0RGVmYXVsdChyZXF1aXJlKFwiLi91cHBlclwiKSk7XG52YXIgbG93ZXJfMSA9IF9faW1wb3J0RGVmYXVsdChyZXF1aXJlKFwiLi9sb3dlclwiKSk7XG5qZXN0Lm1vY2soJy4vdXBwZXInLCBmdW5jdGlvbiAoKSB7IHJldHVybiBmdW5jdGlvbiAocykgeyByZXR1cm4gcy50b1VwcGVyQ2FzZSgpOyB9OyB9KTtcbmRlc2NyaWJlKCdoZWxsbycsIGZ1bmN0aW9uICgpIHtcbiAgICB0ZXN0KCdteSB0ZXN0JywgZnVuY3Rpb24gKCkge1xuICAgICAgICBleHBlY3QodXBwZXJfMS5kZWZhdWx0KCdoZWxsbycpKS50b0JlKCdIRUxMTycpO1xuICAgICAgICBleHBlY3QobG93ZXJfMS5kZWZhdWx0KCdIRUxMTycpKS50b0JlKCdoZWxsbycpO1xuICAgICAgICBqZXN0Lm1vY2soJy4vbG93ZXInLCBmdW5jdGlvbiAoKSB7IHJldHVybiBmdW5jdGlvbiAocykgeyByZXR1cm4gcy50b0xvd2VyQ2FzZSgpOyB9OyB9KTtcbiAgICB9KTtcbn0pO1xuIl19" -`; - -exports[`process stringifyContentPathRegex should create a module with stringified content as export 1`] = `"module.exports=\\"\\\\n
\\\\n some text with \`backtick\`\\\\n
\\\\n\\""`; diff --git a/src/__snapshots__/ts-program.spec.ts.snap b/src/__snapshots__/ts-program.spec.ts.snap deleted file mode 100644 index 499faa137c..0000000000 --- a/src/__snapshots__/ts-program.spec.ts.snap +++ /dev/null @@ -1,39 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`hoisting with babel should not hoist jest.mock() calls 1`] = ` -"\\"use strict\\"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { \\"default\\": mod }; -}; -Object.defineProperty(exports, \\"__esModule\\", { value: true }); -var upper_1 = __importDefault(require(\\"./upper\\")); -var lower_1 = __importDefault(require(\\"./lower\\")); -jest.mock('./upper', function () { return function (s) { return s.toUpperCase(); }; }); -describe('hello', function () { - test('my test', function () { - expect(upper_1.default('hello')).toBe('HELLO'); - expect(lower_1.default('HELLO')).toBe('hello'); - jest.mock('./lower', function () { return function (s) { return s.toLowerCase(); }; }); - }); -}); -" -`; - -exports[`hoisting without babel should hoist jest.mock() calls 1`] = ` -"\\"use strict\\"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { \\"default\\": mod }; -}; -Object.defineProperty(exports, \\"__esModule\\", { value: true }); -jest.mock('./upper', function () { return function (s) { return s.toUpperCase(); }; }); -var upper_1 = __importDefault(require(\\"./upper\\")); -var lower_1 = __importDefault(require(\\"./lower\\")); -describe('hello', function () { - test('my test', function () { - jest.mock('./lower', function () { return function (s) { return s.toLowerCase(); }; }); - expect(upper_1.default('hello')).toBe('HELLO'); - expect(lower_1.default('HELLO')).toBe('hello'); - }); -}); -" -`; diff --git a/src/index.ts b/src/index.ts index 57e63cecba..a28a156bb0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -import TsJestTransformer from './ts-jest-transformer' -import createJestPreset from './utils/create-jest-preset' +import { TsJestTransformer } from './lib/ts-jest-transformer' +import { createJestPreset } from './lib/create-jest-preset' // TODO: allow a `TsJestGlobalOptions` object to be give to createTransformer() // so that presets could totally customize and extend the transfomer; diff --git a/src/utils/backports.ts b/src/lib/backports.ts similarity index 60% rename from src/utils/backports.ts rename to src/lib/backports.ts index fcbba9dfc9..2e3f709922 100644 --- a/src/utils/backports.ts +++ b/src/lib/backports.ts @@ -1,28 +1,26 @@ import { interpolate, Deprecateds } from './messages' - -function warn(...data: any[]) { - console.warn(...data) -} - -function warnConfig(oldPath: string, newPath: string, note?: string) { - warn( - interpolate( - note ? Deprecateds.ConfigOptionWithNote : Deprecateds.ConfigOption, - { - oldPath, - newPath, - note, - }, - ), - ) -} +import { warn } from './debug' export function backportJestConfig< T extends jest.InitialOptions | jest.ProjectConfig ->(config: T = {} as any): T { +>(config: T = {} as any, silent = false): T { const { globals = {} } = config as any const { 'ts-jest': tsJest = {} } = globals as any const mergeTsJest: any = {} + const warnConfig = silent + ? () => undefined + : (oldPath: string, newPath: string, note?: string) => { + warn( + interpolate( + note ? Deprecateds.ConfigOptionWithNote : Deprecateds.ConfigOption, + { + oldPath, + newPath, + note, + }, + ), + ) + } if ('__TS_CONFIG__' in globals) { warnConfig('globals.__TS_CONFIG__', 'globals.ts-jest.tsConfig') @@ -51,30 +49,46 @@ export function backportJestConfig< delete tsJest.tsConfigFile } + if ('enableTsDiagnostics' in tsJest) { + warnConfig( + 'globals.ts-jest.enableTsDiagnostics', + 'globals.ts-jest.diagnostics', + ) + if (tsJest.enableTsDiagnostics) { + mergeTsJest.diagnostics = + typeof tsJest.enableTsDiagnostics === 'string' + ? { pathRegex: tsJest.enableTsDiagnostics } + : true + } else { + mergeTsJest.diagnostics = false + } + delete tsJest.tsConfigFile + } + if ('useBabelrc' in tsJest) { warnConfig( 'globals.ts-jest.useBabelrc', - 'globals.ts-jest.babelJest', + 'globals.ts-jest.babelConfig', Deprecateds.ConfigOptionUseBabelRcNote, ) if (tsJest.useBabelrc != null) { - mergeTsJest.babelJest = tsJest.useBabelrc ? true : {} + mergeTsJest.babelConfig = tsJest.useBabelrc ? true : {} } delete tsJest.useBabelrc } if ('babelConfig' in tsJest) { - warnConfig('globals.ts-jest.babelConfig', 'globals.ts-jest.babelJest') + warnConfig('globals.ts-jest.babelConfig', 'globals.ts-jest.babelConfig') if (tsJest.babelConfig != null) { - mergeTsJest.babelJest = tsJest.babelConfig + mergeTsJest.babelConfig = tsJest.babelConfig } delete tsJest.babelConfig } if ('skipBabel' in tsJest) { - warnConfig('globals.ts-jest.skipBabel', 'globals.ts-jest.babelJest') - if (tsJest.skipBabel === false && !mergeTsJest.babelJest) { - mergeTsJest.babelJest = true + warnConfig('globals.ts-jest.skipBabel', 'globals.ts-jest.babelConfig') + if (tsJest.skipBabel === false && !mergeTsJest.babelConfig) { + mergeTsJest.babelConfig = true } delete tsJest.skipBabel } diff --git a/src/lib/compiler.ts b/src/lib/compiler.ts new file mode 100644 index 0000000000..7350e5bd3a --- /dev/null +++ b/src/lib/compiler.ts @@ -0,0 +1,326 @@ +/** + * This code is heavilly inspired from + * https://github.com/JsCommunity/make-error/blob/v1.3.4/index.js + * Below is the original license: + * + * --- + * + * The MIT License (MIT) + * + * Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import { relative, basename, extname, join } from 'path' +import { readFileSync, writeFileSync } from 'fs' +import { EOL } from 'os' +import mkdirp = require('mkdirp') +import bufferFrom from 'buffer-from' +import stableStringify = require('fast-json-stable-stringify') +import _ts, { CustomTransformers } from 'typescript' +import { wrapWithDebug, debug } from './debug' +import { ConfigSet } from './config-set' +import { sha1 } from './sha1' +import { TsCompiler, MemoryCache, TypeInfo } from './types' +import { Errors, interpolate } from './messages' + +/** + * Register TypeScript compiler. + */ +export function createCompiler(config: ConfigSet): TsCompiler { + const cachedir = config.tsCacheDir + + const memoryCache: MemoryCache = { + contents: Object.create(null), + versions: Object.create(null), + outputs: Object.create(null), + } + + // Require the TypeScript compiler and configuration. + const ts = config.compilerModule + const cwd = config.cwd + + const extensions = ['.ts', '.tsx'] + const { + typescript: { options: compilerOptions, fileNames }, + } = config + + // Enable `allowJs` when flag is set. + if (compilerOptions.allowJs) { + extensions.push('.js') + extensions.push('.jsx') + } + + // Initialize files from TypeScript into project. + for (const path of fileNames) memoryCache.versions[path] = 1 + + /** + * Get the extension for a transpiled file. + */ + const getExtension = + compilerOptions.jsx === ts.JsxEmit.Preserve + ? (path: string) => (/\.[tj]sx$/.test(path) ? '.jsx' : '.js') + : (_: string) => '.js' + + // TODO: grab internal transformers + const transformers: CustomTransformers | undefined = undefined + + /** + * Create the basic required function using transpile mode. + */ + let getOutput = ( + code: string, + fileName: string, + lineOffset = 0, + ): SourceOutput => { + const result = ts.transpileModule(code, { + fileName, + transformers, + compilerOptions, + reportDiagnostics: config.shouldReportDiagnostic(fileName), + }) + + const diagnosticList = result.diagnostics + ? config.filterDiagnostics(result.diagnostics) + : [] + + if (diagnosticList.length) throw config.createTsError(diagnosticList) + + return [result.outputText, result.sourceMapText as string] + } + + let getTypeInfo = ( + _code: string, + _fileName: string, + _position: number, + ): TypeInfo => { + throw new TypeError(Errors.TypesUnavailableWithoutTypeCheck) + } + + // Use full language services when the fast option is disabled. + if (config.tsJest.typeCheck) { + // Set the file contents into cache. + const updateMemoryCache = (code: string, fileName: string) => { + if (memoryCache.contents[fileName] !== code) { + memoryCache.contents[fileName] = code + memoryCache.versions[fileName] = + (memoryCache.versions[fileName] || 0) + 1 + } + } + + // Create the compiler host for type checking. + const serviceHost = { + getScriptFileNames: () => Object.keys(memoryCache.versions), + getScriptVersion: (fileName: string) => { + const version = memoryCache.versions[fileName] + + // We need to return `undefined` and not a string here because TypeScript will use + // `getScriptVersion` and compare against their own version - which can be `undefined`. + // If we don't return `undefined` it results in `undefined === "undefined"` and run + // `createProgram` again (which is very slow). Using a `string` assertion here to avoid + // TypeScript errors from the function signature (expects `(x: string) => string`). + return version === undefined + ? ((undefined as any) as string) + : String(version) + }, + getScriptSnapshot(fileName: string) { + // Read contents into TypeScript memory cache. + if ( + !Object.prototype.hasOwnProperty.call(memoryCache.contents, fileName) + ) { + memoryCache.contents[fileName] = ts.sys.readFile(fileName) + } + + const contents = memoryCache.contents[fileName] + if (contents === undefined) { + return + } + return ts.ScriptSnapshot.fromString(contents) + }, + fileExists: wrapWithDebug('fileExists', ts.sys.fileExists), + readFile: wrapWithDebug('readFile', ts.sys.readFile), + readDirectory: wrapWithDebug('readDirectory', ts.sys.readDirectory), + getDirectories: wrapWithDebug('getDirectories', ts.sys.getDirectories), + directoryExists: wrapWithDebug('directoryExists', ts.sys.directoryExists), + getNewLine: () => EOL, + getCurrentDirectory: () => cwd, + getCompilationSettings: () => compilerOptions, + getDefaultLibFileName: () => ts.getDefaultLibFilePath(compilerOptions), + getCustomTransformers: () => transformers, + } + + const service = ts.createLanguageService(serviceHost) + + getOutput = (code: string, fileName: string, lineOffset: number = 0) => { + // Must set memory cache before attempting to read file. + updateMemoryCache(code, fileName) + + const output = service.getEmitOutput(fileName) + + // Get the relevant diagnostics - this is 3x faster than `getPreEmitDiagnostics`. + const diagnostics = service + .getCompilerOptionsDiagnostics() + .concat(service.getSyntacticDiagnostics(fileName)) + .concat(service.getSemanticDiagnostics(fileName)) + + const diagnosticList = config.filterDiagnostics(diagnostics) + + if (diagnosticList.length) { + throw config.createTsError(diagnosticList) + } + + if (output.emitSkipped) { + throw new TypeError(`${relative(cwd, fileName)}: Emit skipped`) + } + + // Throw an error when requiring `.d.ts` files. + if (output.outputFiles.length === 0) { + throw new TypeError( + interpolate(Errors.UnableToRequireDefinitionFile, { + file: basename(fileName), + }), + ) + } + + return [output.outputFiles[1].text, output.outputFiles[0].text] + } + + getTypeInfo = (code: string, fileName: string, position: number) => { + updateMemoryCache(code, fileName) + + const info = service.getQuickInfoAtPosition(fileName, position) + const name = ts.displayPartsToString(info ? info.displayParts : []) + const comment = ts.displayPartsToString(info ? info.documentation : []) + + return { name, comment } + } + } + + const compile = readThrough(cachedir, memoryCache, getOutput, getExtension) + return { cwd, compile, getTypeInfo, extensions, cachedir, ts } +} + +/** + * Internal source output. + */ +type SourceOutput = [string, string] + +/** + * Wrap the function with caching. + */ +function readThrough( + cachedir: string | undefined, + memoryCache: MemoryCache, + compile: ( + code: string, + fileName: string, + lineOffset?: number, + ) => SourceOutput, + getExtension: (fileName: string) => string, +) { + if (!cachedir) { + return (code: string, fileName: string, lineOffset?: number) => { + debug('readThrough', fileName) + + const [value, sourceMap] = compile(code, fileName, lineOffset) + const output = updateOutput(value, fileName, sourceMap, getExtension) + + memoryCache.outputs[fileName] = output + + return output + } + } + + // Make sure the cache directory exists before continuing. + mkdirp.sync(cachedir) + + return (code: string, fileName: string, lineOffset?: number) => { + debug('readThrough', fileName) + + const cachePath = join(cachedir, getCacheName(code, fileName)) + const extension = getExtension(fileName) + const outputPath = `${cachePath}${extension}` + + try { + const output = readFileSync(outputPath, 'utf8') + if (isValidCacheContent(output)) { + memoryCache.outputs[fileName] = output + return output + } + } catch (err) { + /* Ignore. */ + } + + const [value, sourceMap] = compile(code, fileName, lineOffset) + const output = updateOutput(value, fileName, sourceMap, getExtension) + + memoryCache.outputs[fileName] = output + writeFileSync(outputPath, output) + + return output + } +} + +/** + * Update the output remapping the source map. + */ +function updateOutput( + outputText: string, + fileName: string, + sourceMap: string, + getExtension: (fileName: string) => string, +) { + const base64Map = bufferFrom( + updateSourceMap(sourceMap, fileName), + 'utf8', + ).toString('base64') + const sourceMapContent = `data:application/json;charset=utf-8;base64,${base64Map}` + const sourceMapLength = + `${basename(fileName)}.map`.length + + (getExtension(fileName).length - extname(fileName).length) + + return outputText.slice(0, -sourceMapLength) + sourceMapContent +} + +/** + * Update the source map contents for improved output. + */ +function updateSourceMap(sourceMapText: string, fileName: string) { + const sourceMap = JSON.parse(sourceMapText) + sourceMap.file = fileName + sourceMap.sources = [fileName] + delete sourceMap.sourceRoot + return stableStringify(sourceMap) +} + +/** + * Get the file name for the cache entry. + */ +function getCacheName(sourceCode: string, fileName: string) { + return sha1(extname(fileName), '\x00', sourceCode) +} + +/** + * Ensure the given cached content is valid by sniffing for a base64 encoded '}' + * at the end of the content, which should exist if there is a valid sourceMap present. + */ +function isValidCacheContent(contents: string) { + return /(?:9|0=|Q==)$/.test(contents.slice(-3)) +} diff --git a/src/lib/config-set.ts b/src/lib/config-set.ts new file mode 100644 index 0000000000..eda6593f7d --- /dev/null +++ b/src/lib/config-set.ts @@ -0,0 +1,409 @@ +import { JsonableValue } from './jsonable-value' +import { + BabelConfig, + TsJestConfig, + TsJestGlobalOptions, + TTypeScript, + TsJestHooksMap, + BabelJestTransformer, + TsCompiler, +} from './types' +import { resolve, isAbsolute, join, dirname } from 'path' +import Memoize from './memoize' +import { backportJestConfig } from './backports' +import { Errors, ImportReasons, interpolate } from './messages' +import arrify from 'arrify' +import yn = require('yn') +import json5 from 'json5' +import { existsSync, readFileSync } from 'fs' +import importer from './importer' +import { + Diagnostic, + FormatDiagnosticsHost, + ParsedCommandLine, +} from 'typescript' +import { EOL } from 'os' +import { TSError } from './ts-error' +import { sha1 } from './sha1' +import { stringify } from './json' +import { normalizeSlashes } from './normalize-slashes' +import { createCompiler } from './compiler' + +const DEFAULT_COMPILER_OPTIONS = { + inlineSourceMap: true, + inlineSources: true, +} +const FORCE_COMPILER_OPTIONS = { + sourceMap: true, + inlineSourceMap: false, + inlineSources: true, + declaration: false, + noEmit: false, + outDir: '$$ts-jest$$', + out: undefined, + outFile: undefined, + composite: undefined, + declarationDir: undefined, + declarationMap: undefined, + emitDeclarationOnly: undefined, + module: 'commonjs', + esModuleInterop: true, + removeComments: false, +} + +export class ConfigSet { + constructor( + private _jestConfig: jest.ProjectConfig, + readonly parentOptions?: TsJestGlobalOptions, + ) {} + + @Memoize() + get jest(): jest.ProjectConfig { + const config = backportJestConfig(this._jestConfig) + if (this.parentOptions) { + // TODO: implement correct deep merging instead + ;(config.globals as any)['ts-jest'] = { + ...this.parentOptions, + ...(config.globals as any)['ts-jest'], + } + } + return config + } + + @Memoize() + get tsJest(): TsJestConfig { + const parsedConfig = this.jest + const { globals = {} } = parsedConfig as any + const options: TsJestGlobalOptions = { ...globals['ts-jest'] } + + // tsconfig + const { tsConfig: tsConfigOpt } = options + let tsConfig: TsJestConfig['tsConfig'] + if ( + typeof tsConfigOpt === 'string' || + tsConfigOpt == null || + tsConfigOpt === true + ) { + tsConfig = { + kind: 'file', + value: + typeof tsConfigOpt === 'string' + ? this.resolvePath(tsConfigOpt) + : undefined, + } + } else if (typeof tsConfigOpt === 'object') { + tsConfig = { + kind: 'inline', + value: tsConfigOpt, + } + } + + // babel jest + const { babelConfig: babelConfigOpt } = options + let babelConfig: TsJestConfig['babelConfig'] + if (typeof babelConfigOpt === 'string' || babelConfigOpt === true) { + babelConfig = { + kind: 'file', + value: + babelConfigOpt === true + ? undefined + : this.resolvePath(babelConfigOpt as string), + } + } else if (babelConfigOpt) { + babelConfig = { + kind: 'inline', + value: babelConfigOpt, + } + } + + // diagnostics + let diagnostics: TsJestConfig['diagnostics'] + const { diagnostics: diagnosticsOpt = true } = options + if (diagnosticsOpt === true || diagnosticsOpt == null) { + diagnostics = { ignoreCodes: [], pretty: true } + } else if (diagnosticsOpt === false) { + diagnostics = { + pretty: true, + ignoreCodes: [], + pathRegex: /a^/, // matches nothing + } + } else { + diagnostics = { + pretty: yn(diagnosticsOpt, { default: true }), + ignoreCodes: arrify(diagnosticsOpt.ignoreCodes).map(n => + parseInt(n as string, 10), + ), + pathRegex: diagnosticsOpt.pathRegex + ? new RegExp(diagnosticsOpt.pathRegex) + : undefined, + } + } + diagnostics.ignoreCodes = diagnostics.ignoreCodes + .concat([ + 6059, // "'rootDir' is expected to contain all source files." + 18002, // "The 'files' list in config file is empty." + 18003, // "No inputs were found in config file." + ]) + .filter((code, index, list) => list.indexOf(code) === index) + + // stringifyContentPathRegex option + let { stringifyContentPathRegex: stringifyRegEx } = options + if (typeof stringifyRegEx === 'string') { + try { + stringifyRegEx = RegExp(stringifyRegEx) + } catch (err) { + err.message = `${Errors.InvalidStringifyContentPathRegex}\n${ + err.message + }` + } + } + if (stringifyRegEx) { + if (!(stringifyRegEx instanceof RegExp)) { + throw new TypeError(Errors.InvalidStringifyContentPathRegex) + } + } else { + stringifyRegEx = undefined + } + + // parsed options + return { + version: require('../../package.json').version, + tsConfig, + babelConfig, + diagnostics, + typeCheck: yn(options), + compiler: options.compiler || 'typescript', + stringifyContentPathRegex: stringifyRegEx, + } + } + + get typescript(): ParsedCommandLine { + const { + tsJest: { tsConfig }, + } = this + const config = this.readTsConfig( + tsConfig && tsConfig.kind === 'inline' ? tsConfig.value : undefined, + tsConfig && tsConfig.kind === 'file' ? tsConfig.value : undefined, + tsConfig == null, + ) + const configDiagnosticList = this.filterDiagnostics(config.errors) + if (configDiagnosticList.length) + throw this.createTsError(configDiagnosticList) + return config + } + + get babel(): BabelConfig | undefined { + const { + tsJest: { babelConfig }, + } = this + if (babelConfig == null) return + let base: BabelConfig = { cwd: this.cwd } + if (babelConfig.kind === 'file') { + if (babelConfig.value) { + base = { + ...base, + ...json5.parse(readFileSync(babelConfig.value, 'utf8')), + } + } + } else if (babelConfig.kind === 'inline') { + base = { ...base, ...babelConfig.value } + } + // loadOptions is from babel 7+, and OptionManager is backward compatible but deprecated 6 API + const { OptionManager, loadOptions } = importer.babelCore( + ImportReasons.babelJest, + ) + let config: BabelConfig + if (typeof loadOptions === 'function') { + config = loadOptions(base) as BabelConfig + } else { + config = new OptionManager().init(base) as BabelConfig + } + return config + } + + @Memoize() + get compilerModule(): TTypeScript { + return importer.typescript(ImportReasons.tsJest, this.tsJest.compiler) + } + + @Memoize() + get babelJestTransformer(): BabelJestTransformer | undefined { + const { babel } = this + if (!babel) return + return importer + .babelJest(ImportReasons.babelJest) + .createTransformer(babel) as BabelJestTransformer + } + + @Memoize() + get tsCompiler(): TsCompiler { + return createCompiler(this) + } + + @Memoize() + get hooks(): TsJestHooksMap { + let hooksFile = process.env.TS_JEST_HOOKS + if (hooksFile) { + hooksFile = resolve(this.cwd, hooksFile) + return importer.tryThese(hooksFile) || {} + } + return {} + } + + @Memoize() + get filterDiagnostics() { + const { + tsJest: { + diagnostics: { ignoreCodes }, + }, + shouldReportDiagnostic, + } = this + return (diagnostics: Diagnostic[], filePath?: string): Diagnostic[] => { + if (filePath && !shouldReportDiagnostic(filePath)) return [] + return diagnostics.filter(diagnostic => { + if ( + diagnostic.file && + diagnostic.file.fileName && + !shouldReportDiagnostic(diagnostic.file.fileName) + ) + return false + return ignoreCodes.indexOf(diagnostic.code) === -1 + }) + } + } + + @Memoize() + get shouldReportDiagnostic() { + const { + diagnostics: { pathRegex }, + } = this.tsJest + return (fileName: string) => !pathRegex || pathRegex.test(fileName) + } + + @Memoize() + get createTsError() { + const { + diagnostics: { pretty }, + } = this.tsJest + + const formatDiagnostics = pretty + ? this.compilerModule.formatDiagnosticsWithColorAndContext + : this.compilerModule.formatDiagnostics + + const diagnosticHost: FormatDiagnosticsHost = { + getNewLine: () => EOL, + getCurrentDirectory: () => this.cwd, + getCanonicalFileName: path => path, + } + + return (diagnostics: ReadonlyArray) => { + const diagnosticText = formatDiagnostics(diagnostics, diagnosticHost) + const diagnosticCodes = diagnostics.map(x => x.code) + return new TSError(diagnosticText, diagnosticCodes) + } + } + + @Memoize() + get tsCacheDir(): string | undefined { + if (!this.jest.cache) return + const cacheSufix = sha1( + stringify({ + version: this.compilerModule.version, + compiler: this.tsJest.compiler, + compilerOptions: this.typescript.options, + typeCheck: this.tsJest.typeCheck, + ignoreDiagnostics: this.tsJest.diagnostics.ignoreCodes, + }), + ) + return join(this.jest.cacheDirectory, `ts-jest-${cacheSufix}`) + } + + get rootDir(): string { + return this.jest.rootDir || this.cwd + } + + get cwd(): string { + return this.jest.cwd || process.cwd() + } + + readTsConfig( + compilerOptions?: object, + project?: string | null, + noProject?: boolean | null, + ): ParsedCommandLine { + let config = { compilerOptions: {} } + let basePath = normalizeSlashes(this.cwd) + let configFileName: string | undefined + const ts = this.compilerModule + + // Read project configuration when available. + if (!noProject) { + configFileName = project + ? normalizeSlashes(resolve(this.cwd, project)) + : ts.findConfigFile(normalizeSlashes(this.cwd), ts.sys.fileExists) + + if (configFileName) { + const result = ts.readConfigFile(configFileName, ts.sys.readFile) + + // Return diagnostics. + if (result.error) { + return { errors: [result.error], fileNames: [], options: {} } + } + + config = result.config + basePath = normalizeSlashes(dirname(configFileName)) + } + } + + // Override default configuration options `ts-jest` requires. + config.compilerOptions = { + ...DEFAULT_COMPILER_OPTIONS, + ...config.compilerOptions, + ...compilerOptions, + ...FORCE_COMPILER_OPTIONS, + } + + const result = ts.parseJsonConfigFileContent( + config, + ts.sys, + basePath, + undefined, + configFileName, + ) + + // Target ES5 output by default (instead of ES3). + if (result.options.target === undefined) { + result.options.target = ts.ScriptTarget.ES5 + } + + return result + } + + resolvePath(inputPath: string, noFailIfMissing: boolean = false): string { + let path: string = inputPath + if (path.startsWith('')) + path = resolve(this.rootDir, path.substr(9)) + else if (!isAbsolute(path)) path = resolve(this.cwd, path) + if (!noFailIfMissing && !existsSync(path)) { + throw new Error( + interpolate(Errors.FileNotFound, { inputPath, resolvedPath: path }), + ) + } + return path + } + + @Memoize() + get jsonValue() { + return new JsonableValue({ + jest: this.jest, + tsJest: this.tsJest, + babel: this.babel, + typescript: this.typescript, + }) + } + + @Memoize() + get cacheKey(): string { + return this.jsonValue.serialized + } +} diff --git a/src/utils/create-jest-preset.ts b/src/lib/create-jest-preset.ts similarity index 89% rename from src/utils/create-jest-preset.ts rename to src/lib/create-jest-preset.ts index 7704750b2c..88a0ca65ab 100644 --- a/src/utils/create-jest-preset.ts +++ b/src/lib/create-jest-preset.ts @@ -1,5 +1,5 @@ import * as jestConfig from 'jest-config' -import { CreateJestPresetOptions } from '../types' +import { CreateJestPresetOptions } from './types' // jest 22 doesn't have defaults const defaults = jestConfig.defaults || { @@ -11,7 +11,7 @@ const defaults = jestConfig.defaults || { // TODO: find out if tsconfig that we'll use contains `allowJs` // and change the transform so that it also uses ts-jest for js files -export default function createJestPreset({ +export function createJestPreset({ allowJs = false, }: CreateJestPresetOptions = {}) { return { diff --git a/src/lib/debug.ts b/src/lib/debug.ts new file mode 100644 index 0000000000..8922c7bf08 --- /dev/null +++ b/src/lib/debug.ts @@ -0,0 +1,22 @@ +import yn = require('yn') + +export const DEBUG_MODE: boolean = yn(process.env.TS_JEST_DEBUG) + +export const debug: typeof console.log = DEBUG_MODE + ? (...args: any[]) => console.log('ts-jest', ...args) + : () => undefined + +export const wrapWithDebug: any>( + msg: string, + func: T, +) => T = DEBUG_MODE + ? (msg, func) => + function wrapper(this: any) { + debug(msg) + return func.apply(this, arguments) + } as any + : func => func + +export const warn = (...msg: any[]) => { + console.warn('ts-jest', ...msg) +} diff --git a/src/utils/hacks.ts b/src/lib/hacks.ts similarity index 88% rename from src/utils/hacks.ts rename to src/lib/hacks.ts index be58e686a6..f11b44b267 100644 --- a/src/utils/hacks.ts +++ b/src/lib/hacks.ts @@ -1,4 +1,4 @@ -import { TBabelCore, ModulePatcher } from '../types' +import { TBabelCore, ModulePatcher, BabelConfig } from './types' // tslint:disable-next-line:variable-name export const patchBabelCore_githubIssue6577: ModulePatcher< @@ -15,7 +15,7 @@ export const patchBabelCore_githubIssue6577: ModulePatcher< try { const File = require('babel-core/lib/transformation/file').File File.prototype.initOptions = (original => { - return function(this: any, opt) { + return function(this: any, opt: BabelConfig) { const before = opt.sourceMaps const result = original.apply(this, arguments) if (before && before !== result.sourceMaps) { diff --git a/src/utils/importer.spec.ts b/src/lib/importer.spec.ts similarity index 94% rename from src/utils/importer.spec.ts rename to src/lib/importer.spec.ts index bb2275d9be..67cb947662 100644 --- a/src/utils/importer.spec.ts +++ b/src/lib/importer.spec.ts @@ -1,7 +1,7 @@ // tslint:disable:max-line-length import * as fakers from '../__helpers__/fakers' import mockThese from '../__helpers__/mock-there' -import { TsJestImporter } from '../types' +import { TsJestImporter } from './types' import { ImportReasons } from './messages' beforeEach(() => { @@ -71,7 +71,7 @@ describe('babelCore', () => { }) expect(() => importer().babelCore(fakers.importReason())) .toThrowErrorMatchingInlineSnapshot(` -"[ts-jest] Unable to load any of these modules: \\"@babel/core\\", \\"babel-core\\". because. To fix it: +"Unable to load any of these modules: \\"@babel/core\\", \\"babel-core\\". [[BECAUSE]]. To fix it: • for Babel 7: \`npm i -D babel-jest 'babel-core@^7.0.0-0' @babel/core\` (or \`yarn add --dev babel-jest 'babel-core@^7.0.0-0' @babel/core\`) • for Babel 6: \`npm i -D babel-jest babel-core\` (or \`yarn add --dev babel-jest babel-core\`)" `) diff --git a/src/utils/importer.ts b/src/lib/importer.ts similarity index 96% rename from src/utils/importer.ts rename to src/lib/importer.ts index edda1f3914..2f5e88a589 100644 --- a/src/utils/importer.ts +++ b/src/lib/importer.ts @@ -6,7 +6,7 @@ import { ModulePatcher, TTypeScript, TsJestImporter, -} from '../types' +} from './types' import * as hacks from './hacks' import { ImportReasons, Errors, interpolate, Helps } from './messages' @@ -35,10 +35,6 @@ class Importer implements TsJestImporter { return importDefault(this._import(why, 'closest-file-data')).default } - typeScript(why: ImportReasons): TTypeScript { - return this._import(why, 'typescript') - } - babelJest(why: ImportReasons): TBabelJest { // this is to ensure babel-core is patched this.tryThese('babel-core') @@ -59,6 +55,10 @@ class Importer implements TsJestImporter { }) } + typescript(why: ImportReasons, which: string): TTypeScript { + return this._import(why, which) + } + @Memoize((...args: string[]) => args.join(':')) tryThese(moduleName: string, ...fallbacks: string[]): any { let name: string diff --git a/src/index.spec.ts b/src/lib/index.spec.ts similarity index 93% rename from src/index.spec.ts rename to src/lib/index.spec.ts index 295e308818..238a188c58 100644 --- a/src/index.spec.ts +++ b/src/lib/index.spec.ts @@ -1,4 +1,4 @@ -import * as tsJest from './index' +import * as tsJest from '..' describe('ts-jest', () => { it('should export a `createTransformer` function', () => { diff --git a/src/lib/json.ts b/src/lib/json.ts new file mode 100644 index 0000000000..5f6b87b7d5 --- /dev/null +++ b/src/lib/json.ts @@ -0,0 +1,34 @@ +import stableStringify = require('fast-json-stable-stringify') + +const UNDEFINED = 'undefined' + +export function stringify(input: any): string { + return input === undefined ? UNDEFINED : stableStringify(input) +} + +export function parse(input: string): any { + return input === UNDEFINED ? undefined : JSON.parse(input) +} + +interface NormalizeOptions { + parse?: (input: string) => any +} +export function normalize( + input: string, + { parse: parser = parse }: NormalizeOptions = {}, +): string { + let result: string | undefined + if (normalize.cache.has(input)) { + result = normalize.cache.get(input) + } else { + const data = parser(input) + result = stringify(data) + if (result === input) result = undefined + normalize.cache.set(input, result) + } + return result === undefined ? input : result +} +// tslint:disable-next-line:no-namespace +export namespace normalize { + export const cache = new Map() +} diff --git a/src/lib/jsonable-value.ts b/src/lib/jsonable-value.ts new file mode 100644 index 0000000000..9cd8a9fe20 --- /dev/null +++ b/src/lib/jsonable-value.ts @@ -0,0 +1,30 @@ +import { stringify } from './json' + +export class JsonableValue { + private _serialized!: string + private _value!: V + + constructor(value: V) { + this.value = value + } + + set value(value: V) { + this._value = value + this._serialized = stringify(value) + } + get value(): V { + return this._value + } + + get serialized(): string { + return this._serialized + } + + valueOf(): V { + return this._value + } + + toString() { + return this._serialized + } +} diff --git a/src/utils/memoize.ts b/src/lib/memoize.ts similarity index 100% rename from src/utils/memoize.ts rename to src/lib/memoize.ts diff --git a/src/utils/messages.ts b/src/lib/messages.ts similarity index 50% rename from src/utils/messages.ts rename to src/lib/messages.ts index afb25bf620..cf34b985b6 100644 --- a/src/utils/messages.ts +++ b/src/lib/messages.ts @@ -1,11 +1,14 @@ // tslint:disable:max-line-length export enum Errors { - InvalidStringifyContentPathRegex = '[ts-jest] Option "stringifyContentPathRegex" should be a valid regex pattern.', - UnableToFindPackageJson = '[ts-jest] Unable to find package.json from "{{fromPath}}".', - InvalidDiagnosticsOption = '[ts-jest] Invalid value for diagnostics: {{value}}.', - UnableToLoadOneModule = '[ts-jest] Unable to load the module {{module}}. {{reason}} To fix it:\n{{fix}}', - UnableToLoadAnyModule = '[ts-jest] Unable to load any of these modules: {{module}}. {{reason}}. To fix it:\n{{fix}}', - UnableToFindTsConfig = '[ts-jest] Could not find a TS config file (given: "{{given}}", root: "{{root}}").', + InvalidStringifyContentPathRegex = 'Option "stringifyContentPathRegex" should be a valid regex pattern.', + UnableToFindPackageJson = 'Unable to find package.json from "{{fromPath}}".', + InvalidDiagnosticsOption = 'Invalid value for diagnostics: {{value}}.', + UnableToLoadOneModule = 'Unable to load the module {{module}}. {{reason}} To fix it:\n{{fix}}', + UnableToLoadAnyModule = 'Unable to load any of these modules: {{module}}. {{reason}}. To fix it:\n{{fix}}', + UnableToFindTsConfig = 'Could not find a TS config file (given: "{{given}}", root: "{{root}}").', + TypesUnavailableWithoutTypeCheck = 'Type information is unavailable without "typeCheck"', + UnableToRequireDefinitionFile = 'Unable to require `.d.ts` file.\nThis is usually the result of a faulty configuration or import. Make sure there is a `.js`, `.json` or another executable extension available alongside `{{file}}`.', + FileNotFound = 'File not found: {{inputPath}} (resolved as: {{resolvedPath}})', } export enum Helps { diff --git a/src/lib/normalize-slashes.ts b/src/lib/normalize-slashes.ts new file mode 100644 index 0000000000..235c0c39aa --- /dev/null +++ b/src/lib/normalize-slashes.ts @@ -0,0 +1,3 @@ +export function normalizeSlashes(value: string): string { + return value.replace(/\\/g, '/') +} diff --git a/src/utils/sha1.ts b/src/lib/sha1.ts similarity index 61% rename from src/utils/sha1.ts rename to src/lib/sha1.ts index 4dabb7dde9..a87e35efc1 100644 --- a/src/utils/sha1.ts +++ b/src/lib/sha1.ts @@ -5,7 +5,7 @@ export const cache: { [key: string]: string } = Object.create(null) type DataItem = string | Buffer -export default function sha1(...data: DataItem[]): string { +export function sha1(...data: DataItem[]): string { const canCache = data.length === 1 && typeof data[0] === 'string' // caching let cacheKey!: string @@ -16,10 +16,14 @@ export default function sha1(...data: DataItem[]): string { } } - // we use SHA1 because it's the fastest provided by node and we are not concerned about security + // we use SHA1 because it's the fastest provided by node + // and we are not concerned about security here const hash = createHash('sha1') - data.forEach(item => hash.update(item)) - const res = hash.digest('base64').toString() + data.forEach(item => { + if (typeof item === 'string') hash.update(item, 'utf8') + else hash.update(item) + }) + const res = hash.digest('hex').toString() if (canCache) { cache[cacheKey] = res diff --git a/src/lib/ts-error.ts b/src/lib/ts-error.ts new file mode 100644 index 0000000000..9d307fe8f2 --- /dev/null +++ b/src/lib/ts-error.ts @@ -0,0 +1,25 @@ +import { inspect } from 'util' +import { BaseError } from 'make-error' + +/** + * @internal + */ +export const INSPECT_CUSTOM = inspect.custom || 'inspect' + +/** + * TypeScript diagnostics error. + */ +export class TSError extends BaseError { + name = 'TSError' + + constructor(public diagnosticText: string, public diagnosticCodes: number[]) { + super(`⨯ Unable to compile TypeScript:\n${diagnosticText}`) + } + + /** + * @internal + */ + [INSPECT_CUSTOM]() { + return this.diagnosticText + } +} diff --git a/src/lib/ts-jest-transformer.ts b/src/lib/ts-jest-transformer.ts new file mode 100644 index 0000000000..a4eb9af51f --- /dev/null +++ b/src/lib/ts-jest-transformer.ts @@ -0,0 +1,143 @@ +import { TsJestGlobalOptions } from './types' +import { sha1 } from './sha1' +import { JsonableValue } from './jsonable-value' +import { ConfigSet } from './config-set' +import { stringify, parse } from './json' + +interface ConfigSetIndexItem { + configSet: ConfigSet + jestConfig: JsonableValue +} + +export class TsJestTransformer implements jest.Transformer { + protected static _lastTransformerId: number = 0 + static get lastTransformerId() { + return TsJestTransformer._lastTransformerId + } + protected static get _nextTransformerId() { + return ++TsJestTransformer._lastTransformerId + } + + readonly id: number + readonly options: TsJestGlobalOptions + + private _configSetsIndex: ConfigSetIndexItem[] = [] + + constructor(baseOptions: TsJestGlobalOptions = {}) { + this.options = { ...baseOptions } + this.id = TsJestTransformer._nextTransformerId + } + + configsFor(jestConfig: jest.ProjectConfig | string) { + let csi: ConfigSetIndexItem | undefined + let jestConfigObj: jest.ProjectConfig + if (typeof jestConfig === 'string') { + csi = this._configSetsIndex.find( + cs => cs.jestConfig.serialized === jestConfig, + ) + if (csi) return csi.configSet + jestConfigObj = parse(jestConfig) + } else { + csi = this._configSetsIndex.find(cs => cs.jestConfig.value === jestConfig) + if (csi) return csi.configSet + // try to look-it up by stringified version + const serialized = stringify(jestConfig) + csi = this._configSetsIndex.find( + cs => cs.jestConfig.serialized === serialized, + ) + if (csi) { + // 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 tranformer with the proper object + csi.jestConfig.value = jestConfig + return csi.configSet + } + jestConfigObj = jestConfig + } + + // create the new record in the index + const configSet = new ConfigSet(jestConfigObj, this.options) + this._configSetsIndex.push({ + jestConfig: new JsonableValue(jestConfigObj), + configSet, + }) + return configSet + } + + process( + source: string, + filePath: jest.Path, + jestConfig: jest.ProjectConfig, + transformOptions?: jest.TransformOptions, + ): jest.TransformedSource | string { + let result: string | jest.TransformedSource + + const configs = this.configsFor(jestConfig) + const { tsJest: tsJestConfig, hooks } = configs + + const stringify = + tsJestConfig.stringifyContentPathRegex && + tsJestConfig.stringifyContentPathRegex.test(filePath) + const babelJest = stringify ? undefined : configs.babelJestTransformer + + // get the compiler instance + const compiler = configs.tsCompiler + + // handles here what we should simply stringify + if (stringify) { + source = `module.exports=${JSON.stringify(source)}` + } + + // transpile TS code (source maps are included) + result = filePath.endsWith('.d.ts') + ? '' // do not try to compile declaration files + : compiler.compile(source, filePath) + + // calling babel-jest transformer + if (babelJest) { + result = babelJest.process(result, filePath, jestConfig, transformOptions) + } + + // allows hooks (usefull for testing) + if (hooks.afterProcess) { + const newResult = hooks.afterProcess([...arguments], 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 + * @param fileContent The content of the file + * @param filePath The full path to the file + * @param jestConfigStr The JSON-encoded version of jest config + * @param transformOptions.instrument Whether the content will be instrumented by our transformer (always false) + * @param transformOptions.rootDir Jest current rootDir + */ + getCacheKey( + fileContent: string, + filePath: string, + jestConfigStr: string, + transformOptions: { instrument?: boolean; rootDir?: string } = {}, + ): string { + const configs = this.configsFor(jestConfigStr) + const { instrument = false } = transformOptions + return sha1( + configs.cacheKey, + '\x00', + `instrument:${instrument ? 'on' : 'off'}`, + '\x00', + fileContent, + '\x00', + filePath, + ) + } + + // we let jest doing the instrumentation, it does it well + // get canInstrument() {} +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000000..ca4efb0844 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,187 @@ +import _ts, { CompilerOptions } from 'typescript' +import _closestFileData from 'closest-file-data' +import * as _babel from 'babel__core' +import { IPackageJSON } from 'gist-package-json' + +export type TBabelCore = typeof _babel +export type TTypeScript = typeof _ts +export type TPackageJson = IPackageJSON +export type TClosestFileData = typeof _closestFileData +export type TBabelJest = Required +export type BabelJestTransformer = { + [K in Exclude]: Exclude< + jest.Transformer[K], + undefined + > +} +export type BabelConfig = _babel.TransformOptions + +export interface TsJestGlobalOptions { + /** + * Compiler options. It can be: + * - `true` (or `undefined`, it's the default): use default tsconfig file + * - `false`: do NOT use default config file + * - `path/to/tsconfig.json`: path to a specific tsconfig file ( can be used) + * - `{...}`: an object with inline compiler options + */ + tsConfig?: boolean | string | CompilerOptions + + /** + * Whether to typecheck (slower, default to `false`): + */ + typeCheck?: boolean + + /** + * Compiler to use (default to 'typescript'): + */ + compiler?: string + + /** + * TS diagnostics - more to be reported if `typeCheck` is `true`. It can be: + * - `true` (or `undefined`, it's the default): show all diagnostics + * - `false`: hide diagnostics of all files (kind of useless) + * - `{...}`: an inline object with fine grained settings + */ + diagnostics?: + | boolean + | { + pretty?: boolean + ignoreCodes?: number | string | Array + pathRegex?: RegExp | string // only enable on files matching the regex + } + + /** + * Babel config. It can be: + * - `false` (or `undefined`, it's the default): do NOT use babel + * - `true`: use babel using default babelrc file + * - `path/to/.babelrc`: path to a babelrc file ( can be used) + * - `{...}`: an object with inline babel options + */ + babelConfig?: boolean | string | BabelConfig + + // should this be kept in here? it has nothing to do with TS after all... + /** + * Kept for backward compatibility to handle __TRANSFORM_HTML__ + * Any file which will match this regex will be transpiled as a module + * exporting the content of the file as a string + */ + stringifyContentPathRegex?: string | RegExp +} + +interface TsJestConfig$tsConfig$file { + kind: 'file' + value: string | undefined +} +interface TsJestConfig$tsConfig$inline { + kind: 'inline' + value: CompilerOptions +} +type TsJestConfig$tsConfig = + | TsJestConfig$tsConfig$file + | TsJestConfig$tsConfig$inline + | undefined +interface TsJestConfig$diagnostics { + pretty: boolean + ignoreCodes: number[] + pathRegex?: RegExp | undefined +} +interface TsJestConfig$babelConfig$file { + kind: 'file' + value: string | undefined +} +interface TsJestConfig$babelConfig$inline { + kind: 'inline' + value: BabelConfig +} +type TsJestConfig$babelConfig = + | TsJestConfig$babelConfig$file + | TsJestConfig$babelConfig$inline + | undefined +type TsJestConfig$stringifyContentPathRegex = RegExp | undefined + +export interface TsJestConfig { + version: string + tsConfig: TsJestConfig$tsConfig + typeCheck: boolean + compiler: string + diagnostics: TsJestConfig$diagnostics + babelConfig: TsJestConfig$babelConfig + + // to deprecate / deprecated === === === + stringifyContentPathRegex: TsJestConfig$stringifyContentPathRegex +} + +export interface TsJestProgram { + readonly parsedConfig: _ts.ParsedCommandLine + transpileModule( + path: string, + content: string, + instrument?: boolean, + extraCompilerOptions?: _ts.CompilerOptions, + ): string +} + +export interface TsJestHooksMap { + afterProcess?( + args: any[], + result: string | jest.TransformedSource, + ): string | jest.TransformedSource | void +} + +export interface CreateJestPresetOptions { + allowJs?: boolean +} + +export type ModulePatcher = (module: T) => T + +export interface TsJestImporter { + tryThese(moduleName: string, ...fallbacks: string[]): any +} + +/** + * Common TypeScript interfaces between versions. + */ +export interface TSCommon { + version: typeof _ts.version + sys: typeof _ts.sys + ScriptSnapshot: typeof _ts.ScriptSnapshot + displayPartsToString: typeof _ts.displayPartsToString + createLanguageService: typeof _ts.createLanguageService + getDefaultLibFilePath: typeof _ts.getDefaultLibFilePath + getPreEmitDiagnostics: typeof _ts.getPreEmitDiagnostics + flattenDiagnosticMessageText: typeof _ts.flattenDiagnosticMessageText + transpileModule: typeof _ts.transpileModule + ModuleKind: typeof _ts.ModuleKind + ScriptTarget: typeof _ts.ScriptTarget + findConfigFile: typeof _ts.findConfigFile + readConfigFile: typeof _ts.readConfigFile + parseJsonConfigFileContent: typeof _ts.parseJsonConfigFileContent + formatDiagnostics: typeof _ts.formatDiagnostics + formatDiagnosticsWithColorAndContext: typeof _ts.formatDiagnosticsWithColorAndContext +} + +/** + * Track the project information. + */ +export interface MemoryCache { + contents: { [path: string]: string | undefined } + versions: { [path: string]: number | undefined } + outputs: { [path: string]: string } +} + +/** + * Information retrieved from type info check. + */ +export interface TypeInfo { + name: string + comment: string +} + +export interface TsCompiler { + cwd: string + extensions: string[] + cachedir: string | undefined + ts: TSCommon + compile(code: string, fileName: string, lineOffset?: number): string + getTypeInfo(code: string, fileName: string, position: number): TypeInfo +} diff --git a/src/shims.d.ts b/src/shims.d.ts new file mode 100644 index 0000000000..35a3322e7c --- /dev/null +++ b/src/shims.d.ts @@ -0,0 +1,22 @@ +declare module 'fast-json-stable-stringify' { + const fastJsonStableStringify: (input: any) => string + export = fastJsonStableStringify +} + +declare module 'yn' { + const yn: (val: any, opt?: { default?: boolean }) => boolean + export = yn +} + +declare module 'babel__core' { + import { TransformOptions } from 'babel__core/index' + export * from 'babel__core/index' + export class OptionManager { + init(opt: TransformOptions): TransformOptions + } +} + +declare module 'jest-config' { + import 'jest' + export const defaults: jest.InitialOptions +} diff --git a/src/transformers/README.md b/src/transformers/README.md deleted file mode 100644 index e91c0a18a6..0000000000 --- a/src/transformers/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Transformer - -See https://dev.doctorevidence.com/how-to-write-a-typescript-transform-plugin-fc5308fdd943 - -## Boilerplate - -```ts -import { - TransformationContext, - SourceFile, - Visitor, - visitEachChild, - Transformer, - visitNode, -} from 'typescript'; -import TsProgram from '../ts-program'; - -export default function(prog: TsProgram) { - function createVisitor(ctx: TransformationContext, sf: SourceFile) { - const visitor: Visitor = node => { - // here we can check each node and potentially return - // new nodes if we want to leave the node as is, and - // continue searching through child nodes: - return visitEachChild(node, visitor, ctx); - }; - return visitor; - } - return (ctx: TransformationContext): Transformer => { - return (sf: SourceFile) => visitNode(sf, createVisitor(ctx, sf)); - }; -}; -``` diff --git a/src/transformers/hoisting.ts b/src/transformers/hoisting.ts deleted file mode 100644 index 40db6f2098..0000000000 --- a/src/transformers/hoisting.ts +++ /dev/null @@ -1,74 +0,0 @@ -// tslint:disable:curly -import TsProgram from '../ts-program' -import importer from '../utils/importer' -import { ImportReasons } from '../utils/messages' -// take care of including ONLY TYPES here, for the rest use ts -import { - Node, - ExpressionStatement, - TransformationContext, - SourceFile, - Statement, - Visitor, - Block, - Transformer, -} from 'typescript' - -const ts = importer.typeScript(ImportReasons.tsJest) - -function isJestMockCallExpression(node: Node): node is ExpressionStatement { - return ( - ts.isExpressionStatement(node) && - ts.isCallExpression(node.expression) && - ts.isPropertyAccessExpression(node.expression.expression) && - ts.isIdentifier(node.expression.expression.expression) && - node.expression.expression.expression.text === 'jest' && - ts.isIdentifier(node.expression.expression.name) && - node.expression.expression.name.text === 'mock' - ) -} - -export default function hoisting(prog: TsProgram) { - function createVisitor(ctx: TransformationContext, sf: SourceFile) { - let level = 0 - const hoisted: Statement[][] = [] - const enter = () => { - level++ - // reuse arrays - if (hoisted[level]) { - hoisted[level].splice(0, hoisted[level].length) - } - } - const exit = () => level-- - const hoist = (node: Statement) => { - if (hoisted[level]) { - hoisted[level].push(node) - } else { - hoisted[level] = [node] - } - } - - const visitor: Visitor = node => { - enter() - const resultNode = ts.visitEachChild(node, visitor, ctx) - if (hoisted[level] && hoisted[level].length) { - ;(resultNode as Block).statements = ts.createNodeArray([ - ...hoisted[level], - ...(resultNode as Block).statements, - ]) - } - exit() - - if (isJestMockCallExpression(resultNode)) { - hoist(resultNode as Statement) - return - } - return resultNode - } - return visitor - } - - return (ctx: TransformationContext): Transformer => { - return (sf: SourceFile) => ts.visitNode(sf, createVisitor(ctx, sf)) - } -} diff --git a/src/ts-jest-transformer.spec.ts b/src/ts-jest-transformer.spec.ts deleted file mode 100644 index 6ef6ef2b12..0000000000 --- a/src/ts-jest-transformer.spec.ts +++ /dev/null @@ -1,168 +0,0 @@ -import TsJestTransformerOriginal from './ts-jest-transformer' -import * as fakers from './__helpers__/fakers' -import * as closesPkgJson from './utils/closest-package-json' -import * as TsJestProgram from './ts-program' -import _requrieJsOrJson from './utils/require-js-or-json' -import { join } from 'path' - -jest.mock('./ts-program') -jest.mock('./utils/closest-package-json') -jest.mock('./utils/backports') -jest.mock('./utils/require-js-or-json.ts') - -// typecasting -const requrieJsOrJson = _requrieJsOrJson as jest.Mock - -// mocks -const mocks = { - babelJestCacheKey: undefined as any, - set packageJson(val: any) { - ;(closesPkgJson as any).__default = val - }, - set tsConfig(val: any) { - ;(TsJestProgram as any).__tsConfig = val - }, - reset() { - this.babelJestCacheKey = 'babel-jest-cache-key' - this.packageJson = { name: 'mock' } - this.tsConfig = {} - }, -} -afterEach(() => { - mocks.reset() - requrieJsOrJson.mockReset() -}) - -class TsJestTransformer extends TsJestTransformerOriginal { - babelJestFor(jestCfg: jest.ProjectConfig) { - const bj = super.babelJestFor(jestCfg) - if (bj && !(bj.getCacheKey as any).mock) { - jest - .spyOn(bj, 'getCacheKey') - .mockImplementation(() => mocks.babelJestCacheKey) - } - return bj - } -} - -describe('process', () => { - describe('hoisting', () => { - const transformer = new TsJestTransformer() - it('should hoist jest.mock calls using babel', () => { - const config = fakers.jestConfig({}, { babelJest: true }) - const result = transformer.process( - fakers.transpiledTsSource(), - fakers.filePath('path/to/file.ts'), - config, - ) as jest.TransformedSource - expect(result.code).toMatchSnapshot() - }) - }) // hoisting - - describe('babelJestConfigFor', () => { - const DUMMY_BABELRC = 'dummy-babelrc' - const DUMMY_BABEL_CONF = { foo: 'bar' } - const babelJestConfigFor = (jestConfig: any) => { - return new TsJestTransformer().babelJestConfigFor(jestConfig) - } - - it('should return undefined when disabled or not set', () => { - expect(babelJestConfigFor(fakers.jestConfig())).toBeUndefined() - expect( - babelJestConfigFor(fakers.jestConfig({}, { babelJest: false })), - ).toBeUndefined() - }) - - it('should return the content of given file', () => { - requrieJsOrJson.mockImplementation( - p => p === join(__dirname, DUMMY_BABELRC) && DUMMY_BABELRC, - ) - expect( - babelJestConfigFor( - fakers.jestConfig( - { rootDir: __dirname }, - { babelJest: DUMMY_BABELRC }, - ), - ), - ).toBe(DUMMY_BABELRC) - }) - - it('should return given inline config', () => { - expect( - babelJestConfigFor( - fakers.jestConfig({}, { babelJest: DUMMY_BABEL_CONF as any }), - ), - ).toEqual(DUMMY_BABEL_CONF) - }) - }) - - describe('stringifyContentPathRegex', () => { - const transformer = new TsJestTransformer() - it('should create a module with stringified content as export', () => { - const config = fakers.jestConfig( - {}, - { stringifyContentPathRegex: '\\.html$' }, - ) - const source = fakers.htmlSource() - const result = transformer.process( - source, - fakers.filePath('path/to/file.html'), - config, - ) as string - expect(result).toMatchSnapshot() - const importer = Function( - `const exports = {}, module = {exports:exports};${result};return module.exports;`, - ) - expect(importer).not.toThrow() - expect(importer()).toEqual(source) - }) - }) // stringifyContentPathRegex -}) // process - -describe('getCacheKey', () => { - const fakeSource = fakers.typescriptSource() - const fakeFilePath = fakers.filePath('file.ts') - const fakeJestConfig = JSON.stringify( - fakers.jestConfig({}, { babelJest: true }), - ) - - const call: typeof TsJestTransformer['prototype']['getCacheKey'] = ( - // tslint:disable-next-line:trailing-comma - ...args - ) => { - const tr = new TsJestTransformer() - return tr.getCacheKey(...args) - } - const defaultCall = () => call(fakeSource, fakeFilePath, fakeJestConfig) - - it('should be a 28 chars string, different for each case', () => { - const allCacheKeys = [ - defaultCall(), - call('const b = 2', fakeFilePath, fakeJestConfig), - call(fakeSource, fakers.filePath('other-file.ts'), fakeJestConfig), - call(fakeSource, fakeFilePath, '{"rootDir": "./sub"}'), - call(fakeSource, fakeFilePath, fakeJestConfig, { instrument: true }), - call(fakeSource, fakeFilePath, fakeJestConfig, { rootDir: '/child' }), - ] - - mocks.babelJestCacheKey = 'another-babel-jest-cache-key' - allCacheKeys.push(defaultCall()) - mocks.reset() - - mocks.tsConfig = '{"files": []}' - allCacheKeys.push(defaultCall()) - mocks.reset() - - mocks.packageJson = '{"name": "dummy"}' - allCacheKeys.push(defaultCall()) - mocks.reset() - - // uniq array should equal original - expect( - allCacheKeys.filter((k, i) => allCacheKeys.indexOf(k) === i), - ).toEqual(allCacheKeys) - allCacheKeys.forEach(cacheKey => { - expect(cacheKey).toHaveLength(28) - }) - }) // all different and 28 chars -}) // getCacheKey diff --git a/src/ts-jest-transformer.ts b/src/ts-jest-transformer.ts deleted file mode 100644 index 8f6c5825cf..0000000000 --- a/src/ts-jest-transformer.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { - TsJestGlobalOptions, - TsJestConfig, - BabelConfig, - TsJestHooksMap, - BabelJestTransformer, -} from './types' -import TsProgram from './ts-program' -import Memoize from './utils/memoize' -import { normalizeDiagnosticTypes } from './utils/diagnostics' -import { backportJestConfig } from './utils/backports' -import jestRootDir from './utils/jest-root-dir' -import { sep, resolve } from 'path' -import requireJsOrJson from './utils/require-js-or-json' -import closestPatckageJson from './utils/closest-package-json' -import sha1 from './utils/sha1' -import importer from './utils/importer' -import { Errors, ImportReasons } from './utils/messages' - -export default class TsJestTransformer implements jest.Transformer { - constructor(protected _baseOptions: TsJestGlobalOptions = {}) {} - - @Memoize() - get hooks(): TsJestHooksMap { - let hooksFile = process.env.__TS_JEST_HOOKS - if (hooksFile) { - hooksFile = resolve(process.cwd(), hooksFile) - return importer.tryThese(hooksFile) || {} - } - return {} - } - - @Memoize(jestRootDir) - sanitizedJestConfigFor( - jestConfig: T, - ): T { - const config = { - ...(jestConfig as object), - rootDir: jestRootDir(jestConfig), - } as T - delete config.cacheDirectory - delete config.name - return config - } - - @Memoize(jestRootDir) - babelJestFor( - jestConfig: jest.ProjectConfig, - ): BabelJestTransformer | undefined { - const babelJestConfig = this.babelJestConfigFor(jestConfig) - if (!babelJestConfig) { - return - } - return importer - .babelJest(ImportReasons.babelJest) - .createTransformer(babelJestConfig) as BabelJestTransformer - } - - @Memoize(jestRootDir) - babelJestConfigFor(jestConfig: jest.ProjectConfig): BabelConfig | undefined { - const config = this.configFor(jestConfig) - const rootDir = jestRootDir(jestConfig) - if (!config.babelJest) { - return - } - - let babelConfig!: BabelConfig - if (typeof config.babelJest === 'string') { - // path to a babelrc file - let filePath = config.babelJest.replace('', `${rootDir}${sep}`) - filePath = resolve(rootDir, filePath) - babelConfig = requireJsOrJson(filePath) - } else { - // it's already an object with the config - babelConfig = config.babelJest - } - - return babelConfig - } - - @Memoize(jestRootDir) - configFor(jestConfig: jest.ProjectConfig): TsJestConfig { - const parsedConfig = backportJestConfig(jestConfig) - const { globals = {} } = parsedConfig as any - const options: TsJestGlobalOptions = { ...globals['ts-jest'] } - - // stringifyContentPathRegex option - let { stringifyContentPathRegex: stringifyRegEx } = options - if (typeof stringifyRegEx === 'string') { - try { - stringifyRegEx = RegExp(stringifyRegEx) - } catch (err) { - err.message = `${Errors.InvalidStringifyContentPathRegex}\n${ - err.message - }` - } - } - if (stringifyRegEx) { - if (!(stringifyRegEx instanceof RegExp)) { - throw new TypeError(Errors.InvalidStringifyContentPathRegex) - } - } else { - stringifyRegEx = undefined - } - - // babelJest true => {} - if (options.babelJest === true) { - options.babelJest = {} - } - - // parsed options - return { - tsConfig: options.tsConfig || undefined, - babelJest: options.babelJest || undefined, - diagnostics: normalizeDiagnosticTypes(options.diagnostics), - stringifyContentPathRegex: stringifyRegEx as RegExp | undefined, - } - } - - @Memoize(jestRootDir) - programFor(jestConfig: jest.ProjectConfig): TsProgram { - const myConfig = this.configFor(jestConfig) - return new TsProgram(jestRootDir(jestConfig), myConfig) - } - - process( - source: string, - filePath: jest.Path, - jestConfig: jest.ProjectConfig, - transformOptions?: jest.TransformOptions, - ): jest.TransformedSource | string { - let result: string | jest.TransformedSource - const config = this.configFor(jestConfig) - - const stringify = - config.stringifyContentPathRegex && - config.stringifyContentPathRegex.test(filePath) - const babelJest = !stringify && this.babelJestFor(jestConfig) - - // get the tranformer instance - const program = this.programFor(jestConfig) - const instrument: boolean = - !!transformOptions && transformOptions.instrument - - // handles here what we should simply stringify - if (stringify) { - source = `module.exports=${JSON.stringify(source)}` - } - - // transpile TS code (source maps are included) - result = program.transpileModule(filePath, source, instrument) - - // calling babel-jest transformer - if (babelJest) { - result = babelJest.process(result, filePath, jestConfig, transformOptions) - } - - // allows hooks (usefull for testing) - if (this.hooks.afterProcess) { - const newResult = this.hooks.afterProcess([...arguments], result) - if (newResult !== undefined) { - return newResult - } - } - - return result - } - - // we can cache as for same instance the cache key won't change as soon as the path/content pair - // doesn't change - // TODO: find out if jest is already using this cache strategy and remove it if so - @Memoize((data: string, path: string) => `${path}::${data}`) - getCacheKey( - fileContent: string, - filePath: string, - jestConfigStr: string, - transformOptions: { instrument?: boolean; rootDir?: string } = {}, - ): string { - // tslint:disable-next-line:prefer-const - let { instrument = false, rootDir } = transformOptions - const CHAR0 = '\0' - // will be used as the hashing data source - const hashData: string[] = [] - const hashUpdate = (data: string) => hashData.push(data, CHAR0) - - // add file path and its content - hashUpdate(filePath) - hashUpdate(fileContent) - - // saniize and normalize jest config - const jestConfig: jest.ProjectConfig = JSON.parse(jestConfigStr) - jestConfig.rootDir = rootDir = jestRootDir({ - rootDir: rootDir || jestConfig.rootDir, - }) - const sanitizedJestConfig: jest.ProjectConfig = this.sanitizedJestConfigFor( - jestConfig, - ) - - // add jest config - hashUpdate(JSON.stringify(sanitizedJestConfig)) - // add project's package.json - const projectPkg = closestPatckageJson(rootDir, true) - hashUpdate(projectPkg) - // if using babel jest, adds its cacheKey as well - const babelJest = this.babelJestFor(jestConfig) - if (babelJest) { - hashUpdate( - babelJest.getCacheKey( - fileContent, - filePath, - jestConfigStr, - transformOptions as any, - ), - ) - } - // add tsconfig - const tsConfig = this.programFor(sanitizedJestConfig).parsedConfig - hashUpdate(JSON.stringify(tsConfig)) - // add instrument, even if we don't use it since `canInstrument` is false - hashUpdate(`instrument:${instrument ? 'on' : 'off'}`) - - return sha1(...hashData) - } - - // we let jest doing the instrumentation, it does it well - // get canInstrument() {} -} diff --git a/src/ts-program.spec.ts b/src/ts-program.spec.ts deleted file mode 100644 index 9bdb049e4f..0000000000 --- a/src/ts-program.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import TsProgram from './ts-program' -import { resolve } from 'path' -import * as fakers from './__helpers__/fakers' - -const path = fakers.filePath('path/to/file.ts') -const content = fakers.typescriptSource() - -describe('hoisting', () => { - describe('without babel', () => { - const prog = new TsProgram(resolve(__dirname, '..'), fakers.tsJestConfig()) - - it('should hoist jest.mock() calls', () => { - const result = prog.transpileModule(path, content, undefined, { - inlineSourceMap: false, - }) - expect(result).toMatchSnapshot() - }) - }) - - describe('with babel', () => { - const prog = new TsProgram( - resolve(__dirname, '..'), - fakers.tsJestConfig({ babelJest: {} }), - ) - - it('should not hoist jest.mock() calls', () => { - const result = prog.transpileModule(path, content, undefined, { - inlineSourceMap: false, - }) - expect(result).toMatchSnapshot() - }) - }) -}) diff --git a/src/ts-program.ts b/src/ts-program.ts deleted file mode 100644 index e0214b8404..0000000000 --- a/src/ts-program.ts +++ /dev/null @@ -1,162 +0,0 @@ -// tslint:disable:member-ordering -import { TsJestConfig, TsJestProgram } from './types' -import { sep, resolve, dirname, basename } from 'path' -import { existsSync, readFileSync } from 'fs' -import Memoize from './utils/memoize' -import hoisting from './transformers/hoisting' -import { interpolate, Errors, ImportReasons } from './utils/messages' -import importer from './utils/importer' -// take care of including ONLY TYPES here, for the rest use ts -import { - CompilerOptions, - ParsedCommandLine, - ParseConfigHost, - CustomTransformers, - TransformerFactory, - SourceFile, - TranspileOptions, - Diagnostic, -} from 'typescript' - -const ts = importer.typeScript(ImportReasons.tsJest) -const { sys } = ts - -export const compilerOptionsOverrides: Readonly = { - // ts-jest - module: ts.ModuleKind.CommonJS, - esModuleInterop: true, - inlineSources: false, - sourceMap: false, - inlineSourceMap: true, -} - -export default class TsProgram implements TsJestProgram { - constructor(readonly rootDir: string, readonly tsJestConfig: TsJestConfig) {} - - @Memoize() - get configFile(): string | null { - const given = this.tsJestConfig.tsConfig - let resolved: string | undefined - if (typeof given === 'string') { - // we got a path to a custom (or not) tsconfig - resolved = given.replace('', `${this.rootDir}${sep}`) - resolved = resolve(this.rootDir, resolved) - if (!existsSync(resolved)) { - resolved = undefined - } - } else if (typeof given === 'undefined') { - // we got undefined, go look for the default file - resolved = ts.findConfigFile( - this.rootDir, - sys.fileExists, - 'tsconfig.json', - ) - } else { - // what we got was compiler options - return null - } - // could we find one? - if (!resolved) { - throw new Error( - interpolate(Errors.UnableToFindTsConfig, { given, root: this.rootDir }), - ) - } - return resolved - } - - @Memoize() - get originalCompilerOptions() { - return { ...this.parsedConfig.options } as CompilerOptions - } - - @Memoize() - get overriddenCompilerOptions() { - return { - ...this.originalCompilerOptions, - ...compilerOptionsOverrides, - } as CompilerOptions - } - - @Memoize() - get parsedConfig(): ParsedCommandLine { - const { configFile } = this - const { config, error } = configFile - ? ts.readConfigFile(configFile, sys.readFile) - : { - config: { compilerOptions: this.tsJestConfig.tsConfig }, - error: undefined, - } - if (error) throw error // tslint:disable-line:curly - - const parseConfigHost: ParseConfigHost = { - fileExists: existsSync, - readDirectory: sys.readDirectory, - readFile: file => readFileSync(file, 'utf8'), - useCaseSensitiveFileNames: true, - } - - const result = ts.parseJsonConfigFileContent( - config, - parseConfigHost, - configFile ? dirname(configFile) : this.rootDir, - undefined, - configFile ? basename(configFile) : undefined, - ) - - // will throw if at least one error - this.reportDiagnostic(...result.errors) - - return result - } - - @Memoize() - get transformers(): CustomTransformers { - // https://dev.doctorevidence.com/how-to-write-a-typescript-transform-plugin-fc5308fdd943 - const before: Array> = [] - const after: Array> = [] - - // no babel-jest, we need to handle the hoisting - if (!this.tsJestConfig.babelJest) { - before.push(hoisting(this)) - } - - return { - before, - after, - } - } - - transpileModule( - path: string, - content: string, - instrument: boolean = false, - extraCompilerOptions?: CompilerOptions, - ): string { - const options: TranspileOptions = { - fileName: path, - reportDiagnostics: false, - transformers: this.transformers, - compilerOptions: { - ...this.overriddenCompilerOptions, - ...extraCompilerOptions, - }, - } - const { diagnostics, outputText } = ts.transpileModule(content, options) - - this.reportDiagnostic(...diagnostics) - - // outputText will contain inline sourmaps - return outputText - } - - reportDiagnostic(...diagnostics: Diagnostic[]) { - const diagnostic = diagnostics[0] - if (!diagnostic) return // tslint:disable-line:curly - - const message = ts.flattenDiagnosticMessageText( - diagnostic.messageText, - sys.newLine, - ) - throw new Error(`${diagnostic.code}: ${message}`) - } -} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index bcfd4336e7..0000000000 --- a/src/types.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as _ts from 'typescript' -import _closestFileData from 'closest-file-data' -import * as _babel from 'babel__core' -import { IPackageJSON } from 'gist-package-json' -import { ImportReasons } from './utils/messages' - -export type TBabelCore = typeof _babel -export type TTypeScript = typeof _ts -export type TPackageJson = IPackageJSON -export type TClosestFileData = typeof _closestFileData -export type TBabelJest = Required -export type BabelJestTransformer = { - [K in Exclude]: Exclude< - jest.Transformer[K], - undefined - > -} - -// CAUTION: use same key-value pair allow us to not store a list of values somewhere -export enum DiagnosticTypes { - syntactic = 'syntactic', - options = 'options', - global = 'global', - semantic = 'sementic', -} -export type DiagnosticFilter = DiagnosticTypes[] | DiagnosticTypes -export type TsTransformerFactory = - | _ts.TransformerFactory<_ts.SourceFile> - | string - -// FIXME: find the right typing for this -export type BabelConfig = _babel.TransformOptions - -export interface TsJestGlobalOptions { - // either a path to a tsconfig json file, or inline compiler options - tsConfig?: string | _ts.CompilerOptions - - // what kind of diagnostics to report - diagnostics?: DiagnosticFilter | boolean | undefined - - // whether to use babel jest under the hood or not - // it can be: - // - a path to a babelrc ( can be used) - // - a babel config object - // - a boolean to enable/disable the use of babel-jest - babelJest?: boolean | BabelConfig | string - - // should this be kept in here? it has nothing to do with TS after all... - // kept for backward compatibility to handle __TRANSFORM_HTML__ - stringifyContentPathRegex?: string | RegExp -} - -export interface TsJestConfig { - tsConfig: _ts.CompilerOptions | string | undefined - babelJest: BabelConfig | string | undefined - diagnostics: DiagnosticTypes[] - - // to deprecate / deprecated === === === - stringifyContentPathRegex: RegExp | undefined -} - -export interface TsJestProgram { - readonly parsedConfig: _ts.ParsedCommandLine - transpileModule( - path: string, - content: string, - instrument?: boolean, - extraCompilerOptions?: _ts.CompilerOptions, - ): string -} - -export interface TsJestHooksMap { - afterProcess?( - args: any[], - result: string | jest.TransformedSource, - ): string | jest.TransformedSource | void -} - -export interface CreateJestPresetOptions { - allowJs?: boolean -} - -export type ModulePatcher = (module: T) => T - -export interface TsJestImporter { - tryThese(moduleName: string, ...fallbacks: string[]): any -} diff --git a/src/utils/__mocks__/backports.ts b/src/utils/__mocks__/backports.ts deleted file mode 100644 index bb63564016..0000000000 --- a/src/utils/__mocks__/backports.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as _backports from '../backports' - -export const backportJestConfig: typeof _backports['backportJestConfig'] = val => ({ - ...(val as any), -}) diff --git a/src/utils/__mocks__/closest-package-json.ts b/src/utils/__mocks__/closest-package-json.ts deleted file mode 100644 index a7d3aeb351..0000000000 --- a/src/utils/__mocks__/closest-package-json.ts +++ /dev/null @@ -1,17 +0,0 @@ -import _closestPackageJson from '../closest-package-json' -import { TPackageJson } from '../../types' - -// tslint:disable-next-line:variable-name -export const __byPath: Record = {} -// tslint:disable-next-line:variable-name -export const __default: TPackageJson = { name: 'mock' } - -const closestPackageJson: typeof _closestPackageJson = ( - fromPath: string, - asString: boolean = false, -) => { - const res = fromPath in __byPath ? __byPath[fromPath] : __default - return (asString ? JSON.stringify(res) : res) as any -} - -export default closestPackageJson diff --git a/src/utils/__snapshots__/backports.spec.ts.snap b/src/utils/__snapshots__/backports.spec.ts.snap deleted file mode 100644 index 09ed3c64cc..0000000000 --- a/src/utils/__snapshots__/backports.spec.ts.snap +++ /dev/null @@ -1,203 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`backportJestConfig with "globals.__TRANSFORM_HTML__" set to false should have changed the config correctly: before 1`] = ` -Object { - "globals": Object { - "__TRANSFORM_HTML__": false, - }, -} -`; - -exports[`backportJestConfig with "globals.__TRANSFORM_HTML__" set to false should have changed the config correctly: migrated 1`] = ` -Object { - "globals": Object { - "ts-jest": Object {}, - }, -} -`; - -exports[`backportJestConfig with "globals.__TRANSFORM_HTML__" set to false should wran the user 1`] = `"\\"[jest-config].globals.__TRANSFORM_HTML__\\" is deprecated, use \\"[jest-config].globals.ts-jest.stringifyContentPathRegex\\" instead."`; - -exports[`backportJestConfig with "globals.__TRANSFORM_HTML__" set to true should have changed the config correctly: before 1`] = ` -Object { - "globals": Object { - "__TRANSFORM_HTML__": true, - }, -} -`; - -exports[`backportJestConfig with "globals.__TRANSFORM_HTML__" set to true should have changed the config correctly: migrated 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "stringifyContentPathRegex": "\\\\.html?$", - }, - }, -} -`; - -exports[`backportJestConfig with "globals.__TRANSFORM_HTML__" set to true should wran the user 1`] = `"\\"[jest-config].globals.__TRANSFORM_HTML__\\" is deprecated, use \\"[jest-config].globals.ts-jest.stringifyContentPathRegex\\" instead."`; - -exports[`backportJestConfig with "globals.__TS_CONFIG__" set to { foo: 'bar' } should have changed the config correctly: before 1`] = ` -Object { - "globals": Object { - "__TS_CONFIG__": Object { - "foo": "bar", - }, - }, -} -`; - -exports[`backportJestConfig with "globals.__TS_CONFIG__" set to { foo: 'bar' } should have changed the config correctly: migrated 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "tsConfig": Object { - "foo": "bar", - }, - }, - }, -} -`; - -exports[`backportJestConfig with "globals.__TS_CONFIG__" set to { foo: 'bar' } should wran the user 1`] = `"\\"[jest-config].globals.__TS_CONFIG__\\" is deprecated, use \\"[jest-config].globals.ts-jest.tsConfig\\" instead."`; - -exports[`backportJestConfig with "globals.ts-jest.babelConfig" set to { foo: 'bar' } should have changed the config correctly: before 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "babelConfig": Object { - "foo": "bar", - }, - }, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.babelConfig" set to { foo: 'bar' } should have changed the config correctly: migrated 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "babelJest": Object { - "foo": "bar", - }, - }, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.babelConfig" set to { foo: 'bar' } should wran the user 1`] = `"\\"[jest-config].globals.ts-jest.babelConfig\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelJest\\" instead."`; - -exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to false should have changed the config correctly: before 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "skipBabel": false, - }, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to false should have changed the config correctly: migrated 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "babelJest": true, - }, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to false should wran the user 1`] = `"\\"[jest-config].globals.ts-jest.skipBabel\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelJest\\" instead."`; - -exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to true should have changed the config correctly: before 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "skipBabel": true, - }, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to true should have changed the config correctly: migrated 1`] = ` -Object { - "globals": Object { - "ts-jest": Object {}, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to true should wran the user 1`] = `"\\"[jest-config].globals.ts-jest.skipBabel\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelJest\\" instead."`; - -exports[`backportJestConfig with "globals.ts-jest.tsConfigFile" set to 'tsconfig.build.json' should have changed the config correctly: before 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "tsConfigFile": "tsconfig.build.json", - }, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.tsConfigFile" set to 'tsconfig.build.json' should have changed the config correctly: migrated 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "tsConfig": "tsconfig.build.json", - }, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.tsConfigFile" set to 'tsconfig.build.json' should wran the user 1`] = `"\\"[jest-config].globals.ts-jest.tsConfigFile\\" is deprecated, use \\"[jest-config].globals.ts-jest.tsConfig\\" instead."`; - -exports[`backportJestConfig with "globals.ts-jest.useBabelrc" set to false should have changed the config correctly: before 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "useBabelrc": false, - }, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.useBabelrc" set to false should have changed the config correctly: migrated 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "babelJest": Object {}, - }, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.useBabelrc" set to false should wran the user 1`] = ` -"\\"[jest-config].globals.ts-jest.useBabelrc\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelJest\\" instead. - ↳ See \`babel-jest\` related issue: https://github.com/facebook/jest/issues/3845" -`; - -exports[`backportJestConfig with "globals.ts-jest.useBabelrc" set to true should have changed the config correctly: before 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "useBabelrc": true, - }, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.useBabelrc" set to true should have changed the config correctly: migrated 1`] = ` -Object { - "globals": Object { - "ts-jest": Object { - "babelJest": true, - }, - }, -} -`; - -exports[`backportJestConfig with "globals.ts-jest.useBabelrc" set to true should wran the user 1`] = ` -"\\"[jest-config].globals.ts-jest.useBabelrc\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelJest\\" instead. - ↳ See \`babel-jest\` related issue: https://github.com/facebook/jest/issues/3845" -`; diff --git a/src/utils/backports.spec.ts b/src/utils/backports.spec.ts deleted file mode 100644 index 829ab5ec78..0000000000 --- a/src/utils/backports.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { backportJestConfig } from './backports' -import spyThese from '../__helpers__/spy-these' -import set from 'lodash.set' -import { inspect } from 'util' - -const consoleSpies = spyThese(console, { - warn: () => undefined, -}) -afterEach(() => { - consoleSpies.mockReset() -}) - -describe('backportJestConfig', () => { - const makeTestsFor = (oldPath: string, newPath: string, values: any[]) => { - values.forEach(val => { - let original: any - beforeEach(() => { - original = {} - set(original, oldPath, val) - }) - describe(`with "${oldPath}" set to ${inspect(val)}`, () => { - it(`should wran the user`, () => { - backportJestConfig(original) - expect(consoleSpies.warn).toHaveBeenCalledTimes(1) - expect(consoleSpies.warn.mock.calls[0].join(' ')).toMatchSnapshot() - }) // should warn the user - it(`should have changed the config correctly`, () => { - expect(original).toMatchSnapshot('before') - expect(backportJestConfig(original)).toMatchSnapshot('migrated') - }) // should have changed the config - }) // with xxx set to yyy - }) // for - } // makeTestsFor - - makeTestsFor('globals.__TS_CONFIG__', 'globals.ts-jest.tsConfig', [ - { foo: 'bar' }, - ]) - - makeTestsFor( - 'globals.__TRANSFORM_HTML__', - 'globals.ts-jest.stringifyContentPathRegex', - [true, false], - ) - - makeTestsFor('globals.ts-jest.tsConfigFile', 'globals.ts-jest.tsConfig', [ - 'tsconfig.build.json', - ]) - - makeTestsFor('globals.ts-jest.useBabelrc', 'globals.ts-jest.babelJest', [ - true, - false, - ]) - - makeTestsFor('globals.ts-jest.babelConfig', 'globals.ts-jest.babelJest', [ - { foo: 'bar' }, - ]) - - makeTestsFor('globals.ts-jest.skipBabel', 'globals.ts-jest.babelJest', [ - true, - false, - ]) -}) diff --git a/src/utils/closest-package-json.ts b/src/utils/closest-package-json.ts deleted file mode 100644 index 549d652c11..0000000000 --- a/src/utils/closest-package-json.ts +++ /dev/null @@ -1,68 +0,0 @@ -// we could have used `closest-file-data` but since it's more simple than finding babel -// config, if the user doesn't need babel processing he won't need to add closest-file-data -// to its dependencies -import { resolve, join } from 'path' -import { TPackageJson } from '../types' -import { existsSync, readFileSync } from 'fs' -import { Errors, interpolate } from './messages' - -interface CacheItem { - data: Readonly - _data?: Readonly - str: string -} -export let cache: { - [file: string]: CacheItem -} = Object.create(null) - -export default function ownerPackageData( - fromPath: string, - asString?: false, -): TPackageJson -export default function ownerPackageData( - fromPath: string, - asString: true, -): string -export default function ownerPackageData( - fromPath: string, - asString: boolean = false, -): TPackageJson | string { - if (fromPath in cache) { - return cache[fromPath][asString ? 'str' : 'data'] - } - - let path: string = fromPath - let oldPath: string - let packagePath: string | undefined - do { - oldPath = path - packagePath = join(path, 'package.json') - if (existsSync(packagePath)) { - break - } - } while ( - // tslint:disable-next-line:no-conditional-assignment - (path = resolve(path, '..')) !== oldPath || - // allows to reset the packagePath when exiting the loop - // tslint:disable-next-line:no-conditional-assignment - (packagePath = undefined) - ) - - // fail if not found - if (!packagePath) { - throw new Error(interpolate(Errors.UnableToFindPackageJson, { fromPath })) - } - - const str = readFileSync(packagePath, 'utf8') - const cached: CacheItem = Object.freeze({ - str, - get data() { - return ( - cached._data || - (cached._data = Object.freeze({ ...JSON.parse(cached.str) })) - ) - }, - }) - cache[fromPath] = cached - return asString ? str : cached.data -} diff --git a/src/utils/diagnostics.ts b/src/utils/diagnostics.ts deleted file mode 100644 index 3711a81378..0000000000 --- a/src/utils/diagnostics.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { DiagnosticTypes } from '../types' -import { interpolate, Errors } from './messages' -import { inspect } from 'util' - -export const isDiagnosticType = (val: any): val is DiagnosticTypes => { - return val && DiagnosticTypes[val] === val -} - -export const normalizeDiagnosticTypes = ( - val?: DiagnosticTypes[] | DiagnosticTypes | boolean, -): DiagnosticTypes[] => { - let res!: DiagnosticTypes[] - if (typeof val === 'string') { - // string - if (isDiagnosticType(val)) { - res = [val] - } - } else if (Array.isArray(val)) { - // array - if (val.every(isDiagnosticType)) { - res = val - } - } else if (!val) { - // undeifned or false - res = [] - } else if (val) { - // true - res = [ - DiagnosticTypes.global, - DiagnosticTypes.options, - DiagnosticTypes.semantic, - DiagnosticTypes.syntactic, - ] - } - if (!res) { - throw new TypeError( - interpolate(Errors.InvalidDiagnosticsOption, { value: inspect(val) }), - ) - } - // ensure we have another instance of array with unique items - return res.filter((item, index) => res.indexOf(item) === index) -} diff --git a/src/utils/hacks.spec.ts b/src/utils/hacks.spec.ts deleted file mode 100644 index 80cc29b56e..0000000000 --- a/src/utils/hacks.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as hacks from './hacks' -import { File } from 'babel-core/lib/transformation/file' - -jest.mock('babel-core/lib/transformation/file', () => ({ - File: class { - initOptions(options: any) { - const opt = { ...options } - // this is the buggy part overwriting the sourceMaps option that the patch should fix - if (opt.inputSourceMap) { - opt.sourceMaps = true - } - return opt - } - }, -})) - -describe('patchBabelCore_githubIssue6577', () => { - const INPUT = 'foo:bar' - const initOptions = ({ - version = '6.0.0', - sourceMaps, - }: { version?: any; sourceMaps?: any } = {}) => { - hacks.patchBabelCore_githubIssue6577({ version } as any) - return new File().initOptions({ sourceMaps, inputSourceMap: true }) - } - - it('should not reset it if version of babel is not 6', () => { - expect( - initOptions({ version: null, sourceMaps: INPUT }).sourceMaps, - ).not.toBe(INPUT) - expect( - initOptions({ version: '7.1.0', sourceMaps: INPUT }).sourceMaps, - ).not.toBe(INPUT) - }) - - it('should not reset it if option is falsy', () => { - expect(initOptions({ sourceMaps: false }).sourceMaps).not.toBe(false) - expect(initOptions({ sourceMaps: undefined }).sourceMaps).not.toBe( - undefined, - ) - expect(initOptions({ sourceMaps: null }).sourceMaps).not.toBe(null) - }) - it('should reset to input value if truthy', () => { - expect( - initOptions({ version: '6.9.4-dummy0', sourceMaps: INPUT }).sourceMaps, - ).toBe(INPUT) - expect(initOptions({ sourceMaps: INPUT }).sourceMaps).toBe(INPUT) - expect(initOptions({ sourceMaps: 'dummy' }).sourceMaps).toBe('dummy') - }) -}) diff --git a/src/utils/jest-root-dir.ts b/src/utils/jest-root-dir.ts deleted file mode 100644 index 55b92883d2..0000000000 --- a/src/utils/jest-root-dir.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { resolve } from 'path' - -export default function jestRootDir( - jestConfig: jest.ProjectConfig | jest.InitialOptions, -): string { - return resolve(process.cwd(), jestConfig.rootDir || '.') -} diff --git a/src/utils/parse-json-unsafe.ts b/src/utils/parse-json-unsafe.ts deleted file mode 100644 index 2e592f6ca8..0000000000 --- a/src/utils/parse-json-unsafe.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function parseJsonUnsafe(input: string): T { - return Function('input', `return ${input}\n;`)(input) -} diff --git a/src/utils/require-js-or-json.spec.ts b/src/utils/require-js-or-json.spec.ts deleted file mode 100644 index d1692d4d8e..0000000000 --- a/src/utils/require-js-or-json.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import requireJsOrJson from './require-js-or-json' -import parseJsonUnsafe from './parse-json-unsafe' -import { readFileSync } from 'fs' - -const NON_REQUIRABLE_FILE = '/there/foo.bar' - -jest.mock('fs', () => ({ - readFileSync: jest.fn((path: string) => ({ path })), -})) -jest.mock('./parse-json-unsafe', () => jest.fn(v => v)) -jest.mock('/foo/bar.js', () => ({ path: '/foo/bar.js' }), { virtual: true }) - -describe('require-able', () => { - it('should not call parseJsonUnsafe nor readFileSync', () => { - const v = requireJsOrJson('/foo/bar.js') - expect(v).toEqual({ path: '/foo/bar.js' }) - expect(parseJsonUnsafe).not.toHaveBeenCalled() - expect(readFileSync).not.toHaveBeenCalled() - }) -}) - -describe('not require-able', () => { - it('should fallback to readFileSync + parseJsonUnsafe', () => { - const v = requireJsOrJson(NON_REQUIRABLE_FILE) - expect(v).toEqual({ path: NON_REQUIRABLE_FILE }) - expect(parseJsonUnsafe).toHaveBeenCalledTimes(1) - expect(readFileSync).toHaveBeenCalledTimes(1) - }) -}) diff --git a/src/utils/require-js-or-json.ts b/src/utils/require-js-or-json.ts deleted file mode 100644 index 76af379c5c..0000000000 --- a/src/utils/require-js-or-json.ts +++ /dev/null @@ -1,12 +0,0 @@ -import parseJsonUnsafe from './parse-json-unsafe' -import { readFileSync } from 'fs' - -export default function requireJsOrJson(filePath: string): any { - let res: any - try { - res = require(filePath) - } catch (err) { - res = parseJsonUnsafe(readFileSync(filePath, 'utf8')) - } - return res -} diff --git a/tsconfig.build.json b/tsconfig.build.json index 31477963df..2d8997cfe5 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "sourceMap": true, + "inlineSources": true, "inlineSourceMap": false, "removeComments": true, "outDir": "dist", diff --git a/tsconfig.json b/tsconfig.json index 60f454ad01..57dd73be83 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "moduleResolution": "node", "noEmitOnError": true, "noFallthroughCasesInSwitch": true, - "noImplicitAny": false, + "noImplicitAny": true, "noImplicitReturns": true, "removeComments": true, "strict": true, diff --git a/tslint.json b/tslint.json index 063b7b2d5a..46d5a48a79 100644 --- a/tslint.json +++ b/tslint.json @@ -4,6 +4,8 @@ "tslint:recommended" ], "rules": { + "no-shadowed-variable": false, + "curly": [true, "ignore-same-line"], "variable-name": [ true, "ban-keywords",