cli: rework jest ESM handling by transforming by default

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-10-09 14:24:01 +02:00
parent 8ae4208c93
commit b486adb8c6
6 changed files with 136 additions and 24 deletions
+15
View File
@@ -0,0 +1,15 @@
---
'@backstage/create-app': patch
---
Removed the included `jest` configuration from the root `package.json` as the `transformModules` option no longer exists.
To apply this change to an existing app, make the follow change to the root `package.json`:
```diff
- "jest": {
- "transformModules": [
- "@asyncapi/react-component"
- ]
- }
```
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/cli': minor
---
The Jest configuration that's included with the Backstage CLI has received several changes.
As a part of migrating to more widespread usage of ESM modules, the default configuration now transforms all source files everywhere, including those within `node_modules`. Due to this change the existing `transformModules` option has been removed and will be ignored. There is also a list of known packages that do not require transforms in the CLI, which will evolve over time. If needed there will also be an option to add packages to this list in the future, but it is not included yet to avoid clutter.
To counteract the slowdown of the additional transforms that have been introduced, the default configuration has also been reworked to enable caching across different packages. Previously each package in a Backstage monorepo would have its own isolated Jest cache, but it is now shared between packages that have a similar enough Jest configuration.
Another change that will speed up test execution is that the transformer for `.esm.js` files has been switched. It used to be an ESM transformer based on Babel, but it is also done by sucrase now since it is significantly faster.
The changes above are not strictly breaking as all tests should still work. It may however cause excessive slowdowns in projects that have configured custom transforms in the `jest` field within `package.json` files. In this case it is either best to consider removing the custom transforms, or overriding the `transformIgnorePatterns` to instead use Jest's default `'/node_modules/'` pattern.
-5
View File
@@ -85,10 +85,5 @@
"*.md": [
"node ./scripts/check-docs-quality"
]
},
"jest": {
"transformModules": [
"@asyncapi/react-component"
]
}
}
+42 -14
View File
@@ -16,7 +16,25 @@
const fs = require('fs-extra');
const path = require('path');
const crypto = require('crypto');
const glob = require('util').promisify(require('glob'));
const { version } = require('../package.json');
const transformIgnorePattern = [
'@material-ui',
'@rjsf',
'ajv',
'core-js',
'jest-.*',
'jsdom',
'knex',
'react',
'react-dom',
'highlight\\.js',
'prismjs',
'react-use',
'typescript',
].join('|');
async function getProjectConfig(targetPath, displayName) {
const configJsPath = path.resolve(targetPath, 'jest.config.js');
@@ -58,10 +76,7 @@ async function getProjectConfig(targetPath, displayName) {
currentPath = newPath;
}
// We add an additional Jest config parameter only known by the Backstage CLI
// called `transformModules`. It's a list of modules that we want to apply
// our configured jest transformations for.
// This is useful when packages are published in untranspiled ESM or TS form.
// This is an old deprecated option that is no longer used.
const transformModules = pkgJsonConfigs
.flatMap(conf => {
const modules = conf.transformModules || [];
@@ -70,10 +85,14 @@ async function getProjectConfig(targetPath, displayName) {
})
.map(name => `${name}/`)
.join('|');
const transformModulePattern = transformModules && `(?!${transformModules})`;
if (transformModules.length > 0) {
console.warn(
'The Backstage CLI jest transformModules option is no longer used and will be ignored. All modules are now always transformed.',
);
}
const options = {
displayName,
...(displayName && { displayName }),
rootDir: path.resolve(targetPath, 'src'),
coverageDirectory: path.resolve(targetPath, 'coverage'),
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'],
@@ -82,8 +101,7 @@ async function getProjectConfig(targetPath, displayName) {
},
transform: {
'\\.esm\\.js$': require.resolve('./jestEsmTransform.js'), // See jestEsmTransform.js
'\\.(js|jsx|ts|tsx)$': require.resolve('@sucrase/jest-plugin'),
'\\.(js|jsx|ts|tsx)$': require.resolve('./jestSucraseTransform.js'),
'\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$':
require.resolve('./jestFileTransform.js'),
'\\.(yaml)$': require.resolve('jest-transform-yaml'),
@@ -92,11 +110,7 @@ async function getProjectConfig(targetPath, displayName) {
// A bit more opinionated
testMatch: ['**/?(*.)test.{js,jsx,mjs,ts,tsx}'],
// Default behaviour is to not apply transforms for node_modules, but we still want
// to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages.
transformIgnorePatterns: [
`/node_modules/${transformModulePattern}.*\\.(?:(?<!esm\\.)js|json)$`,
],
transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`],
};
// Use src/setupTests.ts as the default location for configuring test env
@@ -104,7 +118,21 @@ async function getProjectConfig(targetPath, displayName) {
options.setupFilesAfterEnv = ['<rootDir>/setupTests.ts'];
}
return Object.assign(options, ...pkgJsonConfigs);
const config = Object.assign(options, ...pkgJsonConfigs);
// The config name is a cache key that lets us share the jest cache across projects.
// If no explicit name was configured, generated one based on the configuration.
if (!config.name) {
const configHash = crypto
.createHash('md5')
.update(version)
.update(Buffer.alloc(1))
.update(JSON.stringify(config.transform))
.digest('hex');
config.name = `backstage_cli_${configHash}`;
}
return config;
}
// This loads the root jest config, which in turn will either refer to a single
@@ -0,0 +1,66 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { createHash } = require('crypto');
const { transform } = require('sucrase');
const sucrasePkg = require('sucrase/package.json');
const sucrasePluginPkg = require('@sucrase/jest-plugin/package.json');
const ESM_REGEX = /\b(?:import|export)\b/;
function process(source, filePath) {
let transforms;
if (filePath.endsWith('.esm.js')) {
transforms = ['imports'];
} else if (filePath.endsWith('.js')) {
// This is a very rough filter to avoid transforming things that we quickly
// can be sure are definitely not ESM modules.
if (ESM_REGEX.test(source)) {
transforms = ['imports', 'jsx']; // JSX within .js is currently allowed
}
} else if (filePath.endsWith('.jsx')) {
transforms = ['jsx', 'imports'];
} else if (filePath.endsWith('.ts')) {
transforms = ['typescript', 'imports'];
} else if (filePath.endsWith('.tsx')) {
transforms = ['typescript', 'jsx', 'imports'];
}
// Only apply the jest transform to the test files themselves
if (transforms && filePath.includes('.test.')) {
transforms.push('jest');
}
if (transforms) {
return transform(source, { transforms, filePath }).code;
}
return source;
}
// TODO: contribute something like this to @sucrase/jest-plugin
function getCacheKey(sourceText) {
return createHash('md5')
.update(sourceText)
.update(Buffer.alloc(1))
.update(sucrasePkg.version)
.update(Buffer.alloc(1))
.update(sucrasePluginPkg.version)
.digest('hex');
}
module.exports = { process, getCacheKey };
@@ -49,10 +49,5 @@
"*.{json,md}": [
"prettier --write"
]
},
"jest": {
"transformModules": [
"@asyncapi/react-component"
]
}
}