From 70340d0b6c50a864586eaca2fca4b5549dd5ef56 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Wed, 20 Sep 2023 00:20:28 +0200 Subject: [PATCH] feat: bundle Jest's modules with webpack (#12348) --- CHANGELOG.md | 2 + .../circusDeclarationErrors.test.ts.snap | 8 +- .../moduleNameMapper.test.ts.snap | 4 +- .../requireMissingExt.test.ts.snap | 2 +- .../resolveNoFileExtensions.test.ts.snap | 2 +- .../__snapshots__/showConfig.test.ts.snap | 2 +- .../testFailingJasmine.test.ts.snap | 4 +- .../__snapshots__/transform.test.ts.snap | 2 +- e2e/__tests__/stackTrace.test.ts | 2 +- package.json | 3 + packages/jest-circus/package.json | 2 +- .../jest-circus/{runner.js => src/runner.ts} | 5 +- packages/jest-config/src/normalize.ts | 13 +- .../src/readConfigFileAndSetRootDir.ts | 2 +- packages/jest-runner/src/runTest.ts | 3 +- .../jest-util/src/requireOrImportModule.ts | 4 +- scripts/build.mjs | 169 +++----------- scripts/buildUtils.mjs | 210 +++++++++++++++++- scripts/bundleTs.mjs | 11 +- yarn.lock | 53 ++++- 20 files changed, 330 insertions(+), 173 deletions(-) rename packages/jest-circus/{runner.js => src/runner.ts} (70%) diff --git a/CHANGELOG.md b/CHANGELOG.md index de6fdf0fdd37..525ddd793b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ ### Performance +- `[*]` [**BREAKING**] Bundle all of Jest's modules into `index.js` ([#12348](https://github.com/jestjs/jest/pull/12348)) + ### Chore & Maintenance - `[*]` [**BREAKING**] Drop support for Node.js versions 14 and 19 ([#14460](https://github.com/jestjs/jest/pull/14460)) diff --git a/e2e/__tests__/__snapshots__/circusDeclarationErrors.test.ts.snap b/e2e/__tests__/__snapshots__/circusDeclarationErrors.test.ts.snap index 0e49b62527c6..3ce39baaff64 100644 --- a/e2e/__tests__/__snapshots__/circusDeclarationErrors.test.ts.snap +++ b/e2e/__tests__/__snapshots__/circusDeclarationErrors.test.ts.snap @@ -16,7 +16,7 @@ exports[`defining tests and hooks asynchronously throws 1`] = ` 14 | }); 15 | }); - at eventHandler (../../packages/jest-circus/build/eventHandler.js:141:11) + at eventHandler (../../packages/jest-circus/build/jestAdapterInit.js:148:38) at test (__tests__/asyncDefinition.test.js:12:5) ● Test suite failed to run @@ -31,7 +31,7 @@ exports[`defining tests and hooks asynchronously throws 1`] = ` 15 | }); 16 | - at eventHandler (../../packages/jest-circus/build/eventHandler.js:104:11) + at eventHandler (../../packages/jest-circus/build/jestAdapterInit.js:114:38) at afterAll (__tests__/asyncDefinition.test.js:13:5) ● Test suite failed to run @@ -46,7 +46,7 @@ exports[`defining tests and hooks asynchronously throws 1`] = ` 20 | }); 21 | - at eventHandler (../../packages/jest-circus/build/eventHandler.js:141:11) + at eventHandler (../../packages/jest-circus/build/jestAdapterInit.js:148:38) at test (__tests__/asyncDefinition.test.js:18:3) ● Test suite failed to run @@ -60,6 +60,6 @@ exports[`defining tests and hooks asynchronously throws 1`] = ` 20 | }); 21 | - at eventHandler (../../packages/jest-circus/build/eventHandler.js:104:11) + at eventHandler (../../packages/jest-circus/build/jestAdapterInit.js:114:38) at afterAll (__tests__/asyncDefinition.test.js:19:3)" `; diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index dcc24b54322b..4ad15c18169d 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -41,7 +41,7 @@ exports[`moduleNameMapper wrong array configuration 1`] = ` 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:759:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1175:17) at Object.require (index.js:10:1) at Object.require (__tests__/index.js:10:20)" `; @@ -71,7 +71,7 @@ exports[`moduleNameMapper wrong configuration 1`] = ` 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:759:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:1175:17) at Object.require (index.js:10:1) at Object.require (__tests__/index.js:10:20)" `; diff --git a/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap b/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap index 34f55ffd75e3..3faf2d6579f7 100644 --- a/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap +++ b/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap @@ -26,7 +26,7 @@ exports[`shows a proper error from deep requires 1`] = ` 12 | test('dummy', () => { 13 | expect(1).toBe(1); - at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/resolver.js:427:11) + at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/index.js:930:11) at Object. (node_modules/discord.js/src/index.js:21:12) at Object.require (__tests__/test.js:10:1)" `; diff --git a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap index 98867dc0fce7..39bcdf34f0a0 100644 --- a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap +++ b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap @@ -37,7 +37,7 @@ exports[`show error message with matching files 1`] = ` | ^ 9 | - at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/resolver.js:427:11) + at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/index.js:930:11) at Object.require (index.js:8:18) at Object.require (__tests__/test.js:8:11)" `; diff --git a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap index 24f96159efa7..4632bacbaad4 100644 --- a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap +++ b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap @@ -77,7 +77,7 @@ exports[`--showConfig outputs config info and exits 1`] = ` "/node_modules/" ], "testRegex": [], - "testRunner": "<>/jest-circus/runner.js", + "testRunner": "<>/jest-circus/build/runner.js", "transform": [ [ "\\\\.[jt]sx?$", diff --git a/e2e/__tests__/__snapshots__/testFailingJasmine.test.ts.snap b/e2e/__tests__/__snapshots__/testFailingJasmine.test.ts.snap index 976eac00b30a..5c98b400e1ae 100644 --- a/e2e/__tests__/__snapshots__/testFailingJasmine.test.ts.snap +++ b/e2e/__tests__/__snapshots__/testFailingJasmine.test.ts.snap @@ -46,7 +46,7 @@ FAIL __tests__/worksWithConcurrentMode.test.js 15 | }); 16 | - at Function.failing (../../packages/jest-jasmine2/build/jasmineAsyncInstall.js:175:11) + at Function.failing (../../packages/jest-jasmine2/build/index.js:308:17) at Suite.failing (__tests__/worksWithConcurrentMode.test.js:13:17) at Object.describe (__tests__/worksWithConcurrentMode.test.js:8:1) @@ -80,7 +80,7 @@ FAIL __tests__/worksWithConcurrentOnlyMode.test.js 15 | }); 16 | - at Function.failing (../../packages/jest-jasmine2/build/jasmineAsyncInstall.js:175:11) + at Function.failing (../../packages/jest-jasmine2/build/index.js:308:17) at Suite.failing (__tests__/worksWithConcurrentOnlyMode.test.js:13:22) at Object.describe (__tests__/worksWithConcurrentOnlyMode.test.js:8:1) diff --git a/e2e/__tests__/__snapshots__/transform.test.ts.snap b/e2e/__tests__/__snapshots__/transform.test.ts.snap index ff6901ad7061..c4af677ae559 100644 --- a/e2e/__tests__/__snapshots__/transform.test.ts.snap +++ b/e2e/__tests__/__snapshots__/transform.test.ts.snap @@ -6,7 +6,7 @@ exports[`babel-jest ignored tells user to match ignored files 1`] = ` babel-jest: Babel ignores __tests__/ignoredFile.test.js - make sure to include the file in Jest's transformIgnorePatterns as well. - at assertLoadedBabelConfig (../../../packages/babel-jest/build/index.js:105:11)" + at assertLoadedBabelConfig (../../../packages/babel-jest/build/index.js:138:11)" `; exports[`babel-jest instruments only specific files and collects coverage 1`] = ` diff --git a/e2e/__tests__/stackTrace.test.ts b/e2e/__tests__/stackTrace.test.ts index 9dd58e731396..9aa61522caa2 100644 --- a/e2e/__tests__/stackTrace.test.ts +++ b/e2e/__tests__/stackTrace.test.ts @@ -81,7 +81,7 @@ describe('Stack Trace', () => { ); expect(stderr).toMatch( - /\s+at\s(?:.+?)\s\((?:.+?)jest-resolve\/build\/resolver\.js/, + /\s+at\s(?:.+?)\s\((?:.+?)jest-resolve\/build\/index\.js/, ); }); diff --git a/package.json b/package.json index 09e263f3c520..767ddca27987 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "babel-jest": "workspace:^", + "babel-loader": "^8.2.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "chokidar": "^3.3.0", @@ -80,6 +81,8 @@ "tempy": "^1.0.0", "ts-node": "^10.5.0", "typescript": "^5.0.4", + "webpack": "^5.68.0", + "webpack-node-externals": "^3.0.0", "which": "^4.0.0" }, "scripts": { diff --git a/packages/jest-circus/package.json b/packages/jest-circus/package.json index 738eb54e7f9c..8d5729527d97 100644 --- a/packages/jest-circus/package.json +++ b/packages/jest-circus/package.json @@ -15,7 +15,7 @@ "default": "./build/index.js" }, "./package.json": "./package.json", - "./runner": "./runner.js" + "./runner": "./build/runner.js" }, "dependencies": { "@jest/environment": "workspace:^", diff --git a/packages/jest-circus/runner.js b/packages/jest-circus/src/runner.ts similarity index 70% rename from packages/jest-circus/runner.js rename to packages/jest-circus/src/runner.ts index de11d642a346..ab2512230f03 100644 --- a/packages/jest-circus/runner.js +++ b/packages/jest-circus/src/runner.ts @@ -6,5 +6,6 @@ */ // Allow people to use `jest-circus/runner` as a runner. -const runner = require('./build/legacy-code-todo-rewrite/jestAdapter').default; -module.exports = runner; +import runner from './legacy-code-todo-rewrite/jestAdapter'; + +export default runner; diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 23bcf4a1f089..25ef99b2e954 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -60,6 +60,9 @@ export type AllOptions = Config.ProjectConfig & Config.GlobalConfig; const createConfigError = (message: string) => new ValidationError(ERROR, message, DOCUMENTATION_NOTE); +// we wanna avoid webpack trying to be clever +const requireResolve = (module: string) => require.resolve(module); + function verifyDirectoryExists(path: string, key: string) { try { const rootStat = statSync(path); @@ -525,7 +528,7 @@ export default async function normalize( } options.testEnvironment = resolveTestEnvironment({ - requireResolveFunction: require.resolve, + requireResolveFunction: requireResolve, rootDir: options.rootDir, testEnvironment: options.testEnvironment || @@ -667,7 +670,7 @@ export default async function normalize( option && resolveRunner(newOptions.resolver, { filePath: option, - requireResolveFunction: require.resolve, + requireResolveFunction: requireResolve, rootDir: options.rootDir, }); } @@ -951,7 +954,7 @@ export default async function normalize( config: {}, path: resolveWatchPlugin(newOptions.resolver, { filePath: watchPlugin, - requireResolveFunction: require.resolve, + requireResolveFunction: requireResolve, rootDir: options.rootDir, }), }; @@ -960,7 +963,7 @@ export default async function normalize( config: watchPlugin[1] || {}, path: resolveWatchPlugin(newOptions.resolver, { filePath: watchPlugin[0], - requireResolveFunction: require.resolve, + requireResolveFunction: requireResolve, rootDir: options.rootDir, }), }; @@ -995,7 +998,7 @@ export default async function normalize( newOptions.testSequencer = resolveSequencer(newOptions.resolver, { filePath: options.testSequencer || require.resolve(DEFAULT_CONFIG.testSequencer), - requireResolveFunction: require.resolve, + requireResolveFunction: requireResolve, rootDir: options.rootDir, }); diff --git a/packages/jest-config/src/readConfigFileAndSetRootDir.ts b/packages/jest-config/src/readConfigFileAndSetRootDir.ts index fef76c2c7ab6..5de67af65665 100644 --- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts +++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts @@ -112,7 +112,7 @@ function getRegisteredCompiler() { async function registerTsNode(): Promise { try { // Register TypeScript compiler instance - const tsNode = await import('ts-node'); + const tsNode = await import(/* webpackIgnore: true */ 'ts-node'); return tsNode.register({ compilerOptions: { module: 'CommonJS', diff --git a/packages/jest-runner/src/runTest.ts b/packages/jest-runner/src/runTest.ts index ffb458157587..13b623d21e2b 100644 --- a/packages/jest-runner/src/runTest.ts +++ b/packages/jest-runner/src/runTest.ts @@ -98,7 +98,8 @@ async function runTestInternal( } testEnvironment = resolveTestEnvironment({ ...projectConfig, - requireResolveFunction: require.resolve, + // we wanna avoid webpack trying to be clever + requireResolveFunction: module => require.resolve(module), testEnvironment: customEnvironment, }); } diff --git a/packages/jest-util/src/requireOrImportModule.ts b/packages/jest-util/src/requireOrImportModule.ts index ed323338a9d4..7124c8de95ae 100644 --- a/packages/jest-util/src/requireOrImportModule.ts +++ b/packages/jest-util/src/requireOrImportModule.ts @@ -30,7 +30,9 @@ export default async function requireOrImportModule( const moduleUrl = pathToFileURL(filePath); // node `import()` supports URL, but TypeScript doesn't know that - const importedModule = await import(moduleUrl.href); + const importedModule = await import( + /* webpackIgnore: true */ moduleUrl.href + ); if (!applyInteropRequireDefault) { return importedModule; diff --git a/scripts/build.mjs b/scripts/build.mjs index 29f1f79c92e3..2cf4a6aa4f18 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -5,154 +5,55 @@ * LICENSE file in the root directory of this source tree. */ -/** - * script to build (transpile) files. - * By default it transpiles js files for all packages and writes them - * into `build/` directory. - * Non-js files not matching IGNORE_PATTERN will be copied without transpiling. - * - * Example: - * node ./scripts/build.mjs - * node ./scripts/build.mjs /users/123/jest/packages/jest-111/src/111.js - */ - import {strict as assert} from 'assert'; import * as path from 'path'; -import {fileURLToPath} from 'url'; -import babel from '@babel/core'; +import util from 'util'; import chalk from 'chalk'; -import {glob} from 'glob'; import fs from 'graceful-fs'; -import micromatch from 'micromatch'; -import prettier from 'prettier'; -import transformOptions from '../babel.config.js'; -import { - OK, - PACKAGES_DIR, - adjustToTerminalWidth, - getPackages, -} from './buildUtils.mjs'; - -const SRC_DIR = 'src'; -const BUILD_DIR = 'build'; -const JS_FILES_PATTERN = '**/*.js'; -const TS_FILES_PATTERN = '**/*.ts'; -const IGNORE_PATTERN = '**/__{tests,mocks}__/**'; - -const INLINE_REQUIRE_EXCLUDE_LIST = - /packages\/expect|(jest-(circus|diff|get-type|jasmine2|matcher-utils|message-util|regex-util|snapshot))|pretty-format\//; - -const prettierConfig = prettier.resolveConfig.sync( - fileURLToPath(import.meta.url), -); -prettierConfig.trailingComma = 'none'; -prettierConfig.parser = 'babel'; - -function getPackageName(file) { - return path.relative(PACKAGES_DIR, file).split(path.sep)[0]; -} - -function getBuildPath(file, buildFolder) { - const pkgName = getPackageName(file); - const pkgSrcPath = path.resolve(PACKAGES_DIR, pkgName, SRC_DIR); - const pkgBuildPath = path.resolve(PACKAGES_DIR, pkgName, buildFolder); - const relativeToSrcPath = path.relative(pkgSrcPath, file); - return path.resolve(pkgBuildPath, relativeToSrcPath).replace(/\.ts$/, '.js'); -} - -function buildNodePackage({packageDir, pkg}) { - const srcDir = path.resolve(packageDir, SRC_DIR); - const files = glob.sync('**/*', {absolute: true, cwd: srcDir, nodir: true}); +import webpack from 'webpack'; +import {ERROR, OK, createWebpackConfigs} from './buildUtils.mjs'; - process.stdout.write(adjustToTerminalWidth(`${pkg.name}\n`)); +async function buildNodePackages() { + process.stdout.write(chalk.inverse(' Bundling packages \n')); - for (const file of files) buildFile(file, true); - - assert.ok( - fs.existsSync(path.resolve(packageDir, pkg.main)), - `Main file "${pkg.main}" in ${pkg.name} should exist`, + const webpackConfigs = createWebpackConfigs(); + const compiler = webpack( + webpackConfigs + .map(({webpackConfig}) => webpackConfig) + .filter(config => config != null), ); - process.stdout.write(`${OK}\n`); -} - -function buildFile(file, silent) { - const destPath = getBuildPath(file, BUILD_DIR); - - if (micromatch.isMatch(file, IGNORE_PATTERN)) { - silent || - process.stdout.write( - `${ - chalk.dim(' \u2022 ') + path.relative(PACKAGES_DIR, file) - } (ignore)\n`, - ); - return; - } + let stats; + try { + stats = await util.promisify(compiler.run.bind(compiler))(); + await util.promisify(compiler.close.bind(compiler))(); - fs.mkdirSync(path.dirname(destPath), {recursive: true}); - if ( - !micromatch.isMatch(file, JS_FILES_PATTERN) && - !micromatch.isMatch(file, TS_FILES_PATTERN) - ) { - fs.createReadStream(file).pipe(fs.createWriteStream(destPath)); - silent || - process.stdout.write( - `${ - chalk.red(' \u2022 ') + - path.relative(PACKAGES_DIR, file) + - chalk.red(' \u21D2 ') + - path.relative(PACKAGES_DIR, destPath) - } (copy)\n`, - ); - } else { - const options = Object.assign({}, transformOptions); - options.plugins = options.plugins.slice(); + assert.ok(!stats.hasErrors(), 'Must not have errors or warnings'); + } catch (error) { + process.stdout.write(`${ERROR}\n\n`); - if (INLINE_REQUIRE_EXCLUDE_LIST.test(file)) { - // The excluded modules are injected into the user's sandbox - // We need to guard some globals there. - options.plugins.push( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - 'babel-plugin-jest-native-globals.js', - ), - ); - } 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})]; - } + if (stats) { + const info = stats.toJson(); - return plugin; - }); + if (stats.hasErrors()) { + console.error('errors', info.errors); + } } - const transformed = babel.transformFileSync(file, options).code; - const prettyCode = prettier.format(transformed, prettierConfig); - - fs.writeFileSync(destPath, prettyCode); - - silent || - process.stdout.write( - `${ - chalk.green(' \u2022 ') + - path.relative(PACKAGES_DIR, file) + - chalk.green(' \u21D2 ') + - path.relative(PACKAGES_DIR, destPath) - }\n`, - ); + throw error; } -} -const files = process.argv.slice(2); + for (const {packageDir, pkg} of webpackConfigs) { + assert.ok( + fs.existsSync(path.resolve(packageDir, pkg.main)), + `Main file "${pkg.main}" in "${pkg.name}" should exist`, + ); + } -if (files.length > 0) { - for (const file of files) buildFile(file); -} else { - const packages = getPackages(); - process.stdout.write(chalk.inverse(' Building packages \n')); - for (const pkg of packages) buildNodePackage(pkg); + process.stdout.write(`${OK}\n`); } + +buildNodePackages().catch(error => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/buildUtils.mjs b/scripts/buildUtils.mjs index 16ff66578952..50f78804e7cd 100644 --- a/scripts/buildUtils.mjs +++ b/scripts/buildUtils.mjs @@ -13,13 +13,18 @@ import chalk from 'chalk'; import fs from 'graceful-fs'; import {sync as readPkg} from 'read-pkg'; import stringLength from 'string-length'; +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 '); // Get absolute paths of all directories under packages/* export function getPackages() { @@ -28,7 +33,6 @@ export function getPackages() { .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 require = createRequire(import.meta.url); const rootPackage = require('../package.json'); const nodeEngineRequirement = rootPackage.engines.node; @@ -65,7 +69,9 @@ export function getPackages() { Object.assign(mem, {[curr.replace(/\.js$/, '')]: curr}), {}, ), - ...(pkg.name === 'jest-circus' ? {'./runner': './runner.js'} : {}), + ...(pkg.name === 'jest-circus' + ? {'./runner': './build/runner.js'} + : {}), ...(pkg.name === 'expect' ? { './build/matchers': './build/matchers.js', @@ -127,3 +133,203 @@ export function getPackagesWithTsConfig() { 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 createWebpackConfigs() { + 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')} + : {}; + + 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', + }, + }; + }); +} + +// 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); + }); + }); + } +} diff --git a/scripts/bundleTs.mjs b/scripts/bundleTs.mjs index a9c6d5310743..36460c8406a2 100644 --- a/scripts/bundleTs.mjs +++ b/scripts/bundleTs.mjs @@ -18,7 +18,7 @@ import fs from 'graceful-fs'; import {sync as pkgDir} from 'pkg-dir'; import prettier from 'prettier'; import {rimraf} from 'rimraf'; -import {getPackages} from './buildUtils.mjs'; +import {copyrightSnippet, getPackages} from './buildUtils.mjs'; const prettierConfig = prettier.resolveConfig.sync( fileURLToPath(import.meta.url).replace(/\.js$/, '.d.ts'), @@ -27,15 +27,6 @@ const prettierConfig = prettier.resolveConfig.sync( const require = createRequire(import.meta.url); const typescriptCompilerFolder = pkgDir(require.resolve('typescript')); -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(); - const typesNodeReferenceDirective = '/// '; const excludedPackages = new Set(['@jest/globals', '@jest/test-globals']); diff --git a/yarn.lock b/yarn.lock index ec3bd4f4f555..843fd5df0992 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2945,6 +2945,7 @@ __metadata: ansi-regex: ^5.0.1 ansi-styles: ^5.0.0 babel-jest: "workspace:^" + babel-loader: ^8.2.3 camelcase: ^6.2.0 chalk: ^4.0.0 chokidar: ^3.3.0 @@ -2999,6 +3000,8 @@ __metadata: tempy: ^1.0.0 ts-node: ^10.5.0 typescript: ^5.0.4 + webpack: ^5.68.0 + webpack-node-externals: ^3.0.0 which: ^4.0.0 languageName: unknown linkType: soft @@ -6483,6 +6486,21 @@ __metadata: languageName: unknown linkType: soft +"babel-loader@npm:^8.2.3": + version: 8.3.0 + resolution: "babel-loader@npm:8.3.0" + dependencies: + find-cache-dir: ^3.3.1 + loader-utils: ^2.0.0 + make-dir: ^3.1.0 + schema-utils: ^2.6.5 + peerDependencies: + "@babel/core": ^7.0.0 + webpack: ">=2" + checksum: d48bcf9e030e598656ad3ff5fb85967db2eaaf38af5b4a4b99d25618a2057f9f100e6b231af2a46c1913206db506115ca7a8cbdf52c9c73d767070dae4352ab5 + languageName: node + linkType: hard + "babel-loader@npm:^9.1.3": version: 9.1.3 resolution: "babel-loader@npm:9.1.3" @@ -10166,6 +10184,17 @@ __metadata: languageName: node linkType: hard +"find-cache-dir@npm:^3.3.1": + version: 3.3.2 + resolution: "find-cache-dir@npm:3.3.2" + dependencies: + commondir: ^1.0.1 + make-dir: ^3.0.2 + pkg-dir: ^4.1.0 + checksum: 1e61c2e64f5c0b1c535bd85939ae73b0e5773142713273818cc0b393ee3555fb0fd44e1a5b161b8b6c3e03e98c2fcc9c227d784850a13a90a8ab576869576817 + languageName: node + linkType: hard + "find-cache-dir@npm:^4.0.0": version: 4.0.0 resolution: "find-cache-dir@npm:4.0.0" @@ -13933,7 +13962,7 @@ __metadata: languageName: node linkType: hard -"make-dir@npm:^3.0.0, make-dir@npm:^3.1.0": +"make-dir@npm:^3.0.0, make-dir@npm:^3.0.2, make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" dependencies: @@ -16609,7 +16638,7 @@ __metadata: languageName: node linkType: hard -"pkg-dir@npm:^4.2.0": +"pkg-dir@npm:^4.1.0, pkg-dir@npm:^4.2.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" dependencies: @@ -18610,6 +18639,17 @@ __metadata: languageName: node linkType: hard +"schema-utils@npm:^2.6.5": + version: 2.7.1 + resolution: "schema-utils@npm:2.7.1" + dependencies: + "@types/json-schema": ^7.0.5 + ajv: ^6.12.4 + ajv-keywords: ^3.5.2 + checksum: 32c62fc9e28edd101e1bd83453a4216eb9bd875cc4d3775e4452b541908fa8f61a7bbac8ffde57484f01d7096279d3ba0337078e85a918ecbeb72872fb09fb2b + languageName: node + linkType: hard + "schema-utils@npm:^3.0.0, schema-utils@npm:^3.1.1, schema-utils@npm:^3.2.0": version: 3.3.0 resolution: "schema-utils@npm:3.3.0" @@ -21013,6 +21053,13 @@ __metadata: languageName: node linkType: hard +"webpack-node-externals@npm:^3.0.0": + version: 3.0.0 + resolution: "webpack-node-externals@npm:3.0.0" + checksum: 355080c35c821115b97dda8c93d9d0565a90a6012a532324eb0d6a64f8f0d609431fd29504fc7ce414755841ac14f601f3eef99472c2c5dc00233b504ebe73f2 + languageName: node + linkType: hard + "webpack-sources@npm:^3.2.2, webpack-sources@npm:^3.2.3": version: 3.2.3 resolution: "webpack-sources@npm:3.2.3" @@ -21020,7 +21067,7 @@ __metadata: languageName: node linkType: hard -"webpack@npm:^5.88.1": +"webpack@npm:^5.68.0, webpack@npm:^5.88.1": version: 5.88.2 resolution: "webpack@npm:5.88.2" dependencies: