Skip to content

Instantly share code, notes, and snippets.

@a-ignatov-parc
Last active June 29, 2017 07:26
Show Gist options
  • Save a-ignatov-parc/2804f7f20a9e328e6719ae621d5cefb3 to your computer and use it in GitHub Desktop.
Save a-ignatov-parc/2804f7f20a9e328e6719ae621d5cefb3 to your computer and use it in GitHub Desktop.
Jest integration with webpack
// jest/module-transformer.js
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { execSync } = require('child_process');
const hashFiles = require('hash-files');
const { resolveFromRoot } = require('../utils/webpack');
const resolveDependencies = require('./resolve-dependencies');
const webpackPath = require.resolve('webpack/bin/webpack');
const webpackConfigPath = require.resolve('./webpack.config');
const rootPath = resolveFromRoot('.');
/**
* Чтоб небыло рейс-кондишена на запись файлов при сборке модулей из разных процессов
* делаем для каждого процесса уникальную директорию для артефактов на основе его pid.
*/
const outputPath = resolveFromRoot('./tmp/jest-webpack-artifacts/' + process.pid);
const entityRegex = /\.spec\.js$/;
/**
* Резолвим все зависимости вебпаковского конфига чтоб можно было посчитать хеш от всего
* что может повлиять на результат сборки модуля.
*/
const webpackFiles = resolveDependencies(webpackConfigPath);
const webpackSignature = hashFiles.sync({
files: webpackFiles,
algorithm: 'md5',
noGlob: true,
});
module.exports = {
process(source, filepath) {
const entity = path.relative(rootPath, filepath);
const entityName = entity.replace(entityRegex, '');
/**
* Так как jest может резолвить зависимости только синхронно (https://github.com/facebook/jest/issues/2711),
* то приходится прибегнуть к трюку с запуском сборки асинхронного вебпака
* с помощью `execSync` процесса ноды и последующим считыванием артефакта сборки.
*/
try {
execSync(`NODE_ENV=test ${webpackPath} --config ${webpackConfigPath} --output-path ${outputPath} ${entityName}=${filepath}`, {
/**
* - stdin (stdio[0]) родителя пайпим в stdin потомка.
* - stdout (stdio[1]) потомка никуда не выводим. Нет смысла выводить
* логирование вебпака в основную консоль, только лишний мусор.
* - stderr (stdio[2]) потомка пайпим в stderr родителя.
*
* Подробнее тут: https://nodejs.org/api/child_process.html#child_process_options_stdio
*/
stdio: ['pipe', 'ignore', 'pipe'],
});
} catch (error) {
/**
* С ошибкой в данном месте ничего не нужно делать так как `execSync`
* сконфигурирован через `stdio`, чтоб все ошибки пайпились в родительский
* процесс. При этом в jest есть проблема что если не указать try..catch,
* то ошибка не будут отображена.
*/
}
return fs.readFileSync(path.resolve(outputPath, `${entityName}.js`), 'utf-8');
},
getCacheKey(source, filepath) {
return crypto
.createHash('md5')
.update(webpackSignature + source, 'utf8')
.digest('hex');
},
};
{
"jest": {
"transform": {
".*": "<rootDir>/jest/module-transformer.js"
},
"moduleNameMapper": {
"^jest/(.*)": "<rootDir>/jest/$1"
},
"testEnvironment": "node"
}
}
// jest/resolve-dependencies.js
'use strict';
const path = require('path');
const precinct = require('precinct');
module.exports = function resolveDependencies(filePath) {
const fileDir = path.dirname(filePath);
return precinct
.paperwork(filePath, {includeCore: false})
.map(moduleImport => path.resolve(fileDir, moduleImport))
.map(modulePath => {
try {
return require.resolve(modulePath);
} catch(error) {
return null;
}
})
.filter(Boolean)
.reduce((result, modulePath) => {
return result.concat(modulePath, resolveDependencies(modulePath));
}, [])
.filter((modulePath, i, list) => list.indexOf(modulePath) === i);
};
// jest/webpack.config.js
'use strict';
const config = require('../site/webpack.config');
const { resolveFromRoot } = require('../utils/webpack');
const rootPath = resolveFromRoot('.');
// Нас интересует только серверный конфиг.
const [serverConfig] = config.filter(compilation => compilation.target === 'node');
serverConfig.externals = [
function ResolveModules(context, request, callback) {
const parts = request.split('/');
if (serverConfig.resolve.alias[parts[0]]) {
parts[0] = serverConfig.resolve.alias[parts[0]];
return callback(null, parts.join('/'));
}
if (context !== rootPath) {
return callback(null, 'commonjs ' + request);
}
callback();
},
];
// Сбрасываем все параметры. Они у нас будут приходить из аргументов запуска вебпака.
serverConfig.entry = null;
// Изменяем путь куда будет собираться бандл, чтоб он ни в коем случае
// не попал в артефакты.
serverConfig.output.path = resolveFromRoot('./tmp');
module.exports = serverConfig;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment