@nx/jest 22.2.0-beta.0 → 22.2.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/migrations.json CHANGED
@@ -29,6 +29,11 @@
29
29
  },
30
30
  "description": "Replace removed matcher aliases in Jest v30 with their corresponding matcher",
31
31
  "implementation": "./src/migrations/update-21-3-0/replace-removed-matcher-aliases"
32
+ },
33
+ "convert-jest-config-to-cjs": {
34
+ "version": "22.2.0-beta.2",
35
+ "description": "Convert jest.config.ts files from ESM to CJS syntax (export default -> module.exports, import -> require) for projects using CommonJS resolution to ensure correct loading under Node.js type-stripping.",
36
+ "implementation": "./src/migrations/update-22-2-0/convert-jest-config-to-cjs"
32
37
  }
33
38
  },
34
39
  "packageJsonUpdates": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nx/jest",
3
- "version": "22.2.0-beta.0",
3
+ "version": "22.2.0-beta.2",
4
4
  "private": false,
5
5
  "description": "The Nx Plugin for Jest contains executors and generators allowing your workspace to use the powerful Jest testing capabilities.",
6
6
  "repository": {
@@ -37,8 +37,8 @@
37
37
  "dependencies": {
38
38
  "@jest/reporters": "^30.0.2",
39
39
  "@jest/test-result": "^30.0.2",
40
- "@nx/devkit": "22.2.0-beta.0",
41
- "@nx/js": "22.2.0-beta.0",
40
+ "@nx/devkit": "22.2.0-beta.2",
41
+ "@nx/js": "22.2.0-beta.2",
42
42
  "@phenomnomnominal/tsquery": "~5.0.1",
43
43
  "identity-obj-proxy": "3.0.0",
44
44
  "jest-config": "^30.0.2",
@@ -52,7 +52,7 @@
52
52
  "yargs-parser": "21.1.1"
53
53
  },
54
54
  "devDependencies": {
55
- "nx": "22.2.0-beta.0"
55
+ "nx": "22.2.0-beta.2"
56
56
  },
57
57
  "publishConfig": {
58
58
  "access": "public"
@@ -0,0 +1,23 @@
1
+ import { Tree } from '@nx/devkit';
2
+ /**
3
+ * Migration to convert jest.config.ts files from ESM to CJS syntax for projects
4
+ * using CommonJS resolution. This is needed because Node.js type-stripping
5
+ * in newer versions (22+, 24+) can cause issues with ESM syntax in .ts files
6
+ * when the project is configured for CommonJS.
7
+ *
8
+ * This migration only runs if @nx/jest/plugin is registered in nx.json.
9
+ *
10
+ * Conversions:
11
+ * - `export default { ... }` -> `module.exports = { ... }`
12
+ * - `import { x } from 'y'` -> `const { x } = require('y')`
13
+ * - `import x from 'y'` -> `const x = require('y').default ?? require('y')`
14
+ *
15
+ * ESM-only features that cannot be converted (will warn user):
16
+ * - `import.meta`
17
+ * - top-level `await`
18
+ *
19
+ * Projects with `type: module` in package.json will be warned as they are
20
+ * incompatible with @nx/jest/plugin which forces CommonJS resolution.
21
+ */
22
+ export default function convertJestConfigToCjs(tree: Tree): Promise<() => void>;
23
+ //# sourceMappingURL=convert-jest-config-to-cjs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert-jest-config-to-cjs.d.ts","sourceRoot":"","sources":["../../../../../../packages/jest/src/migrations/update-22-2-0/convert-jest-config-to-cjs.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,IAAI,EACL,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAA8B,sBAAsB,CAAC,IAAI,EAAE,IAAI,uBA+F9D"}
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = convertJestConfigToCjs;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const find_plugin_for_config_file_1 = require("@nx/devkit/src/utils/find-plugin-for-config-file");
6
+ const path_1 = require("path");
7
+ /**
8
+ * Migration to convert jest.config.ts files from ESM to CJS syntax for projects
9
+ * using CommonJS resolution. This is needed because Node.js type-stripping
10
+ * in newer versions (22+, 24+) can cause issues with ESM syntax in .ts files
11
+ * when the project is configured for CommonJS.
12
+ *
13
+ * This migration only runs if @nx/jest/plugin is registered in nx.json.
14
+ *
15
+ * Conversions:
16
+ * - `export default { ... }` -> `module.exports = { ... }`
17
+ * - `import { x } from 'y'` -> `const { x } = require('y')`
18
+ * - `import x from 'y'` -> `const x = require('y').default ?? require('y')`
19
+ *
20
+ * ESM-only features that cannot be converted (will warn user):
21
+ * - `import.meta`
22
+ * - top-level `await`
23
+ *
24
+ * Projects with `type: module` in package.json will be warned as they are
25
+ * incompatible with @nx/jest/plugin which forces CommonJS resolution.
26
+ */
27
+ async function convertJestConfigToCjs(tree) {
28
+ // If @nx/jest/plugin not used, then there will not be any problems with graph construction, which
29
+ // is what we're trying to address.
30
+ if (!isJestPluginRegistered(tree))
31
+ return;
32
+ const { tsquery } = require('@phenomnomnominal/tsquery');
33
+ const jestConfigPaths = await (0, devkit_1.globAsync)(tree, ['**/jest.config.ts']);
34
+ const projectsWithEsmOnlyFeatures = [];
35
+ const projectsWithTypeModule = [];
36
+ const modifiedFiles = [];
37
+ for (const configPath of jestConfigPaths) {
38
+ // Skip config files that are excluded from the plugin via include/exclude patterns
39
+ const pluginRegistration = await (0, find_plugin_for_config_file_1.findPluginForConfigFile)(tree, '@nx/jest/plugin', configPath);
40
+ if (!pluginRegistration)
41
+ continue;
42
+ const projectRoot = (0, path_1.dirname)(configPath);
43
+ const packageJsonPath = (0, devkit_1.joinPathFragments)(projectRoot, 'package.json');
44
+ const rootPackageJsonPath = 'package.json';
45
+ // Check project-level package.json first, then root
46
+ let projectPackageJson = null;
47
+ let rootPackageJson = null;
48
+ if (tree.exists(packageJsonPath)) {
49
+ projectPackageJson = (0, devkit_1.readJson)(tree, packageJsonPath);
50
+ }
51
+ if (tree.exists(rootPackageJsonPath)) {
52
+ rootPackageJson = (0, devkit_1.readJson)(tree, rootPackageJsonPath);
53
+ }
54
+ const effectiveType = projectPackageJson?.type ?? rootPackageJson?.type ?? 'commonjs'; // CJS is default if missing
55
+ // If type is "module", warn user - this is incompatible with @nx/jest/plugin
56
+ // Should not be possible, but it's possible that there's a way to get this working that we're unaware of
57
+ if (effectiveType === 'module') {
58
+ projectsWithTypeModule.push(configPath);
59
+ continue;
60
+ }
61
+ let content = tree.read(configPath, 'utf-8');
62
+ // Check for ESM-only features that can't be converted
63
+ const hasImportMeta = tsquery.query(content, 'MetaProperty').length > 0 ||
64
+ content.includes('import.meta');
65
+ const hasTopLevelAwait = checkForTopLevelAwait(content, tsquery);
66
+ if (hasImportMeta || hasTopLevelAwait) {
67
+ projectsWithEsmOnlyFeatures.push(configPath);
68
+ continue;
69
+ }
70
+ content = convertImportsToRequire(content, tsquery);
71
+ content = convertExportDefaultToModuleExports(content, tsquery);
72
+ tree.write(configPath, content);
73
+ modifiedFiles.push(configPath);
74
+ }
75
+ if (modifiedFiles.length > 0) {
76
+ await (0, devkit_1.formatFiles)(tree);
77
+ }
78
+ const hasWarnings = projectsWithEsmOnlyFeatures.length > 0 || projectsWithTypeModule.length > 0;
79
+ if (hasWarnings) {
80
+ return () => {
81
+ if (projectsWithTypeModule.length > 0) {
82
+ devkit_1.logger.warn(`The following projects have "type": "module" in their package.json which is incompatible ` +
83
+ `with @nx/jest/plugin. Consider removing "type": "module" ` +
84
+ `or using a different Jest configuration approach:\n` +
85
+ projectsWithTypeModule.map((p) => ` - ${p}`).join('\n'));
86
+ }
87
+ if (projectsWithEsmOnlyFeatures.length > 0) {
88
+ devkit_1.logger.warn(`The following jest.config.ts files use ESM-only features (import.meta or top-level await) ` +
89
+ `and could not be automatically converted to CommonJS. Please update them manually:\n` +
90
+ projectsWithEsmOnlyFeatures.map((p) => ` - ${p}`).join('\n'));
91
+ }
92
+ };
93
+ }
94
+ }
95
+ function checkForTopLevelAwait(content, tsquery) {
96
+ const ts = require('typescript');
97
+ // Check for await expressions that are not inside a function
98
+ const ast = tsquery.ast(content);
99
+ const awaitExpressions = tsquery.query(ast, 'AwaitExpression');
100
+ for (const awaitExpr of awaitExpressions) {
101
+ let parent = awaitExpr.parent;
102
+ let isInsideFunction = false;
103
+ while (parent) {
104
+ if (ts.isFunctionLike(parent)) {
105
+ isInsideFunction = true;
106
+ break;
107
+ }
108
+ parent = parent.parent;
109
+ }
110
+ if (!isInsideFunction) {
111
+ return true;
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+ function convertImportsToRequire(content, tsquery) {
117
+ const ts = require('typescript');
118
+ const ast = tsquery.ast(content);
119
+ const importDeclarations = tsquery.query(ast, 'ImportDeclaration');
120
+ if (importDeclarations.length === 0) {
121
+ return content;
122
+ }
123
+ // Sort imports by position (descending) to replace from end to start
124
+ // This preserves positions of earlier nodes
125
+ const sortedImports = [...importDeclarations].sort((a, b) => b.getStart() - a.getStart());
126
+ for (const importDecl of sortedImports) {
127
+ const moduleSpecifier = importDecl.moduleSpecifier
128
+ .getText()
129
+ .replace(/['"]/g, '');
130
+ const importClause = importDecl.importClause;
131
+ if (!importClause) {
132
+ // Side-effect import: import 'module'
133
+ const requireStatement = `require('${moduleSpecifier}')`;
134
+ content = replaceNode(content, importDecl, requireStatement);
135
+ continue;
136
+ }
137
+ const parts = [];
138
+ // Default import: import x from 'module'
139
+ if (importClause.name) {
140
+ const defaultName = importClause.name.getText();
141
+ parts.push(`const ${defaultName} = require('${moduleSpecifier}').default ?? require('${moduleSpecifier}')`);
142
+ }
143
+ // Named imports: import { a, b } from 'module'
144
+ if (importClause.namedBindings) {
145
+ if (ts.isNamedImports(importClause.namedBindings)) {
146
+ const namedImports = importClause.namedBindings.elements
147
+ .map((element) => {
148
+ const name = element.name.getText();
149
+ const propertyName = element.propertyName?.getText();
150
+ if (propertyName) {
151
+ return `${propertyName}: ${name}`;
152
+ }
153
+ return name;
154
+ })
155
+ .join(', ');
156
+ parts.push(`const { ${namedImports} } = require('${moduleSpecifier}')`);
157
+ }
158
+ else if (ts.isNamespaceImport(importClause.namedBindings)) {
159
+ // Namespace import: import * as x from 'module'
160
+ const namespaceName = importClause.namedBindings.name.getText();
161
+ parts.push(`const ${namespaceName} = require('${moduleSpecifier}')`);
162
+ }
163
+ }
164
+ const requireStatement = parts.join(';\n');
165
+ content = replaceNode(content, importDecl, requireStatement);
166
+ }
167
+ return content;
168
+ }
169
+ function convertExportDefaultToModuleExports(content, tsquery) {
170
+ // Handle: export default { ... }
171
+ const exportAssignments = tsquery.query(content, 'ExportAssignment');
172
+ if (exportAssignments.length > 0) {
173
+ for (const exportAssignment of exportAssignments) {
174
+ const expression = exportAssignment.expression;
175
+ if (expression) {
176
+ const exportedValue = expression.getText();
177
+ const replacement = `module.exports = ${exportedValue}`;
178
+ content = replaceNode(content, exportAssignment, replacement);
179
+ }
180
+ }
181
+ }
182
+ return content;
183
+ }
184
+ function replaceNode(content, node, replacement) {
185
+ const start = node.getStart();
186
+ const end = node.getEnd();
187
+ // Remove trailing semicolon if present to avoid double semicolons
188
+ let endPos = end;
189
+ if (content[end] === ';') {
190
+ endPos = end + 1;
191
+ }
192
+ return content.slice(0, start) + replacement + ';' + content.slice(endPos);
193
+ }
194
+ function isJestPluginRegistered(tree) {
195
+ if (!tree.exists('nx.json')) {
196
+ return false;
197
+ }
198
+ const nxJson = (0, devkit_1.readJson)(tree, 'nx.json');
199
+ const plugins = nxJson.plugins ?? [];
200
+ return plugins.some((plugin) => {
201
+ const pluginName = typeof plugin === 'string' ? plugin : plugin.plugin;
202
+ return pluginName === '@nx/jest/plugin' || pluginName === '@nx/jest';
203
+ });
204
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../../../packages/jest/src/plugins/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EASd,MAAM,YAAY,CAAC;AA6BpB,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAmBD,eAAO,MAAM,WAAW,EAAE,aAAa,CAAC,iBAAiB,CAiFxD,CAAC;AAEF,eAAO,MAAM,aAAa,kCAAc,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../../../packages/jest/src/plugins/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EASd,MAAM,YAAY,CAAC;AA4BpB,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAmBD,eAAO,MAAM,WAAW,EAAE,aAAa,CAAC,iBAAiB,CAiFxD,CAAC;AAEF,eAAO,MAAM,aAAa,kCAAc,CAAC"}
@@ -15,7 +15,6 @@ const globs_1 = require("nx/src/utils/globs");
15
15
  const installation_directory_1 = require("nx/src/utils/installation-directory");
16
16
  const plugins_1 = require("nx/src/utils/plugins");
17
17
  const workspace_context_1 = require("nx/src/utils/workspace-context");
18
- const semver_1 = require("semver");
19
18
  const versions_1 = require("../utils/versions");
20
19
  const pmc = (0, devkit_1.getPackageManagerCommand)();
21
20
  function readTargetsCache(cachePath) {
@@ -122,19 +121,9 @@ async function buildJestTargets(configFilePath, projectRoot, options, context, p
122
121
  module: 'commonjs',
123
122
  customConditions: null,
124
123
  });
125
- // Jest 30 + Node.js 24 can't parse TS configs with imports.
126
- // This flag does not exist in Node 20/22.
127
- // https://github.com/jestjs/jest/issues/15682
128
- const nodeVersion = (0, semver_1.major)(process.version);
129
124
  const env = {
130
125
  TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions,
131
126
  };
132
- if (nodeVersion >= 24) {
133
- const currentOptions = process.env.NODE_OPTIONS || '';
134
- if (!currentOptions.includes('--no-experimental-strip-types')) {
135
- env.NODE_OPTIONS = (currentOptions + ' --no-experimental-strip-types').trim();
136
- }
137
- }
138
127
  const target = (targets[options.targetName] = {
139
128
  command: 'jest',
140
129
  options: {