UNPKG

@backstage/cli

Version:

CLI for developing Backstage plugins and apps

282 lines (253 loc) • 8.54 kB
/* * Copyright 2020 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 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 envOptions = { oldTests: Boolean(process.env.BACKSTAGE_OLD_TESTS), }; const transformIgnorePattern = [ '@material-ui', 'ajv', 'core-js', 'jest-.*', 'jsdom', 'knex', 'react', 'react-dom', 'highlight\\.js', 'prismjs', 'json-schema', 'react-use', 'typescript', ].join('|'); // Provides additional config that's based on the role of the target package function getRoleConfig(role) { switch (role) { case 'frontend': case 'web-library': case 'common-library': case 'frontend-plugin': case 'frontend-plugin-module': return { testEnvironment: require.resolve('jest-environment-jsdom') }; case 'cli': case 'backend': case 'node-library': case 'backend-plugin': case 'backend-plugin-module': default: return { testEnvironment: require.resolve('jest-environment-node') }; } } async function getProjectConfig(targetPath, extraConfig) { const configJsPath = path.resolve(targetPath, 'jest.config.js'); const configTsPath = path.resolve(targetPath, 'jest.config.ts'); // If the package has it's own jest config, we use that instead. if (await fs.pathExists(configJsPath)) { return require(configJsPath); } else if (await fs.pathExists(configTsPath)) { return require(configTsPath); } // We read all "jest" config fields in package.json files all the way to the filesystem root. // All configs are merged together to create the final config, with longer paths taking precedence. // The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined. const pkgJsonConfigs = []; let closestPkgJson = undefined; let currentPath = targetPath; // Some confidence check to avoid infinite loop for (let i = 0; i < 100; i++) { const packagePath = path.resolve(currentPath, 'package.json'); const exists = fs.pathExistsSync(packagePath); if (exists) { try { const data = fs.readJsonSync(packagePath); if (!closestPkgJson) { closestPkgJson = data; } if (data.jest) { pkgJsonConfigs.unshift(data.jest); } } catch (error) { throw new Error( `Failed to parse package.json file reading jest configs, ${error}`, ); } } const newPath = path.dirname(currentPath); if (newPath === currentPath) { break; } currentPath = newPath; } // This is an old deprecated option that is no longer used. const transformModules = pkgJsonConfigs .flatMap(conf => { const modules = conf.transformModules || []; delete conf.transformModules; return modules; }) .map(name => `${name}/`) .join('|'); 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 = { ...extraConfig, rootDir: path.resolve(targetPath, 'src'), moduleNameMapper: { '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'), }, transform: { '\\.(mjs|cjs|js)$': [ require.resolve('./jestSwcTransform'), { jsc: { parser: { syntax: 'ecmascript', }, }, }, ], '\\.jsx$': [ require.resolve('./jestSwcTransform'), { jsc: { parser: { syntax: 'ecmascript', jsx: true, }, transform: { react: { runtime: 'automatic', }, }, }, }, ], '\\.ts$': [ require.resolve('./jestSwcTransform'), { jsc: { parser: { syntax: 'typescript', }, }, }, ], '\\.tsx$': [ require.resolve('./jestSwcTransform'), { jsc: { parser: { syntax: 'typescript', tsx: true, }, transform: { react: { runtime: 'automatic', }, }, }, }, ], '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$': require.resolve('./jestFileTransform.js'), '\\.(yaml)$': require.resolve('./jestYamlTransform'), }, // A bit more opinionated testMatch: ['**/*.test.{js,jsx,ts,tsx,mjs,cjs}'], runtime: envOptions.oldTests ? undefined : require.resolve('./jestCachingModuleLoader'), transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`], ...getRoleConfig(closestPkgJson?.backstage?.role), }; // Use src/setupTests.ts as the default location for configuring test env if (fs.existsSync(path.resolve(targetPath, 'src/setupTests.ts'))) { options.setupFilesAfterEnv = ['<rootDir>/setupTests.ts']; } const config = Object.assign(options, ...pkgJsonConfigs); // The config id is a cache key that lets us share the jest cache across projects. // If no explicit id was configured, generated one based on the configuration. if (!config.id) { const configHash = crypto .createHash('md5') .update(version) .update(Buffer.alloc(1)) .update(JSON.stringify(config.transform)) .digest('hex'); config.id = `backstage_cli_${configHash}`; } return config; } // This loads the root jest config, which in turn will either refer to a single // configuration for the current package, or a collection of configurations for // the target workspace packages async function getRootConfig() { const targetPath = process.cwd(); const targetPackagePath = path.resolve(targetPath, 'package.json'); const exists = await fs.pathExists(targetPackagePath); const coverageConfig = { coverageDirectory: path.resolve(targetPath, 'coverage'), coverageProvider: envOptions.oldTests ? 'v8' : 'babel', collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'], }; if (!exists) { return getProjectConfig(targetPath, coverageConfig); } // Check whether the current package is a workspace root or not const data = await fs.readJson(targetPackagePath); const workspacePatterns = data.workspaces && data.workspaces.packages; if (!workspacePatterns) { return getProjectConfig(targetPath, coverageConfig); } // If the target package is a workspace root, we find all packages in the // workspace and load those in as separate jest projects instead. const projectPaths = await Promise.all( workspacePatterns.map(pattern => glob(path.join(targetPath, pattern))), ).then(_ => _.flat()); const configs = await Promise.all( projectPaths.flat().map(async projectPath => { const packagePath = path.resolve(projectPath, 'package.json'); if (!(await fs.pathExists(packagePath))) { return undefined; } // We check for the presence of "backstage-cli test" in the package test // script to determine whether a given package should be tested const packageData = await fs.readJson(packagePath); const testScript = packageData.scripts && packageData.scripts.test; const isSupportedTestScript = testScript?.includes('backstage-cli test') || testScript?.includes('backstage-cli package test'); if (testScript && isSupportedTestScript) { return await getProjectConfig(projectPath, { displayName: packageData.name, }); } return undefined; }), ).then(cs => cs.filter(Boolean)); return { rootDir: targetPath, projects: configs, ...coverageConfig, }; } module.exports = getRootConfig();