@nx/devkit 23.1.0-beta.4 → 23.1.0-beta.5

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.
@@ -7,7 +7,7 @@ export { migrateProjectExecutorsToPlugin, migrateProjectExecutorsToPluginV1, NoT
7
7
  export { processTargetOutputs, deleteMatchingProperties, toProjectRelativePath, } from './src/generators/plugin-migrations/plugin-migration-utils';
8
8
  export { determineProjectNameAndRootOptions, ensureRootProjectName, resolveImportPath, } from './src/generators/project-name-and-root-utils';
9
9
  export { promptWhenInteractive } from './src/generators/prompt';
10
- export { addBuildTargetDefaults, addE2eCiTargetDefaults, } from './src/generators/target-defaults-utils';
10
+ export { addBuildTargetDefaults, addE2eCiTargetDefaults, findTargetDefault, readTargetDefaultsForTarget, updateTargetDefault, upsertTargetDefault, } from './src/generators/target-defaults-utils';
11
11
  export { addPlugin } from './src/utils/add-plugin';
12
12
  export { getDeclaredPackageVersion, getInstalledPackageVersion, } from './src/utils/installed-version';
13
13
  export { assertSupportedInstalledPackageVersion, assertSupportedPackageVersion, } from './src/utils/version-floor';
package/dist/internal.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.dasherize = exports.classify = exports.capitalize = exports.camelize = exports.checkAndCleanWithSemver = exports.eachValueFrom = exports.logShowProjectCommand = exports.getNamedInputs = exports.findPluginForConfigFile = exports.clearRequireCache = exports.loadConfigFile = exports.getCatalogManager = exports.calculateHashesForCreateNodes = exports.calculateHashForCreateNodes = exports.mapAsyncIterable = exports.combineAsyncIterables = exports.createAsyncIterable = exports.assertSupportedPackageVersion = exports.assertSupportedInstalledPackageVersion = exports.getInstalledPackageVersion = exports.getDeclaredPackageVersion = exports.addPlugin = exports.addE2eCiTargetDefaults = exports.addBuildTargetDefaults = exports.promptWhenInteractive = exports.resolveImportPath = exports.ensureRootProjectName = exports.determineProjectNameAndRootOptions = exports.toProjectRelativePath = exports.deleteMatchingProperties = exports.processTargetOutputs = exports.NoTargetsToMigrateError = exports.migrateProjectExecutorsToPluginV1 = exports.migrateProjectExecutorsToPlugin = exports.AggregatedLog = exports.forEachExecutorOptions = exports.getE2EWebServerInfo = exports.getRelativeCwd = exports.determineArtifactNameAndDirectoryOptions = exports.PromptResolutionError = exports.resolvePrompt = exports.SchemaResolutionError = exports.ImplementationResolutionError = exports.resolveSchema = exports.resolveImplementation = exports.emitPluginWorkerLog = exports.safeWriteFileCache = exports.PluginCache = exports.createProjectRootMappingsFromProjectConfigurations = exports.signalToCode = void 0;
3
+ exports.checkAndCleanWithSemver = exports.eachValueFrom = exports.logShowProjectCommand = exports.getNamedInputs = exports.findPluginForConfigFile = exports.clearRequireCache = exports.loadConfigFile = exports.getCatalogManager = exports.calculateHashesForCreateNodes = exports.calculateHashForCreateNodes = exports.mapAsyncIterable = exports.combineAsyncIterables = exports.createAsyncIterable = exports.assertSupportedPackageVersion = exports.assertSupportedInstalledPackageVersion = exports.getInstalledPackageVersion = exports.getDeclaredPackageVersion = exports.addPlugin = exports.upsertTargetDefault = exports.updateTargetDefault = exports.readTargetDefaultsForTarget = exports.findTargetDefault = exports.addE2eCiTargetDefaults = exports.addBuildTargetDefaults = exports.promptWhenInteractive = exports.resolveImportPath = exports.ensureRootProjectName = exports.determineProjectNameAndRootOptions = exports.toProjectRelativePath = exports.deleteMatchingProperties = exports.processTargetOutputs = exports.NoTargetsToMigrateError = exports.migrateProjectExecutorsToPluginV1 = exports.migrateProjectExecutorsToPlugin = exports.AggregatedLog = exports.forEachExecutorOptions = exports.getE2EWebServerInfo = exports.getRelativeCwd = exports.determineArtifactNameAndDirectoryOptions = exports.PromptResolutionError = exports.resolvePrompt = exports.SchemaResolutionError = exports.ImplementationResolutionError = exports.resolveSchema = exports.resolveImplementation = exports.emitPluginWorkerLog = exports.safeWriteFileCache = exports.PluginCache = exports.createProjectRootMappingsFromProjectConfigurations = exports.signalToCode = void 0;
4
+ exports.dasherize = exports.classify = exports.capitalize = exports.camelize = void 0;
4
5
  var devkit_internals_1 = require("nx/src/devkit-internals");
5
6
  Object.defineProperty(exports, "signalToCode", { enumerable: true, get: function () { return devkit_internals_1.signalToCode; } });
6
7
  Object.defineProperty(exports, "createProjectRootMappingsFromProjectConfigurations", { enumerable: true, get: function () { return devkit_internals_1.createProjectRootMappingsFromProjectConfigurations; } });
@@ -40,6 +41,10 @@ Object.defineProperty(exports, "promptWhenInteractive", { enumerable: true, get:
40
41
  var target_defaults_utils_1 = require("./src/generators/target-defaults-utils");
41
42
  Object.defineProperty(exports, "addBuildTargetDefaults", { enumerable: true, get: function () { return target_defaults_utils_1.addBuildTargetDefaults; } });
42
43
  Object.defineProperty(exports, "addE2eCiTargetDefaults", { enumerable: true, get: function () { return target_defaults_utils_1.addE2eCiTargetDefaults; } });
44
+ Object.defineProperty(exports, "findTargetDefault", { enumerable: true, get: function () { return target_defaults_utils_1.findTargetDefault; } });
45
+ Object.defineProperty(exports, "readTargetDefaultsForTarget", { enumerable: true, get: function () { return target_defaults_utils_1.readTargetDefaultsForTarget; } });
46
+ Object.defineProperty(exports, "updateTargetDefault", { enumerable: true, get: function () { return target_defaults_utils_1.updateTargetDefault; } });
47
+ Object.defineProperty(exports, "upsertTargetDefault", { enumerable: true, get: function () { return target_defaults_utils_1.upsertTargetDefault; } });
43
48
  // Utils
44
49
  var add_plugin_1 = require("./src/utils/add-plugin");
45
50
  Object.defineProperty(exports, "addPlugin", { enumerable: true, get: function () { return add_plugin_1.addPlugin; } });
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getE2EWebServerInfo = getE2EWebServerInfo;
4
4
  const devkit_exports_1 = require("nx/src/devkit-exports");
5
+ const target_defaults_utils_1 = require("./target-defaults-utils");
5
6
  const find_plugin_for_config_file_1 = require("../utils/find-plugin-for-config-file");
6
7
  async function getE2EWebServerInfo(tree, projectName, pluginOptions, defaultValues, isPluginBeingAdded) {
7
8
  const pm = (0, devkit_exports_1.getPackageManagerCommand)((0, devkit_exports_1.detectPackageManager)(tree.root));
@@ -33,23 +34,18 @@ async function getE2EWebServerInfoForPlugin(tree, projectName, pluginOptions, de
33
34
  }
34
35
  const nxJson = (0, devkit_exports_1.readNxJson)(tree);
35
36
  let e2ePort = defaultValues.defaultE2EPort ?? 4200;
36
- if (nxJson.targetDefaults?.[foundPlugin.options[pluginOptions.serveTargetName] ??
37
- defaultValues.defaultServeTargetName] &&
38
- nxJson.targetDefaults?.[foundPlugin.options[pluginOptions.serveTargetName] ??
39
- defaultValues.defaultServeTargetName].options?.port) {
40
- e2ePort =
41
- nxJson.targetDefaults?.[foundPlugin.options[pluginOptions.serveTargetName] ??
42
- defaultValues.defaultServeTargetName].options?.port;
43
- }
37
+ const serveTargetName = foundPlugin.options[pluginOptions.serveTargetName] ??
38
+ defaultValues.defaultServeTargetName;
39
+ e2ePort =
40
+ (0, target_defaults_utils_1.readTargetDefaultsForTarget)(serveTargetName, nxJson.targetDefaults)?.options
41
+ ?.port ?? e2ePort;
44
42
  const e2eWebServerAddress = defaultValues.defaultE2EWebServerAddress.replace(/:\d+/, `:${e2ePort}`);
45
43
  return {
46
44
  e2eWebServerAddress,
47
- e2eWebServerCommand: `${pm.exec} nx run ${projectName}:${foundPlugin.options[pluginOptions.serveTargetName] ??
48
- defaultValues.defaultServeTargetName}`,
45
+ e2eWebServerCommand: `${pm.exec} nx run ${projectName}:${serveTargetName}`,
49
46
  e2eCiWebServerCommand: `${pm.exec} nx run ${projectName}:${foundPlugin.options[pluginOptions.serveStaticTargetName] ??
50
47
  defaultValues.defaultServeStaticTargetName}`,
51
48
  e2eCiBaseUrl: defaultValues.defaultE2ECiBaseUrl,
52
- e2eDevServerTarget: `${projectName}:${foundPlugin.options[pluginOptions.serveTargetName] ??
53
- defaultValues.defaultServeTargetName}`,
49
+ e2eDevServerTarget: `${projectName}:${serveTargetName}`,
54
50
  };
55
51
  }
@@ -1,5 +1,5 @@
1
1
  import type { ProjectConfiguration } from 'nx/src/config/workspace-json-project-json';
2
- import { type CreateNodes, type ProjectGraph, type TargetConfiguration, type Tree } from 'nx/src/devkit-exports';
2
+ import { type CreateNodes, type NxJsonConfiguration, type ProjectGraph, type TargetConfiguration, type Tree } from 'nx/src/devkit-exports';
3
3
  import { logger as devkitLogger } from 'nx/src/devkit-exports';
4
4
  export type InferredTargetConfiguration = TargetConfiguration & {
5
5
  name: string;
@@ -13,6 +13,7 @@ type SkipProjectFilter = (projectConfiguration: ProjectConfiguration) => false |
13
13
  export declare class NoTargetsToMigrateError extends Error {
14
14
  constructor();
15
15
  }
16
+ export declare function readTargetDefaultsForExecutor(executor: string, targetDefaults: NxJsonConfiguration['targetDefaults'] | undefined): Partial<TargetConfiguration> | undefined;
16
17
  export declare function migrateProjectExecutorsToPlugin<T>(tree: Tree, projectGraph: ProjectGraph, pluginPath: string, createNodesV2: CreateNodes<T>, defaultPluginOptions: T, migrations: Array<{
17
18
  executors: string[];
18
19
  targetPluginOptionMapper: (targetName: string) => Partial<T>;
@@ -2,6 +2,7 @@
2
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
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.NoTargetsToMigrateError = void 0;
5
+ exports.readTargetDefaultsForExecutor = readTargetDefaultsForExecutor;
5
6
  exports.migrateProjectExecutorsToPlugin = migrateProjectExecutorsToPlugin;
6
7
  exports.migrateProjectExecutorsToPluginV1 = migrateProjectExecutorsToPluginV1;
7
8
  const tslib_1 = require("tslib");
@@ -11,6 +12,7 @@ const posix_1 = require("node:path/posix");
11
12
  const devkit_exports_1 = require("nx/src/devkit-exports");
12
13
  const devkit_internals_1 = require("nx/src/devkit-internals");
13
14
  const executor_options_utils_1 = require("../executor-options-utils");
15
+ const target_defaults_utils_1 = require("../target-defaults-utils");
14
16
  const plugin_migration_utils_1 = require("./plugin-migration-utils");
15
17
  const devkit_exports_2 = require("nx/src/devkit-exports");
16
18
  class ExecutorToPluginMigrator {
@@ -169,7 +171,7 @@ _ExecutorToPluginMigrator_projectGraph = new WeakMap(), _ExecutorToPluginMigrato
169
171
  }
170
172
  });
171
173
  }, _ExecutorToPluginMigrator_getTargetDefaultsForExecutor = function _ExecutorToPluginMigrator_getTargetDefaultsForExecutor() {
172
- tslib_1.__classPrivateFieldSet(this, _ExecutorToPluginMigrator_targetDefaultsForExecutor, structuredClone(tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_nxJson, "f").targetDefaults?.[tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_executor, "f")]), "f");
174
+ tslib_1.__classPrivateFieldSet(this, _ExecutorToPluginMigrator_targetDefaultsForExecutor, structuredClone(readTargetDefaultsForExecutor(tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_executor, "f"), tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_nxJson, "f").targetDefaults) ?? {}), "f");
173
175
  }, _ExecutorToPluginMigrator_getCreatedTargetForProjectRoot = function _ExecutorToPluginMigrator_getCreatedTargetForProjectRoot(targetName, projectRoot) {
174
176
  const entry = Object.entries(tslib_1.__classPrivateFieldGet(this, _ExecutorToPluginMigrator_createNodesResultsForTargets, "f").get(targetName)?.projects ?? {}).find(([root]) => root === projectRoot);
175
177
  if (!entry) {
@@ -204,6 +206,22 @@ class NoTargetsToMigrateError extends Error {
204
206
  }
205
207
  }
206
208
  exports.NoTargetsToMigrateError = NoTargetsToMigrateError;
209
+ function readTargetDefaultsForExecutor(executor, targetDefaults) {
210
+ // Preserve the legacy record-shape semantics this migrator used before
211
+ // array support: only an unfiltered default keyed directly by executor
212
+ // applies here. Target-scoped or filtered array entries remain opt-in
213
+ // behaviors for callers that can evaluate them in project context.
214
+ const entry = (0, target_defaults_utils_1.findTargetDefault)(targetDefaults, { executor });
215
+ if (!entry) {
216
+ return undefined;
217
+ }
218
+ const config = { ...entry };
219
+ delete config.target;
220
+ delete config.executor;
221
+ delete config.projects;
222
+ delete config.plugin;
223
+ return config;
224
+ }
207
225
  async function migrateProjectExecutorsToPlugin(tree, projectGraph, pluginPath, createNodesV2, defaultPluginOptions, migrations, specificProjectToMigrate, logger) {
208
226
  const projects = await migrateProjects(tree, projectGraph, pluginPath, undefined, createNodesV2, defaultPluginOptions, migrations, specificProjectToMigrate, logger);
209
227
  return projects;
@@ -1,3 +1,120 @@
1
- import { type TargetConfiguration, type Tree } from 'nx/src/devkit-exports';
1
+ import { type NxJsonConfiguration, type TargetConfiguration, type TargetDefaultEntry, type TargetDefaultFilter, type TargetDefaults, type Tree } from 'nx/src/devkit-exports';
2
+ import { readTargetDefaultsForTarget as readTargetDefaultsForTargetFromNx } from 'nx/src/devkit-internals';
3
+ /**
4
+ * Upsert a `targetDefaults` entry on the provided `nxJson`. Mutates
5
+ * `nxJson` in place and returns it so the caller can chain or batch
6
+ * other edits before persisting via `updateNxJson` exactly once.
7
+ *
8
+ * The input is the logical {@link TargetDefaultEntry} shape
9
+ * (`{ target?, executor?, projects?, plugin?, ...config }`). The `projects` /
10
+ * `plugin` filter is *context* describing what the caller is configuring, used
11
+ * to pick which existing entry to merge into — it is never authored as a new
12
+ * filtered entry. For the key (`target` ?? `executor`):
13
+ *
14
+ * - No value yet → create the generic (plain object) value.
15
+ * - Only a generic value → merge into it.
16
+ * - A filtered entry matching the context filter exists → merge into that one
17
+ * (so a caller updating a project the user already scoped a default for edits
18
+ * that scoped entry rather than clobbering the workspace baseline).
19
+ * - Otherwise → merge into the generic, creating one if needed.
20
+ *
21
+ * A lone unfiltered entry is always stored as a plain object, never a
22
+ * single-element array. The entry must set at least one of `target` /
23
+ * `executor`.
24
+ */
25
+ export declare function upsertTargetDefault(tree: Tree, nxJson: NxJsonConfiguration, options: TargetDefaultEntry): NxJsonConfiguration;
26
+ /**
27
+ * Find a `targetDefaults` entry by its logical locator tuple
28
+ * `(target, executor, projects, plugin)`. Locator keys default to
29
+ * `undefined`, matching only entries that also leave them unset — same
30
+ * semantics as `upsertTargetDefault`.
31
+ *
32
+ * Throws when called with an empty locator (no `target`, `executor`,
33
+ * `projects`, or `plugin`). An empty locator is almost always a bug — the
34
+ * caller intended to find a specific entry but forgot to populate the lookup.
35
+ */
36
+ export declare function findTargetDefault(targetDefaults: TargetDefaults | undefined, locator: Pick<TargetDefaultEntry, 'target' | 'executor' | 'projects' | 'plugin'>): TargetDefaultEntry | undefined;
37
+ /**
38
+ * The subset of a project graph node that the `projects` narrowing reads —
39
+ * names come from the map keys, `root`/`tags` drive `findMatchingProjects`.
40
+ * Mirrors `MatcherProjectNode` without importing it, so the signature stays
41
+ * stable across the supported nx version range.
42
+ */
43
+ type ContextProjectNode = {
44
+ data: {
45
+ root: string;
46
+ tags?: string[];
47
+ };
48
+ };
49
+ /**
50
+ * What the caller is updating, used to pick which `targetDefaults` entries the
51
+ * callback runs against. At least one of `executor` / `projects` is required.
52
+ */
53
+ export interface UpdateTargetDefaultContext {
54
+ /**
55
+ * Visit only entries that resolve to this executor — matched against the map
56
+ * key (executor-keyed form), an entry's `executor` field, or its
57
+ * `filter.executor`. When omitted, entries are not filtered by executor.
58
+ */
59
+ executor?: string;
60
+ /**
61
+ * Project nodes the update is scoped to, keyed by project name. When set, a
62
+ * project-filtered entry is visited only if its `filter.projects` covers
63
+ * *every* one of these projects; unfiltered entries always match. When
64
+ * omitted, project filters are not a constraint and every (executor-matching)
65
+ * entry is visited — the workspace-wide case.
66
+ */
67
+ projects?: Record<string, ContextProjectNode>;
68
+ }
69
+ /**
70
+ * Describes the entry a callback invocation is operating on, so the callback
71
+ * can tell the executor-keyed form from the target-keyed form and read the
72
+ * entry's filter.
73
+ */
74
+ export interface UpdateTargetDefaultEntryInfo {
75
+ /** The `targetDefaults` map key the entry lives under. */
76
+ key: string;
77
+ /** Logical target name — `undefined` for executor-keyed entries. */
78
+ target?: string;
79
+ /** Executor the entry resolves to (from the key, `executor` field, or filter). */
80
+ executor?: string;
81
+ /** The entry's filter, present only for filtered array entries. */
82
+ filter?: TargetDefaultFilter;
83
+ }
84
+ /**
85
+ * Called once per matching entry. Receives the entry's flat config (everything
86
+ * but `filter`) and may mutate it in place. Return a new config to replace the
87
+ * entry's payload, return `null` to drop the entry, or return nothing to keep
88
+ * the (possibly mutated) config.
89
+ */
90
+ export type UpdateTargetDefaultCallback = (config: Partial<TargetConfiguration>, info: UpdateTargetDefaultEntryInfo) => Partial<TargetConfiguration> | null | void;
91
+ /**
92
+ * Walk the `targetDefaults` entries matching `context` and run `callback`
93
+ * against each, mutating `nxJson` in place and returning it. This is the
94
+ * read-modify-write counterpart to {@link upsertTargetDefault}, meant for
95
+ * generators and migrations that need to edit *existing* defaults across both
96
+ * value forms (plain object and filtered array) without re-implementing the
97
+ * normalize → edit → collapse dance by hand.
98
+ *
99
+ * Matching, per the `context`:
100
+ * - `executor` selects entries that reference it as the map key, an `executor`
101
+ * config field, or `filter.executor`.
102
+ * - `projects` narrows to entries whose `filter.projects` covers the scoped
103
+ * projects (unfiltered entries always match). With no `projects` context,
104
+ * project filters are ignored and every executor-matching entry is visited.
105
+ *
106
+ * Removal and collapsing follow the storage-form rules:
107
+ * - Array entry whose callback returns `null` → dropped from the array.
108
+ * - An array that collapses to a single unfiltered entry → stored as the plain
109
+ * object form; an emptied array → the whole key is deleted.
110
+ * - Object-form value whose callback returns `null` → the whole key is deleted.
111
+ * - An emptied `targetDefaults` map → removed from `nxJson` entirely.
112
+ *
113
+ * Throws when neither `executor` nor `projects` is provided — an empty context
114
+ * would match everything and is almost always a bug.
115
+ */
116
+ export declare function updateTargetDefault(nxJson: NxJsonConfiguration, context: UpdateTargetDefaultContext, callback: UpdateTargetDefaultCallback): NxJsonConfiguration;
117
+ export declare function readTargetDefaultsForTarget(targetName: string, targetDefaults: TargetDefaults | undefined, executor?: string, opts?: Parameters<typeof readTargetDefaultsForTargetFromNx>[3]): Partial<TargetConfiguration> | null;
2
118
  export declare function addBuildTargetDefaults(tree: Tree, executorName: string, buildTargetName?: string, extraInputs?: TargetConfiguration['inputs']): void;
3
119
  export declare function addE2eCiTargetDefaults(tree: Tree, e2ePlugin: string, buildTarget: string, pathToE2EConfigFile: string): Promise<void>;
120
+ export {};
@@ -1,14 +1,362 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.upsertTargetDefault = upsertTargetDefault;
4
+ exports.findTargetDefault = findTargetDefault;
5
+ exports.updateTargetDefault = updateTargetDefault;
6
+ exports.readTargetDefaultsForTarget = readTargetDefaultsForTarget;
3
7
  exports.addBuildTargetDefaults = addBuildTargetDefaults;
4
8
  exports.addE2eCiTargetDefaults = addE2eCiTargetDefaults;
5
9
  const devkit_exports_1 = require("nx/src/devkit-exports");
6
10
  const devkit_internals_1 = require("nx/src/devkit-internals");
7
11
  const minimatch_1 = require("minimatch");
12
+ /**
13
+ * Upsert a `targetDefaults` entry on the provided `nxJson`. Mutates
14
+ * `nxJson` in place and returns it so the caller can chain or batch
15
+ * other edits before persisting via `updateNxJson` exactly once.
16
+ *
17
+ * The input is the logical {@link TargetDefaultEntry} shape
18
+ * (`{ target?, executor?, projects?, plugin?, ...config }`). The `projects` /
19
+ * `plugin` filter is *context* describing what the caller is configuring, used
20
+ * to pick which existing entry to merge into — it is never authored as a new
21
+ * filtered entry. For the key (`target` ?? `executor`):
22
+ *
23
+ * - No value yet → create the generic (plain object) value.
24
+ * - Only a generic value → merge into it.
25
+ * - A filtered entry matching the context filter exists → merge into that one
26
+ * (so a caller updating a project the user already scoped a default for edits
27
+ * that scoped entry rather than clobbering the workspace baseline).
28
+ * - Otherwise → merge into the generic, creating one if needed.
29
+ *
30
+ * A lone unfiltered entry is always stored as a plain object, never a
31
+ * single-element array. The entry must set at least one of `target` /
32
+ * `executor`.
33
+ */
34
+ function upsertTargetDefault(tree, nxJson, options) {
35
+ const { target, executor, projects, plugin, ...config } = options;
36
+ const key = target ?? executor;
37
+ if (key === undefined) {
38
+ throw new Error('upsertTargetDefault requires at least one of `target` or `executor` to be set.');
39
+ }
40
+ // `executor` is the map key itself when no `target` is given; when a `target`
41
+ // is present it is a configuration field (a default executor) written onto
42
+ // the value.
43
+ const valueConfig = target !== undefined && executor !== undefined
44
+ ? { executor, ...config }
45
+ : config;
46
+ const targetDefaults = (nxJson.targetDefaults ??= {});
47
+ targetDefaults[key] = collapse(upsertValue(targetDefaults[key], valueConfig, {
48
+ tree,
49
+ projects,
50
+ plugin,
51
+ executor,
52
+ }));
53
+ return nxJson;
54
+ }
55
+ /**
56
+ * Merge `config` into the right entry of a key's value, per the algorithm
57
+ * documented on {@link upsertTargetDefault}. Returns the new value (always an
58
+ * array here; {@link collapse} downgrades a lone unfiltered one to an object).
59
+ */
60
+ function upsertValue(existing, config, context) {
61
+ // No value yet → create the generic.
62
+ if (existing === undefined) {
63
+ return { ...config };
64
+ }
65
+ const entries = Array.isArray(existing)
66
+ ? [...existing]
67
+ : [existing];
68
+ // A filtered entry that covers what's being configured → merge into it,
69
+ // preserving its filter.
70
+ const matchIdx = entries.findIndex((e) => e.filter !== undefined && filterCoversContext(e.filter, context));
71
+ if (matchIdx >= 0) {
72
+ entries[matchIdx] = { ...entries[matchIdx], ...config };
73
+ return entries;
74
+ }
75
+ // Otherwise merge into the generic (catch-all) entry, creating one if absent.
76
+ const genericIdx = entries.findIndex((e) => e.filter === undefined);
77
+ if (genericIdx >= 0) {
78
+ const { filter: _filter, ...rest } = entries[genericIdx];
79
+ entries[genericIdx] = { ...rest, ...config };
80
+ }
81
+ else {
82
+ entries.push({ ...config });
83
+ }
84
+ return entries;
85
+ }
86
+ /**
87
+ * Whether an entry's `filter` applies to what the caller is configuring:
88
+ * `plugin` / `executor` must match exactly, and `projects` is evaluated as
89
+ * *coverage* — every configured project must match the filter's patterns
90
+ * (names, globs, `tag:`, directories, negation) via `findMatchingProjects`,
91
+ * resolved against each project's tags/root in the tree. A `projects` filter
92
+ * can't be confirmed when no project context is supplied (or the project
93
+ * isn't in the tree), so it falls through to the generic.
94
+ */
95
+ function filterCoversContext(filter, context) {
96
+ if (filter.plugin !== undefined && filter.plugin !== context.plugin) {
97
+ return false;
98
+ }
99
+ if (filter.executor !== undefined && filter.executor !== context.executor) {
100
+ return false;
101
+ }
102
+ if (filter.projects !== undefined) {
103
+ const configured = context.projects ?? [];
104
+ const nodes = contextProjectNodes(context.tree, configured);
105
+ if (!projectsFilterCovers(filter.projects, nodes, configured)) {
106
+ return false;
107
+ }
108
+ }
109
+ return true;
110
+ }
111
+ /**
112
+ * Whether a `filter.projects` pattern set covers *every* required project name:
113
+ * each must be returned by `findMatchingProjects` against `nodes`. An empty
114
+ * `requiredNames` can never be "covered", so the entry falls through to the
115
+ * generic baseline. Shared by the upsert and update paths so the coverage
116
+ * semantics live in one place.
117
+ */
118
+ function projectsFilterCovers(patterns, nodes, requiredNames) {
119
+ if (requiredNames.length === 0) {
120
+ return false;
121
+ }
122
+ const matched = (0, devkit_internals_1.findMatchingProjects)([...patterns], nodes);
123
+ return requiredNames.every((name) => matched.includes(name));
124
+ }
125
+ function contextProjectNodes(tree, projectNames) {
126
+ const projects = (0, devkit_exports_1.getProjects)(tree);
127
+ const nodes = {};
128
+ for (const name of projectNames) {
129
+ const config = projects.get(name);
130
+ if (config) {
131
+ nodes[name] = { data: { root: config.root, tags: config.tags } };
132
+ }
133
+ }
134
+ return nodes;
135
+ }
136
+ /**
137
+ * Collapse a key's value back to the plain object form when it has degenerated
138
+ * to a single entry with no filter — at that point the array wrapper carries no
139
+ * information the object form doesn't, and the object form is the tidier,
140
+ * record-shape-compatible representation.
141
+ */
142
+ function collapse(value) {
143
+ if (Array.isArray(value) &&
144
+ value.length === 1 &&
145
+ value[0].filter === undefined) {
146
+ // Drop the (already-undefined) `filter` key so the result matches the
147
+ // object value form, which forbids `filter`.
148
+ const { filter: _filter, ...config } = value[0];
149
+ return config;
150
+ }
151
+ return value;
152
+ }
153
+ /**
154
+ * Find a `targetDefaults` entry by its logical locator tuple
155
+ * `(target, executor, projects, plugin)`. Locator keys default to
156
+ * `undefined`, matching only entries that also leave them unset — same
157
+ * semantics as `upsertTargetDefault`.
158
+ *
159
+ * Throws when called with an empty locator (no `target`, `executor`,
160
+ * `projects`, or `plugin`). An empty locator is almost always a bug — the
161
+ * caller intended to find a specific entry but forgot to populate the lookup.
162
+ */
163
+ function findTargetDefault(targetDefaults, locator) {
164
+ if (locator.target === undefined &&
165
+ locator.executor === undefined &&
166
+ locator.projects === undefined &&
167
+ locator.plugin === undefined) {
168
+ throw new Error('findTargetDefault requires at least one of `target`, `executor`, `projects`, or `plugin` on the locator.');
169
+ }
170
+ for (const entry of logicalTargetDefaultEntries(targetDefaults)) {
171
+ if (entry.target === locator.target &&
172
+ entry.executor === locator.executor &&
173
+ projectsEqual(entry.projects, locator.projects) &&
174
+ entry.plugin === locator.plugin) {
175
+ return entry;
176
+ }
177
+ }
178
+ return undefined;
179
+ }
180
+ /**
181
+ * Expand the nested `targetDefaults` map into its logical {@link
182
+ * TargetDefaultEntry} view, one entry at a time. The key becomes `target` (or
183
+ * `executor` for executor-shaped keys) and any `filter` is un-nested into the
184
+ * flat `projects`/`plugin`/`executor` siblings. Lazy so callers that find a
185
+ * match early stop iterating. Internal to this module — the flat shape is a
186
+ * lookup convenience, never a `targetDefaults` value form.
187
+ */
188
+ function* logicalTargetDefaultEntries(targetDefaults) {
189
+ for (const key of Object.keys(targetDefaults ?? {})) {
190
+ const locator = isExecutorLikeKey(key)
191
+ ? { executor: key }
192
+ : { target: key };
193
+ const value = targetDefaults[key];
194
+ const entries = Array.isArray(value)
195
+ ? value
196
+ : [value ?? {}];
197
+ for (const entry of entries) {
198
+ const { filter, ...config } = entry;
199
+ yield {
200
+ ...locator,
201
+ ...(filter?.projects !== undefined
202
+ ? { projects: filter.projects }
203
+ : {}),
204
+ ...(filter?.plugin !== undefined ? { plugin: filter.plugin } : {}),
205
+ ...(filter?.executor !== undefined
206
+ ? { executor: filter.executor }
207
+ : {}),
208
+ ...config,
209
+ };
210
+ }
211
+ }
212
+ }
213
+ // Mirrors `isGlobPattern` from `nx/src/utils/globs.ts`, which isn't on the
214
+ // devkit-exports surface guaranteed across the supported nx version range.
215
+ //
216
+ // Known limitation: a `:` is also legal in a target name, so a target literally
217
+ // named e.g. `e2e:ci` is classified as an executor key here. This matches how
218
+ // the rest of the target-defaults stack disambiguates keys (executor-shaped
219
+ // keys win), and such target names are vanishingly rare — accepted rather than
220
+ // worked around.
221
+ const GLOB_CHARACTERS = new Set(['*', '|', '{', '}', '(', ')', '[']);
222
+ function isExecutorLikeKey(key) {
223
+ if (!key.includes(':'))
224
+ return false;
225
+ for (const c of key)
226
+ if (GLOB_CHARACTERS.has(c))
227
+ return false;
228
+ return true;
229
+ }
230
+ /**
231
+ * Walk the `targetDefaults` entries matching `context` and run `callback`
232
+ * against each, mutating `nxJson` in place and returning it. This is the
233
+ * read-modify-write counterpart to {@link upsertTargetDefault}, meant for
234
+ * generators and migrations that need to edit *existing* defaults across both
235
+ * value forms (plain object and filtered array) without re-implementing the
236
+ * normalize → edit → collapse dance by hand.
237
+ *
238
+ * Matching, per the `context`:
239
+ * - `executor` selects entries that reference it as the map key, an `executor`
240
+ * config field, or `filter.executor`.
241
+ * - `projects` narrows to entries whose `filter.projects` covers the scoped
242
+ * projects (unfiltered entries always match). With no `projects` context,
243
+ * project filters are ignored and every executor-matching entry is visited.
244
+ *
245
+ * Removal and collapsing follow the storage-form rules:
246
+ * - Array entry whose callback returns `null` → dropped from the array.
247
+ * - An array that collapses to a single unfiltered entry → stored as the plain
248
+ * object form; an emptied array → the whole key is deleted.
249
+ * - Object-form value whose callback returns `null` → the whole key is deleted.
250
+ * - An emptied `targetDefaults` map → removed from `nxJson` entirely.
251
+ *
252
+ * Throws when neither `executor` nor `projects` is provided — an empty context
253
+ * would match everything and is almost always a bug.
254
+ */
255
+ function updateTargetDefault(nxJson, context, callback) {
256
+ const { executor, projects } = context;
257
+ if (executor === undefined && projects === undefined) {
258
+ throw new Error('updateTargetDefault requires at least one of `executor` or `projects` on the context to locate entries to update.');
259
+ }
260
+ const targetDefaults = nxJson.targetDefaults;
261
+ if (!targetDefaults) {
262
+ return nxJson;
263
+ }
264
+ const projectNames = projects ? Object.keys(projects) : [];
265
+ for (const key of Object.keys(targetDefaults)) {
266
+ const value = targetDefaults[key];
267
+ const entries = Array.isArray(value)
268
+ ? value
269
+ : [value];
270
+ const kept = [];
271
+ for (const entry of entries) {
272
+ if (!entryMatchesContext(key, entry, executor, projects, projectNames)) {
273
+ kept.push(entry);
274
+ continue;
275
+ }
276
+ const { filter, ...config } = entry;
277
+ const result = callback(config, {
278
+ key,
279
+ target: isExecutorLikeKey(key) ? undefined : key,
280
+ executor: isExecutorLikeKey(key)
281
+ ? key
282
+ : (config.executor ?? filter?.executor),
283
+ filter,
284
+ });
285
+ // `null` drops the entry; anything else keeps the (mutated or replaced)
286
+ // config, re-attaching the filter so a filtered entry stays filtered.
287
+ if (result === null) {
288
+ continue;
289
+ }
290
+ const nextConfig = result ?? config;
291
+ kept.push(filter !== undefined ? { filter, ...nextConfig } : nextConfig);
292
+ }
293
+ if (kept.length === 0) {
294
+ delete targetDefaults[key];
295
+ }
296
+ else {
297
+ targetDefaults[key] = collapse(kept);
298
+ }
299
+ }
300
+ if (Object.keys(targetDefaults).length === 0) {
301
+ delete nxJson.targetDefaults;
302
+ }
303
+ return nxJson;
304
+ }
305
+ /**
306
+ * Whether `callback` should run against this entry: it must reference the
307
+ * context's `executor` (when given), and — when the context is scoped to
308
+ * `projects` — any `filter.projects` it carries must cover every scoped
309
+ * project. Unfiltered entries always pass the project check.
310
+ */
311
+ function entryMatchesContext(key, entry, executor, projects, projectNames) {
312
+ if (executor !== undefined) {
313
+ const referencesExecutor = key === executor ||
314
+ entry.executor === executor ||
315
+ entry.filter?.executor === executor;
316
+ if (!referencesExecutor) {
317
+ return false;
318
+ }
319
+ }
320
+ if (projects !== undefined && entry.filter?.projects !== undefined) {
321
+ const patterns = Array.isArray(entry.filter.projects)
322
+ ? entry.filter.projects
323
+ : [entry.filter.projects];
324
+ if (!projectsFilterCovers(patterns, projects, projectNames)) {
325
+ return false;
326
+ }
327
+ }
328
+ return true;
329
+ }
330
+ function readTargetDefaultsForTarget(targetName, targetDefaults, executor, opts) {
331
+ // `targetDefaults` is the nested map; the nx reader resolves either the
332
+ // object or array value form for `targetName` natively.
333
+ return (0, devkit_internals_1.readTargetDefaultsForTarget)(targetName, targetDefaults, executor, opts);
334
+ }
335
+ // Order-insensitive equality so re-upserts with reordered patterns merge into
336
+ // the same entry rather than appending a duplicate. Comparing sorted copies
337
+ // keeps duplicates significant (`['a','a']` ≠ `['a']`) without special-casing.
338
+ function projectsEqual(a, b) {
339
+ if (a === b)
340
+ return true;
341
+ if (!a || !b)
342
+ return false;
343
+ if (a.length !== b.length)
344
+ return false;
345
+ const sortedA = [...a].sort();
346
+ const sortedB = [...b].sort();
347
+ return sortedA.every((value, i) => value === sortedB[i]);
348
+ }
8
349
  function addBuildTargetDefaults(tree, executorName, buildTargetName = 'build', extraInputs = []) {
9
- const nxJson = (0, devkit_exports_1.readNxJson)(tree);
10
- nxJson.targetDefaults ??= {};
11
- nxJson.targetDefaults[executorName] ??= {
350
+ const nxJson = (0, devkit_exports_1.readNxJson)(tree) ?? {};
351
+ // Only skip when an *unfiltered* entry already covers this executor.
352
+ // A filtered entry (`projects:` or `plugin:`) only applies to a subset
353
+ // of targets, so it doesn't satisfy the workspace-wide default this
354
+ // helper writes — we still need to add the unfiltered baseline.
355
+ if (findTargetDefault(nxJson.targetDefaults, { executor: executorName })) {
356
+ return;
357
+ }
358
+ upsertTargetDefault(tree, nxJson, {
359
+ executor: executorName,
12
360
  cache: true,
13
361
  dependsOn: [`^${buildTargetName}`],
14
362
  inputs: [
@@ -17,12 +365,12 @@ function addBuildTargetDefaults(tree, executorName, buildTargetName = 'build', e
17
365
  : ['default', '^default']),
18
366
  ...extraInputs,
19
367
  ],
20
- };
368
+ });
21
369
  (0, devkit_exports_1.updateNxJson)(tree, nxJson);
22
370
  }
23
371
  async function addE2eCiTargetDefaults(tree, e2ePlugin, buildTarget, pathToE2EConfigFile) {
24
372
  const nxJson = (0, devkit_exports_1.readNxJson)(tree);
25
- if (!nxJson.plugins) {
373
+ if (!nxJson?.plugins) {
26
374
  return;
27
375
  }
28
376
  const e2ePluginRegistrations = nxJson.plugins.filter((p) => typeof p === 'string' ? p === e2ePlugin : p.plugin === e2ePlugin);
@@ -58,18 +406,13 @@ async function addE2eCiTargetDefaults(tree, e2ePlugin, buildTarget, pathToE2ECon
58
406
  ? 'e2e-ci'
59
407
  : (foundPluginForApplication.options?.ciTargetName ?? 'e2e-ci');
60
408
  const ciTargetNameGlob = `${ciTargetName}--**/**`;
61
- nxJson.targetDefaults ??= {};
62
- const e2eCiTargetDefaults = nxJson.targetDefaults[ciTargetNameGlob];
63
- if (!e2eCiTargetDefaults) {
64
- nxJson.targetDefaults[ciTargetNameGlob] = {
65
- dependsOn: [buildTarget],
66
- };
67
- }
68
- else {
69
- e2eCiTargetDefaults.dependsOn ??= [];
70
- if (!e2eCiTargetDefaults.dependsOn.includes(buildTarget)) {
71
- e2eCiTargetDefaults.dependsOn.push(buildTarget);
72
- }
409
+ const existing = findTargetDefault(nxJson.targetDefaults, {
410
+ target: ciTargetNameGlob,
411
+ });
412
+ const dependsOn = [...(existing?.dependsOn ?? [])];
413
+ if (!dependsOn.includes(buildTarget)) {
414
+ dependsOn.push(buildTarget);
73
415
  }
416
+ upsertTargetDefault(tree, nxJson, { target: ciTargetNameGlob, dependsOn });
74
417
  (0, devkit_exports_1.updateNxJson)(tree, nxJson);
75
418
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nx/devkit",
3
- "version": "23.1.0-beta.4",
3
+ "version": "23.1.0-beta.5",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "files": [
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "jest": "30.3.0",
63
- "nx": "23.1.0-beta.4"
63
+ "nx": "23.1.0-beta.5"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "nx": ">= 22 <= 24 || ^23.0.0-0"