@nx/js 23.2.0-beta.1 → 23.2.0-beta.10

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.
Files changed (82) hide show
  1. package/dist/internal.d.ts +9 -3
  2. package/dist/internal.js +28 -17
  3. package/dist/src/executors/copy-workspace-modules/copy-workspace-modules.js +122 -21
  4. package/dist/src/executors/node/lib/output-file.js +4 -4
  5. package/dist/src/executors/node/node.impl.js +9 -10
  6. package/dist/src/executors/prune-lockfile/prune-lockfile.d.ts +1 -1
  7. package/dist/src/executors/prune-lockfile/prune-lockfile.js +40 -33
  8. package/dist/src/executors/tsc/lib/batch/watch.js +3 -3
  9. package/dist/src/executors/tsc/tsc.batch-impl.d.ts +1 -1
  10. package/dist/src/executors/tsc/tsc.batch-impl.js +2 -2
  11. package/dist/src/executors/verdaccio/verdaccio.impl.js +8 -1
  12. package/dist/src/generators/init/init.js +32 -15
  13. package/dist/src/generators/init/schema.d.ts +1 -1
  14. package/dist/src/generators/init/schema.json +1 -1
  15. package/dist/src/generators/library/library.d.ts +1 -1
  16. package/dist/src/generators/library/library.js +34 -20
  17. package/dist/src/generators/library/schema.d.ts +11 -3
  18. package/dist/src/generators/library/schema.json +15 -4
  19. package/dist/src/generators/library/utils/add-release-config.js +2 -2
  20. package/dist/src/generators/setup-build/generator.js +2 -7
  21. package/dist/src/generators/typescript-sync/typescript-sync.d.ts +1 -1
  22. package/dist/src/generators/typescript-sync/typescript-sync.js +2 -2
  23. package/dist/src/index.d.ts +3 -2
  24. package/dist/src/index.js +8 -8
  25. package/dist/src/migrations/update-23-1-0/add-ignore-deprecations-for-ts6.d.ts +3 -2
  26. package/dist/src/migrations/update-23-1-0/add-ignore-deprecations-for-ts6.js +17 -11
  27. package/dist/src/migrations/update-23-2-0/add-pnpm-deploy-output-cache-inputs.d.ts +14 -0
  28. package/dist/src/migrations/update-23-2-0/add-pnpm-deploy-output-cache-inputs.js +242 -0
  29. package/dist/src/migrations/update-23-2-0/add-pnpm-deploy-output-cache-inputs.md +64 -0
  30. package/dist/src/migrations/update-23-2-0/add-pnpm-prune-lockfile-cache-config.d.ts +2 -0
  31. package/dist/src/migrations/update-23-2-0/add-pnpm-prune-lockfile-cache-config.js +305 -0
  32. package/dist/src/migrations/update-23-2-0/add-pnpm-prune-lockfile-cache-config.md +59 -0
  33. package/dist/src/plugins/typescript/plugin.js +9 -14
  34. package/dist/src/plugins/typescript/util.d.ts +1 -1
  35. package/dist/src/release/utils/update-lock-file.js +5 -7
  36. package/dist/src/release/version-actions.d.ts +12 -3
  37. package/dist/src/release/version-actions.js +236 -76
  38. package/dist/src/utils/add-linting-to-project.d.ts +46 -0
  39. package/dist/src/utils/add-linting-to-project.js +112 -0
  40. package/dist/src/utils/assets/copy-assets-handler.d.ts +1 -1
  41. package/dist/src/utils/assets/copy-assets-handler.js +2 -2
  42. package/dist/src/utils/buildable-libs-utils.js +10 -13
  43. package/dist/src/utils/find-npm-dependencies.js +7 -10
  44. package/dist/src/utils/formatter-setup.d.ts +26 -0
  45. package/dist/src/utils/formatter-setup.js +43 -0
  46. package/dist/src/utils/generate-globs.js +11 -14
  47. package/dist/src/utils/generator-prompts.d.ts +2 -1
  48. package/dist/src/utils/generator-prompts.js +23 -22
  49. package/dist/src/utils/get-main-file-dir.js +2 -2
  50. package/dist/src/utils/linter.d.ts +30 -0
  51. package/dist/src/utils/linter.js +43 -0
  52. package/dist/src/utils/npm-config.d.ts +1 -1
  53. package/dist/src/utils/nx-formatter-internals.d.ts +8 -0
  54. package/dist/src/utils/nx-formatter-internals.js +18 -0
  55. package/dist/src/utils/oxfmt.d.ts +4 -0
  56. package/dist/src/utils/oxfmt.js +35 -0
  57. package/dist/src/utils/package-json/update-package-json.d.ts +1 -1
  58. package/dist/src/utils/package-json/update-package-json.js +11 -22
  59. package/dist/src/utils/package-manager-workspaces.js +2 -2
  60. package/dist/src/utils/pnpm-deploy-output-cache-inputs.d.ts +29 -0
  61. package/dist/src/utils/pnpm-deploy-output-cache-inputs.js +177 -0
  62. package/dist/src/utils/pnpm-install-settings-inputs.d.ts +45 -0
  63. package/dist/src/utils/pnpm-install-settings-inputs.js +77 -0
  64. package/dist/src/utils/prettier.js +14 -33
  65. package/dist/src/utils/target-defaults-matching.d.ts +57 -0
  66. package/dist/src/utils/target-defaults-matching.js +126 -0
  67. package/dist/src/utils/typescript/configuration.js +3 -5
  68. package/dist/src/utils/typescript/create-ts-config.d.ts +1 -1
  69. package/dist/src/utils/typescript/create-ts-config.js +5 -5
  70. package/dist/src/utils/typescript/plugin.js +2 -2
  71. package/dist/src/utils/typescript/run-type-check.js +2 -2
  72. package/dist/src/utils/typescript/ts-config.d.ts +51 -1
  73. package/dist/src/utils/typescript/ts-config.js +114 -0
  74. package/dist/src/utils/typescript/ts-solution-setup.d.ts +1 -1
  75. package/dist/src/utils/typescript/ts-solution-setup.js +3 -3
  76. package/dist/src/utils/versions.d.ts +1 -0
  77. package/dist/src/utils/versions.js +2 -1
  78. package/dist/src/utils/watch-for-single-file-changes.js +2 -2
  79. package/dist/src/utils/workspace-module-sections.d.ts +16 -0
  80. package/dist/src/utils/workspace-module-sections.js +24 -0
  81. package/migrations.json +13 -1
  82. package/package.json +12 -5
