UNPKG

8.54 kBJavaScriptView Raw
1/*
2 * Copyright 2020 The Backstage Authors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17const fs = require('fs-extra');
18const path = require('path');
19const crypto = require('crypto');
20const glob = require('util').promisify(require('glob'));
21const { version } = require('../package.json');
22
23const envOptions = {
24 oldTests: Boolean(process.env.BACKSTAGE_OLD_TESTS),
25};
26
27const transformIgnorePattern = [
28 '@material-ui',
29 'ajv',
30 'core-js',
31 'jest-.*',
32 'jsdom',
33 'knex',
34 'react',
35 'react-dom',
36 'highlight\\.js',
37 'prismjs',
38 'json-schema',
39 'react-use',
40 'typescript',
41].join('|');
42
43// Provides additional config that's based on the role of the target package
44function getRoleConfig(role) {
45 switch (role) {
46 case 'frontend':
47 case 'web-library':
48 case 'common-library':
49 case 'frontend-plugin':
50 case 'frontend-plugin-module':
51 return { testEnvironment: require.resolve('jest-environment-jsdom') };
52 case 'cli':
53 case 'backend':
54 case 'node-library':
55 case 'backend-plugin':
56 case 'backend-plugin-module':
57 default:
58 return { testEnvironment: require.resolve('jest-environment-node') };
59 }
60}
61
62async function getProjectConfig(targetPath, extraConfig) {
63 const configJsPath = path.resolve(targetPath, 'jest.config.js');
64 const configTsPath = path.resolve(targetPath, 'jest.config.ts');
65 // If the package has it's own jest config, we use that instead.
66 if (await fs.pathExists(configJsPath)) {
67 return require(configJsPath);
68 } else if (await fs.pathExists(configTsPath)) {
69 return require(configTsPath);
70 }
71
72 // We read all "jest" config fields in package.json files all the way to the filesystem root.
73 // All configs are merged together to create the final config, with longer paths taking precedence.
74 // The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined.
75 const pkgJsonConfigs = [];
76 let closestPkgJson = undefined;
77 let currentPath = targetPath;
78
79 // Some confidence check to avoid infinite loop
80 for (let i = 0; i < 100; i++) {
81 const packagePath = path.resolve(currentPath, 'package.json');
82 const exists = fs.pathExistsSync(packagePath);
83 if (exists) {
84 try {
85 const data = fs.readJsonSync(packagePath);
86 if (!closestPkgJson) {
87 closestPkgJson = data;
88 }
89 if (data.jest) {
90 pkgJsonConfigs.unshift(data.jest);
91 }
92 } catch (error) {
93 throw new Error(
94 `Failed to parse package.json file reading jest configs, ${error}`,
95 );
96 }
97 }
98
99 const newPath = path.dirname(currentPath);
100 if (newPath === currentPath) {
101 break;
102 }
103 currentPath = newPath;
104 }
105
106 // This is an old deprecated option that is no longer used.
107 const transformModules = pkgJsonConfigs
108 .flatMap(conf => {
109 const modules = conf.transformModules || [];
110 delete conf.transformModules;
111 return modules;
112 })
113 .map(name => `${name}/`)
114 .join('|');
115 if (transformModules.length > 0) {
116 console.warn(
117 'The Backstage CLI jest transformModules option is no longer used and will be ignored. All modules are now always transformed.',
118 );
119 }
120
121 const options = {
122 ...extraConfig,
123 rootDir: path.resolve(targetPath, 'src'),
124 moduleNameMapper: {
125 '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
126 },
127
128 transform: {
129 '\\.(mjs|cjs|js)$': [
130 require.resolve('./jestSwcTransform'),
131 {
132 jsc: {
133 parser: {
134 syntax: 'ecmascript',
135 },
136 },
137 },
138 ],
139 '\\.jsx$': [
140 require.resolve('./jestSwcTransform'),
141 {
142 jsc: {
143 parser: {
144 syntax: 'ecmascript',
145 jsx: true,
146 },
147 transform: {
148 react: {
149 runtime: 'automatic',
150 },
151 },
152 },
153 },
154 ],
155 '\\.ts$': [
156 require.resolve('./jestSwcTransform'),
157 {
158 jsc: {
159 parser: {
160 syntax: 'typescript',
161 },
162 },
163 },
164 ],
165 '\\.tsx$': [
166 require.resolve('./jestSwcTransform'),
167 {
168 jsc: {
169 parser: {
170 syntax: 'typescript',
171 tsx: true,
172 },
173 transform: {
174 react: {
175 runtime: 'automatic',
176 },
177 },
178 },
179 },
180 ],
181 '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$':
182 require.resolve('./jestFileTransform.js'),
183 '\\.(yaml)$': require.resolve('./jestYamlTransform'),
184 },
185
186 // A bit more opinionated
187 testMatch: ['**/*.test.{js,jsx,ts,tsx,mjs,cjs}'],
188
189 runtime: envOptions.oldTests
190 ? undefined
191 : require.resolve('./jestCachingModuleLoader'),
192
193 transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`],
194 ...getRoleConfig(closestPkgJson?.backstage?.role),
195 };
196
197 // Use src/setupTests.ts as the default location for configuring test env
198 if (fs.existsSync(path.resolve(targetPath, 'src/setupTests.ts'))) {
199 options.setupFilesAfterEnv = ['<rootDir>/setupTests.ts'];
200 }
201
202 const config = Object.assign(options, ...pkgJsonConfigs);
203
204 // The config id is a cache key that lets us share the jest cache across projects.
205 // If no explicit id was configured, generated one based on the configuration.
206 if (!config.id) {
207 const configHash = crypto
208 .createHash('md5')
209 .update(version)
210 .update(Buffer.alloc(1))
211 .update(JSON.stringify(config.transform))
212 .digest('hex');
213 config.id = `backstage_cli_${configHash}`;
214 }
215
216 return config;
217}
218
219// This loads the root jest config, which in turn will either refer to a single
220// configuration for the current package, or a collection of configurations for
221// the target workspace packages
222async function getRootConfig() {
223 const targetPath = process.cwd();
224 const targetPackagePath = path.resolve(targetPath, 'package.json');
225 const exists = await fs.pathExists(targetPackagePath);
226
227 const coverageConfig = {
228 coverageDirectory: path.resolve(targetPath, 'coverage'),
229 coverageProvider: envOptions.oldTests ? 'v8' : 'babel',
230 collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'],
231 };
232
233 if (!exists) {
234 return getProjectConfig(targetPath, coverageConfig);
235 }
236
237 // Check whether the current package is a workspace root or not
238 const data = await fs.readJson(targetPackagePath);
239 const workspacePatterns = data.workspaces && data.workspaces.packages;
240 if (!workspacePatterns) {
241 return getProjectConfig(targetPath, coverageConfig);
242 }
243
244 // If the target package is a workspace root, we find all packages in the
245 // workspace and load those in as separate jest projects instead.
246 const projectPaths = await Promise.all(
247 workspacePatterns.map(pattern => glob(path.join(targetPath, pattern))),
248 ).then(_ => _.flat());
249
250 const configs = await Promise.all(
251 projectPaths.flat().map(async projectPath => {
252 const packagePath = path.resolve(projectPath, 'package.json');
253 if (!(await fs.pathExists(packagePath))) {
254 return undefined;
255 }
256
257 // We check for the presence of "backstage-cli test" in the package test
258 // script to determine whether a given package should be tested
259 const packageData = await fs.readJson(packagePath);
260 const testScript = packageData.scripts && packageData.scripts.test;
261 const isSupportedTestScript =
262 testScript?.includes('backstage-cli test') ||
263 testScript?.includes('backstage-cli package test');
264 if (testScript && isSupportedTestScript) {
265 return await getProjectConfig(projectPath, {
266 displayName: packageData.name,
267 });
268 }
269
270 return undefined;
271 }),
272 ).then(cs => cs.filter(Boolean));
273
274 return {
275 rootDir: targetPath,
276 projects: configs,
277 ...coverageConfig,
278 };
279}
280
281module.exports = getRootConfig();