UNPKG

@nx/jest

Version:

The Nx Plugin for Jest contains executors and generators allowing your workspace to use the powerful Jest testing capabilities.

201 lines (200 loc) 9.24 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = convertJestConfigToCjs; const devkit_1 = require("@nx/devkit"); const path_1 = require("path"); /** * Migration to convert jest.config.ts files from ESM to CJS syntax for projects * using CommonJS resolution. This is needed because Node.js type-stripping * in newer versions (22+, 24+) can cause issues with ESM syntax in .ts files * when the project is configured for CommonJS. * * Conversions: * - `export default { ... }` -> `module.exports = { ... }` * - `import { x } from 'y'` -> `const { x } = require('y')` * - `import x from 'y'` -> `const x = require('y').default ?? require('y')` * * ESM-only features that cannot be converted (will warn user): * - `import.meta` * - top-level `await` * * Projects with `type: module` in package.json are skipped and warned about, * as they use ESM semantics and don't need the CJS conversion. */ async function convertJestConfigToCjs(tree) { const { ast: parseAst, query } = require('@phenomnomnominal/tsquery'); const jestConfigPaths = await (0, devkit_1.globAsync)(tree, ['**/jest.config.ts']); const projectsWithEsmOnlyFeatures = []; const projectsWithTypeModule = []; const modifiedFiles = []; for (const configPath of jestConfigPaths) { const projectRoot = (0, path_1.dirname)(configPath); const packageJsonPath = (0, devkit_1.joinPathFragments)(projectRoot, 'package.json'); const rootPackageJsonPath = 'package.json'; // Check project-level package.json first, then root let projectPackageJson = null; let rootPackageJson = null; if (tree.exists(packageJsonPath)) { projectPackageJson = (0, devkit_1.readJson)(tree, packageJsonPath); } if (tree.exists(rootPackageJsonPath)) { rootPackageJson = (0, devkit_1.readJson)(tree, rootPackageJsonPath); } const effectiveType = projectPackageJson?.type ?? rootPackageJson?.type ?? 'commonjs'; // CJS is default if missing // If type is "module", skip conversion: the file already runs under ESM // semantics and does not need the CJS rewrite. if (effectiveType === 'module') { projectsWithTypeModule.push(configPath); continue; } let content = tree.read(configPath, 'utf-8'); // Check for ESM-only features that can't be converted const hasImportMeta = query(content, 'MetaProperty').length > 0 || content.includes('import.meta'); const hasTopLevelAwait = checkForTopLevelAwait(content, parseAst, query); if (hasImportMeta || hasTopLevelAwait) { projectsWithEsmOnlyFeatures.push(configPath); continue; } content = convertImportsToRequire(content, parseAst, query); content = convertExportDefaultToModuleExports(content, query); tree.write(configPath, content); modifiedFiles.push(configPath); } if (modifiedFiles.length > 0) { await (0, devkit_1.formatFiles)(tree); } const hasWarnings = projectsWithEsmOnlyFeatures.length > 0 || projectsWithTypeModule.length > 0; if (hasWarnings) { return () => { if (projectsWithTypeModule.length > 0) { devkit_1.logger.warn(`The following jest.config.ts files belong to projects with "type": "module" in their package.json ` + `and were left as-is. If you use @nx/jest/plugin, it forces CommonJS resolution, so consider ` + `removing "type": "module" or using a different Jest configuration approach:\n` + projectsWithTypeModule.map((p) => ` - ${p}`).join('\n')); } if (projectsWithEsmOnlyFeatures.length > 0) { devkit_1.logger.warn(`The following jest.config.ts files use ESM-only features (import.meta or top-level await) ` + `and could not be automatically converted to CommonJS. Please update them manually:\n` + projectsWithEsmOnlyFeatures.map((p) => ` - ${p}`).join('\n')); } }; } } function checkForTopLevelAwait(content, parseAst, query) { const ts = require('typescript'); // Check for await expressions that are not inside a function const sourceFile = parseAst(content); const awaitExpressions = query(sourceFile, 'AwaitExpression'); for (const awaitExpr of awaitExpressions) { let parent = awaitExpr.parent; let isInsideFunction = false; while (parent) { if (ts.isFunctionLike(parent)) { isInsideFunction = true; break; } parent = parent.parent; } if (!isInsideFunction) { return true; } } return false; } function convertImportsToRequire(content, parseAst, query) { const ts = require('typescript'); const sourceFile = parseAst(content); const importDeclarations = query(sourceFile, 'ImportDeclaration'); if (importDeclarations.length === 0) { return content; } // Sort imports by position (descending) to replace from end to start // This preserves positions of earlier nodes const sortedImports = [...importDeclarations].sort((a, b) => b.getStart() - a.getStart()); for (const importDecl of sortedImports) { const moduleSpecifier = importDecl.moduleSpecifier .getText() .replace(/['"]/g, ''); const importClause = importDecl.importClause; if (!importClause) { // Side-effect import: import 'module' const requireStatement = `require('${moduleSpecifier}')`; content = replaceNode(content, importDecl, requireStatement); continue; } // `import type ...` is erased by Node's type-stripping at runtime, so it // can remain in the file untouched — this preserves editor/tsc type safety. if (importClause.isTypeOnly) { continue; } const parts = []; const typeOnlySpecifiers = []; // Default import: import x from 'module' if (importClause.name) { const defaultName = importClause.name.getText(); parts.push(`const ${defaultName} = require('${moduleSpecifier}').default ?? require('${moduleSpecifier}')`); } if (importClause.namedBindings) { if (ts.isNamedImports(importClause.namedBindings)) { const valueSpecifiers = []; for (const element of importClause.namedBindings.elements) { const name = element.name.getText(); const propertyName = element.propertyName?.getText(); if (element.isTypeOnly) { // Inline `type` modifier: `import { type Foo, bar } from 'x'`. // Preserve the type-only portion so type references still resolve. typeOnlySpecifiers.push(propertyName ? `${propertyName} as ${name}` : name); } else { valueSpecifiers.push(propertyName ? `${propertyName}: ${name}` : name); } } if (valueSpecifiers.length) { parts.push(`const { ${valueSpecifiers.join(', ')} } = require('${moduleSpecifier}')`); } } else if (ts.isNamespaceImport(importClause.namedBindings)) { // Namespace import: import * as x from 'module' const namespaceName = importClause.namedBindings.name.getText(); parts.push(`const ${namespaceName} = require('${moduleSpecifier}')`); } } const replacementParts = []; if (typeOnlySpecifiers.length) { replacementParts.push(`import type { ${typeOnlySpecifiers.join(', ')} } from '${moduleSpecifier}'`); } replacementParts.push(...parts); if (replacementParts.length === 0) { continue; } const replacement = replacementParts.join(';\n'); content = replaceNode(content, importDecl, replacement); } return content; } function convertExportDefaultToModuleExports(content, query) { // Handle: export default { ... } const exportAssignments = query(content, 'ExportAssignment'); if (exportAssignments.length > 0) { for (const exportAssignment of exportAssignments) { const expression = exportAssignment.expression; if (expression) { const exportedValue = expression.getText(); const replacement = `module.exports = ${exportedValue}`; content = replaceNode(content, exportAssignment, replacement); } } } return content; } function replaceNode(content, node, replacement) { const start = node.getStart(); const end = node.getEnd(); // Remove trailing semicolon if present to avoid double semicolons let endPos = end; if (content[end] === ';') { endPos = end + 1; } return content.slice(0, start) + replacement + ';' + content.slice(endPos); }