Skip to content

Instantly share code, notes, and snippets.

@ngbrown
Last active July 14, 2017 16:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ngbrown/8fe7a6aee6ade448d11ba729ff29872c to your computer and use it in GitHub Desktop.
Save ngbrown/8fe7a6aee6ade448d11ba729ff29872c to your computer and use it in GitHub Desktop.
Transformer helper for Jest to take Typescript through Babel, with sourcemap support.
"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