Skip to content

Commit

Permalink
fix(testing): migration for getJestProjects -> getJestProjectsAsync h…
Browse files Browse the repository at this point in the history
…andles 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.

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
  • Loading branch information
jaysoo authored Oct 4, 2024
1 parent 27beba8 commit f743808
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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',
`
Expand All @@ -26,14 +26,86 @@ 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(),
});
"
`);
});

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',
`
Expand All @@ -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,
Expand All @@ -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;
"
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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(
Expand All @@ -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) &&
Expand All @@ -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
);

Expand All @@ -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);
}
}
}
}
Expand Down

0 comments on commit f743808

Please sign in to comment.