@nx/devkit 23.2.0-beta.11 → 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.
@@ -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
+ }