@@ -0,0 +1,59 @@
1
+ #### Update the cache configuration of `@nx/js:prune-lockfile` targets for the pnpm install artifacts
2
+
3
+ In a pnpm workspace, the `@nx/js:prune-lockfile` executor emits more than the pruned `package.json` and `pnpm-lock.yaml`: a settings-only `pnpm-workspace.yaml`, the `.patch` files of a `pnpm patch` workspace under `patches/`, and non-workspace local-path dependencies (`file:` tarballs and directories, `link:` targets) under `local_path_modules/`. On pnpm 11 and above that workspace file is where the build-script approvals and `supportedArchitectures` live; on pnpm 10 and below they go into the emitted `package.json` and the file ships carrying only an empty `packages` list.
4
+
5
+ Targets generated before this change declare only the manifest and the lockfile in `outputs`, so a cache replay in a clean checkout restores just those two files. The missing `patches/` and `local_path_modules/` break the deploy install loudly. On pnpm 11 and above a missing `pnpm-workspace.yaml` is worse than loud: it silently drops the build-script approvals, and the deployed app's native dependencies never run their build scripts.
6
+
7
+ Those artifacts are built from pnpm settings the workspace root declares. The build approvals and `supportedArchitectures` among them are recorded nowhere in the lockfile, so nothing else in the task hash moves when they change, and the same targets declare no `inputs` at all. Revoking a build approval would replay the previous run's artifact from cache and ship an output that still grants it.
8
+
9
+ The three artifact paths are appended to whichever `outputs` array declares the `pnpm-lock.yaml` entry, reusing that entry's own path prefix. That covers a target's own `outputs` and the `targetDefaults` entries applying to prune-lockfile targets: entries keyed by the executor, declaring or filtering on it, and entries under a target-name or glob key that resolves to one of them. Configurations without `outputs`, configurations whose lockfile entry is not pnpm's, and paths already present are left untouched.
10
+
11
+ The two root sources, plus a `runtime` input probing the pnpm major (which decides whether the settings land in the emitted `pnpm-workspace.yaml` or the emitted `package.json` when the root manifest has no `packageManager` field; it is added even when the field currently pins a pnpm version, because the migration runs once and the pin can be removed later), go somewhere else, because a target's own `inputs` array replaces the defaults' rather than merging with it. Each target is classified by its merged `outputs`, so inheriting or spreading the lockfile entry still counts, and the sources are added once: to the target's own `inputs` when it declares them, and otherwise to the `targetDefaults` entry supplying the array it inherits. A target inheriting nothing gets nx's own default spelled out first, so nothing that was hashed stops being hashed. A source the target already hashes is left alone. A workspace with no prune-lockfile target at all, configuring them purely through `targetDefaults`, has the sources added to every compatible entry under the key that declares its own `inputs` (each can be the array a future target inherits, and a sibling's array covers nothing); when no filter-less entry declares any, the spelled-out default is prepended as a new entry, under a name or glob key pinned to the executor so same-name targets of other executors do not inherit it. The fallback is only authored where it cannot change which key a target resolves: under the executor key when a compatible filter-less entry already exists, and under a name or glob key when a compatible filter-less entry already pins the executor. Any other key gets no fallback, whether its compatible entries are all filtered or its filter-less entries do not pin the executor, because a new entry would make that key win selection for targets that previously resolved their defaults elsewhere.
12
+
13
+ The migration only ever appends to an array that already exists or writes that spelled-out default; it never authors a `"..."` of its own. Whether a spread finds anything to expand against depends on the identity resets in nx's own document-order merge, and one that finds nothing would leave the target hashing only the two root files.
14
+
15
+ #### Sample code changes
16
+
17
+ ##### Before
18
+
19
+ ```json title="apps/app1/project.json"
20
+ {
21
+ "targets": {
22
+ "prune-lockfile": {
23
+ "executor": "@nx/js:prune-lockfile",
24
+ "outputs": [
25
+ "{workspaceRoot}/dist/apps/app1/package.json",
26
+ "{workspaceRoot}/dist/apps/app1/pnpm-lock.yaml"
27
+ ]
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ##### After
34
+
35
+ ```json title="apps/app1/project.json"
36
+ {
37
+ "targets": {
38
+ "prune-lockfile": {
39
+ "executor": "@nx/js:prune-lockfile",
40
+ "inputs": [
41
+ "default",
42
+ "^default",
43
+ "{workspaceRoot}/pnpm-workspace.yaml",
44
+ "{workspaceRoot}/package.json",
45
+ {
46
+ "runtime": "node -e \"try{console.log('pnpm major '+require('child_process').execSync('pnpm --version',{stdio:['ignore','pipe','ignore']}).toString().trim().split('.')[0])}catch{console.log('pnpm major unavailable')}\""
47
+ }
48
+ ],
49
+ "outputs": [
50
+ "{workspaceRoot}/dist/apps/app1/package.json",
51
+ "{workspaceRoot}/dist/apps/app1/pnpm-lock.yaml",
52
+ "{workspaceRoot}/dist/apps/app1/pnpm-workspace.yaml",
53
+ "{workspaceRoot}/dist/apps/app1/patches",
54
+ "{workspaceRoot}/dist/apps/app1/local_path_modules"
55
+ ]
56
+ }
57
+ }
58
+ }
59
+ ```
@@ -5,12 +5,7 @@ const internal_1 = require("@nx/devkit/internal");
5
5
  const devkit_1 = require("@nx/devkit");
6
6
  const node_fs_1 = require("node:fs");
7
7
  const node_path_1 = require("node:path");
8
- const file_hasher_1 = require("nx/src/hasher/file-hasher");
9
8
  const picomatch = require("picomatch");
10
- // eslint-disable-next-line @typescript-eslint/no-restricted-imports
11
- const lock_file_1 = require("nx/src/plugins/js/lock-file/lock-file");
12
- const cache_directory_1 = require("nx/src/utils/cache-directory");
13
- const installation_directory_1 = require("nx/src/utils/installation-directory");
14
9
  const util_1 = require("./util");
15
10
  let ts;
16
11
  const resolvedTypescriptPaths = {};
@@ -20,12 +15,12 @@ function resolveTypescriptPath(projectRoot, workspaceRoot) {
20
15
  // path is outside the workspace tree (e.g. pnpm's enableGlobalVirtualStore),
21
16
  // since typescript is not a declared dep.
22
17
  resolvedTypescriptPaths[projectRoot] ??= require.resolve('typescript', {
23
- paths: [projectRoot, ...(0, installation_directory_1.getNxRequirePaths)(workspaceRoot), __dirname],
18
+ paths: [projectRoot, ...(0, internal_1.getNxRequirePaths)(workspaceRoot), __dirname],
24
19
  });
25
20
  return resolvedTypescriptPaths[projectRoot];
26
21
  }
27
22
  const TSCONFIG_CACHE_VERSION = 2;
28
- const TS_CONFIG_CACHE_PATH = (0, node_path_1.join)(cache_directory_1.workspaceDataDirectory, 'tsconfig-files.hash');
23
+ const TS_CONFIG_CACHE_PATH = (0, node_path_1.join)(internal_1.workspaceDataDirectory, 'tsconfig-files.hash');
29
24
  // Module-level cache store — each invocation gets a unique Symbol key
30
25
  const cacheStore = new Map();
31
26
  // Shared tsconfig cache — initialized once per batch, persisted to disk
@@ -117,8 +112,8 @@ const tsConfigGlob = '**/tsconfig*.json';
117
112
  exports.createNodesV2 = [
118
113
  tsConfigGlob,
119
114
  async (configFilePaths, options, context) => {
120
- const optionsHash = (0, file_hasher_1.hashObject)(options);
121
- const targetsCachePath = (0, node_path_1.join)(cache_directory_1.workspaceDataDirectory, `tsc-${optionsHash}.hash`);
115
+ const optionsHash = (0, internal_1.hashObject)(options);
116
+ const targetsCachePath = (0, node_path_1.join)(internal_1.workspaceDataDirectory, `tsc-${optionsHash}.hash`);
122
117
  const targetsCache = readFromCache(targetsCachePath);
123
118
  // Each invocation gets a unique Symbol key — guaranteed no collisions
124
119
  const cacheKey = Symbol('tsc-invocation');
@@ -136,7 +131,7 @@ exports.createNodesV2 = [
136
131
  const normalizedOptions = normalizePluginOptions(options);
137
132
  const packageManager = (0, devkit_1.detectPackageManager)(context.workspaceRoot);
138
133
  const pmc = (0, devkit_1.getPackageManagerCommand)(packageManager);
139
- const lockFileName = (0, lock_file_1.getLockFileName)(packageManager);
134
+ const lockFileName = (0, internal_1.getLockFileName)(packageManager);
140
135
  const { configFilePaths: validConfigFilePaths, hashes, projectRoots, } = await resolveValidConfigFilesAndHashes(configFilePaths, normalizedOptions, optionsHash, context, cache, lockFileName);
141
136
  try {
142
137
  return await (0, devkit_1.createNodesFromFiles)((configFilePath, options, context, idx) => {
@@ -171,7 +166,7 @@ exports.createNodesV2 = [
171
166
  ];
172
167
  exports.createNodes = exports.createNodesV2;
173
168
  async function resolveValidConfigFilesAndHashes(configFilePaths, options, optionsHash, context, cache, lockFileName) {
174
- const lockFileHash = (0, file_hasher_1.hashFile)((0, node_path_1.join)(context.workspaceRoot, lockFileName)) ?? '';
169
+ const lockFileHash = (0, internal_1.hashFile)((0, node_path_1.join)(context.workspaceRoot, lockFileName)) ?? '';
175
170
  const validConfigFilePaths = [];
176
171
  const hashes = [];
177
172
  const projectRoots = [];
@@ -218,7 +213,7 @@ async function getConfigFileHash(configFilePath, workspaceRoot, project, options
218
213
  packageJson = (0, devkit_1.readJsonFile)((0, node_path_1.join)(workspaceRoot, project.root, 'package.json'));
219
214
  }
220
215
  catch { }
221
- return (0, file_hasher_1.hashArray)([
216
+ return (0, devkit_1.hashArray)([
222
217
  ...[
223
218
  fullConfigPath,
224
219
  ...extendedConfigFiles.files.sort(),
@@ -228,7 +223,7 @@ async function getConfigFileHash(configFilePath, workspaceRoot, project, options
228
223
  ...extendedConfigFiles.packages.sort(),
229
224
  lockFileHash,
230
225
  optionsHash,
231
- ...(packageJson ? [(0, file_hasher_1.hashObject)(packageJson)] : []),
226
+ ...(packageJson ? [(0, internal_1.hashObject)(packageJson)] : []),
232
227
  ]);
233
228
  }
234
229
  function checkIfConfigFileShouldBeProject(config, cache) {
@@ -956,7 +951,7 @@ function getFileHash(filePath, workspaceRoot, cache) {
956
951
  const relativePath = posixRelative(workspaceRoot, filePath);
957
952
  if (!cache.fileHashes[relativePath]) {
958
953
  const content = readFile(filePath, workspaceRoot, cache);
959
- cache.fileHashes[relativePath] = (0, file_hasher_1.hashArray)([content]);
954
+ cache.fileHashes[relativePath] = (0, devkit_1.hashArray)([content]);
960
955
  }
961
956
  return cache.fileHashes[relativePath];
962
957
  }
@@ -1,6 +1,6 @@
1
1
  import { type TargetConfiguration } from '@nx/devkit';
2
- import { type PackageManagerCommands } from 'nx/src/utils/package-manager';
3
2
  import { type ParsedCommandLine } from 'typescript';
3
+ import { type PackageManagerCommands } from '@nx/devkit/internal';
4
4
  export type ExtendedConfigFile = {
5
5
  filePath: string;
6
6
  externalPackage?: string;
@@ -3,9 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.updateLockFile = updateLockFile;
4
4
  const devkit_1 = require("@nx/devkit");
5
5
  const child_process_1 = require("child_process");
6
- const client_1 = require("nx/src/daemon/client/client");
7
- // eslint-disable-next-line @typescript-eslint/no-restricted-imports
8
- const lock_file_1 = require("nx/src/plugins/js/lock-file/lock-file");
6
+ const internal_1 = require("@nx/devkit/internal");
9
7
  const semver_1 = require("semver");
10
8
  async function updateLockFile(cwd, { dryRun, verbose, options, }) {
11
9
  if (options?.skipLockFileUpdate) {
@@ -30,10 +28,10 @@ async function updateLockFile(cwd, { dryRun, verbose, options, }) {
30
28
  }
31
29
  return [];
32
30
  }
33
- const isDaemonEnabled = client_1.daemonClient.enabled();
31
+ const isDaemonEnabled = internal_1.daemonClient.enabled();
34
32
  if (!dryRun && isDaemonEnabled) {
35
33
  // if not in dry-run temporarily stop the daemon, as it will error if the lock file is updated
36
- await client_1.daemonClient.stop();
34
+ await internal_1.daemonClient.stop();
37
35
  }
38
36
  const packageManagerCommands = (0, devkit_1.getPackageManagerCommand)(packageManager);
39
37
  let installArgs = options?.installArgs || '';
@@ -48,7 +46,7 @@ async function updateLockFile(cwd, { dryRun, verbose, options, }) {
48
46
  installArgs = `${installArgs} --ignore-scripts`.trim();
49
47
  }
50
48
  }
51
- const lockFile = (0, lock_file_1.getLockFileName)(packageManager);
49
+ const lockFile = (0, internal_1.getLockFileName)(packageManager);
52
50
  const command = `${packageManagerCommands.updateLockFile} ${installArgs}`.trim();
53
51
  if (verbose) {
54
52
  if (dryRun) {
@@ -65,7 +63,7 @@ async function updateLockFile(cwd, { dryRun, verbose, options, }) {
65
63
  execLockFileUpdate(command, cwd, env);
66
64
  if (isDaemonEnabled) {
67
65
  try {
68
- await client_1.daemonClient.startInBackground();
66
+ await internal_1.daemonClient.startInBackground();
69
67
  }
70
68
  catch (e) {
71
69
  // If the daemon fails to start, we don't want to prevent the user from continuing, so we just log the error and move on
@@ -1,9 +1,11 @@
1
1
  import { ProjectGraph, Tree } from '@nx/devkit';
2
- import { AfterAllProjectsVersioned, VersionActions } from 'nx/release';
3
- import type { NxReleaseVersionConfiguration } from 'nx/src/config/nx-json';
2
+ import { VersionActions } from 'nx/release';
3
+ import type { ResolveVersionForDependency } from 'nx/release';
4
+ import type { AfterAllProjectsVersioned, NxReleaseVersionConfiguration } from '@nx/devkit/internal';
4
5
  export declare const afterAllProjectsVersioned: AfterAllProjectsVersioned;
5
6
  export default class JsVersionActions extends VersionActions {
6
7
  validManifestFilenames: string[];
8
+ excludeManifestsFromFormatting: boolean;
7
9
  readCurrentVersionFromSourceManifest(tree: Tree): Promise<{
8
10
  currentVersion: string;
9
11
  manifestPath: string;
@@ -17,6 +19,13 @@ export default class JsVersionActions extends VersionActions {
17
19
  dependencyCollection: string | null;
18
20
  }>;
19
21
  updateProjectVersion(tree: Tree, newVersion: string): Promise<string[]>;
20
- updateProjectDependencies(tree: Tree, projectGraph: ProjectGraph, dependenciesToUpdate: Record<string, string>): Promise<string[]>;
22
+ updateProjectDependencies(tree: Tree, projectGraph: ProjectGraph, dependenciesToUpdate: Record<string, string>, resolveVersionForDependency?: ResolveVersionForDependency): Promise<string[]>;
23
+ private getLocalDependencyProjectLookup;
24
+ private resolveLocalDependencySpecifier;
25
+ private applyVersionPrefix;
26
+ private updateManifestValues;
27
+ private readAndValidateManifest;
28
+ private validateManifestUpdates;
29
+ private detectFormattingOptions;
21
30
  private isLocalDependencyProtocol;
22
31
  }
@@ -5,11 +5,13 @@ const internal_1 = require("@nx/devkit/internal");
5
5
  const devkit_1 = require("@nx/devkit");
6
6
  const node_child_process_1 = require("node:child_process");
7
7
  const node_path_1 = require("node:path");
8
+ const jsonc_parser_1 = require("jsonc-parser");
9
+ const semver_1 = require("semver");
8
10
  const release_1 = require("nx/release");
9
11
  const npm_config_1 = require("../utils/npm-config");
10
12
  const update_lock_file_1 = require("./utils/update-lock-file");
11
13
  const chalk = require("chalk");
12
- const semver_1 = require("./utils/semver");
14
+ const semver_2 = require("./utils/semver");
13
15
  const afterAllProjectsVersioned = async (cwd, { rootVersionActionsOptions, ...opts }) => {
14
16
  return {
15
17
  changedFiles: await (0, update_lock_file_1.updateLockFile)(cwd, {
@@ -22,10 +24,14 @@ const afterAllProjectsVersioned = async (cwd, { rootVersionActionsOptions, ...op
22
24
  exports.afterAllProjectsVersioned = afterAllProjectsVersioned;
23
25
  // Cache at the module level to avoid re-detecting the package manager for each instance
24
26
  let pm;
27
+ // Every project in a release receives the same ProjectGraph. Building these
28
+ // indexes once avoids turning dependency updates into quadratic graph scans.
29
+ const localDependencyProjectsByGraph = new WeakMap();
25
30
  class JsVersionActions extends release_1.VersionActions {
26
31
  constructor() {
27
32
  super(...arguments);
28
33
  this.validManifestFilenames = ['package.json'];
34
+ this.excludeManifestsFromFormatting = true;
29
35
  }
30
36
  async readCurrentVersionFromSourceManifest(tree) {
31
37
  const sourcePackageJsonPath = (0, node_path_1.join)(this.projectGraphNode.data.root, 'package.json');
@@ -90,25 +96,26 @@ class JsVersionActions extends release_1.VersionActions {
90
96
  async readCurrentVersionOfDependency(tree, projectGraph, dependencyProjectName) {
91
97
  const sourcePackageJsonPath = (0, node_path_1.join)(this.projectGraphNode.data.root, 'package.json');
92
98
  const json = (0, devkit_1.readJson)(tree, sourcePackageJsonPath);
93
- // Resolve the package name from the project graph metadata, as it may not match the project name
94
- const dependencyPackageName = projectGraph.nodes[dependencyProjectName].data.metadata?.js?.packageName;
95
99
  const dependencyTypes = [
96
100
  'dependencies',
97
101
  'devDependencies',
98
102
  'peerDependencies',
99
103
  'optionalDependencies',
100
104
  ];
105
+ const dependencyPackageName = projectGraph.nodes[dependencyProjectName]?.data.metadata?.js?.packageName;
101
106
  let currentVersion = null;
102
107
  let dependencyCollection = null;
103
- for (const depType of dependencyTypes) {
104
- if (json[depType] && json[depType][dependencyPackageName]) {
105
- currentVersion = json[depType][dependencyPackageName];
106
- dependencyCollection = depType;
107
- break;
108
+ if (dependencyPackageName) {
109
+ for (const depType of dependencyTypes) {
110
+ if (json[depType]?.[dependencyPackageName]) {
111
+ currentVersion = json[depType][dependencyPackageName];
112
+ dependencyCollection = depType;
113
+ break;
114
+ }
108
115
  }
109
116
  }
110
117
  // Resolve catalog references if needed
111
- if (currentVersion) {
118
+ if (currentVersion && dependencyPackageName) {
112
119
  const catalogManager = (0, internal_1.getCatalogManager)(tree.root);
113
120
  if (catalogManager?.isCatalogReference(currentVersion)) {
114
121
  currentVersion = catalogManager.resolveCatalogReference(tree, dependencyPackageName, currentVersion);
@@ -122,100 +129,253 @@ class JsVersionActions extends release_1.VersionActions {
122
129
  async updateProjectVersion(tree, newVersion) {
123
130
  const logMessages = [];
124
131
  for (const manifestToUpdate of this.manifestsToUpdate) {
125
- (0, devkit_1.updateJson)(tree, manifestToUpdate.manifestPath, (json) => {
126
- json.version = newVersion;
127
- return json;
128
- });
132
+ this.updateManifestValues(tree, manifestToUpdate.manifestPath, [
133
+ { path: ['version'], value: newVersion },
134
+ ]);
129
135
  logMessages.push(`✍️ New version ${newVersion} written to manifest: ${manifestToUpdate.manifestPath}`);
130
136
  }
131
137
  return logMessages;
132
138
  }
133
- async updateProjectDependencies(tree, projectGraph, dependenciesToUpdate) {
134
- let numDependenciesToUpdate = Object.keys(dependenciesToUpdate).length;
135
- if (numDependenciesToUpdate === 0) {
139
+ async updateProjectDependencies(tree, projectGraph, dependenciesToUpdate, resolveVersionForDependency) {
140
+ if (Object.keys(dependenciesToUpdate).length === 0 &&
141
+ !resolveVersionForDependency) {
136
142
  return [];
137
143
  }
138
- const logMessages = [];
144
+ const dependencyTypes = [
145
+ 'dependencies',
146
+ 'devDependencies',
147
+ 'peerDependencies',
148
+ 'optionalDependencies',
149
+ ];
150
+ const localDependencyProjects = this.getLocalDependencyProjectLookup(projectGraph);
151
+ for (const projectName of Object.keys(dependenciesToUpdate)) {
152
+ if (!projectGraph.nodes[projectName]?.data.metadata?.js?.packageName) {
153
+ throw new Error(`Unable to determine the package name for project "${projectName}" from the project graph metadata, please ensure that the "@nx/js" plugin is installed and the project graph has been built. If the issue persists, please report this issue on https://github.com/nrwl/nx/issues`);
154
+ }
155
+ }
156
+ const resolvedVersions = new Map();
157
+ const resolveVersion = (projectName) => {
158
+ let resolution = resolvedVersions.get(projectName);
159
+ if (!resolution) {
160
+ if (!resolveVersionForDependency) {
161
+ throw new Error(`No version resolver was provided for dependency project "${projectName}".`);
162
+ }
163
+ resolution = resolveVersionForDependency(projectName);
164
+ resolvedVersions.set(projectName, resolution);
165
+ }
166
+ return resolution;
167
+ };
168
+ const manifestUpdates = [];
139
169
  const catalogUpdates = [];
