@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
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.finalizeBatchConversion = finalizeBatchConversion;
|
|
4
|
+
const posix_1 = require("node:path/posix");
|
|
5
|
+
const devkit_exports_1 = require("nx/src/devkit-exports");
|
|
6
|
+
const devkit_internals_1 = require("nx/src/devkit-internals");
|
|
7
|
+
const target_defaults_utils_1 = require("../target-defaults-utils");
|
|
8
|
+
const executor_to_plugin_migrator_1 = require("./executor-to-plugin-migrator");
|
|
9
|
+
const conversion_warnings_1 = require("./conversion-warnings");
|
|
10
|
+
/**
|
|
11
|
+
* Batch finalize: the deferred equivalent of Phase 3's hoist + cleanup and
|
|
12
|
+
* Phase 4's verification, run once over every plan the batch session staged.
|
|
13
|
+
* Planning happens on a clone of the final `nx.json`; a single combined
|
|
14
|
+
* verification inference pass (all trusted plugin registrations, in final
|
|
15
|
+
* order) supplies the ownership oracle and the per-pair equivalence inputs;
|
|
16
|
+
* only then is the accepted outcome applied to the Tree as a precomputed byte
|
|
17
|
+
* write-set.
|
|
18
|
+
*
|
|
19
|
+
* Finalization is optional deduplication: the Tree already holds each child's
|
|
20
|
+
* conservative full-residual output. Any failure here (including an apply
|
|
21
|
+
* failure, after restoring the write-set's snapshots) is caught, reported as
|
|
22
|
+
* a single warning, and swallowed so the batch's callbacks still run.
|
|
23
|
+
*/
|
|
24
|
+
async function finalizeBatchConversion(tree, session, logger) {
|
|
25
|
+
try {
|
|
26
|
+
await runFinalize(tree, session.records);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
30
|
+
(logger ?? devkit_exports_1.logger).warn(`convert-to-inferred could not centralize the shared configuration for this batch: ${message}. Every migrated project keeps its full per-project configuration, but shared configuration remains duplicated.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function runFinalize(tree, records) {
|
|
34
|
+
const plans = records.flatMap((record) => record.plans);
|
|
35
|
+
if (plans.length === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const nxJson = (0, devkit_exports_1.readNxJson)(tree);
|
|
39
|
+
const finalPlugins = nxJson.plugins ?? [];
|
|
40
|
+
// Classify every final registration: a registration of a plan's plugin is
|
|
41
|
+
// trusted (the plan's createNodes can run it); anything else is opaque: a
|
|
42
|
+
// pre-existing foreign plugin, or a registration a child added outside the
|
|
43
|
+
// engine. An opaque registration at position k merges after (and can take
|
|
44
|
+
// target identity from) every plan registered before k, invisibly to the
|
|
45
|
+
// combined pass, so those plans must keep their full residuals.
|
|
46
|
+
const pluginPathByRegistrationIndex = new Map();
|
|
47
|
+
const opaqueIndexes = [];
|
|
48
|
+
finalPlugins.forEach((registration, index) => {
|
|
49
|
+
const owner = plans.find((plan) => (0, executor_to_plugin_migrator_1.isRegistrationOfPlugin)(registration, plan.pluginPath));
|
|
50
|
+
if (owner) {
|
|
51
|
+
pluginPathByRegistrationIndex.set(index, owner.pluginPath);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
opaqueIndexes.push(index);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const planStates = plans.map((plan) => ({
|
|
58
|
+
plan,
|
|
59
|
+
log: plan.logger ?? devkit_exports_1.logger,
|
|
60
|
+
firstRegistrationIndex: finalPlugins.findIndex((registration) => (0, executor_to_plugin_migrator_1.isRegistrationOfPlugin)(registration, plan.pluginPath)),
|
|
61
|
+
targets: new Map(),
|
|
62
|
+
packageJsonAuthoredIdentity: false,
|
|
63
|
+
restoredPairs: [],
|
|
64
|
+
}));
|
|
65
|
+
// Plan in final registration order: later-registered plugins merge later, so
|
|
66
|
+
// their proposed entries append after earlier plans' under a shared key.
|
|
67
|
+
planStates.sort((a, b) => (a.firstRegistrationIndex === -1
|
|
68
|
+
? Number.MAX_SAFE_INTEGER
|
|
69
|
+
: a.firstRegistrationIndex) -
|
|
70
|
+
(b.firstRegistrationIndex === -1
|
|
71
|
+
? Number.MAX_SAFE_INTEGER
|
|
72
|
+
: b.firstRegistrationIndex));
|
|
73
|
+
// Executors any plan's inference emits. The union: an appended target-name
|
|
74
|
+
// key resolves as an EXECUTOR key for every target of every plugin in the
|
|
75
|
+
// final workspace, not only the plan's own.
|
|
76
|
+
const unionInferredExecutors = new Set();
|
|
77
|
+
for (const plan of plans) {
|
|
78
|
+
for (const executor of plan.inferredExecutors) {
|
|
79
|
+
unionInferredExecutors.add(executor);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const proposedNxJson = structuredClone(nxJson);
|
|
83
|
+
for (const state of planStates) {
|
|
84
|
+
planTargets(tree, state);
|
|
85
|
+
restorePreMigrationTargets(tree, state);
|
|
86
|
+
applyPlanGates(state, opaqueIndexes, unionInferredExecutors, proposedNxJson);
|
|
87
|
+
// Surviving candidates join the proposed nx.json so later plans' preflight
|
|
88
|
+
// and the combined verification see them.
|
|
89
|
+
proposedNxJson.targetDefaults ??= {};
|
|
90
|
+
for (const targetName of [...state.targets.keys()].sort()) {
|
|
91
|
+
const targetPlan = state.targets.get(targetName);
|
|
92
|
+
if (Object.keys(targetPlan.common).length === 0) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
targetPlan.hoistedEntry = (0, executor_to_plugin_migrator_1.appendPluginScopedTargetDefault)(proposedNxJson, targetName, state.plan.pluginPath, targetPlan.common);
|
|
96
|
+
}
|
|
97
|
+
emitExcludedProjectsWarning(state);
|
|
98
|
+
}
|
|
99
|
+
// Every trusted registration, in exact final order, constructed with its
|
|
100
|
+
// final registration index so partial errors are attributable to a plan.
|
|
101
|
+
const specifiedPlugins = [];
|
|
102
|
+
finalPlugins.forEach((registration, index) => {
|
|
103
|
+
const pluginPath = pluginPathByRegistrationIndex.get(index);
|
|
104
|
+
if (pluginPath === undefined) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const plan = plans.find((p) => p.pluginPath === pluginPath);
|
|
108
|
+
specifiedPlugins.push(new devkit_internals_1.LoadedNxPlugin({
|
|
109
|
+
createNodes: plan.createNodes,
|
|
110
|
+
createNodesV2: plan.createNodesV2,
|
|
111
|
+
name: plan.pluginPath,
|
|
112
|
+
}, registration, index));
|
|
113
|
+
});
|
|
114
|
+
if (specifiedPlugins.length === 0) {
|
|
115
|
+
// No plan has a final registration: every candidate was already retained
|
|
116
|
+
// by the registration gate and nothing can be verified. The conservative
|
|
117
|
+
// Tree state stands.
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// One combined specified-plugin verification pass over every trusted final
|
|
121
|
+
// registration, against the complete proposed nx.json. This replaces the
|
|
122
|
+
// deferred children's Phase 4 passes.
|
|
123
|
+
const { result, errors } = await runCombinedVerificationPass(tree, specifiedPlugins, proposedNxJson);
|
|
124
|
+
if (!result) {
|
|
125
|
+
throw new Error('the verification inference pass returned no result');
|
|
126
|
+
}
|
|
127
|
+
const ownerRootByPath = (0, executor_to_plugin_migrator_1.buildOwnerRootByPath)(plans.flatMap((plan) => [...plan.graphRoots]), plans.flatMap((plan) => [...plan.inferredRoots]));
|
|
128
|
+
const deviationsByProject = new Map();
|
|
129
|
+
for (const state of planStates) {
|
|
130
|
+
const planErrors = errors.filter((error) => error.pluginIndex === undefined ||
|
|
131
|
+
pluginPathByRegistrationIndex.get(error.pluginIndex) ===
|
|
132
|
+
state.plan.pluginPath);
|
|
133
|
+
rejectUnsafeCandidates(state, result, planErrors, ownerRootByPath, proposedNxJson);
|
|
134
|
+
verifyPairs(state, result, planErrors, deviationsByProject);
|
|
135
|
+
}
|
|
136
|
+
planDeadExecutorCleanup(tree, records, plans, planStates, proposedNxJson);
|
|
137
|
+
applyWriteSet(tree, proposedNxJson, deviationsByProject);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Group a plan's residuals by target and partition each target's projects into
|
|
141
|
+
* hoist-eligible and excluded, exactly as the inline hoist does, except the
|
|
142
|
+
* package.json identity is recomputed against the FINAL Tree, since later
|
|
143
|
+
* children in the batch may have edited package.json files after this plan was
|
|
144
|
+
* staged.
|
|
145
|
+
*/
|
|
146
|
+
function planTargets(tree, state) {
|
|
147
|
+
const { plan } = state;
|
|
148
|
+
for (const [projectName, targetMap] of plan.residualByProject) {
|
|
149
|
+
const root = plan.rootByProject.get(projectName);
|
|
150
|
+
for (const [targetName, entry] of targetMap) {
|
|
151
|
+
if (!state.targets.has(targetName)) {
|
|
152
|
+
state.targets.set(targetName, {
|
|
153
|
+
pairs: [],
|
|
154
|
+
excludedProjects: new Set(),
|
|
155
|
+
migratedRoots: new Set(),
|
|
156
|
+
common: {},
|
|
157
|
+
hoistedEntry: undefined,
|
|
158
|
+
rejected: false,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const targetPlan = state.targets.get(targetName);
|
|
162
|
+
targetPlan.pairs.push({ projectName, root, entry });
|
|
163
|
+
targetPlan.migratedRoots.add(root);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const targetName of [...state.targets.keys()].sort()) {
|
|
167
|
+
const targetPlan = state.targets.get(targetName);
|
|
168
|
+
const eligibleResiduals = [];
|
|
169
|
+
for (const pair of targetPlan.pairs) {
|
|
170
|
+
const packageJsonAuthored = (0, executor_to_plugin_migrator_1.packageJsonAuthorsTargetIdentity)(tree, pair.root, targetName);
|
|
171
|
+
if (packageJsonAuthored) {
|
|
172
|
+
state.packageJsonAuthoredIdentity = true;
|
|
173
|
+
if (!pair.entry.keptPreMigration) {
|
|
174
|
+
state.restoredPairs.push({ ...pair, targetName });
|
|
175
|
+
state.log.warn((0, conversion_warnings_1.keptPreMigrationTargetWarning)(targetName, pair.projectName, packageJsonAuthored));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const identityAuthored = pair.entry.residual.executor !== undefined ||
|
|
179
|
+
pair.entry.residual.command !== undefined ||
|
|
180
|
+
packageJsonAuthored;
|
|
181
|
+
if (identityAuthored) {
|
|
182
|
+
targetPlan.excludedProjects.add(pair.projectName);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
eligibleResiduals.push(pair.entry.residual);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
targetPlan.common =
|
|
189
|
+
eligibleResiduals.length >= 2
|
|
190
|
+
? (0, executor_to_plugin_migrator_1.computeStrictCommon)(eligibleResiduals)
|
|
191
|
+
: {};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Put back the pre-migration target of every pair whose package.json identity
|
|
196
|
+
* showed up only after the child wrote the residual. The child's write already
|
|
197
|
+
* removed the executor, which is exactly what lets the identity take the
|
|
198
|
+
* target over, so this runs before any gate or verification (an early return
|
|
199
|
+
* or a thrown verification must not leave the residual in place). Same
|
|
200
|
+
* outcome as the inline write-time guard, one step later.
|
|
201
|
+
*/
|
|
202
|
+
function restorePreMigrationTargets(tree, state) {
|
|
203
|
+
if (state.restoredPairs.length === 0) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const pendingOptionsByPath = new Map(tree.listChanges().map((change) => [change.path, change.options]));
|
|
207
|
+
for (const { root, targetName, entry } of state.restoredPairs) {
|
|
208
|
+
const projectJsonPath = (0, posix_1.join)(root, 'project.json');
|
|
209
|
+
let path;
|
|
210
|
+
if (tree.exists(projectJsonPath)) {
|
|
211
|
+
path = projectJsonPath;
|
|
212
|
+
const projectJson = (0, devkit_exports_1.readJson)(tree, path);
|
|
213
|
+
projectJson.targets ??= {};
|
|
214
|
+
projectJson.targets[targetName] = structuredClone(entry.preMigrationTarget);
|
|
215
|
+
(0, devkit_exports_1.writeJson)(tree, path, projectJson);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
path = (0, posix_1.join)(root, 'package.json');
|
|
219
|
+
const packageJson = (0, devkit_exports_1.readJson)(tree, path);
|
|
220
|
+
packageJson.nx ??= {};
|
|
221
|
+
packageJson.nx.targets ??= {};
|
|
222
|
+
packageJson.nx.targets[targetName] = structuredClone(entry.preMigrationTarget);
|
|
223
|
+
(0, devkit_exports_1.writeJson)(tree, path, packageJson);
|
|
224
|
+
}
|
|
225
|
+
// A write replaces the path's recorded change, dropping any staged
|
|
226
|
+
// `TreeWriteOptions`; re-apply a mode staged before the finalize pass.
|
|
227
|
+
const mode = pendingOptionsByPath.get(path)?.mode;
|
|
228
|
+
if (mode !== undefined) {
|
|
229
|
+
tree.changePermissions(path, mode);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The pre-verification gates, mirroring the inline hoist's order: the
|
|
235
|
+
* registration-tail gate (generalized to positional opaque barriers), then the
|
|
236
|
+
* exact-name / executor-collision gate and the existing-target-default
|
|
237
|
+
* preflight. Rejected targets keep their full residuals and warn with the
|
|
238
|
+
* inline reasons.
|
|
239
|
+
*/
|
|
240
|
+
function applyPlanGates(state, opaqueIndexes, unionInferredExecutors, proposedNxJson) {
|
|
241
|
+
const { plan } = state;
|
|
242
|
+
const retainResiduals = (targetNames, reason) => {
|
|
243
|
+
for (const targetName of targetNames) {
|
|
244
|
+
state.targets.get(targetName).common = {};
|
|
245
|
+
}
|
|
246
|
+
state.log.warn((0, conversion_warnings_1.retainedResidualsWarning)(targetNames, reason));
|
|
247
|
+
};
|
|
248
|
+
const centralizableTargets = () => [...state.targets.entries()]
|
|
249
|
+
.filter(([, targetPlan]) => Object.keys(targetPlan.common).length > 0)
|
|
250
|
+
.map(([targetName]) => targetName)
|
|
251
|
+
.sort();
|
|
252
|
+
const blockedByOpaqueRegistration = state.firstRegistrationIndex === -1 ||
|
|
253
|
+
opaqueIndexes.some((index) => index > state.firstRegistrationIndex);
|
|
254
|
+
if (blockedByOpaqueRegistration) {
|
|
255
|
+
const skippedTargets = centralizableTargets();
|
|
256
|
+
if (skippedTargets.length > 0) {
|
|
257
|
+
retainResiduals(skippedTargets, `another plugin is registered after ${plan.pluginPath} in nx.json and may take over those targets`);
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const preflightTargets = centralizableTargets();
|
|
262
|
+
if (preflightTargets.length === 0) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const projectNodesByName = Object.fromEntries(plan.graphNodeByProject);
|
|
266
|
+
const nonExactNameTargets = [];
|
|
267
|
+
const rejectedTargets = [];
|
|
268
|
+
for (const targetName of preflightTargets) {
|
|
269
|
+
if (!(0, target_defaults_utils_1.isExactTargetNameKey)(targetName) ||
|
|
270
|
+
unionInferredExecutors.has(targetName)) {
|
|
271
|
+
nonExactNameTargets.push(targetName);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const targetPlan = state.targets.get(targetName);
|
|
275
|
+
const eligiblePairs = targetPlan.pairs
|
|
276
|
+
.filter((pair) => !targetPlan.excludedProjects.has(pair.projectName))
|
|
277
|
+
.map((pair) => ({
|
|
278
|
+
projectName: pair.projectName,
|
|
279
|
+
inferredExecutor: plan.inferredExecutorByPair.get(`${pair.projectName}\t${targetName}`),
|
|
280
|
+
}));
|
|
281
|
+
if ((0, executor_to_plugin_migrator_1.hoistChangesExistingTargetDefaults)(proposedNxJson.targetDefaults, targetName, targetPlan.common, plan.pluginPath, eligiblePairs, projectNodesByName)) {
|
|
282
|
+
rejectedTargets.push(targetName);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (nonExactNameTargets.length > 0) {
|
|
286
|
+
retainResiduals(nonExactNameTargets, 'the target name would resolve as an executor or glob targetDefaults key and could apply to other targets');
|
|
287
|
+
}
|
|
288
|
+
if (rejectedTargets.length > 0) {
|
|
289
|
+
retainResiduals(rejectedTargets, 'centralization would change which existing targetDefaults apply');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function emitExcludedProjectsWarning(state) {
|
|
293
|
+
const excludedTargets = [...state.targets.entries()].filter(([, targetPlan]) => targetPlan.excludedProjects.size > 0);
|
|
294
|
+
if (excludedTargets.length === 0) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const excludedProjectNames = [
|
|
298
|
+
...new Set(excludedTargets.flatMap(([, targetPlan]) => [
|
|
299
|
+
...targetPlan.excludedProjects,
|
|
300
|
+
])),
|
|
301
|
+
].sort();
|
|
302
|
+
const targetNames = excludedTargets.map(([targetName]) => targetName).sort();
|
|
303
|
+
state.log.warn((0, conversion_warnings_1.excludedProjectsWarning)(excludedProjectNames, targetNames));
|
|
304
|
+
}
|
|
305
|
+
async function runCombinedVerificationPass(tree, specifiedPlugins, proposedNxJson) {
|
|
306
|
+
global.NX_GRAPH_CREATION = true;
|
|
307
|
+
try {
|
|
308
|
+
return {
|
|
309
|
+
result: await (0, devkit_internals_1.retrieveProjectConfigurations)({ specifiedPlugins, defaultPlugins: [] }, tree.root, structuredClone(proposedNxJson)),
|
|
310
|
+
errors: [],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
if (e instanceof devkit_internals_1.ProjectConfigurationsError) {
|
|
315
|
+
return {
|
|
316
|
+
result: e.partialProjectConfigurationsResult,
|
|
317
|
+
errors: (0, executor_to_plugin_migrator_1.harvestConfigurationErrors)(e).entries,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
throw e;
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
global.NX_GRAPH_CREATION = false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* The owner of a target's identity in the combined result: the plugin the
|
|
328
|
+
* final source maps attribute `executor`/`command` to. Mirrors Nx's
|
|
329
|
+
* `resolveSourcePlugin` (project-configuration/target-defaults.ts), which
|
|
330
|
+
* decides whether a `filter: { plugin }` default resolves for the target; the
|
|
331
|
+
* synthetic `nx/target-defaults` plugin never owns identity.
|
|
332
|
+
*/
|
|
333
|
+
function resolveTargetOwner(result, root, targetName) {
|
|
334
|
+
const sourceMap = result.sourceMaps?.[root];
|
|
335
|
+
for (const identityKey of ['executor', 'command']) {
|
|
336
|
+
const plugin = sourceMap?.[`targets.${targetName}.${identityKey}`]?.[1];
|
|
337
|
+
if (plugin && plugin !== 'nx/target-defaults') {
|
|
338
|
+
return plugin;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Target-wide rejection, the deferred equivalent of the inline verification
|
|
345
|
+
* revert: drop a plan's candidate when the combined pass shows its plugin
|
|
346
|
+
* owning the target on a non-migrated root (that root would inherit the
|
|
347
|
+
* centralized default), or when an attributable error lies outside the plan's
|
|
348
|
+
* migrated roots (a root the pass could not inspect might). Rejected targets
|
|
349
|
+
* keep their full residuals (the conservative Tree state) and the entry is
|
|
350
|
+
* removed from the proposed nx.json.
|
|
351
|
+
*/
|
|
352
|
+
function rejectUnsafeCandidates(state, result, planErrors, ownerRootByPath, proposedNxJson) {
|
|
353
|
+
const revertedTargets = [];
|
|
354
|
+
for (const [targetName, targetPlan] of state.targets) {
|
|
355
|
+
if (!targetPlan.hoistedEntry) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const reachesNonMigratedRoot = Object.entries(result.projects ?? {}).some(([root, projectConfig]) => {
|
|
359
|
+
if (projectConfig.targets?.[targetName] === undefined ||
|
|
360
|
+
targetPlan.migratedRoots.has(root)) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
// Only roots where THIS plugin owns the target inherit its
|
|
364
|
+
// plugin-scoped default. A target another trusted plugin owns is that
|
|
365
|
+
// plugin's concern; an unresolvable owner fails closed.
|
|
366
|
+
const owner = resolveTargetOwner(result, root, targetName);
|
|
367
|
+
return owner === state.plan.pluginPath || owner === undefined;
|
|
368
|
+
});
|
|
369
|
+
const erroredOutsideMigratedRoots = planErrors.some((error) => error.files.some((file) => {
|
|
370
|
+
const ownerRoot = (0, devkit_internals_1.findProjectForPath)(file, ownerRootByPath);
|
|
371
|
+
return ownerRoot == null || !targetPlan.migratedRoots.has(ownerRoot);
|
|
372
|
+
}));
|
|
373
|
+
if (reachesNonMigratedRoot || erroredOutsideMigratedRoots) {
|
|
374
|
+
targetPlan.rejected = true;
|
|
375
|
+
(0, executor_to_plugin_migrator_1.removeHoistedTargetDefault)(proposedNxJson, targetName, targetPlan.hoistedEntry);
|
|
376
|
+
targetPlan.hoistedEntry = undefined;
|
|
377
|
+
revertedTargets.push(targetName);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (revertedTargets.length > 0) {
|
|
381
|
+
state.log.warn((0, conversion_warnings_1.revertedTargetsWarning)(revertedTargets.sort(), planErrors.map((error) => error.message)));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Per-pair equivalence for EVERY migrated pair of the plan, candidates or not
|
|
386
|
+
* (mirrors the inline Phase 4 oracle): the planned write merged with the
|
|
387
|
+
* verified inferred target must equal the pair's baseline. A missing,
|
|
388
|
+
* differently owned, or divergent pair keeps its full residual (the
|
|
389
|
+
* conservative Tree state) instead of receiving a deviation write.
|
|
390
|
+
*/
|
|
391
|
+
function verifyPairs(state, result, planErrors, deviationsByProject) {
|
|
392
|
+
const divergent = [];
|
|
393
|
+
const missing = [];
|
|
394
|
+
let anyReverted = false;
|
|
395
|
+
for (const [targetName, targetPlan] of state.targets) {
|
|
396
|
+
if (targetPlan.rejected) {
|
|
397
|
+
// Full residuals are already in place: the previous engine's exact
|
|
398
|
+
// output, so there is nothing left to verify.
|
|
399
|
+
anyReverted = true;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
for (const pair of targetPlan.pairs) {
|
|
403
|
+
const verifiedInferred = result.projects?.[pair.root]?.targets?.[targetName];
|
|
404
|
+
if (!verifiedInferred) {
|
|
405
|
+
// No deviation is planned for a missing pair, so its child-written or
|
|
406
|
+
// pre-migration target remains
|
|
407
|
+
missing.push({
|
|
408
|
+
pair: `${pair.projectName} > ${targetName}`,
|
|
409
|
+
root: pair.root,
|
|
410
|
+
removed: !pair.entry.keptPreMigration &&
|
|
411
|
+
!targetPlan.excludedProjects.has(pair.projectName) &&
|
|
412
|
+
(0, conversion_warnings_1.isDroppedTarget)(pair.entry.residual),
|
|
413
|
+
});
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const hoisted = targetPlan.hoistedEntry !== undefined &&
|
|
417
|
+
!targetPlan.excludedProjects.has(pair.projectName);
|
|
418
|
+
if (hoisted &&
|
|
419
|
+
resolveTargetOwner(result, pair.root, targetName) !==
|
|
420
|
+
state.plan.pluginPath) {
|
|
421
|
+
// The plugin-scoped default would not resolve for a differently owned
|
|
422
|
+
// pair, so the deviation write would silently drop the common keys.
|
|
423
|
+
divergent.push(`${pair.projectName} > ${targetName}`);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const plannedWrite = hoisted
|
|
427
|
+
? (0, executor_to_plugin_migrator_1.subtractCommon)(pair.entry.residual, targetPlan.common)
|
|
428
|
+
: structuredClone(pair.entry.residual);
|
|
429
|
+
const postMigrationFinal = (0, devkit_internals_1.mergeTargetConfigurations)(structuredClone(plannedWrite), structuredClone(verifiedInferred));
|
|
430
|
+
// stableStringify rather than deepStrictEqual: the staged baseline went
|
|
431
|
+
// through `structuredClone`, whose output can carry another realm's
|
|
432
|
+
// Object prototype, which deepStrictEqual rejects on structurally equal
|
|
433
|
+
// objects.
|
|
434
|
+
if ((0, executor_to_plugin_migrator_1.stableStringify)(postMigrationFinal) !==
|
|
435
|
+
(0, executor_to_plugin_migrator_1.stableStringify)(pair.entry.baselineFinal)) {
|
|
436
|
+
divergent.push(`${pair.projectName} > ${targetName}`);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (hoisted) {
|
|
440
|
+
if (!deviationsByProject.has(pair.projectName)) {
|
|
441
|
+
deviationsByProject.set(pair.projectName, {
|
|
442
|
+
root: pair.root,
|
|
443
|
+
targets: new Map(),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
deviationsByProject
|
|
447
|
+
.get(pair.projectName)
|
|
448
|
+
.targets.set(targetName, plannedWrite);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const anyFallback = divergent.length > 0 || missing.length > 0;
|
|
453
|
+
if (anyFallback) {
|
|
454
|
+
state.log.warn((0, conversion_warnings_1.unverifiedPairsWarning)(divergent, missing, missing.length > 0 ? planErrors.map((error) => error.message) : []));
|
|
455
|
+
}
|
|
456
|
+
const errorsSurfacedByFallbackWarning = missing.length > 0;
|
|
457
|
+
if (planErrors.length > 0 &&
|
|
458
|
+
!anyReverted &&
|
|
459
|
+
!errorsSurfacedByFallbackWarning) {
|
|
460
|
+
state.log.warn((0, conversion_warnings_1.verificationErrorsWarning)(planErrors.map((error) => error.message), anyFallback));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Batch-global dead-executor cleanup, run once (the deferred children leave
|
|
465
|
+
* every executor-keyed default in place). An executor stays live when any
|
|
466
|
+
* plan's inference emits it, any explicit target still carries it, or an
|
|
467
|
+
* untouched (not migrated by any plan) graph pair resolves it. A registration
|
|
468
|
+
* added during the batch that no plan accounts for makes liveness opaque, so
|
|
469
|
+
* cleanup is skipped entirely; per executor, every plan that migrated it must
|
|
470
|
+
* also pass the inline fail-open gates (fresh registration, no
|
|
471
|
+
* package-authored identity).
|
|
472
|
+
*/
|
|
473
|
+
function planDeadExecutorCleanup(tree, records, plans, planStates, proposedNxJson) {
|
|
474
|
+
const anyOpaqueAddedDuringBatch = records.some((record) => {
|
|
475
|
+
const before = new Set(record.pluginsBefore.map((registration) => JSON.stringify(registration)));
|
|
476
|
+
return record.pluginsAfter.some((registration) => !plans.some((plan) => (0, executor_to_plugin_migrator_1.isRegistrationOfPlugin)(registration, plan.pluginPath)) && !before.has(JSON.stringify(registration)));
|
|
477
|
+
});
|
|
478
|
+
if (anyOpaqueAddedDuringBatch) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const liveExecutors = new Set();
|
|
482
|
+
for (const plan of plans) {
|
|
483
|
+
for (const executor of plan.inferredExecutors) {
|
|
484
|
+
liveExecutors.add(executor);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
for (const projectConfig of (0, devkit_exports_1.getProjects)(tree).values()) {
|
|
488
|
+
for (const target of Object.values(projectConfig.targets ?? {})) {
|
|
489
|
+
if (target.executor) {
|
|
490
|
+
liveExecutors.add(target.executor);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const migratedPairs = new Set();
|
|
495
|
+
for (const plan of plans) {
|
|
496
|
+
for (const [projectName, targetMap] of plan.residualByProject) {
|
|
497
|
+
for (const targetName of targetMap.keys()) {
|
|
498
|
+
migratedPairs.add(`${projectName}\t${targetName}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
for (const plan of plans) {
|
|
503
|
+
for (const [pairKey, executor] of plan.graphExecutorByPair) {
|
|
504
|
+
if (!migratedPairs.has(pairKey)) {
|
|
505
|
+
liveExecutors.add(executor);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const statesByExecutor = new Map();
|
|
510
|
+
for (const state of planStates) {
|
|
511
|
+
for (const executor of state.plan.migratedExecutors) {
|
|
512
|
+
if (!statesByExecutor.has(executor)) {
|
|
513
|
+
statesByExecutor.set(executor, []);
|
|
514
|
+
}
|
|
515
|
+
statesByExecutor.get(executor).push(state);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
for (const [executor, states] of statesByExecutor) {
|
|
519
|
+
const removalSafe = states.every((state) => !state.plan.pluginPreRegistered && !state.packageJsonAuthoredIdentity);
|
|
520
|
+
if (removalSafe && !liveExecutors.has(executor)) {
|
|
521
|
+
(0, executor_to_plugin_migrator_1.removeDeadExecutorTargetDefault)(proposedNxJson, executor);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Serialize the final outcome into a byte write-set, snapshot the affected
|
|
527
|
+
* paths' current virtual state, and apply. Project and package files receive
|
|
528
|
+
* direct `tree.write` calls of the precomputed bytes; `nx.json` goes through
|
|
529
|
+
* `updateNxJson` (a single underlying write) so a configuration read through
|
|
530
|
+
* `extends` is projected back onto the local file exactly as the inline path
|
|
531
|
+
* does. On any apply error the snapshots are restored and verified before the
|
|
532
|
+
* error propagates (the caller downgrades it to a single warning).
|
|
533
|
+
*/
|
|
534
|
+
function applyWriteSet(tree, proposedNxJson, deviationsByProject) {
|
|
535
|
+
const writes = [];
|
|
536
|
+
if (proposedNxJson.targetDefaults &&
|
|
537
|
+
Object.keys(proposedNxJson.targetDefaults).length === 0) {
|
|
538
|
+
delete proposedNxJson.targetDefaults;
|
|
539
|
+
}
|
|
540
|
+
for (const [projectName, { root, targets }] of deviationsByProject) {
|
|
541
|
+
const projectJsonPath = (0, posix_1.join)(root, 'project.json');
|
|
542
|
+
if (tree.exists(projectJsonPath)) {
|
|
543
|
+
// The conservative full residual was written through the same helpers
|
|
544
|
+
// moments ago, so mutating the parsed JSON and re-serializing yields the
|
|
545
|
+
// byte output the inline deviation write would have produced.
|
|
546
|
+
const projectJson = (0, devkit_exports_1.readJson)(tree, projectJsonPath);
|
|
547
|
+
projectJson.targets ??= {};
|
|
548
|
+
for (const [targetName, deviation] of targets) {
|
|
549
|
+
if (Object.keys(deviation).length > 0) {
|
|
550
|
+
projectJson.targets[targetName] = deviation;
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
delete projectJson.targets[targetName];
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Mirror `updateProjectConfiguration`'s empty-targets handling: the
|
|
557
|
+
// `// targets` comment exists (ordered before `targets`) exactly when
|
|
558
|
+
// the map is empty.
|
|
559
|
+
if (Object.keys(projectJson.targets).length === 0) {
|
|
560
|
+
delete projectJson.targets;
|
|
561
|
+
projectJson['// targets'] =
|
|
562
|
+
`to see all targets run: nx show project ${projectName} --web`;
|
|
563
|
+
projectJson.targets = {};
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
delete projectJson['// targets'];
|
|
567
|
+
}
|
|
568
|
+
writes.push({
|
|
569
|
+
path: projectJsonPath,
|
|
570
|
+
content: toWrittenJson(projectJson),
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
// Package-based project: the residual lives in package.json `nx.targets`.
|
|
575
|
+
// An empty deviation removes the entry (the package-identity gate already
|
|
576
|
+
// excluded projects where an included same-name script would take over).
|
|
577
|
+
const packageJsonPath = (0, posix_1.join)(root, 'package.json');
|
|
578
|
+
const packageJson = (0, devkit_exports_1.readJson)(tree, packageJsonPath);
|
|
579
|
+
packageJson.nx ??= {};
|
|
580
|
+
packageJson.nx.targets ??= {};
|
|
581
|
+
for (const [targetName, deviation] of targets) {
|
|
582
|
+
if (Object.keys(deviation).length > 0) {
|
|
583
|
+
packageJson.nx.targets[targetName] = deviation;
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
delete packageJson.nx.targets[targetName];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (Object.keys(packageJson.nx.targets).length === 0) {
|
|
590
|
+
delete packageJson.nx.targets;
|
|
591
|
+
}
|
|
592
|
+
writes.push({
|
|
593
|
+
path: packageJsonPath,
|
|
594
|
+
content: toWrittenJson(packageJson),
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const pendingOptionsByPath = new Map(tree.listChanges().map((change) => [change.path, change.options]));
|
|
599
|
+
const affectedPaths = ['nx.json', ...writes.map(({ path }) => path)];
|
|
600
|
+
const snapshots = affectedPaths.map((path) => ({
|
|
601
|
+
path,
|
|
602
|
+
exists: tree.exists(path),
|
|
603
|
+
content: tree.exists(path) ? tree.read(path) : null,
|
|
604
|
+
options: pendingOptionsByPath.get(path),
|
|
605
|
+
}));
|
|
606
|
+
try {
|
|
607
|
+
(0, devkit_exports_1.updateNxJson)(tree, proposedNxJson);
|
|
608
|
+
for (const write of writes) {
|
|
609
|
+
tree.write(write.path, write.content);
|
|
610
|
+
}
|
|
611
|
+
// A write replaces the path's recorded change, dropping any staged
|
|
612
|
+
// `TreeWriteOptions`; re-apply a mode staged before the finalize pass.
|
|
613
|
+
for (const snapshot of snapshots) {
|
|
614
|
+
if (snapshot.options?.mode !== undefined) {
|
|
615
|
+
tree.changePermissions(snapshot.path, snapshot.options.mode);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch (applyError) {
|
|
620
|
+
let restoreNote;
|
|
621
|
+
try {
|
|
622
|
+
for (const snapshot of snapshots) {
|
|
623
|
+
if (snapshot.exists) {
|
|
624
|
+
tree.write(snapshot.path, snapshot.content);
|
|
625
|
+
if (snapshot.options?.mode !== undefined) {
|
|
626
|
+
// Restoring bytes identical to disk removes the recorded change
|
|
627
|
+
// and its options with it; re-stage the mode explicitly.
|
|
628
|
+
tree.changePermissions(snapshot.path, snapshot.options.mode);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
else if (tree.exists(snapshot.path)) {
|
|
632
|
+
tree.delete(snapshot.path);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const restoredChangeByPath = new Map(tree.listChanges().map((change) => [change.path, change]));
|
|
636
|
+
const unrestored = snapshots.filter((snapshot) => tree.exists(snapshot.path) !== snapshot.exists ||
|
|
637
|
+
(snapshot.exists &&
|
|
638
|
+
!tree.read(snapshot.path)?.equals(snapshot.content)) ||
|
|
639
|
+
restoredChangeByPath.get(snapshot.path)?.options?.mode !==
|
|
640
|
+
snapshot.options?.mode);
|
|
641
|
+
restoreNote =
|
|
642
|
+
unrestored.length === 0
|
|
643
|
+
? ' The affected files were restored to their pre-centralization state.'
|
|
644
|
+
: ` Restoring the affected files failed for: ${unrestored
|
|
645
|
+
.map((snapshot) => snapshot.path)
|
|
646
|
+
.join(', ')}; review them manually.`;
|
|
647
|
+
}
|
|
648
|
+
catch (restoreError) {
|
|
649
|
+
restoreNote = ` Restoring the affected files also failed: ${restoreError instanceof Error ? restoreError.message : restoreError}; review them manually.`;
|
|
650
|
+
}
|
|
651
|
+
const message = applyError instanceof Error ? applyError.message : String(applyError);
|
|
652
|
+
throw new Error(`applying the centralized configuration failed: ${message}.${restoreNote}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/** The exact bytes `writeJson` would write for `value`. */
|
|
656
|
+
function toWrittenJson(value) {
|
|
657
|
+
return `${(0, devkit_exports_1.serializeJson)(value)}\n`;
|
|
658
|
+
}
|