@nx/devkit 23.2.0-beta.10 → 23.2.0-beta.12
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/README.md +2 -2
- package/dist/internal.d.ts +4 -2
- package/dist/internal.js +17 -4
- package/dist/ngcli-adapter.js +1 -1
- package/dist/src/generators/e2e-web-server-info-utils.d.ts +6 -0
- package/dist/src/generators/e2e-web-server-info-utils.js +8 -4
- package/dist/src/generators/plugin-migrations/batch-conversion-finalize.d.ts +19 -0
- package/dist/src/generators/plugin-migrations/batch-conversion-finalize.js +658 -0
- package/dist/src/generators/plugin-migrations/batch-conversion-session.d.ts +131 -0
- package/dist/src/generators/plugin-migrations/batch-conversion-session.js +141 -0
- package/dist/src/generators/plugin-migrations/conversion-warnings.d.ts +28 -0
- package/dist/src/generators/plugin-migrations/conversion-warnings.js +72 -0
- package/dist/src/generators/plugin-migrations/executor-to-plugin-migrator.d.ts +250 -1
- package/dist/src/generators/plugin-migrations/executor-to-plugin-migrator.js +1594 -227
- package/dist/src/generators/target-defaults-utils.d.ts +8 -0
- package/dist/src/generators/target-defaults-utils.js +17 -5
- package/dist/src/utils/config-utils.d.ts +3 -0
- package/dist/src/utils/config-utils.js +197 -27
- package/dist/src/utils/package-json.js +1 -1
- package/dist/src/utils/semver.js +1 -1
- package/dist/testing.js +1 -1
- package/package.json +2 -2
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var _ExecutorToPluginMigrator_instances, _ExecutorToPluginMigrator_projectGraph, _ExecutorToPluginMigrator_executor, _ExecutorToPluginMigrator_pluginPath, _ExecutorToPluginMigrator_pluginOptionsBuilder, _ExecutorToPluginMigrator_postTargetTransformer, _ExecutorToPluginMigrator_skipTargetFilter, _ExecutorToPluginMigrator_skipProjectFilter, _ExecutorToPluginMigrator_specificProjectToMigrate, _ExecutorToPluginMigrator_logger, _ExecutorToPluginMigrator_nxJson, _ExecutorToPluginMigrator_targetDefaultsForExecutor, _ExecutorToPluginMigrator_targetAndProjectsToMigrate, _ExecutorToPluginMigrator_createNodes, _ExecutorToPluginMigrator_createNodesV2, _ExecutorToPluginMigrator_createNodesResultsForTargets, _ExecutorToPluginMigrator_skippedProjects, _ExecutorToPluginMigrator_init, _ExecutorToPluginMigrator_migrateTarget, _ExecutorToPluginMigrator_migrateProject, _ExecutorToPluginMigrator_mergeInputs, _ExecutorToPluginMigrator_getTargetAndProjectsToMigrate, _ExecutorToPluginMigrator_getTargetDefaultsForExecutor, _ExecutorToPluginMigrator_getCreatedTargetForProjectRoot, _ExecutorToPluginMigrator_getCreateNodesResults;
|
|
3
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
3
|
exports.NoTargetsToMigrateError = void 0;
|
|
4
|
+
exports.stableStringify = stableStringify;
|
|
5
|
+
exports.collectMigrationScope = collectMigrationScope;
|
|
6
|
+
exports.computeResidualByProject = computeResidualByProject;
|
|
7
|
+
exports.computeStrictCommon = computeStrictCommon;
|
|
8
|
+
exports.subtractCommon = subtractCommon;
|
|
9
|
+
exports.removeDeadExecutorTargetDefault = removeDeadExecutorTargetDefault;
|
|
10
|
+
exports.appendPluginScopedTargetDefault = appendPluginScopedTargetDefault;
|
|
11
|
+
exports.removeHoistedTargetDefault = removeHoistedTargetDefault;
|
|
12
|
+
exports.packageJsonAuthorsTargetIdentity = packageJsonAuthorsTargetIdentity;
|
|
13
|
+
exports.isRegistrationOfPlugin = isRegistrationOfPlugin;
|
|
14
|
+
exports.hoistChangesExistingTargetDefaults = hoistChangesExistingTargetDefaults;
|
|
5
15
|
exports.readTargetDefaultsForExecutor = readTargetDefaultsForExecutor;
|
|
6
16
|
exports.migrateProjectExecutorsToPlugin = migrateProjectExecutorsToPlugin;
|
|
7
17
|
exports.migrateProjectExecutorsToPluginV1 = migrateProjectExecutorsToPluginV1;
|
|
8
|
-
|
|
18
|
+
exports.inferOncePerOptionSet = inferOncePerOptionSet;
|
|
19
|
+
exports.generatedIncludeRoots = generatedIncludeRoots;
|
|
20
|
+
exports.harvestConfigurationErrors = harvestConfigurationErrors;
|
|
21
|
+
exports.buildOwnerRootByPath = buildOwnerRootByPath;
|
|
9
22
|
const minimatch_1 = require("minimatch");
|
|
10
23
|
const node_assert_1 = require("node:assert");
|
|
11
24
|
const posix_1 = require("node:path/posix");
|
|
@@ -13,81 +26,255 @@ const devkit_exports_1 = require("nx/src/devkit-exports");
|
|
|
13
26
|
const devkit_internals_1 = require("nx/src/devkit-internals");
|
|
14
27
|
const executor_options_utils_1 = require("../executor-options-utils");
|
|
15
28
|
const target_defaults_utils_1 = require("../target-defaults-utils");
|
|
29
|
+
const batch_conversion_session_1 = require("./batch-conversion-session");
|
|
30
|
+
const conversion_warnings_1 = require("./conversion-warnings");
|
|
16
31
|
const plugin_migration_utils_1 = require("./plugin-migration-utils");
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
32
|
+
function stableStringify(value) {
|
|
33
|
+
if (value === null || typeof value !== 'object') {
|
|
34
|
+
return JSON.stringify(value) ?? 'undefined';
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
38
|
+
}
|
|
39
|
+
const keys = Object.keys(value).sort();
|
|
40
|
+
return `{${keys
|
|
41
|
+
.map((k) => JSON.stringify(k) +
|
|
42
|
+
':' +
|
|
43
|
+
stableStringify(value[k]))
|
|
44
|
+
.join(',')}}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Phase 0: Collect (once). Fold `forEachExecutorOptions` over every
|
|
48
|
+
* migration/executor into a single scope object, applying the skip filters with
|
|
49
|
+
* the exact same warn-vs-throw semantics the migrator used before (a
|
|
50
|
+
* `specificProjectToMigrate` skip throws instead of warning). This is the single
|
|
51
|
+
* authority for filtering; downstream phases only read the returned maps.
|
|
52
|
+
*/
|
|
53
|
+
function collectMigrationScope(tree, projectGraph, migrations, defaultPluginOptions, specificProjectToMigrate, logger) {
|
|
54
|
+
const log = logger ?? devkit_exports_1.logger;
|
|
55
|
+
const pluginOptionsByProject = new Map();
|
|
56
|
+
// Same content as pluginOptionsByProject, accumulated in walk order so the
|
|
57
|
+
// conflict check below sees options claimed by earlier targets.
|
|
58
|
+
// pluginOptionsByProject itself is populated in the grouping loop to keep
|
|
59
|
+
// its target-grouped insertion order, which addPluginRegistrations turns
|
|
60
|
+
// into the registration order in nx.json.
|
|
61
|
+
const claimedOptionsByProject = new Map();
|
|
62
|
+
const executorScopes = [];
|
|
63
|
+
const optionSetGroupsByKey = new Map();
|
|
64
|
+
for (const migration of migrations) {
|
|
65
|
+
const skipProjectFilter = migration.skipProjectFilter ?? ((..._args) => false);
|
|
66
|
+
const skipTargetFilter = migration.skipTargetFilter ?? ((..._args) => false);
|
|
67
|
+
for (const executor of migration.executors) {
|
|
68
|
+
const targetAndProjects = new Map();
|
|
69
|
+
const inferenceOptionSetIdsByTarget = new Map();
|
|
70
|
+
const inferenceOptionsByTarget = new Map();
|
|
71
|
+
// Fresh per executor to preserve the previous per-migrator semantics
|
|
72
|
+
// (each executor got its own migrator + skipped set).
|
|
73
|
+
const skippedForExecutor = new Set();
|
|
74
|
+
(0, executor_options_utils_1.forEachExecutorOptions)(tree, executor, (options, projectName, targetName, configurationName) => {
|
|
75
|
+
if (skippedForExecutor.has(projectName) || configurationName) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (specificProjectToMigrate &&
|
|
79
|
+
projectName !== specificProjectToMigrate) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const skipProjectReason = skipProjectFilter(projectGraph.nodes[projectName].data);
|
|
83
|
+
if (skipProjectReason) {
|
|
84
|
+
skippedForExecutor.add(projectName);
|
|
85
|
+
const errorMsg = `The "${projectName}" project cannot be migrated. ${skipProjectReason}`;
|
|
86
|
+
if (specificProjectToMigrate) {
|
|
87
|
+
throw new Error(errorMsg);
|
|
88
|
+
}
|
|
89
|
+
log.warn(errorMsg);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const skipTargetReason = skipTargetFilter(options, projectGraph.nodes[projectName].data);
|
|
93
|
+
if (skipTargetReason) {
|
|
94
|
+
const errorMsg = `The ${targetName} target on project "${projectName}" cannot be migrated. ${skipTargetReason}`;
|
|
95
|
+
if (specificProjectToMigrate) {
|
|
96
|
+
throw new Error(errorMsg);
|
|
97
|
+
}
|
|
98
|
+
log.warn(errorMsg);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!inferenceOptionsByTarget.has(targetName)) {
|
|
102
|
+
inferenceOptionsByTarget.set(targetName, migration.targetPluginOptionMapper(targetName));
|
|
103
|
+
}
|
|
104
|
+
const inferenceOptions = inferenceOptionsByTarget.get(targetName);
|
|
105
|
+
// One registration cannot map different target names through the
|
|
106
|
+
// same option
|
|
107
|
+
const existing = claimedOptionsByProject.get(projectName);
|
|
108
|
+
const takenOption = Object.keys(inferenceOptions).find((option) => existing?.[option] !== undefined &&
|
|
109
|
+
!isDeepEqual(existing[option], inferenceOptions[option]));
|
|
110
|
+
if (takenOption && !migration.allowSharedOptionOverwrite) {
|
|
111
|
+
const errorMsg = `The ${targetName} target on project "${projectName}" cannot be migrated. The "${takenOption}" plugin option is already set to "${existing[takenOption]}" by another target of the project, and the plugin can only infer one target per option.`;
|
|
112
|
+
if (specificProjectToMigrate) {
|
|
113
|
+
throw new Error(errorMsg);
|
|
114
|
+
}
|
|
115
|
+
log.warn(`${errorMsg} The target keeps its current configuration; rerun the migration once the other target is inferred to convert it.`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
claimedOptionsByProject.set(projectName, {
|
|
119
|
+
...(existing ?? {}),
|
|
120
|
+
...inferenceOptions,
|
|
121
|
+
});
|
|
122
|
+
if (!targetAndProjects.has(targetName)) {
|
|
123
|
+
targetAndProjects.set(targetName, new Set());
|
|
124
|
+
}
|
|
125
|
+
targetAndProjects.get(targetName).add(projectName);
|
|
126
|
+
});
|
|
127
|
+
for (const [targetName, projs] of targetAndProjects) {
|
|
128
|
+
const inferenceOptions = inferenceOptionsByTarget.get(targetName);
|
|
129
|
+
const key = stableStringify(inferenceOptions);
|
|
130
|
+
if (!optionSetGroupsByKey.has(key)) {
|
|
131
|
+
optionSetGroupsByKey.set(key, {
|
|
132
|
+
id: optionSetGroupsByKey.size,
|
|
133
|
+
options: inferenceOptions,
|
|
134
|
+
targetNames: new Set(),
|
|
135
|
+
migratedRoots: new Set(),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const optionSetGroup = optionSetGroupsByKey.get(key);
|
|
139
|
+
optionSetGroup.targetNames.add(targetName);
|
|
140
|
+
inferenceOptionSetIdsByTarget.set(targetName, optionSetGroup.id);
|
|
141
|
+
for (const project of projs) {
|
|
142
|
+
optionSetGroup.migratedRoots.add(projectGraph.nodes[project].data.root);
|
|
143
|
+
pluginOptionsByProject.set(project, {
|
|
144
|
+
...(pluginOptionsByProject.get(project) ?? {}),
|
|
145
|
+
...inferenceOptions,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (targetAndProjects.size === 0) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
executorScopes.push({
|
|
153
|
+
executor,
|
|
154
|
+
migration,
|
|
155
|
+
targetAndProjects,
|
|
156
|
+
inferenceOptionSetIdsByTarget,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Apply default plugin options (registration options only).
|
|
161
|
+
for (const [project, options] of pluginOptionsByProject) {
|
|
162
|
+
pluginOptionsByProject.set(project, {
|
|
163
|
+
...defaultPluginOptions,
|
|
164
|
+
...options,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
pluginOptionsByProject,
|
|
169
|
+
optionSetGroups: [...optionSetGroupsByKey.values()],
|
|
170
|
+
executorScopes,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function stripInferredTarget(fullInferredTarget) {
|
|
174
|
+
const stripped = structuredClone(fullInferredTarget);
|
|
175
|
+
delete stripped.command;
|
|
176
|
+
delete stripped.options?.cwd;
|
|
177
|
+
return stripped;
|
|
178
|
+
}
|
|
179
|
+
function getFullInferredTarget(inferredTargetsByOptionSet, optionSetId, projectRoot, targetName) {
|
|
180
|
+
const inferredByRoot = inferredTargetsByOptionSet.get(optionSetId);
|
|
181
|
+
const inferredTargetsByName = inferredByRoot?.get(projectRoot);
|
|
182
|
+
if (!inferredTargetsByName) {
|
|
183
|
+
throw new Error(`The nx plugin did not find a project inside ${projectRoot}. File an issue at https://github.com/nrwl/nx with information about your project structure.`);
|
|
184
|
+
}
|
|
185
|
+
const inferredTarget = inferredTargetsByName.get(targetName);
|
|
186
|
+
if (!inferredTarget) {
|
|
187
|
+
// The plugin found the project but did not infer a "${targetName}" target
|
|
188
|
+
// for it under this option set: the migration and inference disagree on
|
|
189
|
+
// the target name.
|
|
190
|
+
throw new Error(`The nx plugin found a project inside ${projectRoot} but did not infer a "${targetName}" target for it. File an issue at https://github.com/nrwl/nx with information about your project structure.`);
|
|
191
|
+
}
|
|
192
|
+
return inferredTarget;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Phase 2: Per-project residual (in-memory, no inference). For each
|
|
196
|
+
* `(project, target)` computes the residual exactly as the previous engine did
|
|
197
|
+
* (`mergeTargetConfigurations` with the executor target defaults ->
|
|
198
|
+
* `deleteMatchingProperties` -> input merge -> the plugin's
|
|
199
|
+
* `postTargetTransformer`), plus `baselineFinal = merge(residual, inferred)` as
|
|
200
|
+
* the equivalence oracle. Does NOT write project.json.
|
|
201
|
+
*/
|
|
202
|
+
async function computeResidualByProject(tree, projectGraph, scope, inferredTargetsByOptionSet, nxJson, projectConfigsByName = (0, devkit_exports_1.getProjects)(tree)) {
|
|
203
|
+
const residualByProject = new Map();
|
|
204
|
+
for (const executorScope of scope.executorScopes) {
|
|
205
|
+
const targetDefaultsForExecutor = readTargetDefaultsForExecutor(executorScope.executor, nxJson.targetDefaults) ?? {};
|
|
206
|
+
for (const [targetName, projectNames] of executorScope.targetAndProjects) {
|
|
207
|
+
const optionSetId = executorScope.inferenceOptionSetIdsByTarget.get(targetName);
|
|
208
|
+
if (optionSetId === undefined) {
|
|
209
|
+
throw new Error(`Could not find an inference option set for target "${targetName}".`);
|
|
210
|
+
}
|
|
211
|
+
for (const projectName of projectNames) {
|
|
212
|
+
const root = projectGraph.nodes[projectName].data.root;
|
|
213
|
+
const fullInferredTarget = getFullInferredTarget(inferredTargetsByOptionSet, optionSetId, root, targetName);
|
|
214
|
+
const strippedInferredTarget = stripInferredTarget(fullInferredTarget);
|
|
215
|
+
const projectConfig = readCachedProjectConfiguration(tree, projectConfigsByName, projectName);
|
|
216
|
+
const preMigrationTarget = structuredClone(projectConfig.targets[targetName]);
|
|
217
|
+
// Later steps mutate the merged target, which may retain nested
|
|
218
|
+
// references from either input
|
|
219
|
+
let projectTarget = (0, devkit_internals_1.mergeTargetConfigurations)(structuredClone(projectConfig.targets[targetName]), structuredClone(targetDefaultsForExecutor));
|
|
220
|
+
delete projectTarget.executor;
|
|
221
|
+
(0, plugin_migration_utils_1.deleteMatchingProperties)(projectTarget, strippedInferredTarget);
|
|
222
|
+
if (projectTarget.inputs && strippedInferredTarget.inputs) {
|
|
223
|
+
mergeInputs(projectTarget, strippedInferredTarget);
|
|
224
|
+
}
|
|
225
|
+
projectTarget = await executorScope.migration.postTargetTransformer(projectTarget, tree, { projectName, root }, { ...strippedInferredTarget, name: targetName });
|
|
226
|
+
if (projectTarget.options &&
|
|
227
|
+
Object.keys(projectTarget.options).length === 0) {
|
|
228
|
+
delete projectTarget.options;
|
|
229
|
+
}
|
|
230
|
+
const residual = projectTarget;
|
|
231
|
+
const baselineFinal = (0, devkit_internals_1.mergeTargetConfigurations)(structuredClone(residual), structuredClone(fullInferredTarget));
|
|
232
|
+
if (!residualByProject.has(projectName)) {
|
|
233
|
+
residualByProject.set(projectName, new Map());
|
|
234
|
+
}
|
|
235
|
+
residualByProject
|
|
236
|
+
.get(projectName)
|
|
237
|
+
.set(targetName, { residual, baselineFinal, preMigrationTarget });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return residualByProject;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Phase 3 (single-project-mode variant): write the full residual into each
|
|
245
|
+
* project.json, exactly reproducing the previous per-(project, target) write
|
|
246
|
+
* sequence. Single-project mode never centralizes, so this is its permanent
|
|
247
|
+
* write path.
|
|
248
|
+
*/
|
|
249
|
+
function writeResiduals(tree, projectConfigsByName, projectGraph, scope, residualByProject, logger) {
|
|
250
|
+
for (const executorScope of scope.executorScopes) {
|
|
251
|
+
for (const [targetName, projectNames] of executorScope.targetAndProjects) {
|
|
252
|
+
for (const projectName of projectNames) {
|
|
253
|
+
const entry = residualByProject.get(projectName)?.get(targetName);
|
|
254
|
+
if (!entry) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
entry.keptPreMigration = !writeResidualTarget(tree, projectConfigsByName, projectName, targetName, structuredClone(entry.residual), logger);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Write a single residual target into project.json (or delete it if empty).
|
|
264
|
+
* Returns false, writing nothing, when package.json authors the target's
|
|
265
|
+
* identity: without the explicit executor an included same-name script (or an
|
|
266
|
+
* `nx.targets` executor/command) would replace the inferred target in the
|
|
267
|
+
* default plugin layer, so the pre-migration target is kept as is.
|
|
268
|
+
*/
|
|
269
|
+
function writeResidualTarget(tree, projectConfigsByName, projectName, targetName, residual, logger) {
|
|
270
|
+
const projectConfig = readCachedProjectConfiguration(tree, projectConfigsByName, projectName);
|
|
271
|
+
const identitySource = packageJsonAuthorsTargetIdentity(tree, projectConfig.root, targetName);
|
|
272
|
+
if (identitySource) {
|
|
273
|
+
(logger ?? devkit_exports_1.logger).warn((0, conversion_warnings_1.keptPreMigrationTargetWarning)(targetName, projectName, identitySource));
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
if (Object.keys(residual).length > 0) {
|
|
277
|
+
projectConfig.targets[targetName] = residual;
|
|
91
278
|
}
|
|
92
279
|
else {
|
|
93
280
|
delete projectConfig.targets[targetName];
|
|
@@ -96,8 +283,633 @@ _ExecutorToPluginMigrator_projectGraph = new WeakMap(), _ExecutorToPluginMigrato
|
|
|
96
283
|
projectConfig['// targets'] =
|
|
97
284
|
`to see all targets run: nx show project ${projectName} --web`;
|
|
98
285
|
}
|
|
99
|
-
(0, devkit_exports_1.updateProjectConfiguration)(
|
|
100
|
-
|
|
286
|
+
(0, devkit_exports_1.updateProjectConfiguration)(tree, projectName, projectConfig);
|
|
287
|
+
// `updateProjectConfiguration` writes a package-based project (no
|
|
288
|
+
// project.json) by spreading the config over package.json `nx`, and deletes an
|
|
289
|
+
// empty `targets` from this very (cached) object before the spread. Two
|
|
290
|
+
// consequences: the old `nx.targets` entry (executor included) survives in
|
|
291
|
+
// package.json, and the cache can no longer take a later write (the Phase 4
|
|
292
|
+
// revert). Delete the entry here and keep the cache writable.
|
|
293
|
+
if (!projectConfig.targets) {
|
|
294
|
+
projectConfig.targets = {};
|
|
295
|
+
deletePackageJsonTarget(tree, projectConfig.root, targetName);
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
function deletePackageJsonTarget(tree, root, targetName) {
|
|
300
|
+
const packageJsonPath = (0, posix_1.join)(root, 'package.json');
|
|
301
|
+
const packageJson = (0, devkit_exports_1.readJson)(tree, packageJsonPath);
|
|
302
|
+
if (!packageJson.nx?.targets?.[targetName]) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
delete packageJson.nx.targets[targetName];
|
|
306
|
+
if (Object.keys(packageJson.nx.targets).length === 0) {
|
|
307
|
+
delete packageJson.nx.targets;
|
|
308
|
+
}
|
|
309
|
+
(0, devkit_exports_1.writeJson)(tree, packageJsonPath, packageJson);
|
|
310
|
+
}
|
|
311
|
+
function readCachedProjectConfiguration(tree, projectConfigsByName, projectName) {
|
|
312
|
+
const projectConfig = projectConfigsByName.get(projectName);
|
|
313
|
+
if (projectConfig) {
|
|
314
|
+
return projectConfig;
|
|
315
|
+
}
|
|
316
|
+
const readProjectConfig = (0, devkit_exports_1.readProjectConfiguration)(tree, projectName);
|
|
317
|
+
projectConfigsByName.set(projectName, readProjectConfig);
|
|
318
|
+
return readProjectConfig;
|
|
319
|
+
}
|
|
320
|
+
function isDeepEqual(a, b) {
|
|
321
|
+
try {
|
|
322
|
+
(0, node_assert_1.deepStrictEqual)(a, b);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Phase 3: the strict-common residual across ALL migrated projects for a
|
|
331
|
+
* target: the values that are deep-equal across every project's residual.
|
|
332
|
+
* Granularity: whole value for top-level target props (`inputs`, `outputs`,
|
|
333
|
+
* `cache`, `dependsOn`, `configurations`, ...); per-key for `options`. A key is
|
|
334
|
+
* common only when EVERY residual carries it with an identical value.
|
|
335
|
+
*/
|
|
336
|
+
function computeStrictCommon(residuals) {
|
|
337
|
+
if (residuals.length === 0) {
|
|
338
|
+
return {};
|
|
339
|
+
}
|
|
340
|
+
const [first, ...rest] = residuals;
|
|
341
|
+
const common = {};
|
|
342
|
+
for (const key of Object.keys(first)) {
|
|
343
|
+
if (key === 'options') {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const value = first[key];
|
|
347
|
+
const isCommon = rest.every((residual) => {
|
|
348
|
+
const r = residual;
|
|
349
|
+
return key in r && isDeepEqual(r[key], value);
|
|
350
|
+
});
|
|
351
|
+
if (isCommon) {
|
|
352
|
+
common[key] = structuredClone(value);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const firstOptions = first.options;
|
|
356
|
+
if (firstOptions && typeof firstOptions === 'object') {
|
|
357
|
+
const commonOptions = {};
|
|
358
|
+
for (const optKey of Object.keys(firstOptions)) {
|
|
359
|
+
const optValue = firstOptions[optKey];
|
|
360
|
+
const isCommon = rest.every((residual) => {
|
|
361
|
+
const options = residual.options;
|
|
362
|
+
return (options &&
|
|
363
|
+
typeof options === 'object' &&
|
|
364
|
+
optKey in options &&
|
|
365
|
+
isDeepEqual(options[optKey], optValue));
|
|
366
|
+
});
|
|
367
|
+
if (isCommon) {
|
|
368
|
+
commonOptions[optKey] = structuredClone(optValue);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (Object.keys(commonOptions).length > 0) {
|
|
372
|
+
common.options = commonOptions;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return common;
|
|
376
|
+
}
|
|
377
|
+
/** `residual` with every property that the strict-common config carries removed. */
|
|
378
|
+
function subtractCommon(residual, common) {
|
|
379
|
+
const deviation = structuredClone(residual);
|
|
380
|
+
for (const key of Object.keys(common)) {
|
|
381
|
+
if (key === 'options') {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const d = deviation;
|
|
385
|
+
if (key in d &&
|
|
386
|
+
isDeepEqual(d[key], common[key])) {
|
|
387
|
+
delete d[key];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (common.options && deviation.options) {
|
|
391
|
+
const commonOptions = common.options;
|
|
392
|
+
const deviationOptions = deviation.options;
|
|
393
|
+
for (const optKey of Object.keys(commonOptions)) {
|
|
394
|
+
if (optKey in deviationOptions &&
|
|
395
|
+
isDeepEqual(deviationOptions[optKey], commonOptions[optKey])) {
|
|
396
|
+
delete deviationOptions[optKey];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (Object.keys(deviationOptions).length === 0) {
|
|
400
|
+
delete deviation.options;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return deviation;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Remove the now-dead executor-keyed target default that Phase 2 inlined into
|
|
407
|
+
* every migrated project (mirrors `readTargetDefaultsForExecutor`'s match: the
|
|
408
|
+
* unfiltered entry keyed directly by the executor string).
|
|
409
|
+
*/
|
|
410
|
+
function removeDeadExecutorTargetDefault(nxJson, executor) {
|
|
411
|
+
(0, target_defaults_utils_1.updateTargetDefault)(nxJson, { executor }, (_config, info) => info.key === executor && info.filter === undefined ? null : undefined);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Append the hoisted common as a plugin-scoped entry after whatever value the
|
|
415
|
+
* key already holds. Existing entries, the workspace catch-all and any
|
|
416
|
+
* user-authored filtered entries, are never modified, so targets outside this
|
|
417
|
+
* plugin resolve exactly what they resolved before the migration. The entry is
|
|
418
|
+
* appended (never merged into an existing one) so the verification pass can
|
|
419
|
+
* revert precisely this entry and nothing else.
|
|
420
|
+
*/
|
|
421
|
+
function appendPluginScopedTargetDefault(nxJson, targetName, pluginPath, common) {
|
|
422
|
+
const entry = {
|
|
423
|
+
filter: { plugin: pluginPath },
|
|
424
|
+
...structuredClone(common),
|
|
425
|
+
};
|
|
426
|
+
nxJson.targetDefaults ??= {};
|
|
427
|
+
const existing = nxJson.targetDefaults[targetName];
|
|
428
|
+
nxJson.targetDefaults[targetName] =
|
|
429
|
+
existing === undefined
|
|
430
|
+
? [entry]
|
|
431
|
+
: Array.isArray(existing)
|
|
432
|
+
? [...existing, entry]
|
|
433
|
+
: [existing, entry];
|
|
434
|
+
return entry;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Remove a previously appended plugin-scoped entry, collapsing the value back
|
|
438
|
+
* to the plain object form when only a lone unfiltered entry remains. The
|
|
439
|
+
* appended entry survives an `updateNxJson`/`readNxJson` round trip only by
|
|
440
|
+
* value, so the last deep-equal occurrence (append order puts ours last) is
|
|
441
|
+
* the one removed.
|
|
442
|
+
*/
|
|
443
|
+
function removeHoistedTargetDefault(nxJson, targetName, entry) {
|
|
444
|
+
const value = nxJson.targetDefaults?.[targetName];
|
|
445
|
+
if (!Array.isArray(value)) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const entryKey = stableStringify(entry);
|
|
449
|
+
const remaining = [...value];
|
|
450
|
+
for (let i = remaining.length - 1; i >= 0; i--) {
|
|
451
|
+
if (stableStringify(remaining[i]) === entryKey) {
|
|
452
|
+
remaining.splice(i, 1);
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (remaining.length === 0) {
|
|
457
|
+
delete nxJson.targetDefaults[targetName];
|
|
458
|
+
}
|
|
459
|
+
else if (remaining.length === 1 && remaining[0].filter === undefined) {
|
|
460
|
+
const { filter: _filter, ...config } = remaining[0];
|
|
461
|
+
nxJson.targetDefaults[targetName] = config;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
nxJson.targetDefaults[targetName] = remaining;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Which `package.json` signal, if any, authors an identity for `targetName` in
|
|
469
|
+
* the DEFAULT plugin layer. The package-json plugin turns every included script into
|
|
470
|
+
* an `nx:run-script` target and honors `nx.targets`; either way the target gains
|
|
471
|
+
* an `executor`/`command` in a default layer, which makes Nx's
|
|
472
|
+
* `resolveSourcePlugin` refuse a `filter: { plugin }` targetDefault for it.
|
|
473
|
+
* The hoist uses this to keep the full residual per project instead of silently
|
|
474
|
+
* dropping the centralized keys; the residual write uses it to keep the
|
|
475
|
+
* pre-migration target instead of letting the package.json identity take the
|
|
476
|
+
* target over.
|
|
477
|
+
*
|
|
478
|
+
* Read through the Tree so this sees the same in-memory package.json the rest of
|
|
479
|
+
* the generator reads and writes, rather than a possibly-stale copy on disk.
|
|
480
|
+
*/
|
|
481
|
+
function packageJsonAuthorsTargetIdentity(tree, root, targetName) {
|
|
482
|
+
if (!root) {
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
const packageJsonPath = (0, posix_1.join)(root, 'package.json');
|
|
486
|
+
if (!tree.exists(packageJsonPath)) {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
let packageJson;
|
|
490
|
+
try {
|
|
491
|
+
packageJson = (0, devkit_exports_1.readJson)(tree, packageJsonPath);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
// Nx reads this file with the same jsonc-tolerant parser (`//` comments,
|
|
495
|
+
// trailing commas), so a failure here means inference cannot be trusted for
|
|
496
|
+
// it either. Fail closed: treat the identity as authored rather than hoist
|
|
497
|
+
// on a guess.
|
|
498
|
+
return 'unparseable';
|
|
499
|
+
}
|
|
500
|
+
const scripts = packageJson?.scripts ?? {};
|
|
501
|
+
// `readTargetsFromPackageJson` turns each *included* script into an
|
|
502
|
+
// `nx:run-script` target (identity). `nx.includedScripts`, when present,
|
|
503
|
+
// restricts which scripts become targets. Normalize a malformed (non-array)
|
|
504
|
+
// value to the default (all scripts) instead of letting `.includes` throw an
|
|
505
|
+
// uncaught TypeError mid-generator.
|
|
506
|
+
const nxIncludedScripts = packageJson?.nx?.includedScripts;
|
|
507
|
+
const includedScripts = Array.isArray(nxIncludedScripts)
|
|
508
|
+
? nxIncludedScripts
|
|
509
|
+
: Object.keys(scripts);
|
|
510
|
+
if (includedScripts.includes(targetName)) {
|
|
511
|
+
return 'script';
|
|
512
|
+
}
|
|
513
|
+
// The `nx.targets` check below reads PRE-migration state. In a package-based
|
|
514
|
+
// workspace the target being migrated lives in `nx.targets` itself (that is
|
|
515
|
+
// where its executor is authored), so reading it here would flag every project
|
|
516
|
+
// as authoring its own identity, exclude them all, and centralize nothing.
|
|
517
|
+
// A package-based project's residual is written back to `nx.targets` without
|
|
518
|
+
// the migrated executor (a residual that still carries an executor/command is
|
|
519
|
+
// excluded by the caller before this gate), so post-migration it authors no
|
|
520
|
+
// identity and the hoisted `filter:{plugin}` default resolves. Gate on the
|
|
521
|
+
// same `project.json` signal `updateProjectConfiguration` uses to decide where
|
|
522
|
+
// config lives: only trust an `nx.targets` identity when the project keeps its
|
|
523
|
+
// config in `project.json` (there a package.json `nx.targets` entry is
|
|
524
|
+
// genuinely separate from the migrated target).
|
|
525
|
+
if (!tree.exists((0, posix_1.join)(root, 'project.json'))) {
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
// An `nx.targets` entry authors identity only when it says how to run.
|
|
529
|
+
const nxTarget = packageJson?.nx?.targets?.[targetName];
|
|
530
|
+
return nxTarget != null &&
|
|
531
|
+
(nxTarget.executor !== undefined || nxTarget.command !== undefined)
|
|
532
|
+
? 'nxTargets'
|
|
533
|
+
: undefined;
|
|
534
|
+
}
|
|
535
|
+
function isRegistrationOfPlugin(registration, pluginPath) {
|
|
536
|
+
return typeof registration === 'string'
|
|
537
|
+
? registration === pluginPath
|
|
538
|
+
: registration.plugin === pluginPath;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Whether every nx.json `plugins` entry after the migrated plugin's first
|
|
542
|
+
* registration is also the migrated plugin. Only then is the migrated plugin
|
|
543
|
+
* guaranteed to merge last for the targets it infers, keeping the
|
|
544
|
+
* executor/command attribution (and with it `filter: { plugin }`
|
|
545
|
+
* target-default resolution) with this plugin. No registration at all fails
|
|
546
|
+
* closed.
|
|
547
|
+
*/
|
|
548
|
+
function pluginRegistrationsFormTail(nxJson, pluginPath) {
|
|
549
|
+
const plugins = nxJson.plugins ?? [];
|
|
550
|
+
const isMigratedPlugin = (plugin) => isRegistrationOfPlugin(plugin, pluginPath);
|
|
551
|
+
const firstIndex = plugins.findIndex(isMigratedPlugin);
|
|
552
|
+
if (firstIndex === -1) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
return plugins.slice(firstIndex + 1).every(isMigratedPlugin);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* The effective executor each migrated (project, target) pair resolves to
|
|
559
|
+
* after the migration: the INFERRED target's executor, with a top-level
|
|
560
|
+
* `command` resolving to `nx:run-commands` (mirrors
|
|
561
|
+
* `resolveCommandSyntacticSugar`). Keyed by `"<project>\t<target>"`. Input to
|
|
562
|
+
* the target-default preflight, inline and at batch finalize.
|
|
563
|
+
*/
|
|
564
|
+
function computeInferredExecutorByPair(scope, projectGraph, inferredTargetsByOptionSet) {
|
|
565
|
+
const inferredExecutorByPair = new Map();
|
|
566
|
+
for (const executorScope of scope.executorScopes) {
|
|
567
|
+
for (const [targetName, projectNames] of executorScope.targetAndProjects) {
|
|
568
|
+
const optionSetId = executorScope.inferenceOptionSetIdsByTarget.get(targetName);
|
|
569
|
+
for (const projectName of projectNames) {
|
|
570
|
+
const root = projectGraph.nodes[projectName]?.data.root;
|
|
571
|
+
const inferredTarget = getFullInferredTarget(inferredTargetsByOptionSet, optionSetId, root, targetName);
|
|
572
|
+
const inferredExecutor = inferredTarget.executor ??
|
|
573
|
+
(inferredTarget.command ? 'nx:run-commands' : undefined);
|
|
574
|
+
inferredExecutorByPair.set(`${projectName}\t${targetName}`, inferredExecutor);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return inferredExecutorByPair;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Whether appending the plugin-scoped `targetDefaults[targetName]` entry would
|
|
582
|
+
* change what the existing target defaults resolve to for any eligible migrated
|
|
583
|
+
* pair. Two hazards, both invisible to the Phase 4 verification (it merges no
|
|
584
|
+
* target defaults):
|
|
585
|
+
*
|
|
586
|
+
* - Key displacement: an exact target-name key takes precedence over a glob
|
|
587
|
+
* key (`build-*`), so appending one can silently stop a glob default from
|
|
588
|
+
* contributing to the migrated targets.
|
|
589
|
+
* - Executor masking: an executor-keyed default for the plugin's INFERRED
|
|
590
|
+
* executor (e.g. `nx:run-commands` for command-based inferred targets) takes
|
|
591
|
+
* precedence over the exact key, so the appended entry would never resolve
|
|
592
|
+
* and its keys would be silently dropped.
|
|
593
|
+
*
|
|
594
|
+
* The check resolves the defaults for each pair without and with the
|
|
595
|
+
* hypothetical entry, through the production reader. The hoist is a pure
|
|
596
|
+
* "residual moved into a default" only when the with-entry resolution equals
|
|
597
|
+
* the without-entry resolution with the common merged on top; anything else
|
|
598
|
+
* changes behavior, so the target keeps its full residuals.
|
|
599
|
+
*/
|
|
600
|
+
function hoistChangesExistingTargetDefaults(targetDefaults, targetName, common, pluginPath, eligiblePairs, projectNodesByName) {
|
|
601
|
+
const hypotheticalNxJson = {
|
|
602
|
+
targetDefaults: structuredClone(targetDefaults) ?? {},
|
|
603
|
+
};
|
|
604
|
+
appendPluginScopedTargetDefault(hypotheticalNxJson, targetName, pluginPath, common);
|
|
605
|
+
// Per-pair context only matters to `filter.projects` entries under a key
|
|
606
|
+
// this target's lookup can select (its own name, one of the pairs' effective
|
|
607
|
+
// executors, or a glob matching the name); everything else resolves purely
|
|
608
|
+
// from the pair's effective executor, so pairs sharing one resolve
|
|
609
|
+
// identically and only the first needs checking. The appended entry carries
|
|
610
|
+
// no `projects` filter, so scanning the current map covers the hypothetical
|
|
611
|
+
// one too.
|
|
612
|
+
const pairExecutors = new Set();
|
|
613
|
+
for (const { inferredExecutor } of eligiblePairs) {
|
|
614
|
+
if (inferredExecutor !== undefined) {
|
|
615
|
+
pairExecutors.add(inferredExecutor);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const hasRelevantProjectScopedEntry = Object.entries(targetDefaults ?? {}).some(([key, value]) => {
|
|
619
|
+
// The glob arm mirrors the reader's own selection test exactly; a bare
|
|
620
|
+
// `minimatch` would over-match keys the reader never treats as globs
|
|
621
|
+
// (e.g. `?`, which is not in Nx's glob character set).
|
|
622
|
+
const keyParticipates = key === targetName ||
|
|
623
|
+
pairExecutors.has(key) ||
|
|
624
|
+
((0, devkit_internals_1.isGlobPattern)(key) && (0, minimatch_1.minimatch)(targetName, key));
|
|
625
|
+
return (keyParticipates &&
|
|
626
|
+
(Array.isArray(value) ? value : [value ?? {}]).some((entry) => entry.filter?.projects !== undefined));
|
|
627
|
+
});
|
|
628
|
+
const checkedContexts = new Set();
|
|
629
|
+
for (const { projectName, inferredExecutor } of eligiblePairs) {
|
|
630
|
+
const contextKey = hasRelevantProjectScopedEntry
|
|
631
|
+
? `${projectName}\t${inferredExecutor}`
|
|
632
|
+
: `${inferredExecutor}`;
|
|
633
|
+
if (checkedContexts.has(contextKey)) {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
checkedContexts.add(contextKey);
|
|
637
|
+
const opts = {
|
|
638
|
+
projectName,
|
|
639
|
+
projectNode: projectNodesByName[projectName],
|
|
640
|
+
// The migrated pair's residual carries no executor/command, so the
|
|
641
|
+
// specified layer (this plugin) owns the identity attribution.
|
|
642
|
+
sourcePlugin: pluginPath,
|
|
643
|
+
};
|
|
644
|
+
const before = (0, devkit_internals_1.readTargetDefaultsForTarget)(targetName, targetDefaults, inferredExecutor, opts);
|
|
645
|
+
const after = (0, devkit_internals_1.readTargetDefaultsForTarget)(targetName, hypotheticalNxJson.targetDefaults, inferredExecutor, opts);
|
|
646
|
+
// The reader merges a key's later entries on top of earlier ones, and the
|
|
647
|
+
// appended entry is last, so an unchanged resolution is exactly the prior
|
|
648
|
+
// result with the common merged over it.
|
|
649
|
+
const expected = before === null
|
|
650
|
+
? structuredClone(common)
|
|
651
|
+
: (0, devkit_internals_1.mergeTargetConfigurations)(structuredClone(common), structuredClone(before));
|
|
652
|
+
// stableStringify rather than isDeepEqual: deepStrictEqual also compares
|
|
653
|
+
// prototype identity, which rejects structurally equal objects here.
|
|
654
|
+
if (stableStringify(after ?? {}) !== stableStringify(expected)) {
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Phase 3 (centralized variant): hoist the strict-common residual per target
|
|
662
|
+
* into `nx.json` `targetDefaults[targetName]` as a plugin-scoped array entry,
|
|
663
|
+
* remove the dead executor-keyed entries, and write only per-project
|
|
664
|
+
* deviations to project.json. Used for whole-workspace migrations;
|
|
665
|
+
* single-project mode keeps the full residual. Hoisting also requires the
|
|
666
|
+
* plugin's registrations to form the tail of nx.json `plugins` (see
|
|
667
|
+
* `pluginRegistrationsFormTail`), and the appended
|
|
668
|
+
* entry to leave existing target-default resolution unchanged (see
|
|
669
|
+
* `hoistChangesExistingTargetDefaults`); otherwise the affected projects keep
|
|
670
|
+
* their full residuals.
|
|
671
|
+
*
|
|
672
|
+
* Returns the appended entry per target so the verification pass can revert a
|
|
673
|
+
* hoist that turns out to reach a target the migration did not migrate.
|
|
674
|
+
*/
|
|
675
|
+
function hoistCommonAndWrite(tree, projectConfigsByName, scope, residualByProject, pluginPath, projectGraph, inferredTargetsByOptionSet, inferredExecutors, pluginPreRegistered, logger) {
|
|
676
|
+
// Group residuals by target name across all migrated projects, tracking which
|
|
677
|
+
// projects contribute each target so we can inspect their default-layer config.
|
|
678
|
+
const residualsByTarget = new Map();
|
|
679
|
+
const projectsByTarget = new Map();
|
|
680
|
+
for (const [projectName, targetMap] of residualByProject) {
|
|
681
|
+
for (const [targetName, entry] of targetMap) {
|
|
682
|
+
if (!residualsByTarget.has(targetName)) {
|
|
683
|
+
residualsByTarget.set(targetName, []);
|
|
684
|
+
projectsByTarget.set(targetName, []);
|
|
685
|
+
}
|
|
686
|
+
residualsByTarget.get(targetName).push(entry.residual);
|
|
687
|
+
projectsByTarget.get(targetName).push(projectName);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const commonByTarget = new Map();
|
|
691
|
+
// Projects whose target identity is authored in a DEFAULT layer, per target.
|
|
692
|
+
// Nx's `resolveSourcePlugin` refuses a `filter: { plugin }` default for such a
|
|
693
|
+
// target (see nx/.../project-configuration/target-defaults.ts), so those
|
|
694
|
+
// projects can't inherit the hoisted keys and must carry the full config
|
|
695
|
+
// themselves. The partition is PER-PROJECT: one project with authored identity
|
|
696
|
+
// no longer blocks centralization for its siblings.
|
|
697
|
+
const excludedProjectsByTarget = new Map();
|
|
698
|
+
// package.json can reintroduce a migrated executor at resolution after the
|
|
699
|
+
// explicit target is rewritten, invisible to the liveness scan below (the
|
|
700
|
+
// package config is ignored when a sibling project.json exists).
|
|
701
|
+
let packageJsonAuthoredIdentity = false;
|
|
702
|
+
// Deterministic target-name order for stable nx.json output.
|
|
703
|
+
for (const targetName of [...residualsByTarget.keys()].sort()) {
|
|
704
|
+
const residuals = residualsByTarget.get(targetName);
|
|
705
|
+
const projects = projectsByTarget.get(targetName) ?? [];
|
|
706
|
+
// Partition the target's projects. A target's identity is authored in a
|
|
707
|
+
// default layer when either:
|
|
708
|
+
// - the project.json residual carries `executor`/`command` (e.g. @nx/detox
|
|
709
|
+
// stamps a per-project `command`), or
|
|
710
|
+
// - the package-json plugin authors it: a package.json script byte-equal
|
|
711
|
+
// to the target name becomes an `nx:run-script` target (and `nx.targets`
|
|
712
|
+
// is honored).
|
|
713
|
+
// Such projects are EXCLUDED from the hoist (they keep the full residual,
|
|
714
|
+
// or the pre-migration target when package.json authors the identity, see
|
|
715
|
+
// `writeResidualTarget`); the rest are hoist-eligible.
|
|
716
|
+
const excludedProjects = new Set();
|
|
717
|
+
const eligibleResiduals = [];
|
|
718
|
+
for (let i = 0; i < projects.length; i++) {
|
|
719
|
+
const projectName = projects[i];
|
|
720
|
+
const residual = residuals[i];
|
|
721
|
+
const packageJsonAuthored = packageJsonAuthorsTargetIdentity(tree, projectConfigsByName.get(projectName)?.root, targetName);
|
|
722
|
+
if (packageJsonAuthored) {
|
|
723
|
+
packageJsonAuthoredIdentity = true;
|
|
724
|
+
}
|
|
725
|
+
const identityAuthored = residual.executor !== undefined ||
|
|
726
|
+
residual.command !== undefined ||
|
|
727
|
+
packageJsonAuthored;
|
|
728
|
+
if (identityAuthored) {
|
|
729
|
+
excludedProjects.add(projectName);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
eligibleResiduals.push(residual);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
excludedProjectsByTarget.set(targetName, excludedProjects);
|
|
736
|
+
// Centralization only pays off when at least two ELIGIBLE projects share the
|
|
737
|
+
// target; the common is computed over eligible residuals only, so an excluded
|
|
738
|
+
// project's config never leaks into the shared default.
|
|
739
|
+
const common = eligibleResiduals.length >= 2
|
|
740
|
+
? computeStrictCommon(eligibleResiduals)
|
|
741
|
+
: {};
|
|
742
|
+
commonByTarget.set(targetName, common);
|
|
743
|
+
}
|
|
744
|
+
// Drop the given targets' centralization (they keep full residuals) and say
|
|
745
|
+
// why. The retention is lossless: the resolved configuration is unchanged,
|
|
746
|
+
// only the deduplication into `targetDefaults` is skipped.
|
|
747
|
+
const retainResiduals = (targetNames, reason) => {
|
|
748
|
+
for (const targetName of targetNames) {
|
|
749
|
+
commonByTarget.set(targetName, {});
|
|
750
|
+
}
|
|
751
|
+
(logger ?? devkit_exports_1.logger).warn((0, conversion_warnings_1.retainedResidualsWarning)(targetNames, reason));
|
|
752
|
+
};
|
|
753
|
+
const centralizableTargets = () => [...commonByTarget.entries()]
|
|
754
|
+
.filter(([, common]) => Object.keys(common).length > 0)
|
|
755
|
+
.map(([targetName]) => targetName)
|
|
756
|
+
.sort();
|
|
757
|
+
// A specified plugin registered AFTER the migrated plugin merges later, so a
|
|
758
|
+
// same-name target it authors takes the executor/command attribution;
|
|
759
|
+
// `resolveSourcePlugin` then names that plugin and rejects this plugin's
|
|
760
|
+
// `filter: { plugin }` default, silently dropping the hoisted keys. The
|
|
761
|
+
// verification pass loads only the migrated plugin, so it cannot observe
|
|
762
|
+
// this. Registrations are written before this runs, so the plugins array is
|
|
763
|
+
// final for THIS generator invocation: hoist only when the migrated plugin's
|
|
764
|
+
// registrations form its tail; otherwise keep the full residuals (the
|
|
765
|
+
// previous engine's output). A batch runner invoking several conversions
|
|
766
|
+
// against one Tree appends more registrations after this one returns, where
|
|
767
|
+
// this tail check would pass on a plugins array that is not final; such
|
|
768
|
+
// invocations never reach this hoist, since a batch child defers
|
|
769
|
+
// centralization entirely to the finalize pass (see BatchConversionSession).
|
|
770
|
+
if (!pluginRegistrationsFormTail((0, devkit_exports_1.readNxJson)(tree), pluginPath)) {
|
|
771
|
+
const skippedTargets = centralizableTargets();
|
|
772
|
+
if (skippedTargets.length > 0) {
|
|
773
|
+
retainResiduals(skippedTargets, `another plugin is registered after ${pluginPath} in nx.json and may take over those targets`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// Target-default preflight: even with the plugin merging last, appending the
|
|
777
|
+
// exact target-name entry can change what the EXISTING defaults resolve to
|
|
778
|
+
// for the migrated pairs (see `hoistChangesExistingTargetDefaults`). Checked
|
|
779
|
+
// per pair with the production reader; a rejected target keeps its full
|
|
780
|
+
// residuals. In-memory only, no inference or graph pass.
|
|
781
|
+
const preflightTargets = centralizableTargets();
|
|
782
|
+
if (preflightTargets.length > 0) {
|
|
783
|
+
const currentTargetDefaults = (0, devkit_exports_1.readNxJson)(tree).targetDefaults;
|
|
784
|
+
// The pair's effective executor decides whether an executor-keyed default
|
|
785
|
+
// outranks the appended exact key.
|
|
786
|
+
const inferredExecutorByPair = computeInferredExecutorByPair(scope, projectGraph, inferredTargetsByOptionSet);
|
|
787
|
+
const nonExactNameTargets = [];
|
|
788
|
+
const rejectedTargets = [];
|
|
789
|
+
for (const targetName of preflightTargets) {
|
|
790
|
+
// A `targetDefaults` key resolves as an EXECUTOR key for any target
|
|
791
|
+
// whose effective executor equals it (executor strings are plain
|
|
792
|
+
// strings, no `:` required), and as a glob key when it contains glob
|
|
793
|
+
// characters. So a candidate name that is executor-like (`a:b`),
|
|
794
|
+
// glob-like, or equal to ANY executor this plugin's inference emits
|
|
795
|
+
// cannot be hoisted: the appended key would apply the common to OTHER
|
|
796
|
+
// targets of this plugin, invisible to the per-pair check below (it
|
|
797
|
+
// resolves only the migrated pairs) and to the Phase 4 exposure revert
|
|
798
|
+
// (it looks only for this target name). Rejecting here also keeps the
|
|
799
|
+
// surviving candidates non-interacting, so checking each against the
|
|
800
|
+
// current map alone stays sound.
|
|
801
|
+
if (!(0, target_defaults_utils_1.isExactTargetNameKey)(targetName) ||
|
|
802
|
+
inferredExecutors.has(targetName)) {
|
|
803
|
+
nonExactNameTargets.push(targetName);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
const excludedProjects = excludedProjectsByTarget.get(targetName) ?? new Set();
|
|
807
|
+
const eligiblePairs = (projectsByTarget.get(targetName) ?? [])
|
|
808
|
+
.filter((projectName) => !excludedProjects.has(projectName))
|
|
809
|
+
.map((projectName) => ({
|
|
810
|
+
projectName,
|
|
811
|
+
inferredExecutor: inferredExecutorByPair.get(`${projectName}\t${targetName}`),
|
|
812
|
+
}));
|
|
813
|
+
if (hoistChangesExistingTargetDefaults(currentTargetDefaults, targetName, commonByTarget.get(targetName), pluginPath, eligiblePairs, projectGraph.nodes)) {
|
|
814
|
+
rejectedTargets.push(targetName);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (nonExactNameTargets.length > 0) {
|
|
818
|
+
retainResiduals(nonExactNameTargets, 'the target name would resolve as an executor or glob targetDefaults key and could apply to other targets');
|
|
819
|
+
}
|
|
820
|
+
if (rejectedTargets.length > 0) {
|
|
821
|
+
retainResiduals(rejectedTargets, 'centralization would change which existing targetDefaults apply');
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
// Write per-project deviations first so migrated targets drop their executor
|
|
825
|
+
// before we decide whether an executor-keyed target default is still needed.
|
|
826
|
+
for (const executorScope of scope.executorScopes) {
|
|
827
|
+
for (const [targetName, projectNames] of executorScope.targetAndProjects) {
|
|
828
|
+
const common = commonByTarget.get(targetName) ?? {};
|
|
829
|
+
const excludedProjects = excludedProjectsByTarget.get(targetName) ?? new Set();
|
|
830
|
+
for (const projectName of projectNames) {
|
|
831
|
+
const entry = residualByProject.get(projectName)?.get(targetName);
|
|
832
|
+
if (!entry) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
// Excluded projects keep the FULL residual: the plugin-scoped default
|
|
836
|
+
// won't resolve for them, so nothing may be subtracted. Eligible projects
|
|
837
|
+
// drop the hoisted common and keep only their deviation.
|
|
838
|
+
const toWrite = excludedProjects.has(projectName)
|
|
839
|
+
? structuredClone(entry.residual)
|
|
840
|
+
: subtractCommon(entry.residual, common);
|
|
841
|
+
entry.keptPreMigration = !writeResidualTarget(tree, projectConfigsByName, projectName, targetName, toWrite, logger);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// Update nx.json targetDefaults: hoist commons, drop dead executor entries.
|
|
846
|
+
const nxJson = (0, devkit_exports_1.readNxJson)(tree);
|
|
847
|
+
nxJson.targetDefaults ??= {};
|
|
848
|
+
const hoistedByTarget = new Map();
|
|
849
|
+
for (const targetName of [...commonByTarget.keys()].sort()) {
|
|
850
|
+
const common = commonByTarget.get(targetName);
|
|
851
|
+
if (Object.keys(common).length === 0) {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
hoistedByTarget.set(targetName, appendPluginScopedTargetDefault(nxJson, targetName, pluginPath, common));
|
|
855
|
+
}
|
|
856
|
+
// A pre-existing registration of this plugin (or a plugin after it) can win
|
|
857
|
+
// a migrated pair's identity after the explicit target is rewritten, and a
|
|
858
|
+
// package-authored identity resurfaces the same way. A registration this
|
|
859
|
+
// migration ADDS is appended last and wins identity itself, so only fresh
|
|
860
|
+
// registrations remove dead executor entries; otherwise fail open.
|
|
861
|
+
// `pluginPreRegistered` is captured by the caller BEFORE registrations are
|
|
862
|
+
// written (they now precede this).
|
|
863
|
+
if (!pluginPreRegistered && !packageJsonAuthoredIdentity) {
|
|
864
|
+
// An executor-keyed targetDefault applies to any RESOLVED target carrying
|
|
865
|
+
// the executor, so liveness needs every source of one: post-write explicit
|
|
866
|
+
// targets, graph targets of untouched pairs (migrated pairs still show the
|
|
867
|
+
// pre-write executor), and everything this plugin's own inference emits.
|
|
868
|
+
const liveExecutors = new Set(inferredExecutors);
|
|
869
|
+
for (const projectConfig of projectConfigsByName.values()) {
|
|
870
|
+
for (const target of Object.values(projectConfig.targets ?? {})) {
|
|
871
|
+
if (target.executor) {
|
|
872
|
+
liveExecutors.add(target.executor);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
for (const [projectName, node] of Object.entries(projectGraph.nodes)) {
|
|
877
|
+
for (const [targetName, target] of Object.entries(node.data.targets ?? {})) {
|
|
878
|
+
if (target.executor &&
|
|
879
|
+
!residualByProject.get(projectName)?.has(targetName)) {
|
|
880
|
+
liveExecutors.add(target.executor);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
for (const executorScope of scope.executorScopes) {
|
|
885
|
+
if (!liveExecutors.has(executorScope.executor)) {
|
|
886
|
+
removeDeadExecutorTargetDefault(nxJson, executorScope.executor);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (nxJson.targetDefaults &&
|
|
891
|
+
Object.keys(nxJson.targetDefaults).length === 0) {
|
|
892
|
+
delete nxJson.targetDefaults;
|
|
893
|
+
}
|
|
894
|
+
(0, devkit_exports_1.updateNxJson)(tree, nxJson);
|
|
895
|
+
// A per-project exclusion is otherwise silent (unlike a Phase 4 revert, which
|
|
896
|
+
// warns): the excluded projects simply keep their per-project configuration
|
|
897
|
+
// and nothing is centralized for them. Surface it so a partial or total
|
|
898
|
+
// non-centralization is never indistinguishable from "centralization did not
|
|
899
|
+
// apply".
|
|
900
|
+
const excludedTargets = [...excludedProjectsByTarget.entries()].filter(([, projects]) => projects.size > 0);
|
|
901
|
+
if (excludedTargets.length > 0) {
|
|
902
|
+
const excludedProjectNames = [
|
|
903
|
+
...new Set(excludedTargets.flatMap(([, projects]) => [...projects])),
|
|
904
|
+
].sort();
|
|
905
|
+
const targetNames = excludedTargets
|
|
906
|
+
.map(([targetName]) => targetName)
|
|
907
|
+
.sort();
|
|
908
|
+
(logger ?? devkit_exports_1.logger).warn((0, conversion_warnings_1.excludedProjectsWarning)(excludedProjectNames, targetNames));
|
|
909
|
+
}
|
|
910
|
+
return hoistedByTarget;
|
|
911
|
+
}
|
|
912
|
+
function mergeInputs(target, inferredTarget) {
|
|
101
913
|
const isInputInferred = (input) => {
|
|
102
914
|
return inferredTarget.inputs.some((inferredInput) => {
|
|
103
915
|
try {
|
|
@@ -132,73 +944,7 @@ _ExecutorToPluginMigrator_projectGraph = new WeakMap(), _ExecutorToPluginMigrato
|
|
|
132
944
|
...inferredTargetExternalDependencyInput.externalDependencies,
|
|
133
945
|
]));
|
|
134
946
|
}
|
|
135
|
-
}
|
|
136
|
-
(0, executor_options_utils_1.forEachExecutorOptions)(this.tree, tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_executor, "f"), (options, projectName, targetName, configurationName) => {
|
|
137
|
-
if (tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_skippedProjects, "f").has(projectName) || configurationName) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_specificProjectToMigrate, "f") &&
|
|
141
|
-
projectName !== tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_specificProjectToMigrate, "f")) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const skipProjectReason = tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_skipProjectFilter, "f").call(this, tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_projectGraph, "f").nodes[projectName].data);
|
|
145
|
-
if (skipProjectReason) {
|
|
146
|
-
tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_skippedProjects, "f").add(projectName);
|
|
147
|
-
const errorMsg = `The "${projectName}" project cannot be migrated. ${skipProjectReason}`;
|
|
148
|
-
if (tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_specificProjectToMigrate, "f")) {
|
|
149
|
-
throw new Error(errorMsg);
|
|
150
|
-
}
|
|
151
|
-
tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_logger, "f").warn(errorMsg);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const skipTargetReason = tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_skipTargetFilter, "f").call(this, options, tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_projectGraph, "f").nodes[projectName].data);
|
|
155
|
-
if (skipTargetReason) {
|
|
156
|
-
const errorMsg = `The ${targetName} target on project "${projectName}" cannot be migrated. ${skipTargetReason}`;
|
|
157
|
-
if (tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_specificProjectToMigrate, "f")) {
|
|
158
|
-
throw new Error(errorMsg);
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_logger, "f").warn(errorMsg);
|
|
162
|
-
}
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
if (tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_targetAndProjectsToMigrate, "f").has(targetName)) {
|
|
166
|
-
tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_targetAndProjectsToMigrate, "f").get(targetName).add(projectName);
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_targetAndProjectsToMigrate, "f").set(targetName, new Set([projectName]));
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
}, _ExecutorToPluginMigrator_getTargetDefaultsForExecutor = function _ExecutorToPluginMigrator_getTargetDefaultsForExecutor() {
|
|
173
|
-
tslib_1.__classPrivateFieldSet(this, _ExecutorToPluginMigrator_targetDefaultsForExecutor, structuredClone(readTargetDefaultsForExecutor(tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_executor, "f"), tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_nxJson, "f").targetDefaults) ?? {}), "f");
|
|
174
|
-
}, _ExecutorToPluginMigrator_getCreatedTargetForProjectRoot = function _ExecutorToPluginMigrator_getCreatedTargetForProjectRoot(targetName, projectRoot) {
|
|
175
|
-
const entry = Object.entries(tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_createNodesResultsForTargets, "f").get(targetName)?.projects ?? {}).find(([root]) => root === projectRoot);
|
|
176
|
-
if (!entry) {
|
|
177
|
-
throw new Error(`The nx plugin did not find a project inside ${projectRoot}. File an issue at https://github.com/nrwl/nx with information about your project structure.`);
|
|
178
|
-
}
|
|
179
|
-
const createdProject = entry[1];
|
|
180
|
-
const createdTarget = structuredClone(createdProject.targets[targetName]);
|
|
181
|
-
delete createdTarget.command;
|
|
182
|
-
delete createdTarget.options?.cwd;
|
|
183
|
-
return createdTarget;
|
|
184
|
-
}, _ExecutorToPluginMigrator_getCreateNodesResults = async function _ExecutorToPluginMigrator_getCreateNodesResults() {
|
|
185
|
-
if (tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_targetAndProjectsToMigrate, "f").size === 0) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
global.NX_GRAPH_CREATION = true;
|
|
189
|
-
try {
|
|
190
|
-
for (const targetName of tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_targetAndProjectsToMigrate, "f").keys()) {
|
|
191
|
-
const result = await getCreateNodesResultsForPlugin(this.tree, {
|
|
192
|
-
plugin: tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_pluginPath, "f"),
|
|
193
|
-
options: tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_pluginOptionsBuilder, "f").call(this, targetName),
|
|
194
|
-
}, tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_pluginPath, "f"), tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_createNodes, "f"), tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_createNodesV2, "f"), tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_nxJson, "f"));
|
|
195
|
-
tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_createNodesResultsForTargets, "f").set(targetName, result);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
finally {
|
|
199
|
-
global.NX_GRAPH_CREATION = false;
|
|
200
|
-
}
|
|
201
|
-
};
|
|
947
|
+
}
|
|
202
948
|
class NoTargetsToMigrateError extends Error {
|
|
203
949
|
constructor() {
|
|
204
950
|
super('Could not find any targets to migrate.');
|
|
@@ -229,101 +975,739 @@ async function migrateProjectExecutorsToPluginV1(tree, projectGraph, pluginPath,
|
|
|
229
975
|
const projects = await migrateProjects(tree, projectGraph, pluginPath, createNodes, undefined, defaultPluginOptions, migrations, specificProjectToMigrate);
|
|
230
976
|
return projects;
|
|
231
977
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
978
|
+
/**
|
|
979
|
+
* Phase 1: Infer (once per distinct option set). Runs a whole-workspace
|
|
980
|
+
* inference per distinct plugin-option set (usually one) instead of once per
|
|
981
|
+
* target and once per project. Builds `inferredTargetsByOptionSet` (option set
|
|
982
|
+
* id -> project root -> target name -> FULL inferred target; residual
|
|
983
|
+
* computation strips `command` / `options.cwd` at the point of use), which
|
|
984
|
+
* Phase 2 (`computeResidualByProject`) reads to compute residuals and
|
|
985
|
+
* `baselineFinal`; plus the matched config files owned by an inferred project
|
|
986
|
+
* root, which Phase 3's registration step reads for analytic include coverage.
|
|
987
|
+
*/
|
|
988
|
+
async function inferOncePerOptionSet(tree, pluginPath, createNodes, createNodesV2, nxJson, scope) {
|
|
989
|
+
const inferredTargetsByOptionSet = new Map();
|
|
990
|
+
const rawMatchedFiles = new Set();
|
|
991
|
+
const inferredRoots = new Set();
|
|
992
|
+
const erroredConfigFiles = new Set();
|
|
993
|
+
const inferredExecutors = new Set();
|
|
994
|
+
if (scope.optionSetGroups.length === 0) {
|
|
995
|
+
return {
|
|
996
|
+
inferredTargetsByOptionSet,
|
|
997
|
+
rawMatchedConfigFiles: [],
|
|
998
|
+
matchedConfigFiles: [],
|
|
999
|
+
erroredConfigFiles: [],
|
|
1000
|
+
inferredExecutors,
|
|
1001
|
+
inferredRoots,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
global.NX_GRAPH_CREATION = true;
|
|
1005
|
+
try {
|
|
1006
|
+
for (const group of scope.optionSetGroups) {
|
|
1007
|
+
const inferredByRoot = new Map();
|
|
1008
|
+
inferredTargetsByOptionSet.set(group.id, inferredByRoot);
|
|
1009
|
+
const { result, erroredConfigFiles: groupErroredConfigFiles } = await getCreateNodesResultsForPlugin(tree, { plugin: pluginPath, options: group.options }, pluginPath, createNodes, createNodesV2, nxJson);
|
|
1010
|
+
for (const file of groupErroredConfigFiles) {
|
|
1011
|
+
erroredConfigFiles.add(file);
|
|
1012
|
+
}
|
|
1013
|
+
// The plugin's glob pattern is option-independent, so every option set
|
|
1014
|
+
// matches the same config files; union defensively.
|
|
1015
|
+
for (const file of result?.matchingProjectFiles ?? []) {
|
|
1016
|
+
rawMatchedFiles.add(file);
|
|
1017
|
+
}
|
|
1018
|
+
for (const [root, projectConfig] of Object.entries(result?.projects ?? {})) {
|
|
1019
|
+
// Every root the plugin produces a project for. Config files whose
|
|
1020
|
+
// owning root is NOT one of these produce no project and can be safely
|
|
1021
|
+
// excluded by an `include` without changing the inferred set: this is
|
|
1022
|
+
// why include-necessity must be judged against inferred roots, not the
|
|
1023
|
+
// raw glob (which also matches package.json/project.json/etc.).
|
|
1024
|
+
inferredRoots.add(root);
|
|
1025
|
+
// Executor-keyed targetDefaults can apply to ANY inferred target name,
|
|
1026
|
+
// so the Phase 3 dead-entry removal and the target-default preflight
|
|
1027
|
+
// must see every EFFECTIVE executor: a command-based target resolves
|
|
1028
|
+
// to `nx:run-commands` (resolveCommandSyntacticSugar).
|
|
1029
|
+
for (const target of Object.values(projectConfig.targets ?? {})) {
|
|
1030
|
+
if (target.executor) {
|
|
1031
|
+
inferredExecutors.add(target.executor);
|
|
1032
|
+
}
|
|
1033
|
+
else if (target.command) {
|
|
1034
|
+
inferredExecutors.add('nx:run-commands');
|
|
247
1035
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1036
|
+
}
|
|
1037
|
+
// Retain clones only for this option set's migrated roots: Phase 2
|
|
1038
|
+
// reads them for migrated (project, target) pairs alone, so keeping
|
|
1039
|
+
// every inferred root would retain up to (option sets x roots) targets
|
|
1040
|
+
// for nothing.
|
|
1041
|
+
if (!group.migratedRoots.has(root)) {
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
for (const targetName of group.targetNames) {
|
|
1045
|
+
const inferredTarget = projectConfig.targets?.[targetName];
|
|
1046
|
+
if (!inferredTarget) {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
// Store the FULL inferred (command-based) target. Residual
|
|
1050
|
+
// computation strips `command`/`options.cwd` at the point of use;
|
|
1051
|
+
// `baselineFinal` (the equivalence oracle) needs the full form.
|
|
1052
|
+
if (!inferredByRoot.has(root)) {
|
|
1053
|
+
inferredByRoot.set(root, new Map());
|
|
1054
|
+
}
|
|
1055
|
+
inferredByRoot
|
|
1056
|
+
.get(root)
|
|
1057
|
+
.set(targetName, structuredClone(inferredTarget));
|
|
252
1058
|
}
|
|
253
1059
|
}
|
|
254
1060
|
}
|
|
255
1061
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
projects.set(project, {
|
|
259
|
-
...defaultPluginOptions,
|
|
260
|
-
...pluginOptions,
|
|
261
|
-
});
|
|
1062
|
+
finally {
|
|
1063
|
+
global.NX_GRAPH_CREATION = false;
|
|
262
1064
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
1065
|
+
// Keep only config files owned by an inferred project root (i.e. files that
|
|
1066
|
+
// actually contribute a project). Include-coverage is decided against these.
|
|
1067
|
+
// Ownership is an O(path depth) ancestor walk against the root Set, not an
|
|
1068
|
+
// O(roots) scan per file: both sets scale with project count.
|
|
1069
|
+
const matchedConfigFiles = [...rawMatchedFiles].filter((file) => isFileOwnedByAnyRoot(file, inferredRoots));
|
|
1070
|
+
return {
|
|
1071
|
+
inferredTargetsByOptionSet,
|
|
1072
|
+
// Every config file the plugin's glob matched, independent of whether this
|
|
1073
|
+
// pass inferred a project from it. The registration step needs the full set
|
|
1074
|
+
// to decide include coverage against non-migrated PROJECT roots: a project
|
|
1075
|
+
// the plugin infers only on a later pass (e.g. cache/error asymmetry) is
|
|
1076
|
+
// absent from `matchedConfigFiles` but its config is still globbed here.
|
|
1077
|
+
rawMatchedConfigFiles: [...rawMatchedFiles],
|
|
1078
|
+
matchedConfigFiles,
|
|
1079
|
+
// Config files the plugin's glob matched but could not infer this pass.
|
|
1080
|
+
// These produced no project (so they're absent from `matchedConfigFiles`),
|
|
1081
|
+
// yet the registration step must still keep its `include` scoped when one
|
|
1082
|
+
// sits outside the migrated roots, otherwise the plugin is widened to that
|
|
1083
|
+
// root, re-hits the same failure at verification, and reverts the hoist.
|
|
1084
|
+
erroredConfigFiles: [...erroredConfigFiles],
|
|
1085
|
+
inferredExecutors,
|
|
1086
|
+
inferredRoots,
|
|
1087
|
+
};
|
|
266
1088
|
}
|
|
267
|
-
|
|
1089
|
+
/**
|
|
1090
|
+
* Whether `file` (workspace-relative) is owned by any root in `roots`. Walks the
|
|
1091
|
+
* file's ancestor directories and checks Set membership: O(path depth), not
|
|
1092
|
+
* O(roots). Equivalent to `roots.some((root) => isFileUnderRoot(file, root))`:
|
|
1093
|
+
* the root project (`.`) owns workspace-root-level files (no path separator),
|
|
1094
|
+
* and a nested root owns the file when it is (or is an ancestor directory of)
|
|
1095
|
+
* the file.
|
|
1096
|
+
*/
|
|
1097
|
+
function isFileOwnedByAnyRoot(file, roots) {
|
|
1098
|
+
if (roots.has(file)) {
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1101
|
+
const firstSlash = file.indexOf('/');
|
|
1102
|
+
if (firstSlash === -1) {
|
|
1103
|
+
// A workspace-root-level file is owned only by the root project.
|
|
1104
|
+
return roots.has('.');
|
|
1105
|
+
}
|
|
1106
|
+
let dir = file;
|
|
1107
|
+
let slash = dir.lastIndexOf('/');
|
|
1108
|
+
while (slash !== -1) {
|
|
1109
|
+
dir = dir.slice(0, slash);
|
|
1110
|
+
if (roots.has(dir)) {
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
slash = dir.lastIndexOf('/');
|
|
1114
|
+
}
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
// Minimatch metacharacters plus extglob prefix operators (`!+@()`). A path
|
|
1118
|
+
// segment containing any of these has glob semantics and cannot be reduced to a
|
|
1119
|
+
// literal project root.
|
|
1120
|
+
// Not reused from `isGlobPattern` (nx/src/utils/globs.ts): its `GLOB_CHARACTERS`
|
|
1121
|
+
// set omits `?`, `!`, `+`, and `@`, so it would treat an extglob/negation as a
|
|
1122
|
+
// literal root and wrongly widen the include.
|
|
1123
|
+
const GLOB_METACHARACTERS = /[*?[\]{}!+@()]/;
|
|
1124
|
+
/**
|
|
1125
|
+
* The set of project roots a generated `include` list scopes to, or `undefined`
|
|
1126
|
+
* if any entry is not one of the two shapes this generator emits: `*` (the root
|
|
1127
|
+
* project) or a literal root followed by a trailing globstar segment (a nested
|
|
1128
|
+
* root). A user-authored include or any `exclude` falls back to the glob engine.
|
|
1129
|
+
*
|
|
1130
|
+
* The globstar branch only qualifies when the root prefix is a LITERAL path.
|
|
1131
|
+
* A prefix carrying glob metacharacters (a wildcard, brace, or extglob segment
|
|
1132
|
+
* before the trailing globstar) is not a shape this generator emits and can't be
|
|
1133
|
+
* reduced to root ownership by string equality, so it falls back to the glob
|
|
1134
|
+
* engine.
|
|
1135
|
+
*/
|
|
1136
|
+
function generatedIncludeRoots(include) {
|
|
1137
|
+
const roots = new Set();
|
|
1138
|
+
for (const glob of include) {
|
|
1139
|
+
if (glob === '*') {
|
|
1140
|
+
roots.add('.');
|
|
1141
|
+
}
|
|
1142
|
+
else if (glob.endsWith('/**/*') && glob.length > '/**/*'.length) {
|
|
1143
|
+
const root = glob.slice(0, -'/**/*'.length);
|
|
1144
|
+
if (GLOB_METACHARACTERS.test(root)) {
|
|
1145
|
+
return undefined;
|
|
1146
|
+
}
|
|
1147
|
+
roots.add(root);
|
|
1148
|
+
}
|
|
1149
|
+
else {
|
|
1150
|
+
return undefined;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return roots;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Whether a registration's `include` globs already cover every config file the
|
|
1157
|
+
* plugin globs that is owned by an inferred project root (so the registration
|
|
1158
|
+
* can be left unscoped). Plugin inference is a pure function of the matched
|
|
1159
|
+
* config-file set, so this answers what the old per-project
|
|
1160
|
+
* `arePluginIncludesRequired` re-inference computed without running any
|
|
1161
|
+
* additional inference, differing only on matched files that contribute no
|
|
1162
|
+
* project: those are invisible here, so an `include` the old diff-based check
|
|
1163
|
+
* kept for their sake is now deleted, letting future config files in such
|
|
1164
|
+
* locations infer eagerly. The Phase 4 verification pass guards the current
|
|
1165
|
+
* behavioral outcome either way.
|
|
1166
|
+
*
|
|
1167
|
+
* Every glob this generator emits scopes to a single root (`*` for the root
|
|
1168
|
+
* project, a nested-root globstar for the rest), so coverage reduces to root
|
|
1169
|
+
* ownership with no glob engine: an O(files * path depth) walk instead of
|
|
1170
|
+
* running minimatch (whose `#matchGlobstar`/`slashSplit` dominate at scale) per
|
|
1171
|
+
* (file, glob). Only user-authored includes or any `exclude` fall through.
|
|
1172
|
+
*/
|
|
1173
|
+
function includeCoversAllConfigFiles(include, exclude, configFiles) {
|
|
1174
|
+
if (!include || include.length === 0) {
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
if (!exclude || exclude.length === 0) {
|
|
1178
|
+
const includeRoots = generatedIncludeRoots(include);
|
|
1179
|
+
if (includeRoots) {
|
|
1180
|
+
return configFiles.every((file) => isFileOwnedByAnyRoot(file, includeRoots));
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
// Fallback for user-authored includes / any exclude: defer to the SAME matcher
|
|
1184
|
+
// Nx uses to bind config files to a registration. A hand-rolled
|
|
1185
|
+
// `include.some() && !exclude.some()` cannot express an ordered-override
|
|
1186
|
+
// negation like `["packages/**/*", "!packages/legacy/**/*"]`: `.some()` reports
|
|
1187
|
+
// every file "covered" and deletes the include, which widens the registration
|
|
1188
|
+
// so the fenced-off configs get inferred (and executed). `findMatchingConfigFiles`
|
|
1189
|
+
// applies include/exclude with Nx's exact ordered semantics, so coverage holds
|
|
1190
|
+
// only when every config file survives the same filter Nx would run.
|
|
1191
|
+
return ((0, devkit_internals_1.findMatchingConfigFiles)(configFiles, include, exclude ?? []).length ===
|
|
1192
|
+
configFiles.length);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Harvest the diagnostic messages and the errored config-file paths from a
|
|
1196
|
+
* `ProjectConfigurationsError`. `ProjectConfigurationsError.errors` is a closed
|
|
1197
|
+
* 5-member union; the two members that name a failing config file are
|
|
1198
|
+
* `AggregateCreateNodesError` (a `[file, error]` list) and `MergeNodesError` (a
|
|
1199
|
+
* single `.file`). `ProjectsWithNoNameError` / `MultipleProjectsWithSameNameError`
|
|
1200
|
+
* are artifacts of running with no `project.json` layer (nothing supplies names),
|
|
1201
|
+
* so they are dropped; `WorkspaceValidityError` carries no file and is exempt.
|
|
1202
|
+
*/
|
|
1203
|
+
function harvestConfigurationErrors(e) {
|
|
1204
|
+
const entries = [];
|
|
1205
|
+
for (const error of e.errors) {
|
|
1206
|
+
if ((0, devkit_internals_1.isProjectsWithNoNameError)(error) ||
|
|
1207
|
+
(0, devkit_internals_1.isMultipleProjectsWithSameNameError)(error)) {
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
const message = error.message ?? String(error);
|
|
1211
|
+
if ((0, devkit_internals_1.isAggregateCreateNodesError)(error)) {
|
|
1212
|
+
entries.push({
|
|
1213
|
+
message,
|
|
1214
|
+
files: error.errors
|
|
1215
|
+
.map(([file]) => file)
|
|
1216
|
+
.filter((file) => !!file),
|
|
1217
|
+
pluginIndex: error.pluginIndex,
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
else if ((0, devkit_internals_1.isMergeNodesError)(error)) {
|
|
1221
|
+
entries.push({
|
|
1222
|
+
message,
|
|
1223
|
+
files: error.file ? [error.file] : [],
|
|
1224
|
+
pluginIndex: error.pluginIndex,
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
else if ((0, devkit_internals_1.isWorkspaceValidityError)(error)) {
|
|
1228
|
+
// Carries no config file and no plugin attribution.
|
|
1229
|
+
entries.push({ message, files: [], pluginIndex: undefined });
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
// Exhaustiveness guard: `ProjectConfigurationsError.errors` is a closed
|
|
1233
|
+
// union. If a sixth member is added, this assignment stops compiling until
|
|
1234
|
+
// it is handled here.
|
|
1235
|
+
const _exhaustive = error;
|
|
1236
|
+
void _exhaustive;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return {
|
|
1240
|
+
messages: entries.map((entry) => entry.message),
|
|
1241
|
+
erroredConfigFiles: [...new Set(entries.flatMap((entry) => entry.files))],
|
|
1242
|
+
entries,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Attribution map for errored-config ownership: every root the engine knows
|
|
1247
|
+
* owns a project, i.e. graph project roots plus the roots the plugin inference
|
|
1248
|
+
* produced (a project discovered from a config file alone has no graph node;
|
|
1249
|
+
* graph roots win a collision). Keys are normalized for the
|
|
1250
|
+
* `findProjectForPath` walk; values are the raw roots the per-target
|
|
1251
|
+
* migrated-root sets hold.
|
|
1252
|
+
*/
|
|
1253
|
+
function buildOwnerRootByPath(graphRoots, inferredRoots) {
|
|
1254
|
+
const normalizeRoot = (root) => root === '' ? '.' : root.endsWith('/') ? root.slice(0, -1) : root;
|
|
1255
|
+
const ownerRootByPath = new Map();
|
|
1256
|
+
for (const root of graphRoots) {
|
|
1257
|
+
ownerRootByPath.set(normalizeRoot(root), root);
|
|
1258
|
+
}
|
|
1259
|
+
for (const root of inferredRoots) {
|
|
1260
|
+
if (!ownerRootByPath.has(normalizeRoot(root))) {
|
|
1261
|
+
ownerRootByPath.set(normalizeRoot(root), root);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return ownerRootByPath;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Phase 4: a single verification inference pass over the whole workspace with
|
|
1268
|
+
* the updated `nx.json` plugin registrations. One
|
|
1269
|
+
* `retrieveProjectConfigurations` call runs every registration for this plugin
|
|
1270
|
+
* (the plugin's `createNodes` executes once per registration group). The
|
|
1271
|
+
* equivalence oracle + fallback in `verifyAndFallback` consume the result.
|
|
1272
|
+
*/
|
|
1273
|
+
async function runVerificationPass(tree, pluginPath, createNodes, createNodesV2) {
|
|
268
1274
|
const nxJson = (0, devkit_exports_1.readNxJson)(tree);
|
|
269
|
-
|
|
270
|
-
|
|
1275
|
+
const registrations = (nxJson.plugins ?? []).filter((plugin) => plugin === pluginPath ||
|
|
1276
|
+
(typeof plugin !== 'string' && plugin.plugin === pluginPath));
|
|
1277
|
+
if (registrations.length === 0) {
|
|
1278
|
+
return { result: undefined, errors: [], erroredConfigFiles: [] };
|
|
1279
|
+
}
|
|
271
1280
|
global.NX_GRAPH_CREATION = true;
|
|
272
1281
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
1282
|
+
const plugins = registrations.map((registration) => new devkit_internals_1.LoadedNxPlugin({ createNodes, createNodesV2, name: pluginPath }, registration));
|
|
1283
|
+
return {
|
|
1284
|
+
result: await (0, devkit_internals_1.retrieveProjectConfigurations)({ specifiedPlugins: plugins, defaultPlugins: [] }, tree.root, nxJson),
|
|
1285
|
+
errors: [],
|
|
1286
|
+
erroredConfigFiles: [],
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
catch (e) {
|
|
1290
|
+
if (e instanceof devkit_internals_1.ProjectConfigurationsError) {
|
|
1291
|
+
// Verify against the partial result, but keep the causes: they are the
|
|
1292
|
+
// only diagnostic for why affected projects fall back or a hoist reverts.
|
|
1293
|
+
// Recording the errored config files is what makes the revert fail-closed:
|
|
1294
|
+
// a hoist is reverted when an errored file lies outside every migrated root.
|
|
1295
|
+
const { messages, erroredConfigFiles } = harvestConfigurationErrors(e);
|
|
1296
|
+
return {
|
|
1297
|
+
result: e.partialProjectConfigurationsResult,
|
|
1298
|
+
errors: messages,
|
|
1299
|
+
erroredConfigFiles,
|
|
1300
|
+
};
|
|
279
1301
|
}
|
|
1302
|
+
throw e;
|
|
280
1303
|
}
|
|
281
1304
|
finally {
|
|
282
1305
|
global.NX_GRAPH_CREATION = false;
|
|
283
1306
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Phase 4: run the single verification inference pass, then apply the
|
|
1310
|
+
* equivalence oracle. `retrieveProjectConfigurations` already merges
|
|
1311
|
+
* `targetDefaults` into the inferred targets, so the real post-migration
|
|
1312
|
+
* effective config for a target is approximated by `merge(project.json
|
|
1313
|
+
* deviation, verified inferred+targetDefaults)`. It must deep-equal
|
|
1314
|
+
* `baselineFinal` (the previous engine's migrated effective config, from Phase
|
|
1315
|
+
* 2). CAVEAT: this pass runs with no DEFAULT layer (neither `project.json` nor
|
|
1316
|
+
* `package.json`), so it cannot observe cases where a default layer authors a
|
|
1317
|
+
* target's identity and thereby changes whether a `targetDefaults` entry applies
|
|
1318
|
+
* at all, e.g. a `filter: { plugin }` entry is rejected by
|
|
1319
|
+
* `resolveSourcePlugin` once the target carries `executor` / `command` in a
|
|
1320
|
+
* default layer. That identity can come from the project.json residual itself OR
|
|
1321
|
+
* from the package-json plugin (a script or `nx.targets` entry byte-equal to the
|
|
1322
|
+
* target name). Those targets are kept per-project at the hoist site (they never
|
|
1323
|
+
* reach a plugin-scoped entry), not caught here. Any project that fails,
|
|
1324
|
+
* or that the intended target no longer infers for at all, is restored to the
|
|
1325
|
+
* exact pre-centralization migration output (a full residual; an empty
|
|
1326
|
+
* residual removes the target), and every fallback is summarized in a single
|
|
1327
|
+
* `logger.warn` that asks for manual review rather than asserting equivalence
|
|
1328
|
+
* the pass could not establish.
|
|
1329
|
+
*/
|
|
1330
|
+
async function verifyAndFallback(tree, projectGraph, pluginPath, createNodes, createNodesV2, residualByProject, projectConfigsByName, hoistedByTarget, inferredRoots, singleProjectMode, logger) {
|
|
1331
|
+
// Single-project mode never hoists, so there is nothing to reconcile, and
|
|
1332
|
+
// no reason to pay for a whole-workspace verification inference.
|
|
1333
|
+
if (singleProjectMode) {
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const { result: verifyResult, errors: verificationErrors, erroredConfigFiles, } = await runVerificationPass(tree, pluginPath, createNodes, createNodesV2);
|
|
1337
|
+
if (!verifyResult) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
// The hoist's contract is: behavioral equivalence for migrated targets, no
|
|
1341
|
+
// effect on anything else. A root this plugin infers the target for that was
|
|
1342
|
+
// NOT migrated (inferred-only, or covered by a pre-existing registration)
|
|
1343
|
+
// would inherit the plugin-scoped default, so revert that target's hoist
|
|
1344
|
+
// entirely: drop the appended entry and restore every full residual.
|
|
1345
|
+
const migratedRootsByTarget = new Map();
|
|
1346
|
+
for (const [projectName, targetMap] of residualByProject) {
|
|
1347
|
+
const root = projectGraph.nodes[projectName]?.data?.root;
|
|
1348
|
+
for (const targetName of targetMap.keys()) {
|
|
1349
|
+
if (!migratedRootsByTarget.has(targetName)) {
|
|
1350
|
+
migratedRootsByTarget.set(targetName, new Set());
|
|
1351
|
+
}
|
|
1352
|
+
migratedRootsByTarget.get(targetName).add(root);
|
|
289
1353
|
}
|
|
290
|
-
|
|
291
|
-
|
|
1354
|
+
}
|
|
1355
|
+
const ownerRootByPath = buildOwnerRootByPath(Object.values(projectGraph.nodes).map((node) => node.data.root), inferredRoots);
|
|
1356
|
+
const revertedTargets = new Set();
|
|
1357
|
+
for (const [targetName] of hoistedByTarget) {
|
|
1358
|
+
const migratedRoots = migratedRootsByTarget.get(targetName) ?? new Set();
|
|
1359
|
+
const reachesNonMigratedRoot = Object.entries(verifyResult.projects ?? {}).some(([root, projectConfig]) => projectConfig.targets?.[targetName] !== undefined &&
|
|
1360
|
+
!migratedRoots.has(root));
|
|
1361
|
+
// Fail closed on inference errors. `reachesNonMigratedRoot` is keyed on the
|
|
1362
|
+
// partial result, so an errored root never appears there, yet it may be an
|
|
1363
|
+
// inferred-only root that would inherit the plugin-scoped default once its
|
|
1364
|
+
// config is fixed. Attribute each errored file to the CLOSEST known
|
|
1365
|
+
// project root (not any migrated ancestor: a nested non-migrated project
|
|
1366
|
+
// must not be absorbed by the migrated project above it) and revert unless
|
|
1367
|
+
// that root is one of this target's migrated roots (errors on migrated
|
|
1368
|
+
// roots are handled by the per-project divergence oracle below).
|
|
1369
|
+
const erroredOutsideMigratedRoots = erroredConfigFiles.some((file) => {
|
|
1370
|
+
const ownerRoot = (0, devkit_internals_1.findProjectForPath)(file, ownerRootByPath);
|
|
1371
|
+
return ownerRoot == null || !migratedRoots.has(ownerRoot);
|
|
1372
|
+
});
|
|
1373
|
+
if (reachesNonMigratedRoot || erroredOutsideMigratedRoots) {
|
|
1374
|
+
revertedTargets.add(targetName);
|
|
292
1375
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
1376
|
+
}
|
|
1377
|
+
if (revertedTargets.size > 0) {
|
|
1378
|
+
const nxJson = (0, devkit_exports_1.readNxJson)(tree);
|
|
1379
|
+
for (const targetName of revertedTargets) {
|
|
1380
|
+
removeHoistedTargetDefault(nxJson, targetName, hoistedByTarget.get(targetName));
|
|
1381
|
+
}
|
|
1382
|
+
(0, devkit_exports_1.updateNxJson)(tree, nxJson);
|
|
1383
|
+
for (const [projectName, targetMap] of residualByProject) {
|
|
1384
|
+
for (const [targetName, entry] of targetMap) {
|
|
1385
|
+
if (!revertedTargets.has(targetName) || entry.keptPreMigration) {
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
writeResidualTarget(tree, projectConfigsByName, projectName, targetName, structuredClone(entry.residual), logger);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// A revert can be triggered by an inference error the pass could not
|
|
1392
|
+
// inspect; surface those errors so the incomplete verification is not silent.
|
|
1393
|
+
(logger ?? devkit_exports_1.logger).warn((0, conversion_warnings_1.revertedTargetsWarning)([...revertedTargets].sort(), verificationErrors));
|
|
1394
|
+
}
|
|
1395
|
+
const divergent = [];
|
|
1396
|
+
const missing = [];
|
|
1397
|
+
for (const [projectName, targetMap] of residualByProject) {
|
|
1398
|
+
const root = projectGraph.nodes[projectName]?.data?.root;
|
|
1399
|
+
for (const [targetName, entry] of targetMap) {
|
|
1400
|
+
if (revertedTargets.has(targetName) || entry.keptPreMigration) {
|
|
1401
|
+
// Restored to the full pre-centralization residual above (or never
|
|
1402
|
+
// migrated): already the intended output, nothing left to verify.
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
const verifiedInferred = verifyResult.projects?.[root]?.targets?.[targetName];
|
|
1406
|
+
let equivalent = false;
|
|
1407
|
+
if (verifiedInferred) {
|
|
1408
|
+
const projectConfig = readCachedProjectConfiguration(tree, projectConfigsByName, projectName);
|
|
1409
|
+
const deviation = projectConfig.targets?.[targetName] ?? {};
|
|
1410
|
+
const postMigrationFinal = (0, devkit_internals_1.mergeTargetConfigurations)(structuredClone(deviation), structuredClone(verifiedInferred));
|
|
1411
|
+
equivalent = isDeepEqual(postMigrationFinal, entry.baselineFinal);
|
|
1412
|
+
}
|
|
1413
|
+
if (!equivalent) {
|
|
1414
|
+
// Drop this project's hoist: restore its full residual as an explicit
|
|
1415
|
+
// project.json override (which wins over the shared targetDefaults).
|
|
1416
|
+
const written = writeResidualTarget(tree, projectConfigsByName, projectName, targetName, structuredClone(entry.residual), logger);
|
|
1417
|
+
const pair = `${projectName} > ${targetName}`;
|
|
1418
|
+
if (verifiedInferred) {
|
|
1419
|
+
divergent.push(pair);
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
missing.push({
|
|
1423
|
+
pair,
|
|
1424
|
+
root,
|
|
1425
|
+
removed: written && (0, conversion_warnings_1.isDroppedTarget)(entry.residual),
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
const anyFallback = divergent.length > 0 || missing.length > 0;
|
|
1432
|
+
if (anyFallback) {
|
|
1433
|
+
// Surface the pass's own errors only when a target vanished from the
|
|
1434
|
+
// verification result: the one fallback class an inference error can
|
|
1435
|
+
// explain. Divergence fallbacks have a verified config; appending
|
|
1436
|
+
// unrelated pass errors to them would misattribute the cause.
|
|
1437
|
+
(logger ?? devkit_exports_1.logger).warn((0, conversion_warnings_1.unverifiedPairsWarning)(divergent, missing, missing.length > 0 ? verificationErrors : []));
|
|
1438
|
+
}
|
|
1439
|
+
// Whenever the verification pass reported errors they must appear in at least
|
|
1440
|
+
// one warning. The revert warning always carries them; the fallback warning
|
|
1441
|
+
// carries them only for a target missing from the verification result (a
|
|
1442
|
+
// divergence fallback has a verified config, so attaching unrelated errors to
|
|
1443
|
+
// it would misattribute the cause). Fire this standalone warning whenever the
|
|
1444
|
+
// errors were surfaced by neither, i.e. nothing reverted AND no fallback
|
|
1445
|
+
// carried them (no fallback at all, or only divergence fallbacks). Otherwise a
|
|
1446
|
+
// broken config file leaves the pass unable to see the whole workspace with no
|
|
1447
|
+
// trace. (`verificationErrors` here has already had the no-project-name noise
|
|
1448
|
+
// filtered out, so it means inference genuinely broke.)
|
|
1449
|
+
const errorsSurfacedByFallbackWarning = missing.length > 0;
|
|
1450
|
+
if (verificationErrors.length > 0 &&
|
|
1451
|
+
revertedTargets.size === 0 &&
|
|
1452
|
+
!errorsSurfacedByFallbackWarning) {
|
|
1453
|
+
// Only claim the migrated targets matched when nothing fell back; a
|
|
1454
|
+
// divergence fallback means at least one did not.
|
|
1455
|
+
(logger ?? devkit_exports_1.logger).warn((0, conversion_warnings_1.verificationErrorsWarning)(verificationErrors, anyFallback));
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Recover the default option keys a plugin fills into its options object during
|
|
1460
|
+
* `createNodes` (Phase 1 mutated each option-set object in place if the plugin
|
|
1461
|
+
* does so). A key qualifies only if it is not one of our own
|
|
1462
|
+
* `defaultPluginOptions` and appears with an identical value across every
|
|
1463
|
+
* option set: the signature of a plugin default fill. (A mapper-provided key
|
|
1464
|
+
* that is constant across every option set is indistinguishable and also
|
|
1465
|
+
* qualifies; harmless, since per-project options are spread over these.)
|
|
1466
|
+
* Reproduces the previous engine's incidental option enrichment without extra
|
|
1467
|
+
* inference.
|
|
1468
|
+
*/
|
|
1469
|
+
function derivePluginFilledDefaults(optionSetGroups, defaultPluginOptions) {
|
|
1470
|
+
const filled = {};
|
|
1471
|
+
if (optionSetGroups.length === 0) {
|
|
1472
|
+
return filled;
|
|
1473
|
+
}
|
|
1474
|
+
const defaults = (defaultPluginOptions ?? {});
|
|
1475
|
+
const seen = new Map();
|
|
1476
|
+
for (const group of optionSetGroups) {
|
|
1477
|
+
for (const [key, value] of Object.entries((group.options ?? {}))) {
|
|
1478
|
+
if (key in defaults) {
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
const existing = seen.get(key);
|
|
1482
|
+
if (!existing) {
|
|
1483
|
+
seen.set(key, { value, count: 1, consistent: true });
|
|
1484
|
+
}
|
|
1485
|
+
else {
|
|
1486
|
+
existing.count++;
|
|
1487
|
+
if (stableStringify(existing.value) !== stableStringify(value)) {
|
|
1488
|
+
existing.consistent = false;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
for (const [key, entry] of seen) {
|
|
1494
|
+
if (entry.consistent && entry.count === optionSetGroups.length) {
|
|
1495
|
+
filled[key] = entry.value;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return filled;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Deferred (batch) variant of Phase 3's hoist + Phase 4: instead of running
|
|
1502
|
+
* them, capture everything the batch finalize pass needs to run them once over
|
|
1503
|
+
* the whole batch. The session clones the mutable structures at staging time.
|
|
1504
|
+
*/
|
|
1505
|
+
function stageDeferredPlan(batchStaging, pluginPath, createNodes, createNodesV2, logger, pluginPreRegistered, scope, projectGraph, residualByProject, inferredTargetsByOptionSet, inferredExecutors, inferredRoots, matchedConfigFiles, erroredConfigFiles) {
|
|
1506
|
+
const rootByProject = new Map();
|
|
1507
|
+
const graphNodeByProject = new Map();
|
|
1508
|
+
for (const projectName of residualByProject.keys()) {
|
|
1509
|
+
rootByProject.set(projectName, projectGraph.nodes[projectName]?.data.root);
|
|
1510
|
+
const node = projectGraph.nodes[projectName];
|
|
1511
|
+
if (node) {
|
|
1512
|
+
graphNodeByProject.set(projectName, node);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
const graphRoots = new Set();
|
|
1516
|
+
for (const node of Object.values(projectGraph.nodes)) {
|
|
1517
|
+
graphRoots.add(node.data.root);
|
|
1518
|
+
}
|
|
1519
|
+
// Executors of the pre-migration graph targets, for the finalize liveness
|
|
1520
|
+
// scan (mirrors the inline scan in `hoistCommonAndWrite`, which reads them
|
|
1521
|
+
// from the same graph).
|
|
1522
|
+
const graphExecutorByPair = new Map();
|
|
1523
|
+
for (const [projectName, node] of Object.entries(projectGraph.nodes)) {
|
|
1524
|
+
for (const [targetName, target] of Object.entries(node.data.targets ?? {})) {
|
|
1525
|
+
if (target.executor) {
|
|
1526
|
+
graphExecutorByPair.set(`${projectName}\t${targetName}`, target.executor);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
batchStaging.stagePlan({
|
|
1531
|
+
pluginPath,
|
|
1532
|
+
createNodes,
|
|
1533
|
+
createNodesV2,
|
|
1534
|
+
logger,
|
|
1535
|
+
pluginPreRegistered,
|
|
1536
|
+
residualByProject,
|
|
1537
|
+
rootByProject,
|
|
1538
|
+
graphNodeByProject,
|
|
1539
|
+
graphRoots,
|
|
1540
|
+
inferredExecutorByPair: computeInferredExecutorByPair(scope, projectGraph, inferredTargetsByOptionSet),
|
|
1541
|
+
inferredExecutors,
|
|
1542
|
+
inferredRoots,
|
|
1543
|
+
matchedConfigFiles,
|
|
1544
|
+
erroredConfigFiles,
|
|
1545
|
+
migratedExecutors: scope.executorScopes.map((executorScope) => executorScope.executor),
|
|
1546
|
+
graphExecutorByPair,
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
async function migrateProjects(tree, projectGraph, pluginPath, createNodes, createNodesV2, defaultPluginOptions, migrations, specificProjectToMigrate, logger) {
|
|
1550
|
+
const projects = new Map();
|
|
1551
|
+
const spinner = devkit_internals_1.globalSpinner.start(`Calculating migration scope...`, pluginPath);
|
|
1552
|
+
// Phase 0: Collect (once). Scope + filtering + per-project options.
|
|
1553
|
+
const scope = collectMigrationScope(tree, projectGraph, migrations, defaultPluginOptions, specificProjectToMigrate, logger);
|
|
1554
|
+
const projectConfigsByName = (0, devkit_exports_1.getProjects)(tree);
|
|
1555
|
+
// Phase 1: Infer (once per distinct option set) for the whole workspace.
|
|
1556
|
+
const nxJson = (0, devkit_exports_1.readNxJson)(tree);
|
|
1557
|
+
const { inferredTargetsByOptionSet, matchedConfigFiles, erroredConfigFiles: erroredConfigFilesFromInference, rawMatchedConfigFiles, inferredExecutors, inferredRoots, } = await inferOncePerOptionSet(tree, pluginPath, createNodes, createNodesV2, nxJson, scope);
|
|
1558
|
+
// Phase 2: Per-project residual (in-memory, no re-inference). Also captures
|
|
1559
|
+
// `baselineFinal` (the equivalence oracle consumed in Phase 4).
|
|
1560
|
+
const residualByProject = await computeResidualByProject(tree, projectGraph, scope, inferredTargetsByOptionSet, nxJson, projectConfigsByName);
|
|
1561
|
+
// Some plugins' `createNodes` normalize their options object in place (e.g.
|
|
1562
|
+
// `options.devTargetName ??= 'dev'`). The previous engine inferred once per
|
|
1563
|
+
// project passing the project's registration-options object by reference, so
|
|
1564
|
+
// those objects picked up the plugin's default keys. Phase 1 runs the same
|
|
1565
|
+
// inference on each option-set object, so recover exactly those plugin-filled
|
|
1566
|
+
// default keys (without any extra inference) and merge them back so the
|
|
1567
|
+
// emitted registration options stay identical.
|
|
1568
|
+
const pluginFilledDefaults = derivePluginFilledDefaults(scope.optionSetGroups, defaultPluginOptions);
|
|
1569
|
+
for (const [project, options] of scope.pluginOptionsByProject) {
|
|
1570
|
+
projects.set(project, {
|
|
1571
|
+
...pluginFilledDefaults,
|
|
1572
|
+
...options,
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
// Phase 3: one-shot plugin registration + analytic include computation.
|
|
1576
|
+
// Runs before the hoist: the hoist is gated on the FINAL position of this
|
|
1577
|
+
// plugin's registrations in the plugins array (a later registration of
|
|
1578
|
+
// another plugin can take a target's identity over), so the array must be in
|
|
1579
|
+
// its final state first. The pre-registration state is captured here for the
|
|
1580
|
+
// hoist's dead-entry removal gate, which needs to know whether this
|
|
1581
|
+
// migration added the registration.
|
|
1582
|
+
const pluginPreRegistered = ((0, devkit_exports_1.readNxJson)(tree).plugins ?? []).some((plugin) => isRegistrationOfPlugin(plugin, pluginPath));
|
|
1583
|
+
addPluginRegistrations(tree, projects, pluginPath, defaultPluginOptions, projectGraph, spinner, matchedConfigFiles, erroredConfigFilesFromInference, rawMatchedConfigFiles);
|
|
1584
|
+
// An open batch session (`infer-targets` with several plugins selected)
|
|
1585
|
+
// defers centralization: full residuals are written now and the hoist,
|
|
1586
|
+
// dead-default cleanup, and verification run once at the batch finalize
|
|
1587
|
+
// pass, over every conversion's staged plan. Single-project mode never
|
|
1588
|
+
// centralizes, so it stays on its inline path even inside a batch.
|
|
1589
|
+
const batchStaging = specificProjectToMigrate
|
|
1590
|
+
? undefined
|
|
1591
|
+
: (0, batch_conversion_session_1.getActiveBatchStaging)(tree);
|
|
1592
|
+
// Phase 3: Derive strict-common + write. Single-project mode never hoists
|
|
1593
|
+
// (would leak shared config to sibling projects), so it keeps the full
|
|
1594
|
+
// residual in project.json, as does a deferred batch conversion (the batch
|
|
1595
|
+
// finalize pass hoists later); whole-workspace mode hoists the common config
|
|
1596
|
+
// to a plugin-scoped `targetDefaults` entry and writes only per-project
|
|
1597
|
+
// deviations.
|
|
1598
|
+
let hoistedByTarget = new Map();
|
|
1599
|
+
if (specificProjectToMigrate || batchStaging) {
|
|
1600
|
+
writeResiduals(tree, projectConfigsByName, projectGraph, scope, residualByProject, logger);
|
|
1601
|
+
}
|
|
1602
|
+
else {
|
|
1603
|
+
hoistedByTarget = hoistCommonAndWrite(tree, projectConfigsByName, scope, residualByProject, pluginPath, projectGraph, inferredTargetsByOptionSet, inferredExecutors, pluginPreRegistered, logger);
|
|
1604
|
+
}
|
|
1605
|
+
if (batchStaging) {
|
|
1606
|
+
stageDeferredPlan(batchStaging, pluginPath, createNodes, createNodesV2, logger, pluginPreRegistered, scope, projectGraph, residualByProject, inferredTargetsByOptionSet, inferredExecutors, inferredRoots, matchedConfigFiles, erroredConfigFilesFromInference);
|
|
1607
|
+
}
|
|
1608
|
+
else {
|
|
1609
|
+
// Phase 4: single verification inference pass + equivalence oracle. Any
|
|
1610
|
+
// project whose centralized config cannot be verified as equivalent falls
|
|
1611
|
+
// back to a full project.json override (summarized in one logger.warn).
|
|
1612
|
+
await verifyAndFallback(tree, projectGraph, pluginPath, createNodes, createNodesV2, residualByProject, projectConfigsByName, hoistedByTarget, inferredRoots, Boolean(specificProjectToMigrate), logger);
|
|
1613
|
+
}
|
|
1614
|
+
spinner.succeed(`Migrated configuration for ${projects.size} project(s).\n`);
|
|
1615
|
+
return projects;
|
|
1616
|
+
}
|
|
1617
|
+
function addPluginRegistrations(tree, projects, pluginPath, defaultPluginOptions, projectGraph, spinner,
|
|
1618
|
+
// The matched config files owned by an inferred project root (the filtered
|
|
1619
|
+
// subset from the Phase 1 inference: files outside any inferred root
|
|
1620
|
+
// contribute no project and are deliberately excluded). Used to decide
|
|
1621
|
+
// analytically whether a registration's `include` globs already cover
|
|
1622
|
+
// everything (so it can be left unscoped).
|
|
1623
|
+
matchedConfigFiles,
|
|
1624
|
+
// Config files the Phase 1 inference could not load. These produce no project
|
|
1625
|
+
// (so they're absent from `matchedConfigFiles`), but an errored file OUTSIDE
|
|
1626
|
+
// the migrated roots must still keep the `include` scoped: leaving the plugin
|
|
1627
|
+
// unscoped would widen it onto that root, where the verification pass re-hits
|
|
1628
|
+
// the failure and reverts the whole hoist. A harmless project-free config (a
|
|
1629
|
+
// shared/base config that loads fine) is NOT errored, so it does not keep the
|
|
1630
|
+
// include: the plugin may safely apply workspace-wide over it.
|
|
1631
|
+
erroredConfigFiles,
|
|
1632
|
+
// The FULL set of config files the plugin's glob matched, regardless of what
|
|
1633
|
+
// this migration's inference pass produced a project from. Used to detect
|
|
1634
|
+
// config files owned by NON-MIGRATED projects that the plugin would infer once
|
|
1635
|
+
// the registration is workspace-wide, see below.
|
|
1636
|
+
rawMatchedConfigFiles) {
|
|
1637
|
+
const nxJson = (0, devkit_exports_1.readNxJson)(tree);
|
|
1638
|
+
// Config files owned by a NON-MIGRATED project keep the `include` scoped.
|
|
1639
|
+
// `matchedConfigFiles` only lists roots THIS pass inferred; a non-migrated
|
|
1640
|
+
// project the plugin infers only later (cache/error asymmetry) is absent from
|
|
1641
|
+
// it, so an unscoped registration would let the verification pass infer that
|
|
1642
|
+
// root and `reachesNonMigratedRoot` would revert the whole hoist. Judging
|
|
1643
|
+
// coverage against project-graph membership, not this pass's inference,
|
|
1644
|
+
// closes that hole. (A config owned by NO project, e.g. a shared/base config,
|
|
1645
|
+
// is not counted, so it does not force a scoped include.)
|
|
1646
|
+
const migratedRoots = new Set();
|
|
1647
|
+
for (const project of projects.keys()) {
|
|
1648
|
+
const root = projectGraph.nodes[project]?.data?.root;
|
|
1649
|
+
if (root) {
|
|
1650
|
+
migratedRoots.add(root);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
const nonMigratedProjectRoots = new Set();
|
|
1654
|
+
for (const node of Object.values(projectGraph.nodes)) {
|
|
1655
|
+
const root = node?.data?.root;
|
|
1656
|
+
if (root && !migratedRoots.has(root)) {
|
|
1657
|
+
nonMigratedProjectRoots.add(root);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const nonMigratedProjectConfigFiles = rawMatchedConfigFiles.filter((file) => isFileOwnedByAnyRoot(file, nonMigratedProjectRoots));
|
|
1661
|
+
// Fold errored files (broken configs the plugin would fail to infer) and
|
|
1662
|
+
// non-migrated project configs (roots the plugin would infer workspace-wide)
|
|
1663
|
+
// into the coverage set: an unscoped `include` is chosen only when neither lies
|
|
1664
|
+
// outside the migrated roots.
|
|
1665
|
+
const coverageConfigFiles = [
|
|
1666
|
+
...matchedConfigFiles,
|
|
1667
|
+
...erroredConfigFiles,
|
|
1668
|
+
...nonMigratedProjectConfigFiles,
|
|
1669
|
+
];
|
|
1670
|
+
const registrationGroups = new Map();
|
|
297
1671
|
for (const [project, options] of projects.entries()) {
|
|
1672
|
+
const projectIncludeGlob = projectGraph.nodes[project].data.root === '.'
|
|
1673
|
+
? '*'
|
|
1674
|
+
: (0, posix_1.join)(projectGraph.nodes[project].data.root, '**/*');
|
|
1675
|
+
const key = stableStringify(options);
|
|
1676
|
+
if (!registrationGroups.has(key)) {
|
|
1677
|
+
registrationGroups.set(key, { options, include: [] });
|
|
1678
|
+
}
|
|
1679
|
+
registrationGroups.get(key).include.push(projectIncludeGlob);
|
|
1680
|
+
}
|
|
1681
|
+
let index = 0;
|
|
1682
|
+
for (const { options, include } of registrationGroups.values()) {
|
|
298
1683
|
index++;
|
|
299
|
-
spinner.updateText(`${index}/${
|
|
1684
|
+
spinner.updateText(`${index}/${registrationGroups.size} - Applying plugin configuration...`);
|
|
300
1685
|
const existingPlugin = nxJson.plugins?.find((plugin) => typeof plugin !== 'string' &&
|
|
301
1686
|
plugin.plugin === pluginPath &&
|
|
302
1687
|
Object.keys(options).every((key) => plugin.options[key] === options[key] ||
|
|
303
1688
|
(plugin.options[key] === undefined &&
|
|
304
1689
|
options[key] === defaultPluginOptions[key])));
|
|
305
|
-
const projectIncludeGlob = projectGraph.nodes[project].data.root === '.'
|
|
306
|
-
? '*'
|
|
307
|
-
: (0, posix_1.join)(projectGraph.nodes[project].data.root, '**/*');
|
|
308
1690
|
if (!existingPlugin) {
|
|
309
1691
|
nxJson.plugins ??= [];
|
|
310
1692
|
const plugin = {
|
|
311
1693
|
plugin: pluginPath,
|
|
312
1694
|
options,
|
|
313
|
-
include: [
|
|
1695
|
+
include: [...include],
|
|
314
1696
|
};
|
|
315
|
-
if (
|
|
1697
|
+
if (includeCoversAllConfigFiles(plugin.include, plugin.exclude, coverageConfigFiles)) {
|
|
316
1698
|
delete plugin.include;
|
|
317
1699
|
}
|
|
318
1700
|
nxJson.plugins.push(plugin);
|
|
319
1701
|
}
|
|
320
1702
|
else if (existingPlugin.include) {
|
|
321
|
-
|
|
322
|
-
existingPlugin.include.
|
|
323
|
-
|
|
324
|
-
delete existingPlugin.include;
|
|
1703
|
+
for (const projectIncludeGlob of include) {
|
|
1704
|
+
if (!existingPlugin.include.some((include) => (0, minimatch_1.minimatch)(projectIncludeGlob, include, { dot: true }))) {
|
|
1705
|
+
existingPlugin.include.push(projectIncludeGlob);
|
|
325
1706
|
}
|
|
326
1707
|
}
|
|
1708
|
+
if (includeCoversAllConfigFiles(existingPlugin.include, existingPlugin.exclude, coverageConfigFiles)) {
|
|
1709
|
+
delete existingPlugin.include;
|
|
1710
|
+
}
|
|
327
1711
|
}
|
|
328
1712
|
}
|
|
329
1713
|
spinner.updateText(`Migrations done`);
|
|
@@ -331,6 +1715,7 @@ async function addPluginRegistrations(tree, projects, pluginPath, createNodes, c
|
|
|
331
1715
|
}
|
|
332
1716
|
async function getCreateNodesResultsForPlugin(tree, pluginConfiguration, pluginPath, createNodes, createNodesV2, nxJson) {
|
|
333
1717
|
let projectConfigs;
|
|
1718
|
+
let erroredConfigFiles = [];
|
|
334
1719
|
try {
|
|
335
1720
|
const plugin = new devkit_internals_1.LoadedNxPlugin({ createNodes, createNodesV2, name: pluginPath }, pluginConfiguration);
|
|
336
1721
|
projectConfigs = await (0, devkit_internals_1.retrieveProjectConfigurations)({ specifiedPlugins: [plugin], defaultPlugins: [] }, tree.root, nxJson);
|
|
@@ -338,33 +1723,15 @@ async function getCreateNodesResultsForPlugin(tree, pluginConfiguration, pluginP
|
|
|
338
1723
|
catch (e) {
|
|
339
1724
|
if (e instanceof devkit_internals_1.ProjectConfigurationsError) {
|
|
340
1725
|
projectConfigs = e.partialProjectConfigurationsResult;
|
|
1726
|
+
// Capture the config files this pass could not infer. The registration
|
|
1727
|
+
// step keeps its `include` scoped to migrated roots when an errored file
|
|
1728
|
+
// sits outside them, so the plugin is never widened to a root it would
|
|
1729
|
+
// fail to infer (which the verification pass would otherwise revert).
|
|
1730
|
+
erroredConfigFiles = harvestConfigurationErrors(e).erroredConfigFiles;
|
|
341
1731
|
}
|
|
342
1732
|
else {
|
|
343
1733
|
throw e;
|
|
344
1734
|
}
|
|
345
1735
|
}
|
|
346
|
-
return projectConfigs;
|
|
347
|
-
}
|
|
348
|
-
// Checks if two objects are structurely equal, without caring
|
|
349
|
-
// about the order of the keys.
|
|
350
|
-
function deepEqual(a, b, logKey = '') {
|
|
351
|
-
const aKeys = Object.keys(a);
|
|
352
|
-
const bKeys = new Set(Object.keys(b));
|
|
353
|
-
if (aKeys.length !== bKeys.size) {
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
|
-
for (const key of aKeys) {
|
|
357
|
-
if (!bKeys.has(key)) {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
if (typeof a[key] === 'object' && typeof b[key] === 'object') {
|
|
361
|
-
if (!deepEqual(a[key], b[key], logKey + '.' + key)) {
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
else if (a[key] !== b[key]) {
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return true;
|
|
1736
|
+
return { result: projectConfigs, erroredConfigFiles };
|
|
370
1737
|
}
|