mirror of https://github.com/facebook/jest.git
348 lines
10 KiB
JavaScript
348 lines
10 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import {strict as assert} from 'assert';
|
|
import {createRequire} from 'module';
|
|
import * as path from 'path';
|
|
import {fileURLToPath} from 'url';
|
|
import chalk from 'chalk';
|
|
import fs from 'graceful-fs';
|
|
import {sync as readPkg} from 'read-pkg';
|
|
import webpack from 'webpack';
|
|
import nodeExternals from 'webpack-node-externals';
|
|
import babelConfig from '../babel.config.js';
|
|
|
|
export const PACKAGES_DIR = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'../packages',
|
|
);
|
|
const require = createRequire(import.meta.url);
|
|
|
|
export const OK = chalk.reset.inverse.bold.green(' DONE ');
|
|
export const ERROR = chalk.reset.inverse.bold.red(' BOOM ');
|
|
|
|
export const typeOnlyPackages = new Set([
|
|
'babel-preset-jest',
|
|
'@jest/environment',
|
|
'@jest/globals',
|
|
'@jest/types',
|
|
'@jest/test-globals',
|
|
]);
|
|
|
|
// Get absolute paths of all directories under packages/*
|
|
export function getPackages() {
|
|
const packages = fs
|
|
.readdirSync(PACKAGES_DIR)
|
|
.map(file => path.resolve(PACKAGES_DIR, file))
|
|
.filter(f => fs.lstatSync(path.resolve(f)).isDirectory())
|
|
.filter(f => fs.existsSync(path.join(path.resolve(f), 'package.json')));
|
|
const rootPackage = require('../package.json');
|
|
|
|
const nodeEngineRequirement = rootPackage.engines.node;
|
|
|
|
return packages.map(packageDir => {
|
|
const pkg = readPkg({cwd: packageDir});
|
|
|
|
assert.ok(pkg.engines, `Engine requirement in "${pkg.name}" should exist`);
|
|
|
|
assert.strictEqual(
|
|
pkg.engines.node,
|
|
nodeEngineRequirement,
|
|
`Engine requirement in "${pkg.name}" should match root`,
|
|
);
|
|
|
|
assert.ok(
|
|
pkg.exports,
|
|
`Package "${pkg.name}" is missing \`exports\` field`,
|
|
);
|
|
assert.deepStrictEqual(
|
|
pkg.exports,
|
|
{
|
|
'.':
|
|
pkg.types == null
|
|
? pkg.main
|
|
: typeOnlyPackages.has(pkg.name)
|
|
? /* eslint-disable sort-keys */
|
|
{
|
|
types: pkg.types,
|
|
default: pkg.main,
|
|
}
|
|
: {
|
|
types: pkg.types,
|
|
require: pkg.main,
|
|
import: pkg.main.replace(/\.js$/, '.mjs'),
|
|
default: pkg.main,
|
|
},
|
|
/* eslint-enable */
|
|
'./package.json': './package.json',
|
|
...Object.values(pkg.bin || {}).reduce(
|
|
(mem, curr) =>
|
|
Object.assign(mem, {[curr.replace(/\.js$/, '')]: curr}),
|
|
{},
|
|
),
|
|
...(pkg.name === 'jest-circus'
|
|
? {'./runner': './build/runner.js'}
|
|
: {}),
|
|
...(pkg.name === 'expect'
|
|
? {
|
|
'./build/matchers': './build/matchers.js',
|
|
'./build/toThrowMatchers': './build/toThrowMatchers.js',
|
|
}
|
|
: {}),
|
|
},
|
|
`Package "${pkg.name}" does not export correct files`,
|
|
);
|
|
|
|
if (pkg.types) {
|
|
assert.strictEqual(
|
|
pkg.main,
|
|
'./build/index.js',
|
|
`Package "${pkg.name}" should have "./build/index.js" as main`,
|
|
);
|
|
assert.strictEqual(
|
|
pkg.types,
|
|
'./build/index.d.ts',
|
|
`Package "${pkg.name}" should have "./build/index.d.ts" as types`,
|
|
);
|
|
} else {
|
|
assert.strictEqual(
|
|
pkg.main,
|
|
'./index.js',
|
|
`Package "${pkg.name}" should have "./index.js" as main`,
|
|
);
|
|
}
|
|
|
|
if (pkg.bin) {
|
|
for (const [binName, binPath] of Object.entries(pkg.bin)) {
|
|
const fullBinPath = path.resolve(packageDir, binPath);
|
|
|
|
if (!fs.existsSync(fullBinPath)) {
|
|
throw new Error(
|
|
`Binary in package "${pkg.name}" with name "${binName}" at ${binPath} does not exist`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {packageDir, pkg};
|
|
});
|
|
}
|
|
|
|
export function getPackagesWithTsConfig() {
|
|
return getPackages().filter(p =>
|
|
fs.existsSync(path.resolve(p.packageDir, 'tsconfig.json')),
|
|
);
|
|
}
|
|
|
|
export const INLINE_REQUIRE_EXCLUDE_LIST =
|
|
/packages\/expect|(jest-(circus|diff|get-type|jasmine2|matcher-utils|message-util|regex-util|snapshot))|pretty-format\//;
|
|
|
|
export const copyrightSnippet = `
|
|
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
`.trim();
|
|
|
|
export function createBuildConfigs() {
|
|
const packages = getPackages();
|
|
|
|
return packages.map(({packageDir, pkg}) => {
|
|
const input = `${packageDir}/src/index.ts`;
|
|
|
|
if (!fs.existsSync(input)) {
|
|
return {packageDir, pkg};
|
|
}
|
|
|
|
const options = Object.assign({}, babelConfig);
|
|
options.plugins = options.plugins.slice();
|
|
|
|
if (INLINE_REQUIRE_EXCLUDE_LIST.test(input)) {
|
|
// The excluded modules are injected into the user's sandbox
|
|
// We need to guard some globals there.
|
|
options.plugins.push(
|
|
require.resolve('./babel-plugin-jest-native-globals'),
|
|
);
|
|
} else {
|
|
options.plugins = options.plugins.map(plugin => {
|
|
if (
|
|
Array.isArray(plugin) &&
|
|
plugin[0] === '@babel/plugin-transform-modules-commonjs'
|
|
) {
|
|
return [plugin[0], Object.assign({}, plugin[1], {lazy: true})];
|
|
}
|
|
|
|
return plugin;
|
|
});
|
|
}
|
|
|
|
const separateChunks =
|
|
pkg.name === 'jest-worker'
|
|
? {
|
|
processChild: path.resolve(
|
|
packageDir,
|
|
'./src/workers/processChild.ts',
|
|
),
|
|
threadChild: path.resolve(
|
|
packageDir,
|
|
'./src/workers/threadChild.ts',
|
|
),
|
|
}
|
|
: pkg.name === 'jest-haste-map'
|
|
? {worker: path.resolve(packageDir, './src/worker.ts')}
|
|
: pkg.name === '@jest/reporters'
|
|
? {CoverageWorker: path.resolve(packageDir, './src/CoverageWorker.ts')}
|
|
: pkg.name === 'jest-runner'
|
|
? {testWorker: path.resolve(packageDir, './src/testWorker.ts')}
|
|
: pkg.name === 'jest-circus'
|
|
? {
|
|
jestAdapterInit: path.resolve(
|
|
packageDir,
|
|
'./src/legacy-code-todo-rewrite/jestAdapterInit.ts',
|
|
),
|
|
}
|
|
: pkg.name === 'jest-jasmine2'
|
|
? {
|
|
'jasmine/jasmineLight': path.resolve(
|
|
packageDir,
|
|
'./src/jasmine/jasmineLight.ts',
|
|
),
|
|
jestExpect: path.resolve(packageDir, './src/jestExpect.ts'),
|
|
setup_jest_globals: path.resolve(
|
|
packageDir,
|
|
'./src/setup_jest_globals.ts',
|
|
),
|
|
}
|
|
: pkg.name === 'jest-repl'
|
|
? {repl: path.resolve(packageDir, './src/cli/repl.ts')}
|
|
: pkg.name === 'jest-snapshot'
|
|
? {worker: path.resolve(packageDir, './src/worker.ts')}
|
|
: {};
|
|
|
|
const extraEntryPoints =
|
|
// skip expect for now
|
|
pkg.name === 'expect'
|
|
? {}
|
|
: Object.keys(pkg.exports)
|
|
.filter(
|
|
key =>
|
|
key !== '.' &&
|
|
key !== './package.json' &&
|
|
!key.startsWith('./bin'),
|
|
)
|
|
.reduce((previousValue, currentValue) => {
|
|
return {
|
|
...previousValue,
|
|
// skip `./`
|
|
[currentValue.slice(2)]: path.resolve(
|
|
packageDir,
|
|
'./src',
|
|
`${currentValue}.ts`,
|
|
),
|
|
};
|
|
}, {});
|
|
|
|
return {
|
|
packageDir,
|
|
pkg,
|
|
webpackConfig: {
|
|
context: packageDir,
|
|
devtool: false,
|
|
entry: {
|
|
index: input,
|
|
...separateChunks,
|
|
...extraEntryPoints,
|
|
},
|
|
externals: nodeExternals(),
|
|
mode: 'production',
|
|
module: {
|
|
rules: [
|
|
{
|
|
test: /.ts$/,
|
|
use: {
|
|
loader: 'babel-loader',
|
|
options,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
optimization: {
|
|
minimize: false,
|
|
moduleIds: 'named',
|
|
},
|
|
output: {
|
|
filename: '[name].js',
|
|
library: {
|
|
type: 'commonjs2',
|
|
},
|
|
path: path.resolve(packageDir, 'build'),
|
|
},
|
|
plugins: [
|
|
new webpack.BannerPlugin(copyrightSnippet),
|
|
new IgnoreDynamicRequire(separateChunks),
|
|
],
|
|
resolve: {
|
|
extensions: ['.ts', '.js'],
|
|
},
|
|
target: 'node',
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
export function createWebpackConfigs(webpackConfigs = createBuildConfigs()) {
|
|
return webpackConfigs
|
|
.map(({webpackConfig}) => webpackConfig)
|
|
.filter(config => config != null);
|
|
}
|
|
|
|
// inspired by https://framagit.org/Glandos/webpack-ignore-dynamic-require
|
|
class IgnoreDynamicRequire {
|
|
constructor(extraEntries) {
|
|
this.separateFiles = new Set(
|
|
Object.keys(extraEntries).map(entry => `./${entry}`),
|
|
);
|
|
}
|
|
|
|
apply(compiler) {
|
|
compiler.hooks.normalModuleFactory.tap('IgnoreDynamicRequire', factory => {
|
|
factory.hooks.parser
|
|
.for('javascript/auto')
|
|
.tap('IgnoreDynamicRequire', parser => {
|
|
// This is a SyncBailHook, so returning anything stops the parser, and nothing (undefined) allows to continue
|
|
parser.hooks.call
|
|
.for('require')
|
|
.tap('IgnoreDynamicRequire', expression => {
|
|
if (expression.arguments.length === 0) {
|
|
return undefined;
|
|
}
|
|
const arg = parser.evaluateExpression(expression.arguments[0]);
|
|
if (arg.isString() && !arg.string.startsWith('.')) {
|
|
return true;
|
|
}
|
|
if (!arg.isString() && !arg.isConditional()) {
|
|
return true;
|
|
}
|
|
|
|
if (arg.isString() && this.separateFiles.has(arg.string)) {
|
|
return true;
|
|
}
|
|
return undefined;
|
|
});
|
|
parser.hooks.call
|
|
.for('require.resolve')
|
|
.tap('IgnoreDynamicRequire', () => true);
|
|
parser.hooks.call
|
|
.for('require.resolve.paths')
|
|
.tap('IgnoreDynamicRequire', () => true);
|
|
});
|
|
});
|
|
}
|
|
}
|