From f743808bd58c563d74d374e35c4cac58271a57d9 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Fri, 4 Oct 2024 11:04:21 -0400 Subject: [PATCH] fix(testing): migration for getJestProjects -> getJestProjectsAsync handles both CJS and ESM (#28299) This PR updates the Jest migration so it handles both CJS and ESM format for Jest config file. We now generate with ESM so those need to be handled. There are four combinations: 1. `require` (CJS) with `module.export` (CJS) 2. `import` (ESM) with `export default` (ESM) 3. `require` (CJS) with `export default` (ESM) 4. `import` (ESM) with `module.export` (CJS) (1) and (2) should cover almost all cases, and (3) and (4) are there just in case. If the format isn't matching what we generate, then just bail. ## Current Behavior ## Expected Behavior ## Related Issue(s) Fixes # --- ...Projects-with-getJestProjectsAsync.spec.ts | 132 +++++++++++++++++- ...tJestProjects-with-getJestProjectsAsync.ts | 66 +++++++-- 2 files changed, 185 insertions(+), 13 deletions(-) diff --git a/packages/jest/src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync.spec.ts b/packages/jest/src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync.spec.ts index e654118a65884..b136c3b05a1c8 100644 --- a/packages/jest/src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync.spec.ts +++ b/packages/jest/src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync.spec.ts @@ -9,7 +9,7 @@ describe('replace-getJestProjects-with-getJestProjectsAsync', () => { tree = createTree(); }); - it('should replace getJestProjects with getJestProjectsAsync', async () => { + it('should replace getJestProjects with getJestProjectsAsync using `require`', async () => { tree.write( 'jest.config.ts', ` @@ -26,6 +26,30 @@ describe('replace-getJestProjects-with-getJestProjectsAsync', () => { " const { getJestProjectsAsync } = require('@nx/jest'); + module.exports = async () => ({ + projects: await getJestProjectsAsync(), + }); + " + `); + }); + + it('should replace getJestProjects with getJestProjectsAsync using `import`', async () => { + tree.write( + 'jest.config.ts', + ` + import { getJestProjects } from '@nx/jest'; + + export default { + projects: getJestProjects(), + }; + ` + ); + await update(tree); + const updatedJestConfig = tree.read('jest.config.ts')?.toString(); + expect(updatedJestConfig).toMatchInlineSnapshot(` + " + import { getJestProjectsAsync } from '@nx/jest'; + export default async () => ({ projects: await getJestProjectsAsync(), }); @@ -33,7 +57,55 @@ describe('replace-getJestProjects-with-getJestProjectsAsync', () => { `); }); - it('should replace getJestProjects with getJestProjectsAsync with additonal properties', async () => { + it('should replace getJestProjects with getJestProjectsAsync using `require` with `export default`', async () => { + tree.write( + 'jest.config.ts', + ` + const { getJestProjects } = require('@nx/jest'); + + export default { + projects: getJestProjects(), + }; + ` + ); + await update(tree); + const updatedJestConfig = tree.read('jest.config.ts')?.toString(); + expect(updatedJestConfig).toMatchInlineSnapshot(` + " + const { getJestProjectsAsync } = require('@nx/jest'); + + export default async () => ({ + projects: await getJestProjectsAsync(), + }); + " + `); + }); + + it('should replace getJestProjects with getJestProjectsAsync using `import` with `module.exports`', async () => { + tree.write( + 'jest.config.ts', + ` + import { getJestProjects } from '@nx/jest'; + + module.exports = { + projects: getJestProjects(), + }; + ` + ); + await update(tree); + const updatedJestConfig = tree.read('jest.config.ts')?.toString(); + expect(updatedJestConfig).toMatchInlineSnapshot(` + " + import { getJestProjectsAsync } from '@nx/jest'; + + module.exports = async () => ({ + projects: await getJestProjectsAsync(), + }); + " + `); + }); + + it('should replace getJestProjects with getJestProjectsAsync with additional properties', async () => { tree.write( 'jest.config.ts', ` @@ -53,7 +125,7 @@ describe('replace-getJestProjects-with-getJestProjectsAsync', () => { " const { getJestProjectsAsync } = require('@nx/jest'); - export default async () => ({ + module.exports = async () => ({ projects: await getJestProjectsAsync(), filename: __filename, env: process.env, @@ -62,4 +134,58 @@ describe('replace-getJestProjects-with-getJestProjectsAsync', () => { " `); }); + + it('should not update config that are not in supported format', async () => { + // Users don't tend to update the root jest config file since it's only meant to be able to run + // `jest` command from the root of the repo. If the AST doesn't match what we generate + // then bail on the update. Users will still see that `getJestProjects` is deprecated when + // viewing the file. + tree.write( + 'jest.config.ts', + ` + import { getJestProjects } from '@nx/jest'; + + const obj = { + projects: getJestProjects(), + }; + export default obj + ` + ); + await update(tree); + let updatedJestConfig = tree.read('jest.config.ts')?.toString(); + expect(updatedJestConfig).toMatchInlineSnapshot(` + " + import { getJestProjects } from '@nx/jest'; + + const obj = { + projects: getJestProjects(), + }; + export default obj + " + `); + + tree.write( + 'jest.config.ts', + ` + const { getJestProjects } = require('@nx/jest'); + + const obj = { + projects: getJestProjects(), + }; + module.exports = obj; + ` + ); + await update(tree); + updatedJestConfig = tree.read('jest.config.ts')?.toString(); + expect(updatedJestConfig).toMatchInlineSnapshot(` + " + const { getJestProjects } = require('@nx/jest'); + + const obj = { + projects: getJestProjects(), + }; + module.exports = obj; + " + `); + }); }); diff --git a/packages/jest/src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync.ts b/packages/jest/src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync.ts index ae005222f1cdc..a607beda9c617 100644 --- a/packages/jest/src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync.ts +++ b/packages/jest/src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync.ts @@ -4,7 +4,11 @@ import { globAsync, Tree } from '@nx/devkit'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; -import { BinaryExpression, ExpressionStatement } from 'typescript'; +import { + BinaryExpression, + ExpressionStatement, + ExportAssignment, +} from 'typescript'; let tsModule: typeof import('typescript'); @@ -26,8 +30,8 @@ export default async function update(tree: Tree) { true ); - // find the import statement for @nx/jest - const importStatement = sourceFile.statements.find( + // find `require('@nx/jest')` or `import { getJestProjects } from '@nx/jest` + const requireStatement = sourceFile.statements.find( (statement) => tsModule.isVariableStatement(statement) && statement.declarationList.declarations.some( @@ -39,9 +43,14 @@ export default async function update(tree: Tree) { declaration.initializer.arguments[0].text === '@nx/jest' ) ); - if (importStatement) { - // find export statement with `projects: getJestProjects()` - let exportStatement = sourceFile.statements.find( + const importStatement = sourceFile.statements.find( + (statement) => + tsModule.isImportDeclaration(statement) && + statement.moduleSpecifier.getText() === `'@nx/jest'` + ); + if (requireStatement || importStatement) { + // find `module.exports` statement with `projects: getJestProjects()` + const moduleExports = sourceFile.statements.find( (statement) => tsModule.isExpressionStatement(statement) && tsModule.isBinaryExpression(statement.expression) && @@ -65,18 +74,18 @@ export default async function update(tree: Tree) { ) ) as ExpressionStatement; - if (exportStatement) { + if (moduleExports) { // replace getJestProjects with getJestProjectsAsync in export statement const rightExpression = ( - exportStatement.expression as BinaryExpression + moduleExports.expression as BinaryExpression ).right.getText(); const newExpression = rightExpression.replace( 'getJestProjects()', 'await getJestProjectsAsync()' ); - const newStatement = `export default async () => (${newExpression});`; + const newStatement = `module.exports = async () => (${newExpression});`; let newContent = oldContent.replace( - exportStatement.getText(), + moduleExports.getText(), newStatement ); @@ -87,6 +96,43 @@ export default async function update(tree: Tree) { ); tree.write(jestConfigPath, newContent); + } else { + // find `export default` statement with `projects: getJestProjects()` + const exportAssignment = sourceFile.statements.find((statement) => + tsModule.isExportAssignment(statement) + ) as ExportAssignment; + const defaultExport = + exportAssignment?.expression && + tsModule.isObjectLiteralExpression(exportAssignment?.expression) + ? exportAssignment?.expression + : null; + const projectProperty = defaultExport?.properties.find( + (property) => + tsModule.isPropertyAssignment(property) && + property.name.getText() === 'projects' && + tsModule.isCallExpression(property.initializer) && + tsModule.isIdentifier(property.initializer.expression) && + property.initializer.expression.escapedText === 'getJestProjects' + ); + if (projectProperty) { + // replace getJestProjects with getJestProjectsAsync in export statement + const newExpression = defaultExport + .getText() + .replace('getJestProjects()', 'await getJestProjectsAsync()'); + const newStatement = `export default async () => (${newExpression});`; + let newContent = oldContent.replace( + exportAssignment.getText(), + newStatement + ); + + // replace getJestProjects with getJestProjectsAsync in import statement + newContent = newContent.replace( + 'getJestProjects', + 'getJestProjectsAsync' + ); + + tree.write(jestConfigPath, newContent); + } } } }