140
170
  const catalogManager = (0, internal_1.getCatalogManager)(tree.root);
141
171
  for (const manifestToUpdate of this.manifestsToUpdate) {
142
- (0, devkit_1.updateJson)(tree, manifestToUpdate.manifestPath, (json) => {
143
- const dependencyTypes = [
144
- 'dependencies',
145
- 'devDependencies',
146
- 'peerDependencies',
147
- 'optionalDependencies',
148
- ];
149
- const preserveMatchingDependencyRanges = this.finalConfigForProject.preserveMatchingDependencyRanges === true
150
- ? dependencyTypes
151
- : this.finalConfigForProject.preserveMatchingDependencyRanges ===
152
- false
153
- ? []
154
- : this.finalConfigForProject.preserveMatchingDependencyRanges ||
155
- dependencyTypes;
156
- for (const depType of dependencyTypes) {
157
- if (json[depType]) {
158
- for (const [dep, version] of Object.entries(dependenciesToUpdate)) {
159
- // Resolve the package name from the project graph metadata, as it may not match the project name
160
- const packageName = projectGraph.nodes[dep].data.metadata?.js?.packageName;
161
- if (!packageName) {
162
- throw new Error(`Unable to determine the package name for project "${dep}" from the project graph metadata, please ensure that the "@nx/js" plugin is installed and the project graph has been built. If the issue persists, please report this issue on https://github.com/nrwl/nx/issues`);
172
+ const json = (0, devkit_1.readJson)(tree, manifestToUpdate.manifestPath);
173
+ const updates = [];
174
+ const preserveMatchingDependencyRanges = this.finalConfigForProject.preserveMatchingDependencyRanges === true
175
+ ? dependencyTypes
176
+ : this.finalConfigForProject.preserveMatchingDependencyRanges ===
177
+ false
178
+ ? []
179
+ : this.finalConfigForProject.preserveMatchingDependencyRanges ||
180
+ dependencyTypes;
181
+ for (const depType of dependencyTypes) {
182
+ if (json[depType]) {
183
+ for (const [dependencyName, currentVersion] of Object.entries(json[depType])) {
184
+ const targetProject = localDependencyProjects.get(dependencyName);
185
+ if (!targetProject) {
186
+ continue;
187
+ }
188
+ let version = dependenciesToUpdate[targetProject.projectName];
189
+ if (version !== undefined) {
190
+ if (catalogManager?.isCatalogReference(currentVersion)) {
191
+ // collect the catalog updates so we can update the catalog definitions later
192
+ const catalogRef = catalogManager.parseCatalogReference(currentVersion);
193
+ catalogUpdates.push({
194
+ packageName: dependencyName,
195
+ version,
196
+ catalogName: catalogRef.catalogName,
197
+ });
198
+ continue;
163
199
  }
164
- const currentVersion = json[depType][packageName];
165
- if (currentVersion) {
166
- if (catalogManager?.isCatalogReference(currentVersion)) {
167
- // collect the catalog updates so we can update the catalog definitions later
168
- const catalogRef = catalogManager.parseCatalogReference(currentVersion);
169
- catalogUpdates.push({
170
- packageName,
171
- version,
172
- catalogName: catalogRef.catalogName,
173
- });
174
- numDependenciesToUpdate--;
175
- continue;
200
+ if (manifestToUpdate.preserveLocalDependencyProtocols &&
201
+ this.isLocalDependencyProtocol(currentVersion)) {
202
+ continue;
203
+ }
204
+ if (this.isLocalDependencyProtocol(currentVersion)) {
205
+ version = this.applyVersionPrefix(currentVersion, version);
206
+ }
207
+ if (preserveMatchingDependencyRanges.includes(depType) &&
208
+ !this.isLocalDependencyProtocol(currentVersion)) {
209
+ // If the dependency is specified using a range, do some additional processing to determine whether to update the version
210
+ if ((0, semver_2.isValidRange)(currentVersion) &&
211
+ !(0, semver_2.isMatchingDependencyRange)(version, currentVersion)) {
212
+ throw new Error(`"preserveMatchingDependencyRanges" is enabled for "${depType}" and the new version "${version}" is outside the current range for "${dependencyName}" in manifest "${manifestToUpdate.manifestPath}". Please update the range before releasing.`);
176
213
  }
177
- // Check if other local dependency protocols should be preserved
178
- else if (manifestToUpdate.preserveLocalDependencyProtocols &&
179
- this.isLocalDependencyProtocol(currentVersion)) {
180
- // Reduce the count appropriately to avoid confusing user-facing logs
181
- numDependenciesToUpdate--;
214
+ else if ((0, semver_2.isValidRange)(currentVersion)) {
182
215
  continue;
183
216
  }
184
- else if (preserveMatchingDependencyRanges.includes(depType) &&
185
- !this.isLocalDependencyProtocol(currentVersion)) {
186
- // If the dependency is specified using a range, do some additional processing to determine whether to update the version
187
- if ((0, semver_1.isValidRange)(currentVersion) &&
188
- !(0, semver_1.isMatchingDependencyRange)(version, currentVersion)) {
189
- throw new Error(`"preserveMatchingDependencyRanges" is enabled for "${depType}" and the new version "${version}" is outside the current range for "${packageName}" in manifest "${manifestToUpdate.manifestPath}". Please update the range before releasing.`);
190
- }
191
- else if ((0, semver_1.isValidRange)(currentVersion)) {
192
- // it is a range, but it is valid
193
- continue;
194
- }
195
- }
196
- json[depType][packageName] = version;
197
217
  }
198
218
  }
219
+ else if (resolveVersionForDependency &&
220
+ !manifestToUpdate.preserveLocalDependencyProtocols &&
221
+ this.isLocalDependencyProtocol(currentVersion)) {
222
+ try {
223
+ version = await this.resolveLocalDependencySpecifier(dependencyName, currentVersion, targetProject, resolveVersion);
224
+ }
225
+ catch (error) {
226
+ const message = error instanceof Error ? error.message : String(error);
227
+ throw new Error(`Unable to replace local dependency protocol "${currentVersion}" for "${dependencyName}" in manifest "${manifestToUpdate.manifestPath}". ${message}`);
228
+ }
229
+ }
230
+ if (version !== undefined) {
231
+ updates.push({
232
+ path: [depType, dependencyName],
233
+ value: version,
234
+ });
235
+ }
199
236
  }
200
237
  }
201
- return json;
238
+ }
239
+ manifestUpdates.push({
240
+ manifestPath: manifestToUpdate.manifestPath,
241
+ updates,
202
242
  });
203
- // If we ignored local dependecy protocols, then we could have dynamically ended up with zero here and we should not log anything related to dependencies
204
- if (numDependenciesToUpdate === 0) {
205
- return [];
243
+ }
244
+ // Resolve every requested version before writing any manifest so one
245
+ // failed resolver cannot leave a partially updated set of manifests.
246
+ const logMessages = [];
247
+ for (const manifestUpdate of manifestUpdates) {
248
+ this.updateManifestValues(tree, manifestUpdate.manifestPath, manifestUpdate.updates);
249
+ if (manifestUpdate.updates.length > 0) {
250
+ const depText = manifestUpdate.updates.length === 1 ? 'dependency' : 'dependencies';
251
+ logMessages.push(`✍️ Updated ${manifestUpdate.updates.length} ${depText} in manifest: ${manifestUpdate.manifestPath}`);
206
252
  }
207
- const depText = numDependenciesToUpdate === 1 ? 'dependency' : 'dependencies';
208
- logMessages.push(`✍️ Updated ${numDependenciesToUpdate} ${depText} in manifest: ${manifestToUpdate.manifestPath}`);
209
253
  }
210
- // Update catalog definitions in pnpm-workspace.yaml
254
+ // Update catalog definitions in the package manager's catalog file
211
255
  if (catalogUpdates.length > 0) {
212
256
  // catalogManager is guaranteed to be defined when there are catalog updates
213
257
  catalogManager.updateCatalogVersions(tree, catalogUpdates);
214
258
  const catalogText = catalogUpdates.length === 1 ? 'entry' : 'entries';
215
- logMessages.push(`✍️ Updated ${catalogUpdates.length} catalog ${catalogText} in pnpm-workspace.yaml`);
259
+ logMessages.push(`✍️ Updated ${catalogUpdates.length} catalog ${catalogText} in ${catalogManager
260
+ .getCatalogDefinitionFilePaths()
261
+ .join(', ')}`);
216
262
  }
217
263
  return logMessages;
218
264
  }
265
+ getLocalDependencyProjectLookup(projectGraph) {
266
+ let lookup = localDependencyProjectsByGraph.get(projectGraph);
267
+ if (lookup) {
268
+ return lookup;
269
+ }
270
+ // This lookup requires the dependency key to match the package name.
271
+ // Package aliases need relationship-specific manifest data that the
272
+ // project graph does not retain. Support is tracked in
273
+ // https://github.com/nrwl/nx/issues/36630.
274
+ lookup = new Map();
275
+ for (const [projectName, node] of Object.entries(projectGraph.nodes)) {
276
+ const packageName = node.data.metadata?.js?.packageName;
277
+ if (!packageName) {
278
+ continue;
279
+ }
280
+ lookup.set(packageName, { projectName });
281
+ }
282
+ localDependencyProjectsByGraph.set(projectGraph, lookup);
283
+ return lookup;
284
+ }
285
+ async resolveLocalDependencySpecifier(dependencyName, versionSpecifier, targetProject, resolveVersion) {
286
+ if (versionSpecifier.startsWith('file:')) {
287
+ const resolvedVersion = await resolveVersion(targetProject.projectName);
288
+ return this.applyVersionPrefix(versionSpecifier, resolvedVersion);
289
+ }
290
+ const range = versionSpecifier.slice('workspace:'.length);
291
+ if (range !== '' &&
292
+ range !== '*' &&
293
+ range !== '^' &&
294
+ range !== '~' &&
295
+ (0, semver_1.validRange)(range) !== null) {
296
+ return range;
297
+ }
298
+ const isRelativePath = range.startsWith('.');
299
+ if (!isRelativePath &&
300
+ range !== '' &&
301
+ range !== '*' &&
302
+ range !== '^' &&
303
+ range !== '~') {
304
+ throw new Error(`The workspace protocol used by "${dependencyName}" is not a supported range or relative workspace path.`);
305
+ }
306
+ const resolvedVersion = await resolveVersion(targetProject.projectName);
307
+ return this.applyVersionPrefix(isRelativePath ? 'workspace:*' : `workspace:${range}`, resolvedVersion);
308
+ }
309
+ applyVersionPrefix(versionSpecifier, resolvedVersion) {
310
+ const configuredPrefix = this.finalConfigForProject.versionPrefix;
311
+ let prefix = '';
312
+ if (configuredPrefix === '~' ||
313
+ configuredPrefix === '^' ||
314
+ configuredPrefix === '=') {
315
+ prefix = configuredPrefix;
316
+ }
317
+ else if (configuredPrefix === 'auto' &&
318
+ versionSpecifier.startsWith('workspace:')) {
319
+ const range = versionSpecifier.slice('workspace:'.length);
320
+ if (range.startsWith('^')) {
321
+ prefix = '^';
322
+ }
323
+ else if (range.startsWith('~')) {
324
+ prefix = '~';
325
+ }
326
+ }
327
+ return `${prefix}${resolvedVersion.replace(/^[~^=]/, '')}`;
328
+ }
329
+ updateManifestValues(tree, manifestPath, updates) {
330
+ if (updates.length === 0) {
331
+ return;
332
+ }
333
+ let content = this.readAndValidateManifest(tree, manifestPath);
334
+ const formattingOptions = this.detectFormattingOptions(content);
335
+ for (const update of updates) {
336
+ content = (0, jsonc_parser_1.applyEdits)(content, (0, jsonc_parser_1.modify)(content, update.path, update.value, { formattingOptions }));
337
+ }
338
+ this.validateManifestUpdates(content, manifestPath, updates);
339
+ tree.write(manifestPath, content);
340
+ }
341
+ readAndValidateManifest(tree, manifestPath) {
342
+ const content = tree.read(manifestPath, 'utf-8');
343
+ try {
344
+ // Match readJson's support for comments and trailing commas while
345
+ // retaining the original text for targeted edits.
346
+ (0, devkit_1.parseJson)(content);
347
+ }
348
+ catch (error) {
349
+ throw new Error(`Cannot parse ${manifestPath}: ${error.message}`);
350
+ }
351
+ return content;
352
+ }
353
+ validateManifestUpdates(content, manifestPath, updates) {
354
+ const manifest = (0, devkit_1.parseJson)(content);
355
+ for (const update of updates) {
356
+ let actualValue = manifest;
357
+ for (const pathSegment of update.path) {
358
+ if (actualValue === null ||
359
+ typeof actualValue !== 'object' ||
360
+ !(pathSegment in actualValue)) {
361
+ actualValue = undefined;
362
+ break;
363
+ }
364
+ actualValue = actualValue[pathSegment];
365
+ }
366
+ if (actualValue !== update.value) {
367
+ throw new Error(`Cannot update ${manifestPath}: "${update.path.join('.')}" resolves to ${JSON.stringify(actualValue)} instead of ${JSON.stringify(update.value)} after editing. The manifest may contain duplicate keys.`);
368
+ }
369
+ }
370
+ }
371
+ detectFormattingOptions(content) {
372
+ const indentation = content.match(/^[\t ]+(?=")/m)?.[0] ?? ' ';
373
+ const insertSpaces = !indentation.includes('\t');
374
+ return {
375
+ insertSpaces,
376
+ tabSize: insertSpaces ? indentation.length : 1,
377
+ };
378
+ }
219
379
  // NOTE: The TODOs were carried over from the original implementation, they are not yet implemented
220
380
  isLocalDependencyProtocol(versionSpecifier) {
221
381
  const localPackageProtocols = [