UNPKG

7.55 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 nextTests: Boolean(process.env.BACKSTAGE_NEXT_TESTS),
25 enableSourceMaps: Boolean(process.env.ENABLE_SOURCE_MAPS),
26};
27
28const transformIgnorePattern = [
29 '@material-ui',
30 '@rjsf',
31 'ajv',
32 'core-js',
33 'jest-.*',
34 'jsdom',
35 'knex',
36 'react',
37 'react-dom',
38 'highlight\\.js',
39 'prismjs',
40 'react-use',
41 'typescript',
42].join('|');
43
44// Provides additional config that's based on the role of the target package
45function getRoleConfig(role) {
46 switch (role) {
47 case 'frontend':
48 case 'web-library':
49 case 'common-library':
50 case 'frontend-plugin':
51 case 'frontend-plugin-module':
52 return { testEnvironment: 'jsdom' };
53 case 'cli':
54 case 'backend':
55 case 'node-library':
56 case 'backend-plugin':
57 case 'backend-plugin-module':
58 default:
59 return { testEnvironment: 'node' };
60 }
61}
62
63async function getProjectConfig(targetPath, displayName) {
64 const configJsPath = path.resolve(targetPath, 'jest.config.js');
65 const configTsPath = path.resolve(targetPath, 'jest.config.ts');
66 // If the package has it's own jest config, we use that instead.
67 if (await fs.pathExists(configJsPath)) {
68 return require(configJsPath);
69 } else if (await fs.pathExists(configTsPath)) {
70 return require(configTsPath);
71 }
72
73 // We read all "jest" config fields in package.json files all the way to the filesystem root.
74 // All configs are merged together to create the final config, with longer paths taking precedence.
75 // The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined.
76 const pkgJsonConfigs = [];
77 let closestPkgJson = undefined;
78 let currentPath = targetPath;
79
80 // Some sanity check to avoid infinite loop
81 for (let i = 0; i < 100; i++) {
82 const packagePath = path.resolve(currentPath, 'package.json');
83 const exists = fs.pathExistsSync(packagePath);
84 if (exists) {
85 try {
86 const data = fs.readJsonSync(packagePath);
87 if (!closestPkgJson) {
88 closestPkgJson = data;
89 }
90 if (data.jest) {
91 pkgJsonConfigs.unshift(data.jest);
92 }
93 } catch (error) {
94 throw new Error(
95 `Failed to parse package.json file reading jest configs, ${error}`,
96 );
97 }
98 }
99
100 const newPath = path.dirname(currentPath);
101 if (newPath === currentPath) {
102 break;
103 }
104 currentPath = newPath;
105 }
106
107 // This is an old deprecated option that is no longer used.
108 const transformModules = pkgJsonConfigs
109 .flatMap(conf => {
110 const modules = conf.transformModules || [];
111 delete conf.transformModules;
112 return modules;
113 })
114 .map(name => `${name}/`)
115 .join('|');
116 if (transformModules.length > 0) {
117 console.warn(
118 'The Backstage CLI jest transformModules option is no longer used and will be ignored. All modules are now always transformed.',
119 );
120 }
121
122 const options = {
123 ...(displayName && { displayName }),
124 rootDir: path.resolve(targetPath, 'src'),
125 coverageDirectory: path.resolve(targetPath, 'coverage'),
126 coverageProvider: envOptions.nextTests ? 'babel' : 'v8',
127 collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'],
128 moduleNameMapper: {
129 '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
130 },
131
132 transform: {
133 '\\.(js|jsx|ts|tsx|mjs|cjs)$': [
134 require.resolve('./jestSucraseTransform.js'),
135 {
136 enableSourceMaps: envOptions.enableSourceMaps || envOptions.nextTests,
137 },
138 ],
139 '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$':
140 require.resolve('./jestFileTransform.js'),
141 '\\.(yaml)$': require.resolve('jest-transform-yaml'),
142 },
143
144 // A bit more opinionated
145 testMatch: ['**/*.test.{js,jsx,ts,tsx,mjs,cjs}'],
146
147 moduleLoader: envOptions.nextTests
148 ? require.resolve('./jestCachingModuleLoader')
149 : undefined,
150
151 transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`],
152
153 ...getRoleConfig(closestPkgJson?.backstage?.role),
154 };
155
156 // Use src/setupTests.ts as the default location for configuring test env
157 if (fs.existsSync(path.resolve(targetPath, 'src/setupTests.ts'))) {
158 options.setupFilesAfterEnv = ['<rootDir>/setupTests.ts'];
159 }
160
161 const config = Object.assign(options, ...pkgJsonConfigs);
162
163 // The config name is a cache key that lets us share the jest cache across projects.
164 // If no explicit name was configured, generated one based on the configuration.
165 if (!config.name) {
166 const configHash = crypto
167 .createHash('md5')
168 .update(version)
169 .update(Buffer.alloc(1))
170 .update(JSON.stringify(config.transform))
171 .digest('hex');
172 config.name = `backstage_cli_${configHash}`;
173 }
174
175 return config;
176}
177
178// This loads the root jest config, which in turn will either refer to a single
179// configuration for the current package, or a collection of configurations for
180// the target workspace packages
181async function getRootConfig() {
182 const targetPath = process.cwd();
183 const targetPackagePath = path.resolve(targetPath, 'package.json');
184 const exists = await fs.pathExists(targetPackagePath);
185
186 if (!exists) {
187 return getProjectConfig(targetPath);
188 }
189
190 // Check whether the current package is a workspace root or not
191 const data = await fs.readJson(targetPackagePath);
192 const workspacePatterns = data.workspaces && data.workspaces.packages;
193 if (!workspacePatterns) {
194 return getProjectConfig(targetPath);
195 }
196
197 // If the target package is a workspace root, we find all packages in the
198 // workspace and load those in as separate jest projects instead.
199 const projectPaths = await Promise.all(
200 workspacePatterns.map(pattern => glob(path.join(targetPath, pattern))),
201 ).then(_ => _.flat());
202
203 const configs = await Promise.all(
204 projectPaths.flat().map(async projectPath => {
205 const packagePath = path.resolve(projectPath, 'package.json');
206 if (!(await fs.pathExists(packagePath))) {
207 return undefined;
208 }
209
210 // We check for the presence of "backstage-cli test" in the package test
211 // script to determine whether a given package should be tested
212 const packageData = await fs.readJson(packagePath);
213 const testScript = packageData.scripts && packageData.scripts.test;
214 const isSupportedTestScript =
215 testScript?.includes('backstage-cli test') ||
216 testScript?.includes('backstage-cli package test');
217 if (testScript && isSupportedTestScript) {
218 return await getProjectConfig(projectPath, packageData.name);
219 }
220
221 return undefined;
222 }),
223 ).then(cs => cs.filter(Boolean));
224
225 return {
226 rootDir: targetPath,
227 projects: configs,
228 };
229}
230
231module.exports = getRootConfig();