@nx/jest 24.0.0 → 24.0.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/dist/src/generators/configuration/files/common/tsconfig.spec.json__tmpl__ +2 -1
- package/dist/src/generators/configuration/lib/create-files.js +4 -0
- package/dist/src/generators/configuration/lib/create-jest-config.d.ts +1 -1
- package/dist/src/generators/configuration/lib/create-jest-config.js +1 -68
- package/dist/src/generators/configuration/lib/update-tsconfig.js +23 -0
- package/dist/src/migrations/update-24-0-0/remove-deprecated-executors.d.ts +1 -1
- package/dist/src/migrations/update-24-0-0/remove-deprecated-executors.js +218 -3
- package/dist/src/migrations/update-24-0-0/remove-deprecated-executors.md +16 -0
- package/dist/src/plugins/plugin.js +147 -26
- package/dist/src/utils/config/get-jest-projects.d.ts +3 -3
- package/dist/src/utils/config/get-jest-projects.js +10 -22
- package/package.json +5 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "<%= extendedConfig %>",
|
|
3
3
|
"compilerOptions": {
|
|
4
|
-
"outDir": "<%= outDir %>",<% if (
|
|
4
|
+
"outDir": "<%= outDir %>",<% if (rootDir) { %>
|
|
5
|
+
"rootDir": "<%= rootDir %>",<% } if (module) { %>
|
|
5
6
|
"module": "<%= module %>",<% } if (module === 'commonjs') { %>
|
|
6
7
|
"moduleResolution": "<%= moduleResolution %>",<% } if (isolatedModules) { %>
|
|
7
8
|
"isolatedModules": true,<% } if (supportTsx) { %>
|
|
@@ -56,6 +56,9 @@ function createFiles(tree, options, presetExt) {
|
|
|
56
56
|
const isolatedModules = options.isTsSolutionSetup &&
|
|
57
57
|
transformer === 'ts-jest' &&
|
|
58
58
|
moduleResolution === 'node10';
|
|
59
|
+
// ts-jest drops `composite` and, with `isolatedModules`, compiles each test
|
|
60
|
+
// file alone. TypeScript 6 then fails with TS5011 unless `rootDir` is set.
|
|
61
|
+
const rootDir = options.isTsSolutionSetup && transformer === 'ts-jest' ? '.' : undefined;
|
|
59
62
|
(0, devkit_1.generateFiles)(tree, (0, path_1.join)(__dirname, commonFilesFolder), projectConfig.root, {
|
|
60
63
|
tmpl: '',
|
|
61
64
|
...options,
|
|
@@ -74,6 +77,7 @@ function createFiles(tree, options, presetExt) {
|
|
|
74
77
|
outDir: options.isTsSolutionSetup
|
|
75
78
|
? `./out-tsc/jest`
|
|
76
79
|
: `${rootOffset}dist/out-tsc`,
|
|
80
|
+
rootDir,
|
|
77
81
|
module: usesTsJestModuleSettings ? 'commonjs' : undefined,
|
|
78
82
|
moduleResolution,
|
|
79
83
|
isolatedModules,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Tree } from '@nx/devkit';
|
|
2
2
|
import { type JestPresetExtension } from '../../../utils/config/config-file';
|
|
3
3
|
import type { NormalizedJestProjectSchema } from '../schema';
|
|
4
4
|
export declare function writeRootJestPreset(tree: Tree, presetExt: JestPresetExtension): void;
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.writeRootJestPreset = writeRootJestPreset;
|
|
4
4
|
exports.createJestConfig = createJestConfig;
|
|
5
|
-
const devkit_1 = require("@nx/devkit");
|
|
6
5
|
const config_file_1 = require("../../../utils/config/config-file");
|
|
7
|
-
const internal_1 = require("@nx/js/internal");
|
|
8
|
-
const internal_2 = require("@nx/devkit/internal");
|
|
9
6
|
function writeRootJestPreset(tree, presetExt) {
|
|
10
7
|
if (!tree.exists(`jest.preset.${presetExt}`)) {
|
|
11
8
|
if (presetExt === 'mjs') {
|
|
@@ -28,72 +25,8 @@ async function createJestConfig(tree, options, presetExt) {
|
|
|
28
25
|
// will copy the template config file
|
|
29
26
|
return;
|
|
30
27
|
}
|
|
31
|
-
|
|
32
|
-
if (!rootJestPath) {
|
|
33
|
-
// if there's not root jest config, we will create one and return
|
|
34
|
-
// this can happen when:
|
|
35
|
-
// - root jest config was renamed => in which case there is migration needed
|
|
36
|
-
// - root project didn't have jest setup => again, no migration is needed
|
|
28
|
+
if (!(0, config_file_1.findRootJestConfig)(tree)) {
|
|
37
29
|
generateGlobalConfig(tree, options.js);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (tree.exists(rootJestPath)) {
|
|
41
|
-
// moving from root project config to monorepo-style config
|
|
42
|
-
const { nodes: projects } = await (0, devkit_1.createProjectGraphAsync)();
|
|
43
|
-
const projectConfigurations = Object.values(projects);
|
|
44
|
-
const rootProject = projectConfigurations.find((projectNode) => projectNode.data?.root === '.');
|
|
45
|
-
// root project might have been removed,
|
|
46
|
-
// if it's missing there's nothing to migrate
|
|
47
|
-
if (rootProject) {
|
|
48
|
-
const jestTarget = Object.entries(rootProject.data?.targets ?? {}).find(([_, t]) => ((t?.executor === '@nx/jest:jest' ||
|
|
49
|
-
t?.executor === '@nrwl/jest:jest') &&
|
|
50
|
-
t?.options?.jestConfig === rootJestPath) ||
|
|
51
|
-
(t?.executor === 'nx:run-commands' && t?.options?.command === 'jest'));
|
|
52
|
-
if (!jestTarget) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const [jestTargetName, jestTargetConfigInGraph] = jestTarget;
|
|
56
|
-
// if root project doesn't have jest target, there's nothing to migrate
|
|
57
|
-
const rootProjectConfig = (0, devkit_1.readProjectConfiguration)(tree, rootProject.name);
|
|
58
|
-
if (rootProjectConfig.targets?.['test']?.executor === 'nx:run-commands'
|
|
59
|
-
? rootProjectConfig.targets?.['test']?.command !== 'jest'
|
|
60
|
-
: rootProjectConfig.targets?.['test']?.options?.jestConfig !==
|
|
61
|
-
rootJestPath) {
|
|
62
|
-
// Jest target has already been updated
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const jestProjectConfig = `jest.config.${(0, internal_1.getProjectType)(tree, rootProjectConfig.root, rootProjectConfig.projectType) === 'application'
|
|
66
|
-
? 'app'
|
|
67
|
-
: 'lib'}.${options.js ? 'js' : 'ts'}`;
|
|
68
|
-
tree.rename(rootJestPath, jestProjectConfig);
|
|
69
|
-
const nxJson = (0, devkit_1.readNxJson)(tree);
|
|
70
|
-
const targetDefaults = (0, internal_2.readTargetDefaultsForTarget)(jestTargetName, nxJson.targetDefaults, jestTargetConfigInGraph.executor);
|
|
71
|
-
const target = (rootProjectConfig.targets[jestTargetName] ??=
|
|
72
|
-
jestTargetConfigInGraph.executor === 'nx:run-commands'
|
|
73
|
-
? { command: `jest --config ${jestProjectConfig}` }
|
|
74
|
-
: {
|
|
75
|
-
executor: jestTargetConfigInGraph.executor,
|
|
76
|
-
options: {},
|
|
77
|
-
});
|
|
78
|
-
if (target.executor === '@nx/jest:jest') {
|
|
79
|
-
target.options.jestConfig = jestProjectConfig;
|
|
80
|
-
}
|
|
81
|
-
if (targetDefaults?.cache === undefined) {
|
|
82
|
-
target.cache = jestTargetConfigInGraph.cache;
|
|
83
|
-
}
|
|
84
|
-
if (targetDefaults?.inputs === undefined) {
|
|
85
|
-
target.inputs = jestTargetConfigInGraph.inputs;
|
|
86
|
-
}
|
|
87
|
-
if (targetDefaults?.outputs === undefined) {
|
|
88
|
-
target.outputs = jestTargetConfigInGraph.outputs;
|
|
89
|
-
}
|
|
90
|
-
if (targetDefaults?.dependsOn === undefined) {
|
|
91
|
-
target.dependsOn = jestTargetConfigInGraph.dependsOn;
|
|
92
|
-
}
|
|
93
|
-
(0, devkit_1.updateProjectConfiguration)(tree, rootProject.name, rootProjectConfig);
|
|
94
|
-
// generate new global config as it was move to project config or is missing
|
|
95
|
-
generateGlobalConfig(tree, options.js);
|
|
96
|
-
}
|
|
97
30
|
}
|
|
98
31
|
}
|
|
99
32
|
function generateGlobalConfig(tree, isJS) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.updateTsConfig = updateTsConfig;
|
|
4
4
|
const devkit_1 = require("@nx/devkit");
|
|
5
|
+
const path_1 = require("path");
|
|
5
6
|
const internal_1 = require("@nx/js/internal");
|
|
6
7
|
function updateTsConfig(host, options) {
|
|
7
8
|
const { root, projectType: _projectType } = (0, devkit_1.readProjectConfiguration)(host, options.project);
|
|
@@ -55,9 +56,31 @@ function updateTsConfig(host, options) {
|
|
|
55
56
|
json.exclude = [...uniqueExclude];
|
|
56
57
|
return json;
|
|
57
58
|
});
|
|
59
|
+
// A composite spec config reaches runtime sources only through a project
|
|
60
|
+
// reference. Skip it when the runtime config references the spec config.
|
|
61
|
+
const specTsconfigPath = (0, devkit_1.joinPathFragments)(root, 'tsconfig.spec.json');
|
|
62
|
+
const runtimeReference = `./${path_1.posix.relative(root, runtimeTsconfigPath)}`;
|
|
63
|
+
if (options.isTsSolutionSetup &&
|
|
64
|
+
canBeReferencedBy(host, runtimeTsconfigPath, specTsconfigPath)) {
|
|
65
|
+
(0, devkit_1.updateJson)(host, specTsconfigPath, (json) => {
|
|
66
|
+
json.references ??= [];
|
|
67
|
+
if (!json.references.some((r) => r.path === runtimeReference)) {
|
|
68
|
+
json.references.push({ path: runtimeReference });
|
|
69
|
+
}
|
|
70
|
+
return json;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
58
73
|
}
|
|
59
74
|
else {
|
|
60
75
|
devkit_1.logger.warn(`Couldn't find a runtime tsconfig file at ${runtimeTsconfigPath} to exclude the test files from. ` +
|
|
61
76
|
`If you're using a different filename for your runtime tsconfig, please provide it with the '--runtimeTsconfigFileName' flag.`);
|
|
62
77
|
}
|
|
63
78
|
}
|
|
79
|
+
function canBeReferencedBy(tree, tsconfigPath, referrerPath) {
|
|
80
|
+
const ts = (0, internal_1.ensureTypescript)();
|
|
81
|
+
const host = (0, internal_1.createTreeParseConfigHost)(tree);
|
|
82
|
+
const { options, projectReferences } = ts.parseJsonConfigFileContent(ts.readConfigFile(tsconfigPath, host.readFile).config, host, path_1.posix.dirname(tsconfigPath));
|
|
83
|
+
return (options.composite === true &&
|
|
84
|
+
!options.noEmit &&
|
|
85
|
+
!projectReferences?.some((r) => ts.resolveProjectReferencePath(r) === referrerPath));
|
|
86
|
+
}
|
|
@@ -1,10 +1,225 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = update;
|
|
4
|
+
const devkit_1 = require("@nx/devkit");
|
|
4
5
|
const internal_1 = require("@nx/devkit/internal");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const node_util_1 = require("node:util");
|
|
8
|
+
const config_file_1 = require("../../utils/config/config-file");
|
|
9
|
+
const removedExecutors = ['@nx/jest:jest', '@nrwl/jest:jest'];
|
|
10
|
+
// The removed executor set this on every run; @nx/jest/plugin sets the same value
|
|
11
|
+
const tsNodeCompilerOptions = JSON.stringify({
|
|
12
|
+
moduleResolution: 'node10',
|
|
13
|
+
module: 'commonjs',
|
|
14
|
+
customConditions: null,
|
|
15
|
+
});
|
|
16
|
+
const isBoolean = (value) => typeof value === 'boolean';
|
|
17
|
+
const isPositiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
|
|
18
|
+
/**
|
|
19
|
+
* The only options a rewritten target may set. Generators wrote jestConfig,
|
|
20
|
+
* tsConfig, passWithNoTests, ci and codeCoverage. Jest 29 and 30 read the other
|
|
21
|
+
* flags from --name=value as the values the executor passed to runCLI.
|
|
22
|
+
*/
|
|
23
|
+
const acceptedOptions = new Map([
|
|
24
|
+
['jestConfig', isPlainPath],
|
|
25
|
+
['tsConfig', (value) => typeof value === 'string'],
|
|
26
|
+
['codeCoverage', isBoolean],
|
|
27
|
+
['bail', isBoolean],
|
|
28
|
+
['ci', isBoolean],
|
|
29
|
+
['detectOpenHandles', isBoolean],
|
|
30
|
+
['forceExit', isBoolean],
|
|
31
|
+
['logHeapUsage', isBoolean],
|
|
32
|
+
['passWithNoTests', isBoolean],
|
|
33
|
+
['runInBand', isBoolean],
|
|
34
|
+
['silent', isBoolean],
|
|
35
|
+
['maxWorkers', isPositiveInteger],
|
|
36
|
+
['testTimeout', isPositiveInteger],
|
|
37
|
+
]);
|
|
5
38
|
function update(tree) {
|
|
6
|
-
return (0, internal_1.migrateRemovedExecutors)(tree,
|
|
7
|
-
const
|
|
8
|
-
|
|
39
|
+
return (0, internal_1.migrateRemovedExecutors)(tree, removedExecutors, async (tree, options) => {
|
|
40
|
+
const converter = require('../../generators/convert-to-inferred/convert-to-inferred');
|
|
41
|
+
const unconverted = rewriteTargetsWithUndetectedConfig(tree);
|
|
42
|
+
try {
|
|
43
|
+
return await converter.convertToInferred(tree, options);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
restoreTargets(tree, unconverted);
|
|
47
|
+
}
|
|
9
48
|
});
|
|
10
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* @nx/jest/plugin infers a project's target only from `jest.config.<ext>` in
|
|
52
|
+
* its root, so a target running another config becomes an explicit command.
|
|
53
|
+
* A target that can't be rewritten is removed and returned, because the
|
|
54
|
+
* converter would switch it to the project's `jest.config.<ext>`.
|
|
55
|
+
*/
|
|
56
|
+
function rewriteTargetsWithUndetectedConfig(tree) {
|
|
57
|
+
const targetDefaults = (0, devkit_1.readNxJson)(tree)?.targetDefaults;
|
|
58
|
+
const unconvertedByProject = new Map();
|
|
59
|
+
for (const [projectName, project] of (0, devkit_1.getProjects)(tree)) {
|
|
60
|
+
const unconverted = {};
|
|
61
|
+
let changed = false;
|
|
62
|
+
for (const [targetName, target] of Object.entries(project.targets ?? {})) {
|
|
63
|
+
if (!removedExecutors.includes(target.executor)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const readDefaults = (executor) => (0, internal_1.readTargetDefaultsForTarget)(targetName, targetDefaults, executor, {
|
|
67
|
+
projectName,
|
|
68
|
+
projectNode: { name: projectName, type: 'lib', data: project },
|
|
69
|
+
});
|
|
70
|
+
let runsUndetectedConfig = false;
|
|
71
|
+
try {
|
|
72
|
+
const executorDefaults = readDefaults(target.executor);
|
|
73
|
+
const resolved = resolveTarget(target, executorDefaults, project, `${projectName}:${targetName}`);
|
|
74
|
+
const jestConfig = resolved.options?.jestConfig;
|
|
75
|
+
if (!jestConfig || isProjectRootJestConfig(jestConfig, project.root)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
runsUndetectedConfig = true;
|
|
79
|
+
const rewritten = toRunCommandsTarget(resolved);
|
|
80
|
+
assertDefaultsKeepTarget(rewritten, executorDefaults, readDefaults('nx:run-commands'));
|
|
81
|
+
project.targets[targetName] = rewritten;
|
|
82
|
+
changed = true;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
devkit_1.logger.warn(`Could not rewrite target "${targetName}" in project "${projectName}" to run Jest with its config file: ${String(error)}`);
|
|
86
|
+
if (runsUndetectedConfig) {
|
|
87
|
+
unconverted[targetName] = target;
|
|
88
|
+
delete project.targets[targetName];
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (changed) {
|
|
94
|
+
(0, devkit_1.updateProjectConfiguration)(tree, projectName, project);
|
|
95
|
+
}
|
|
96
|
+
if (Object.keys(unconverted).length) {
|
|
97
|
+
unconvertedByProject.set(projectName, unconverted);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return unconvertedByProject;
|
|
101
|
+
}
|
|
102
|
+
function restoreTargets(tree, targetsByProject) {
|
|
103
|
+
for (const [projectName, targets] of targetsByProject) {
|
|
104
|
+
// Read again to keep what the converter did to sibling targets
|
|
105
|
+
const project = (0, devkit_1.readProjectConfiguration)(tree, projectName);
|
|
106
|
+
project.targets = { ...project.targets, ...targets };
|
|
107
|
+
(0, devkit_1.updateProjectConfiguration)(tree, projectName, project);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The target merged with the defaults the removed executor received, with Nx
|
|
112
|
+
* tokens resolved.
|
|
113
|
+
*/
|
|
114
|
+
function resolveTarget(target, defaults, project, key) {
|
|
115
|
+
const resolved = (0, internal_1.mergeTargetConfigurations)(structuredClone(target), structuredClone(defaults ?? {}));
|
|
116
|
+
if (resolved.options) {
|
|
117
|
+
resolved.options = (0, internal_1.resolveNxTokensInOptions)(resolved.options, project, key);
|
|
118
|
+
}
|
|
119
|
+
for (const name of Object.keys(resolved.configurations ?? {})) {
|
|
120
|
+
resolved.configurations[name] = (0, internal_1.resolveNxTokensInOptions)(resolved.configurations[name], project, `${key}:${name}`);
|
|
121
|
+
}
|
|
122
|
+
return resolved;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Target defaults are matched by executor, so the rewritten target can get
|
|
126
|
+
* entries the removed executor never did. They must not change it.
|
|
127
|
+
*/
|
|
128
|
+
function assertDefaultsKeepTarget(target, executorDefaults, runCommandsDefaults) {
|
|
129
|
+
if (!runCommandsDefaults) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// The executor got these entries too, and the target holds their values
|
|
133
|
+
// translated for Jest
|
|
134
|
+
if ((0, node_util_1.isDeepStrictEqual)(executorDefaults, runCommandsDefaults)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!(0, node_util_1.isDeepStrictEqual)(withDefaults(target, runCommandsDefaults), withDefaults(target, {}))) {
|
|
138
|
+
throw new Error('The target defaults that apply to nx:run-commands targets would change it.');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Resolves '...' spreads too, and drops the undefined values the merge leaves
|
|
142
|
+
function withDefaults(target, defaults) {
|
|
143
|
+
return JSON.parse(JSON.stringify((0, internal_1.mergeTargetConfigurations)(structuredClone(target), structuredClone(defaults), undefined, undefined, undefined, false)));
|
|
144
|
+
}
|
|
145
|
+
function isProjectRootJestConfig(jestConfig, projectRoot) {
|
|
146
|
+
const path = node_path_1.posix.normalize(jestConfig);
|
|
147
|
+
return config_file_1.jestConfigExtensions.some((ext) => path === node_path_1.posix.join(projectRoot, `jest.config.${ext}`));
|
|
148
|
+
}
|
|
149
|
+
// Relative, and left unchanged by sh, cmd.exe and nx:run-commands
|
|
150
|
+
function isPlainPath(value) {
|
|
151
|
+
return typeof value === 'string' && /^[\w@+.-][\w@+./-]*$/.test(value);
|
|
152
|
+
}
|
|
153
|
+
function assertAcceptedOptions(options) {
|
|
154
|
+
for (const [name, value] of Object.entries(options)) {
|
|
155
|
+
if (!acceptedOptions.get(name)?.(value)) {
|
|
156
|
+
throw new Error(`The rewrite does not accept the "${name}" option set to ${JSON.stringify(value)}.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// jestConfig becomes the command, and the executor never passed tsConfig to Jest
|
|
161
|
+
function toJestOptions({ jestConfig: _jestConfig, tsConfig: _tsConfig, codeCoverage, ...flags }) {
|
|
162
|
+
return codeCoverage === undefined
|
|
163
|
+
? flags
|
|
164
|
+
: { ...flags, coverage: codeCoverage };
|
|
165
|
+
}
|
|
166
|
+
// The leading ./ is what the move generator rewrites for a root project
|
|
167
|
+
function jestCommand(jestConfig) {
|
|
168
|
+
return `jest --config ${(0, internal_1.toProjectRelativePath)(node_path_1.posix.normalize(jestConfig), '.')}`;
|
|
169
|
+
}
|
|
170
|
+
function optionsFor(target, configuration) {
|
|
171
|
+
return {
|
|
172
|
+
...target.options,
|
|
173
|
+
...(configuration ? target.configurations[configuration] : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function toRunCommandsTarget(target) {
|
|
177
|
+
const { executor: _executor, options, configurations, ...rest } = target;
|
|
178
|
+
const configurationNames = Object.keys(configurations ?? {});
|
|
179
|
+
for (const values of [options, ...Object.values(configurations ?? {})]) {
|
|
180
|
+
assertAcceptedOptions(values);
|
|
181
|
+
}
|
|
182
|
+
for (const name of [undefined, ...configurationNames]) {
|
|
183
|
+
const values = optionsFor(target, name);
|
|
184
|
+
// runCLI accepted this pair, but Jest's CLI rejects it
|
|
185
|
+
if (values.runInBand === true && 'maxWorkers' in values) {
|
|
186
|
+
throw new Error('Jest rejects runInBand with maxWorkers on its CLI.');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const rewritten = {
|
|
190
|
+
executor: 'nx:run-commands',
|
|
191
|
+
...rest,
|
|
192
|
+
options: {
|
|
193
|
+
command: jestCommand(options.jestConfig),
|
|
194
|
+
// The executor ran from where nx started, usually the workspace root.
|
|
195
|
+
// Set explicitly, so an nx:run-commands default's cwd can't apply.
|
|
196
|
+
cwd: '.',
|
|
197
|
+
env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions },
|
|
198
|
+
...toJestOptions(options),
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
if (configurations) {
|
|
202
|
+
rewritten.configurations = Object.fromEntries(configurationNames.map((name) => {
|
|
203
|
+
const { jestConfig } = configurations[name];
|
|
204
|
+
return [
|
|
205
|
+
name,
|
|
206
|
+
{
|
|
207
|
+
...(jestConfig ? { command: jestCommand(jestConfig) } : {}),
|
|
208
|
+
...toJestOptions(configurations[name]),
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
const outputOptions = [
|
|
214
|
+
...(rest.outputs ?? []).join(' ').matchAll(/\{options\.(\w+)/g),
|
|
215
|
+
].map(([, name]) => name);
|
|
216
|
+
for (const name of [undefined, ...configurationNames]) {
|
|
217
|
+
const before = optionsFor(target, name);
|
|
218
|
+
const after = optionsFor(rewritten, name);
|
|
219
|
+
const changed = outputOptions.find((option) => !(0, node_util_1.isDeepStrictEqual)(before[option], after[option]));
|
|
220
|
+
if (changed) {
|
|
221
|
+
throw new Error(`An output reads the "${changed}" option, which the rewrite changes.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return rewritten;
|
|
225
|
+
}
|
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Nx 24 removes the `@nx/jest:jest` executor and its `@nrwl/jest:jest` alias. The migration registers `@nx/jest/plugin` in `nx.json` and converts the targets that use those executors to inferred tasks. Options the plugin does not infer stay on the target. A target with nothing left to override is removed from project configuration. When several projects share the same remaining options, those options move to a `targetDefaults` entry scoped to the plugin. `targetDefaults` entries keyed by a removed executor are removed once no target uses that executor.
|
|
4
4
|
|
|
5
|
+
The plugin only infers a target from `jest.config.<ext>` in the project root. A target that runs any other config file, such as a root project's `jest.config.app.ts`, becomes an `nx:run-commands` target that runs `jest --config <file>` from the workspace root. It keeps the options, configurations and target defaults it used. The executor ran Jest from the directory `nx` started in, so a run started from another directory now uses the workspace root too.
|
|
6
|
+
|
|
7
|
+
The rewrite accepts only these options, in the target and in each configuration:
|
|
8
|
+
|
|
9
|
+
- `jestConfig`, set to a relative path made of ASCII letters, digits and `_ @ + . / -`. A configuration's `jestConfig` becomes its own command.
|
|
10
|
+
- `codeCoverage`, `bail`, `ci`, `detectOpenHandles`, `forceExit`, `logHeapUsage`, `passWithNoTests`, `runInBand` and `silent`, set to `true` or `false`. `codeCoverage` becomes `--coverage`.
|
|
11
|
+
- `maxWorkers` and `testTimeout`, set to a positive integer.
|
|
12
|
+
- `tsConfig`, which Jest never read. The rewrite drops it.
|
|
13
|
+
|
|
14
|
+
The target keeps the removed executor, and the migration lists it, in these cases:
|
|
15
|
+
|
|
16
|
+
- It sets any other option, or sets an accepted option to another kind of value.
|
|
17
|
+
- It sets `runInBand: true` together with `maxWorkers`, also through a configuration. Jest's CLI rejects that pair.
|
|
18
|
+
- An output reads an option the rewrite removes or renames, such as `{options.tsConfig}`.
|
|
19
|
+
- `targetDefaults` that apply to `nx:run-commands` targets would change it.
|
|
20
|
+
|
|
5
21
|
The migration lists the targets the converter cannot convert safely. In agentic runs, an AI prompt finishes them and validates the project graph.
|
|
6
22
|
|
|
7
23
|
#### Sample code changes
|
|
@@ -5,6 +5,8 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const internal_1 = require("@nx/devkit/internal");
|
|
6
6
|
const devkit_1 = require("@nx/devkit");
|
|
7
7
|
const minimatch_1 = require("minimatch");
|
|
8
|
+
const picomatch = require("picomatch");
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
10
|
const node_fs_1 = require("node:fs");
|
|
9
11
|
const node_path_1 = require("node:path");
|
|
10
12
|
const posix = tslib_1.__importStar(require("node:path/posix"));
|
|
@@ -206,7 +208,6 @@ async function buildJestTargets(rawConfig, needsDtsInputs, configFilePath, proje
|
|
|
206
208
|
[groupName]: targetGroup,
|
|
207
209
|
},
|
|
208
210
|
};
|
|
209
|
-
const specIgnoreRegexes = rawConfig.testPathIgnorePatterns?.map((p) => new RegExp(replaceRootDirInPath(projectRoot, p)));
|
|
210
211
|
for (const testPath of specs) {
|
|
211
212
|
const relativePath = (0, devkit_1.normalizePath)((0, node_path_1.relative)((0, node_path_1.join)(context.workspaceRoot, projectRoot), testPath));
|
|
212
213
|
if (relativePath.includes('../')) {
|
|
@@ -219,9 +220,6 @@ async function buildJestTargets(rawConfig, needsDtsInputs, configFilePath, proje
|
|
|
219
220
|
testMatch,
|
|
220
221
|
}, null, 2)}`);
|
|
221
222
|
}
|
|
222
|
-
if (specIgnoreRegexes?.some((regex) => regex.test(relativePath))) {
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
223
|
const targetName = `${options.ciTargetName}--${relativePath}`;
|
|
226
224
|
dependsOn.push({
|
|
227
225
|
target: targetName,
|
|
@@ -606,21 +604,152 @@ function requireJestUtil(packageName, projectRoot, workspaceRoot) {
|
|
|
606
604
|
paths: [(0, node_path_1.dirname)(resolvedJestCorePaths[jestPath])],
|
|
607
605
|
}));
|
|
608
606
|
}
|
|
607
|
+
// Defaults copied from https://github.com/jestjs/jest/blob/d1a2ed7/packages/jest-config/src/Defaults.ts
|
|
608
|
+
const DEFAULT_TEST_MATCH = [
|
|
609
|
+
'**/__tests__/**/*.?([mc])[jt]s?(x)',
|
|
610
|
+
'**/?(*.)+(spec|test).?([mc])[jt]s?(x)',
|
|
611
|
+
];
|
|
612
|
+
const DEFAULT_MODULE_FILE_EXTENSIONS = [
|
|
613
|
+
'js',
|
|
614
|
+
'mjs',
|
|
615
|
+
'cjs',
|
|
616
|
+
'jsx',
|
|
617
|
+
'ts',
|
|
618
|
+
'mts',
|
|
619
|
+
'cts',
|
|
620
|
+
'tsx',
|
|
621
|
+
'json',
|
|
622
|
+
'node',
|
|
623
|
+
];
|
|
624
|
+
// Mirrors how Jest selects test files: jest-haste-map's file list, then
|
|
625
|
+
// SearchSource's checks, all on the absolute path
|
|
609
626
|
async function getTestPaths(projectRoot, rawConfig, rootDir, context, presetCache) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
627
|
+
// Jest resolves rootDir's symlinks. Resolve the workspace root with it so
|
|
628
|
+
// file paths still compare; a default rootDir needs neither
|
|
629
|
+
let workspaceRoot = context.workspaceRoot;
|
|
630
|
+
if (rawConfig.rootDir) {
|
|
631
|
+
rootDir = tryRealpath(rootDir);
|
|
632
|
+
workspaceRoot = tryRealpath(workspaceRoot);
|
|
633
|
+
}
|
|
634
|
+
const preset = await loadPresetConfig(rawConfig, rootDir, presetCache);
|
|
635
|
+
// jest-config lets a key the config sets override the preset, even when empty
|
|
636
|
+
const getOption = (name, defaultValue) => (Object.hasOwn(rawConfig, name) ? rawConfig : preset)?.[name] ??
|
|
637
|
+
defaultValue;
|
|
638
|
+
const toArray = (value) => Array.isArray(value) ? value : [value];
|
|
639
|
+
const posixRootDir = (0, devkit_1.normalizePath)(rootDir);
|
|
640
|
+
const toRegExp = (patterns) => patterns.length > 0
|
|
641
|
+
? new RegExp(patterns
|
|
642
|
+
.map((pattern) => pattern.replaceAll('<rootDir>', posixRootDir))
|
|
643
|
+
.join('|'))
|
|
644
|
+
: undefined;
|
|
645
|
+
// jest-config reads a falsy testRegex as none
|
|
646
|
+
const testRegex = toArray(getOption('testRegex', []) || []);
|
|
647
|
+
// jest-config drops the default testMatch when testRegex is set
|
|
648
|
+
const testMatch = toArray(getOption('testMatch', testRegex.length > 0 ? [] : DEFAULT_TEST_MATCH));
|
|
649
|
+
const roots = getOption('roots', ['<rootDir>']).map((root) => `${(0, devkit_1.normalizePath)((0, node_path_1.resolve)(rootDir, replaceRootDirInPath(rootDir, root)))}/`);
|
|
650
|
+
const extensions = new Set([
|
|
651
|
+
'snap',
|
|
652
|
+
...getOption('moduleFileExtensions', DEFAULT_MODULE_FILE_EXTENSIONS),
|
|
653
|
+
]);
|
|
654
|
+
// jest-config swaps its default haste, which sets forceNodeFilesystemAPI,
|
|
655
|
+
// for any haste the config or preset sets, even an empty one
|
|
656
|
+
const hasteConfig = [rawConfig, preset].find((config) => config && Object.hasOwn(config, 'haste'));
|
|
657
|
+
const useNativeFind = !!hasteConfig &&
|
|
658
|
+
!hasteConfig.haste?.forceNodeFilesystemAPI &&
|
|
659
|
+
(await hasNativeFindSupport());
|
|
660
|
+
// jest-haste-map's native find crawler matches extensions as
|
|
661
|
+
// case-insensitive suffixes, its Node API crawler as the last extension
|
|
662
|
+
const suffixes = [...extensions].map((ext) => `.${ext}`.toLowerCase());
|
|
663
|
+
const hasExtension = useNativeFind
|
|
664
|
+
? (path) => {
|
|
665
|
+
const lowerCasePath = path.toLowerCase();
|
|
666
|
+
return suffixes.some((suffix) => lowerCasePath.endsWith(suffix));
|
|
667
|
+
}
|
|
668
|
+
: (path) => extensions.has((0, node_path_1.extname)(path).slice(1));
|
|
669
|
+
// jest-config concatenates preset and config modulePathIgnorePatterns
|
|
670
|
+
const modulePathIgnorePattern = toRegExp((preset?.modulePathIgnorePatterns ?? []).concat(rawConfig.modulePathIgnorePatterns ?? []));
|
|
671
|
+
const testPathIgnorePattern = toRegExp(getOption('testPathIgnorePatterns', ['/node_modules/']));
|
|
672
|
+
const testRegexes = testRegex.map((regex) => new RegExp(regex));
|
|
673
|
+
// Globbing from the project root already applies `**/` globs, so only other
|
|
674
|
+
// globs need a match check
|
|
675
|
+
const testMatchers = testMatch.every((pattern) => pattern.startsWith('**/'))
|
|
676
|
+
? []
|
|
677
|
+
: testMatch.map((pattern) => {
|
|
678
|
+
// Built like jest-util's: minimatch would read `(a|b)` groups literally
|
|
679
|
+
const isMatch = picomatch(replacePathSepForGlob(replaceRootDirInPath(escapeGlobCharacters(rootDir), pattern)), { dot: true }, true);
|
|
680
|
+
return {
|
|
681
|
+
isMatch,
|
|
682
|
+
negated: isMatch.state.negated || !!isMatch.state.negatedExtglob,
|
|
683
|
+
};
|
|
684
|
+
});
|
|
685
|
+
// Unless every testMatch glob is negated, a file must match a non-negated one
|
|
686
|
+
const includePatterns = testMatch.filter((_, i) => !testMatchers[i]?.negated);
|
|
687
|
+
const candidates = await (0, internal_1.globWithWorkspaceContext)(context.workspaceRoot, includePatterns.length > 0
|
|
688
|
+
? includePatterns.map((pattern) => {
|
|
689
|
+
const path = replaceRootDirInPath(rootDir, pattern);
|
|
690
|
+
return (0, node_path_1.isAbsolute)(path)
|
|
691
|
+
? (0, devkit_1.normalizePath)((0, node_path_1.relative)(workspaceRoot, path))
|
|
692
|
+
: posix.join(projectRoot, pattern);
|
|
693
|
+
})
|
|
694
|
+
: [posix.join(projectRoot, '**/*')], []);
|
|
695
|
+
const workspacePath = (0, devkit_1.normalizePath)((0, node_path_1.join)(workspaceRoot, '/'));
|
|
696
|
+
const specs = candidates.filter((file) => {
|
|
697
|
+
const path = workspacePath + file;
|
|
698
|
+
return (hasExtension(path) &&
|
|
699
|
+
!modulePathIgnorePattern?.test(path) &&
|
|
700
|
+
roots.some((root) => path.includes(root)) &&
|
|
701
|
+
(testMatchers.length === 0 || matchesGlobs(testMatchers, path)) &&
|
|
702
|
+
!testPathIgnorePattern?.test(path) &&
|
|
703
|
+
(testRegexes.length === 0 || testRegexes.some((r) => r.test(path))));
|
|
704
|
+
});
|
|
705
|
+
return { specs, testMatch };
|
|
706
|
+
}
|
|
707
|
+
function matchesGlobs(matchers, path) {
|
|
708
|
+
let kept;
|
|
709
|
+
for (const { isMatch, negated } of matchers) {
|
|
710
|
+
const matched = isMatch(path);
|
|
711
|
+
if (!matched && negated) {
|
|
712
|
+
kept = false;
|
|
713
|
+
}
|
|
714
|
+
else if (matched && !negated) {
|
|
715
|
+
kept = true;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return matchers.every(({ negated }) => negated)
|
|
719
|
+
? kept !== false
|
|
720
|
+
: kept === true;
|
|
721
|
+
}
|
|
722
|
+
// Ports of the jest-config and jest-util helpers Jest uses to put rootDir into
|
|
723
|
+
// testMatch globs
|
|
724
|
+
function escapeGlobCharacters(path) {
|
|
725
|
+
return path.replaceAll(/([!()*?[\\\]{}])/g, '\\$1');
|
|
726
|
+
}
|
|
727
|
+
function replacePathSepForGlob(path) {
|
|
728
|
+
return path.replaceAll(/\\(?![$()+.?^{}])/g, '/');
|
|
729
|
+
}
|
|
730
|
+
function tryRealpath(path) {
|
|
731
|
+
try {
|
|
732
|
+
return node_fs_1.realpathSync.native(path);
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
return path;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
let nativeFindSupport;
|
|
739
|
+
// Port of jest-haste-map's check for a find that supports -iname, cached
|
|
740
|
+
// because the find on PATH doesn't change during a process lifetime
|
|
741
|
+
function hasNativeFindSupport() {
|
|
742
|
+
nativeFindSupport ??= new Promise((done) => {
|
|
743
|
+
try {
|
|
744
|
+
const child = (0, node_child_process_1.spawn)('find', ['.', '-type', 'f', '(', '-iname', '*.ts', '-o', '-iname', '*.js', ')'], { cwd: __dirname, stdio: 'ignore', windowsHide: true });
|
|
745
|
+
child.on('error', () => done(false));
|
|
746
|
+
child.on('exit', (code) => done(code === 0));
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
done(false);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
return nativeFindSupport;
|
|
624
753
|
}
|
|
625
754
|
/**
|
|
626
755
|
* Collects workspace-relative paths to files whose CONTENT the plugin reads
|
|
@@ -939,11 +1068,3 @@ function getTsJestMajorVersion() {
|
|
|
939
1068
|
}
|
|
940
1069
|
return cachedTsJestMajorVersion;
|
|
941
1070
|
}
|
|
942
|
-
async function getJestOption(rawConfig, rootDir, optionName, presetCache) {
|
|
943
|
-
if (rawConfig[optionName])
|
|
944
|
-
return rawConfig[optionName];
|
|
945
|
-
const preset = await loadPresetConfig(rawConfig, rootDir, presetCache);
|
|
946
|
-
if (preset?.[optionName])
|
|
947
|
-
return preset[optionName];
|
|
948
|
-
return undefined;
|
|
949
|
-
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Get a list of paths to all the jest config files
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* used by `nx:run-commands` targets running `jest`,
|
|
4
|
+
* including the targets `@nx/jest/plugin` infers.
|
|
5
5
|
*
|
|
6
6
|
* This is used to configure Jest multi-project support.
|
|
7
7
|
*
|
|
8
|
-
* To add a project
|
|
8
|
+
* To add a project without such a target:
|
|
9
9
|
* export default async () => ({
|
|
10
10
|
* projects: [...(await getJestProjectsAsync()), '<rootDir>/path/to/jest.config.ts'];
|
|
11
11
|
* });
|
|
@@ -10,12 +10,12 @@ function getJestConfigProjectPath(projectJestConfigPath) {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Get a list of paths to all the jest config files
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* used by `nx:run-commands` targets running `jest`,
|
|
14
|
+
* including the targets `@nx/jest/plugin` infers.
|
|
15
15
|
*
|
|
16
16
|
* This is used to configure Jest multi-project support.
|
|
17
17
|
*
|
|
18
|
-
* To add a project
|
|
18
|
+
* To add a project without such a target:
|
|
19
19
|
* export default async () => ({
|
|
20
20
|
* projects: [...(await getJestProjectsAsync()), '<rootDir>/path/to/jest.config.ts'];
|
|
21
21
|
* });
|
|
@@ -33,11 +33,7 @@ async function getJestProjectsAsync() {
|
|
|
33
33
|
continue;
|
|
34
34
|
}
|
|
35
35
|
for (const targetConfiguration of Object.values(projectConfig.targets)) {
|
|
36
|
-
if (targetConfiguration.executor === '
|
|
37
|
-
targetConfiguration.executor === '@nrwl/jest:jest') {
|
|
38
|
-
collectJestConfigFromJestExecutor(targetConfiguration, jestConfigurations);
|
|
39
|
-
}
|
|
40
|
-
else if (targetConfiguration.executor === 'nx:run-commands') {
|
|
36
|
+
if (targetConfiguration.executor === 'nx:run-commands') {
|
|
41
37
|
collectJestConfigFromRunCommandsExecutor(targetConfiguration, projectConfig.root, jestConfigurations);
|
|
42
38
|
}
|
|
43
39
|
}
|
|
@@ -55,18 +51,6 @@ function removeDuplicates(configs) {
|
|
|
55
51
|
configs.delete(dir);
|
|
56
52
|
});
|
|
57
53
|
}
|
|
58
|
-
function collectJestConfigFromJestExecutor(targetConfiguration, jestConfigurations) {
|
|
59
|
-
if (targetConfiguration.options?.jestConfig) {
|
|
60
|
-
jestConfigurations.add(getJestConfigProjectPath(targetConfiguration.options.jestConfig));
|
|
61
|
-
}
|
|
62
|
-
if (targetConfiguration.configurations) {
|
|
63
|
-
for (const configurationObject of Object.values(targetConfiguration.configurations)) {
|
|
64
|
-
if (configurationObject.jestConfig) {
|
|
65
|
-
jestConfigurations.add(getJestConfigProjectPath(configurationObject.jestConfig));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
54
|
function collectJestConfigFromRunCommandsExecutor(targetConfiguration, projectRoot, jestConfigurations) {
|
|
71
55
|
if (targetConfiguration.options?.command) {
|
|
72
56
|
collectJestConfigFromCommand(targetConfiguration.options.command, targetConfiguration.options.cwd ?? projectRoot, jestConfigurations);
|
|
@@ -80,12 +64,16 @@ function collectJestConfigFromRunCommandsExecutor(targetConfiguration, projectRo
|
|
|
80
64
|
if (targetConfiguration.configurations) {
|
|
81
65
|
for (const configurationObject of Object.values(targetConfiguration.configurations)) {
|
|
82
66
|
if (configurationObject.command) {
|
|
83
|
-
collectJestConfigFromCommand(configurationObject.command, configurationObject.cwd ??
|
|
67
|
+
collectJestConfigFromCommand(configurationObject.command, configurationObject.cwd ??
|
|
68
|
+
targetConfiguration.options?.cwd ??
|
|
69
|
+
projectRoot, jestConfigurations);
|
|
84
70
|
}
|
|
85
71
|
else if (configurationObject.commands) {
|
|
86
72
|
for (const command of configurationObject.commands) {
|
|
87
73
|
const commandScript = typeof command === 'string' ? command : command.command;
|
|
88
|
-
collectJestConfigFromCommand(commandScript, configurationObject.cwd ??
|
|
74
|
+
collectJestConfigFromCommand(commandScript, configurationObject.cwd ??
|
|
75
|
+
targetConfiguration.options?.cwd ??
|
|
76
|
+
projectRoot, jestConfigurations);
|
|
89
77
|
}
|
|
90
78
|
}
|
|
91
79
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nx/jest",
|
|
3
|
-
"version": "24.0.
|
|
3
|
+
"version": "24.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"files": [
|
|
@@ -103,21 +103,22 @@
|
|
|
103
103
|
}
|
|
104
104
|
},
|
|
105
105
|
"dependencies": {
|
|
106
|
-
"@nx/devkit": "24.0.
|
|
107
|
-
"@nx/js": "24.0.
|
|
106
|
+
"@nx/devkit": "24.0.2",
|
|
107
|
+
"@nx/js": "24.0.2",
|
|
108
108
|
"@phenomnomnominal/tsquery": "~6.2.0",
|
|
109
109
|
"identity-obj-proxy": "3.0.0",
|
|
110
110
|
"jest-config": "^30.0.2",
|
|
111
111
|
"jest-resolve": "^30.0.2",
|
|
112
112
|
"jsonc-parser": "^3.2.0",
|
|
113
113
|
"minimatch": "10.2.5",
|
|
114
|
+
"picomatch": "4.0.4",
|
|
114
115
|
"resolve.exports": "2.0.3",
|
|
115
116
|
"semver": "^7.6.3",
|
|
116
117
|
"tslib": "^2.3.0",
|
|
117
118
|
"yargs-parser": "21.1.1"
|
|
118
119
|
},
|
|
119
120
|
"devDependencies": {
|
|
120
|
-
"nx": "24.0.
|
|
121
|
+
"nx": "24.0.2"
|
|
121
122
|
},
|
|
122
123
|
"publishConfig": {
|
|
123
124
|
"access": "public"
|