Last active
July 14, 2017 16:09
-
-
Save ngbrown/8fe7a6aee6ade448d11ba729ff29872c to your computer and use it in GitHub Desktop.
Transformer helper for Jest to take Typescript through Babel, with sourcemap support.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
/** | |
* | |
* This source code is licensed under the BSD-style license. | |
* Portions of code derived from "babel-jest", copyrighted by | |
* Facebook, Inc. and released under a BSD-style license. | |
* | |
*/ | |
const crypto = require('crypto'); | |
const fs = require('fs'); | |
const path = require('path'); | |
const jestPreset = require('babel-preset-jest'); | |
const BABELRC_FILENAME = '.babelrc'; | |
const BABELRC_JS_FILENAME = '.babelrc.js'; | |
const BABEL_CONFIG_KEY = 'babel'; | |
const PACKAGE_JSON = 'package.json'; | |
const TSCONFIG_FILENAME = 'tsconfig.json'; | |
const THIS_FILE = fs.readFileSync(__filename); | |
let babel; | |
let tsc; | |
const createTransformer = options => { | |
const tsConfigCache = Object.create(null); | |
const babelRcCache = Object.create(null); | |
const getTsConfig = filename => { | |
const paths = []; | |
let directory = filename; | |
while (directory !== (directory = path.dirname(directory))) { | |
if (tsConfigCache[directory]) { | |
break; | |
} | |
paths.push(directory); | |
const configFilePath = path.join(directory, TSCONFIG_FILENAME); | |
if (fs.existsSync(configFilePath)) { | |
tsConfigCache[directory] = {directory, json: fs.readFileSync(configFilePath, 'utf8')}; | |
break; | |
} | |
} | |
paths.forEach(directoryPath => tsConfigCache[directoryPath] = tsConfigCache[directory]); | |
return tsConfigCache[directory] || {directory: process.cwd(), json: "{}"}; | |
}; | |
const getBabelRC = filename => { | |
const paths = []; | |
let directory = filename; | |
while (directory !== (directory = path.dirname(directory))) { | |
if (babelRcCache[directory]) { | |
break; | |
} | |
paths.push(directory); | |
const configFilePath = path.join(directory, BABELRC_FILENAME); | |
if (fs.existsSync(configFilePath)) { | |
babelRcCache[directory] = fs.readFileSync(configFilePath, 'utf8'); | |
break; | |
} | |
const configJsFilePath = path.join(directory, BABELRC_JS_FILENAME); | |
if (fs.existsSync(configJsFilePath)) { | |
babelRcCache[directory] = JSON.stringify(require(configJsFilePath)); | |
break; | |
} | |
const packageJsonFilePath = path.join(directory, PACKAGE_JSON); | |
if (fs.existsSync(packageJsonFilePath)) { | |
const packageJsonFileContents = require(packageJsonFilePath); | |
if (packageJsonFileContents[BABEL_CONFIG_KEY]) { | |
babelRcCache[directory] = JSON.stringify( | |
packageJsonFileContents[BABEL_CONFIG_KEY]); | |
break; | |
} | |
} | |
} | |
paths.forEach(directoryPath => babelRcCache[directoryPath] = babelRcCache[directory]); | |
return babelRcCache[directory] || ''; | |
}; | |
options = Object.assign({}, options, { | |
plugins: options && options.plugins || [], | |
presets: (options && options.presets || []).concat([jestPreset]), | |
retainLines: true }); | |
delete options.cacheDirectory; | |
delete options.filename; | |
const processBabel = ( | |
src, | |
filename, | |
config, | |
transformOptions, | |
babelOptions) => { | |
if (!babel) { | |
babel = require('babel-core'); | |
} | |
const theseOptions = Object.assign({filename}, options, babelOptions); | |
if (transformOptions && transformOptions.instrument) { | |
theseOptions.auxiliaryCommentBefore = ' istanbul ignore next '; | |
theseOptions.plugins = theseOptions.plugins.concat([ | |
[ | |
require('babel-plugin-istanbul').default, | |
{ | |
// files outside `cwd` will not be instrumented | |
cwd: config.rootDir, | |
exclude: [] | |
} | |
] | |
]); | |
} | |
const {code, map, ast} = babel.transform(src, theseOptions); | |
return code; | |
}; | |
return { | |
canInstrument: true, | |
getCacheKey( | |
fileData, | |
filename, | |
configString, _ref) { | |
return crypto.createHash("md5") | |
.update(THIS_FILE) | |
.update("\0", "utf8") | |
.update(getTsConfig(filename).json) | |
.update("\0", "utf8") | |
.update(fileData) | |
.update('\0', 'utf8') | |
.update(configString) | |
.update('\0', 'utf8') | |
.update(getBabelRC(filename)) | |
.update('\0', 'utf8') | |
.update(_ref.instrument ? 'instrument' : '') | |
.digest("hex"); | |
}, | |
process(src, filename, config, transformOptions) { | |
const isTs = filename.endsWith('.ts'); | |
const isTsx = filename.endsWith('.tsx'); | |
let babelOptions = {}; | |
if (isTs || isTsx) { | |
if (!tsc) { | |
tsc = require('typescript'); | |
} | |
const tsConfig = getTsConfig(filename); | |
const { config, error } = tsc.parseConfigFileTextToJson(TSCONFIG_FILENAME, tsConfig.json); | |
if (error) { | |
throw new Error(error); | |
} | |
const settings = tsc.convertCompilerOptionsFromJson(config["compilerOptions"], tsConfig.directory); | |
if (!settings.options) { | |
throw new Error(settings.errors) | |
} | |
const compilerOptionsOverride = { | |
moduleResolution: tsc.ModuleResolutionKind.NodeJs, | |
}; | |
const compilerOptions = Object.assign({}, settings.options, compilerOptionsOverride); | |
src = tsc.transpileModule(src, { compilerOptions, fileName: filename } ); | |
babelOptions.inputSourceMap = JSON.parse(src.sourceMapText); | |
src = src.outputText; | |
} | |
if (isTs || isTsx || filename.endsWith('.js') || filename.endsWith('.jsx')) { | |
src = processBabel(src, filename, config, transformOptions, babelOptions); | |
} | |
return src; | |
}, | |
} | |
}; | |
module.exports = createTransformer(); | |
module.exports.createTransformer = createTransformer; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment