nx 23.0.0-beta.21 → 23.0.0-beta.22

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 (48) hide show
  1. package/dist/src/adapter/compat.d.ts +1 -1
  2. package/dist/src/adapter/compat.js +1 -0
  3. package/dist/src/command-line/examples.js +4 -4
  4. package/dist/src/command-line/migrate/agentic/prompts/generic-validation.d.ts +5 -0
  5. package/dist/src/command-line/migrate/agentic/prompts/generic-validation.js +1 -0
  6. package/dist/src/command-line/migrate/agentic/prompts/hybrid-prompt-migration.d.ts +5 -0
  7. package/dist/src/command-line/migrate/agentic/prompts/hybrid-prompt-migration.js +1 -0
  8. package/dist/src/command-line/migrate/agentic/prompts/prompt-migration.d.ts +5 -0
  9. package/dist/src/command-line/migrate/agentic/prompts/prompt-migration.js +1 -0
  10. package/dist/src/command-line/migrate/agentic/prompts/shared-rendering.d.ts +1 -0
  11. package/dist/src/command-line/migrate/agentic/prompts/shared-rendering.js +15 -0
  12. package/dist/src/command-line/migrate/agentic/run-step.d.ts +7 -0
  13. package/dist/src/command-line/migrate/agentic/run-step.js +3 -1
  14. package/dist/src/command-line/migrate/agentic/select.js +120 -32
  15. package/dist/src/command-line/migrate/command-object.d.ts +42 -0
  16. package/dist/src/command-line/migrate/command-object.js +37 -7
  17. package/dist/src/command-line/migrate/migrate-config.d.ts +27 -0
  18. package/dist/src/command-line/migrate/migrate-config.js +103 -0
  19. package/dist/src/command-line/migrate/migrate.d.ts +37 -2
  20. package/dist/src/command-line/migrate/migrate.js +97 -8
  21. package/dist/src/command-line/release/changelog/version-plan-filtering.d.ts +3 -1
  22. package/dist/src/command-line/release/changelog/version-plan-filtering.js +7 -3
  23. package/dist/src/command-line/release/changelog.d.ts +7 -0
  24. package/dist/src/command-line/release/changelog.js +22 -9
  25. package/dist/src/command-line/release/release.js +65 -55
  26. package/dist/src/command-line/release/utils/git.d.ts +6 -0
  27. package/dist/src/command-line/release/utils/git.js +33 -0
  28. package/dist/src/command-line/release/version/derive-specifier-from-conventional-commits.js +3 -2
  29. package/dist/src/command-line/release/version.d.ts +3 -0
  30. package/dist/src/command-line/release/version.js +13 -3
  31. package/dist/src/config/misc-interfaces.d.ts +8 -0
  32. package/dist/src/config/nx-json.d.ts +49 -0
  33. package/dist/src/core/graph/main.js +1 -1
  34. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  35. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  36. package/dist/src/plugins/js/lock-file/lock-file.d.ts +5 -0
  37. package/dist/src/plugins/js/lock-file/lock-file.js +34 -24
  38. package/dist/src/plugins/js/project-graph/affected/lock-file-changes.d.ts +2 -4
  39. package/dist/src/plugins/js/project-graph/affected/lock-file-changes.js +121 -43
  40. package/dist/src/project-graph/file-utils.d.ts +7 -0
  41. package/dist/src/project-graph/file-utils.js +78 -10
  42. package/dist/src/tasks-runner/init-tasks-runner.d.ts +2 -2
  43. package/dist/src/tasks-runner/init-tasks-runner.js +6 -6
  44. package/dist/src/tasks-runner/task-orchestrator.d.ts +2 -2
  45. package/dist/src/tasks-runner/task-orchestrator.js +6 -6
  46. package/migrations.json +18 -9
  47. package/package.json +11 -11
  48. package/schemas/nx-schema.json +41 -0
@@ -74,6 +74,7 @@ export declare class Migrator {
74
74
  factory?: string;
75
75
  prompt?: string;
76
76
  requires?: Record<string, string>;
77
+ documentation?: string;
77
78
  }[];
78
79
  }>;
79
80
  private createMigrateJson;
@@ -118,7 +119,7 @@ export declare function resolveCanonicalNxPackage(targetVersion: string): 'nx' |
118
119
  export declare function resolveMode(mode: MigrateMode | undefined, targetPackage: string, targetVersion: string, context?: {
119
120
  hasFrom: boolean;
120
121
  hasExcludeAppliedMigrations: boolean;
121
- }): Promise<MigrateMode>;
122
+ }, configuredMode?: MigrateMode): Promise<MigrateMode>;
122
123
  type GenerateMigrations = {
123
124
  type: 'generateMigrations';
124
125
  targetPackage: string;
@@ -172,6 +173,7 @@ type ExecutableMigration = {
172
173
  implementation?: string;
173
174
  factory?: string;
174
175
  prompt?: string;
176
+ documentation?: string;
175
177
  };
176
178
  export { isPromptOnlyMigration, isHybridMigration };
177
179
  export declare function resolveAgenticRunId(migrations: ExecutableMigration[]): string;
@@ -235,7 +237,10 @@ export declare function runNxOrAngularMigration(root: string, migration: {
235
237
  name: string;
236
238
  description?: string;
237
239
  version: string;
238
- }, isVerbose: boolean, captureGeneratorOutput?: boolean): Promise<{
240
+ }, isVerbose: boolean, captureGeneratorOutput?: boolean, resolvedCollection?: {
241
+ collection: MigrationsJson;
242
+ collectionPath: string;
243
+ }): Promise<{
239
244
  changes: FileChange[];
240
245
  nextSteps: string[];
241
246
  agentContext: string[];
@@ -258,4 +263,34 @@ export declare function getImplementationPath(collection: MigrationsJson, collec
258
263
  path: string;
259
264
  fnSymbol: string;
260
265
  };
266
+ /**
267
+ * Resolves a migration's collection once and derives everything the run loop
268
+ * needs from that single read: the implementation context (`collection` +
269
+ * `collectionPath`, handed to `runNxOrAngularMigration`) and, for agentic runs,
270
+ * the workspace-relative documentation path handed to the agent.
271
+ *
272
+ * Read fresh per migration (not cached across the loop) so a prior migration's
273
+ * reinstall is reflected, exactly as before. Error handling matches each field's
274
+ * role:
275
+ * - Migrations that run an implementation REQUIRE the collection; an unreadable
276
+ * collection throws and aborts that migration (caught by the run loop).
277
+ * - Prompt-only migrations don't run an implementation, so the collection is
278
+ * read only to resolve documentation - a failure there is non-fatal: the
279
+ * prompt still runs and the supplementary doc is skipped with a warning.
280
+ */
281
+ export declare function resolveMigrationForRun(root: string, migration: {
282
+ package: string;
283
+ name: string;
284
+ documentation?: string;
285
+ implementation?: string;
286
+ factory?: string;
287
+ prompt?: string;
288
+ }, resolveDocumentation: boolean): {
289
+ resolvedCollection?: {
290
+ collection: MigrationsJson;
291
+ collectionPath: string;
292
+ };
293
+ documentationPath?: string;
294
+ };
295
+ export declare function resolveDocumentationFileToWorkspacePath(root: string, migrationsDir: string, documentation: string): string | undefined;
261
296
  export declare function nxCliPath(nxWorkspaceRoot?: string): Promise<string>;
@@ -17,6 +17,8 @@ exports.migrate = migrate;
17
17
  exports.runMigration = runMigration;
18
18
  exports.readMigrationCollection = readMigrationCollection;
19
19
  exports.getImplementationPath = getImplementationPath;
20
+ exports.resolveMigrationForRun = resolveMigrationForRun;
21
+ exports.resolveDocumentationFileToWorkspacePath = resolveDocumentationFileToWorkspacePath;
20
22
  exports.nxCliPath = nxCliPath;
21
23
  const tslib_1 = require("tslib");
22
24
  const pc = tslib_1.__importStar(require("picocolors"));
@@ -56,6 +58,7 @@ const catalog_1 = require("../../utils/catalog");
56
58
  const multi_major_1 = require("./multi-major");
57
59
  const prompt_files_1 = require("./prompt-files");
58
60
  const command_object_1 = require("./command-object");
61
+ const migrate_config_1 = require("./migrate-config");
59
62
  const handoff_gitignore_1 = require("./agentic/handoff-gitignore");
60
63
  const migrate_commits_1 = require("./migrate-commits");
61
64
  const migrate_output_1 = require("./migrate-output");
@@ -529,13 +532,18 @@ function resolveCanonicalNxPackage(targetVersion) {
529
532
  async function resolveMode(mode, targetPackage, targetVersion, context = {
530
533
  hasFrom: false,
531
534
  hasExcludeAppliedMigrations: false,
532
- }) {
535
+ }, configuredMode) {
533
536
  if (mode) {
534
537
  return mode;
535
538
  }
536
539
  if (!(0, version_utils_1.isNxEquivalentTarget)(targetPackage, targetVersion)) {
537
540
  return 'all';
538
541
  }
542
+ // nx.json `migrate.mode` pre-selects the value the interactive prompt would
543
+ // ask for; it applies only to Nx targets (non-Nx returned 'all' above).
544
+ if (configuredMode) {
545
+ return configuredMode;
546
+ }
539
547
  if (!process.stdin.isTTY || (0, is_ci_1.isCI)()) {
540
548
  return 'all';
541
549
  }
@@ -656,6 +664,13 @@ async function parseMigrationsOptions(options) {
656
664
  });
657
665
  targetVersion = multiMajorResult.chosen;
658
666
  if (mode === 'third-party') {
667
+ // `mode` can resolve to third-party via nx.json, which bypasses the early
668
+ // CLI-only check above; re-assert against the resolved mode.
669
+ assertThirdPartyModeFlagCompatibility({
670
+ mode,
671
+ from: options.from,
672
+ excludeAppliedMigrations: options.excludeAppliedMigrations,
673
+ });
659
674
  assertThirdPartyTargetBounds({
660
675
  targetPackage,
661
676
  targetVersion,
@@ -706,7 +721,7 @@ async function resolveTargetAndMode(args) {
706
721
  const mode = await resolveMode(options.mode, targetPackage ?? 'nx', targetVersion ?? 'latest', {
707
722
  hasFrom: Object.keys(from).length > 0,
708
723
  hasExcludeAppliedMigrations: options.excludeAppliedMigrations === true,
709
- });
724
+ }, options.modeFromConfig);
710
725
  let installedNxVersion;
711
726
  // For third-party, anchor `targetPackage`/`targetVersion` to the installed
712
727
  // canonical when the positional was either omitted or a bare package name
@@ -1572,9 +1587,16 @@ function resolveCreateCommits(args) {
1572
1587
  }
1573
1588
  return { effective: true, agenticHasDiffContext: true };
1574
1589
  }
1590
+ // Commits aren't enabled here. A custom prefix only reaches this path via
1591
+ // nx.json (e.g. `migrate.commitPrefix` + `migrate.agentic` when the agentic
1592
+ // flow resolves to disabled); surface that it has no effect rather than
1593
+ // dropping it silently.
1575
1594
  return {
1576
1595
  effective: createCommits === true,
1577
1596
  agenticHasDiffContext: false,
1597
+ warning: commitPrefixIsCustom && createCommits !== true
1598
+ ? 'A custom migrate commit prefix is configured, but commits are not enabled for this run, so it has no effect. Set `migrate.createCommits` to `true` (or pass `--create-commits`) to create a commit per migration.'
1599
+ : undefined,
1578
1600
  };
1579
1601
  }
1580
1602
  /**
@@ -1716,6 +1738,11 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1716
1738
  // already-dirty shared file like `package.json`) doesn't collapse.
1717
1739
  const baselineWorkingTreeSnapshot = (0, git_utils_1.getUncommittedChangesSnapshot)(root);
1718
1740
  try {
1741
+ // Read this migration's collection once and derive everything from it:
1742
+ // the implementation context (passed to runNxOrAngularMigration) and the
1743
+ // documentation path (passed to the agent). Read fresh per iteration so a
1744
+ // prior migration's reinstall is reflected.
1745
+ const { resolvedCollection, documentationPath } = resolveMigrationForRun(root, m, !!agenticRun);
1719
1746
  let outcome;
1720
1747
  let commit = { kind: 'none' };
1721
1748
  if ((0, migration_shape_1.isPromptOnlyMigration)(m)) {
@@ -1726,6 +1753,7 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1726
1753
  agentic: agenticRun.agentic,
1727
1754
  runDir: agenticRun.runDir,
1728
1755
  installDepsIfChanged,
1756
+ documentationPath,
1729
1757
  });
1730
1758
  commit = await attemptMigrationCommit(m);
1731
1759
  (0, migrate_output_1.logAgenticSuccessOutcome)(stepResult.ambiguous ? 'Marked complete by user' : 'Applied', commit.kind === 'landed' ? commit.sha : null, stepResult.summary);
@@ -1740,7 +1768,7 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1740
1768
  }
1741
1769
  else if ((0, migration_shape_1.isHybridMigration)(m)) {
1742
1770
  const { changes, nextSteps, agentContext, logs, madeChanges } = await runNxOrAngularMigration(root, m, isVerbose,
1743
- /* captureGeneratorOutput: */ !!agenticRun);
1771
+ /* captureGeneratorOutput: */ !!agenticRun, resolvedCollection);
1744
1772
  migrationEmittedNextSteps.push(...nextSteps);
1745
1773
  if (agenticRun) {
1746
1774
  // Install any deps the deterministic phase added/bumped before the
@@ -1753,6 +1781,7 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1753
1781
  agentic: agenticRun.agentic,
1754
1782
  runDir: agenticRun.runDir,
1755
1783
  installDepsIfChanged,
1784
+ documentationPath,
1756
1785
  implContext: {
1757
1786
  logs,
1758
1787
  changes,
@@ -1801,7 +1830,7 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1801
1830
  // changes uncommitted in the working tree for the user to review.
1802
1831
  const validationRun = agenticRun && shouldRunValidation ? agenticRun : undefined;
1803
1832
  const { changes, nextSteps, agentContext, logs, madeChanges } = await runNxOrAngularMigration(root, m, isVerbose,
1804
- /* captureGeneratorOutput: */ !!validationRun);
1833
+ /* captureGeneratorOutput: */ !!validationRun, resolvedCollection);
1805
1834
  migrationEmittedNextSteps.push(...nextSteps);
1806
1835
  const canRunValidation = !!validationRun && changes.length > 0;
1807
1836
  if (canRunValidation) {
@@ -1814,6 +1843,7 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1814
1843
  agentic: validationRun.agentic,
1815
1844
  runDir: validationRun.runDir,
1816
1845
  installDepsIfChanged,
1846
+ documentationPath,
1817
1847
  implContext: {
1818
1848
  logs,
1819
1849
  changes,
@@ -1957,8 +1987,8 @@ function logSkippedPostMigrationInstall(root) {
1957
1987
  bodyLines: [`Run "${installCommand}" to install the updated dependencies.`],
1958
1988
  });
1959
1989
  }
1960
- async function runNxOrAngularMigration(root, migration, isVerbose, captureGeneratorOutput = false) {
1961
- const { collection, collectionPath } = readMigrationCollection(migration.package, root);
1990
+ async function runNxOrAngularMigration(root, migration, isVerbose, captureGeneratorOutput = false, resolvedCollection) {
1991
+ const { collection, collectionPath } = resolvedCollection ?? readMigrationCollection(migration.package, root);
1962
1992
  let changes = [];
1963
1993
  let nextSteps = [];
1964
1994
  let agentContext = [];
@@ -2219,13 +2249,15 @@ function filterStrings(value) {
2219
2249
  async function migrate(root, args, rawArgs) {
2220
2250
  await client_1.daemonClient.stop();
2221
2251
  return (0, handle_errors_1.handleErrors)(process.env.NX_VERBOSE_LOGGING === 'true', async () => {
2222
- const opts = await parseMigrationsOptions(args);
2252
+ const mergedArgs = (0, migrate_config_1.applyNxJsonMigrateDefaults)(args, (0, configuration_1.readNxJson)().migrate);
2253
+ (0, migrate_config_1.assertCommitPrefixHasCommits)(mergedArgs);
2254
+ const opts = await parseMigrationsOptions(mergedArgs);
2223
2255
  if (opts.type === 'generateMigrations') {
2224
2256
  await generateMigrationsJsonAndUpdatePackageJson(root, opts);
2225
2257
  }
2226
2258
  else {
2227
2259
  try {
2228
- return await runMigrations(root, opts, rawArgs, args['verbose'], args['createCommits'], args['commitPrefix'], args['skipInstall']);
2260
+ return await runMigrations(root, opts, rawArgs, mergedArgs['verbose'], mergedArgs['createCommits'], mergedArgs['commitPrefix'] ?? command_object_1.DEFAULT_MIGRATION_COMMIT_PREFIX, mergedArgs['skipInstall']);
2229
2261
  }
2230
2262
  catch (e) {
2231
2263
  // The remediation guidance is already logged by `runInstall`; swallow
@@ -2297,6 +2329,63 @@ function getImplementationPath(collection, collectionPath, name, migrationVersio
2297
2329
  }
2298
2330
  return { path: implPath, fnSymbol };
2299
2331
  }
2332
+ /**
2333
+ * Resolves a migration's collection once and derives everything the run loop
2334
+ * needs from that single read: the implementation context (`collection` +
2335
+ * `collectionPath`, handed to `runNxOrAngularMigration`) and, for agentic runs,
2336
+ * the workspace-relative documentation path handed to the agent.
2337
+ *
2338
+ * Read fresh per migration (not cached across the loop) so a prior migration's
2339
+ * reinstall is reflected, exactly as before. Error handling matches each field's
2340
+ * role:
2341
+ * - Migrations that run an implementation REQUIRE the collection; an unreadable
2342
+ * collection throws and aborts that migration (caught by the run loop).
2343
+ * - Prompt-only migrations don't run an implementation, so the collection is
2344
+ * read only to resolve documentation - a failure there is non-fatal: the
2345
+ * prompt still runs and the supplementary doc is skipped with a warning.
2346
+ */
2347
+ function resolveMigrationForRun(root, migration, resolveDocumentation) {
2348
+ let resolvedCollection;
2349
+ if (!(0, migration_shape_1.isPromptOnlyMigration)(migration)) {
2350
+ resolvedCollection = readMigrationCollection(migration.package, root);
2351
+ }
2352
+ else if (resolveDocumentation && migration.documentation) {
2353
+ try {
2354
+ resolvedCollection = readMigrationCollection(migration.package, root);
2355
+ }
2356
+ catch {
2357
+ // Non-fatal: documentation is supplementary; the warning below fires.
2358
+ }
2359
+ }
2360
+ let documentationPath;
2361
+ if (resolveDocumentation && migration.documentation) {
2362
+ documentationPath = resolvedCollection
2363
+ ? resolveDocumentationFileToWorkspacePath(root, (0, path_1.dirname)(resolvedCollection.collectionPath), migration.documentation)
2364
+ : undefined;
2365
+ if (!documentationPath) {
2366
+ logger_1.logger.warn(`Could not resolve the "documentation" file "${migration.documentation}" declared for migration "${migration.package}: ${migration.name}". It will be skipped as additional context for the AI agent.`);
2367
+ }
2368
+ }
2369
+ return { resolvedCollection, documentationPath };
2370
+ }
2371
+ // Resolves a `documentation` path (relative to the package's migrations dir) to
2372
+ // a workspace-relative path - or the absolute path when it resolves outside the
2373
+ // workspace (unusual hoisted/symlinked layouts). The agent runs with cwd =
2374
+ // workspace root, so the workspace-relative form is preferred. Returns
2375
+ // undefined when the file can't be resolved.
2376
+ function resolveDocumentationFileToWorkspacePath(root, migrationsDir, documentation) {
2377
+ let documentationFile;
2378
+ try {
2379
+ documentationFile = require.resolve(documentation, {
2380
+ paths: [migrationsDir],
2381
+ });
2382
+ }
2383
+ catch {
2384
+ return undefined;
2385
+ }
2386
+ const relativePath = (0, path_1.relative)(root, documentationFile);
2387
+ return relativePath.startsWith('..') ? documentationFile : relativePath;
2388
+ }
2300
2389
  class MigrationImplementationMissingError extends Error {
2301
2390
  constructor(baseMessage, collectionPath, migrationVersion) {
2302
2391
  super(buildMigrationMissingMessage(baseMessage, collectionPath, migrationVersion));
@@ -16,7 +16,7 @@ export declare function filterVersionPlansByCommitRange(versionPlans: RawVersion
16
16
  * Resolves the "from SHA" for changelog purposes.
17
17
  * This determines the starting point for changelog generation and optional version plan filtering.
18
18
  */
19
- export declare function resolveChangelogFromSHA({ fromRef, tagPattern, tagPatternValues, checkAllBranchesWhen, preid, requireSemver, strictPreid, useAutomaticFromRef, resolveRepositoryTags, }: {
19
+ export declare function resolveChangelogFromSHA({ fromRef, tagPattern, tagPatternValues, checkAllBranchesWhen, preid, requireSemver, strictPreid, useAutomaticFromRef, resolveRepositoryTags, projectRoot, }: {
20
20
  fromRef?: string;
21
21
  tagPattern: string;
22
22
  tagPatternValues: Record<string, string>;
@@ -26,6 +26,8 @@ export declare function resolveChangelogFromSHA({ fromRef, tagPattern, tagPatter
26
26
  strictPreid: boolean;
27
27
  useAutomaticFromRef: boolean;
28
28
  resolveRepositoryTags: RepoGitTags['resolveTags'];
29
+ /** When provided, scopes the fallback to the project's first commit instead of the repo's first commit */
30
+ projectRoot?: string;
29
31
  }): Promise<string | null>;
30
32
  /**
31
33
  * Helper function for workspace-level "from SHA" resolution.
@@ -89,7 +89,7 @@ async function getFilesAddedInCommitRange(fromSHA, toSHA, isVerbose) {
89
89
  * Resolves the "from SHA" for changelog purposes.
90
90
  * This determines the starting point for changelog generation and optional version plan filtering.
91
91
  */
92
- async function resolveChangelogFromSHA({ fromRef, tagPattern, tagPatternValues, checkAllBranchesWhen, preid, requireSemver, strictPreid, useAutomaticFromRef, resolveRepositoryTags, }) {
92
+ async function resolveChangelogFromSHA({ fromRef, tagPattern, tagPatternValues, checkAllBranchesWhen, preid, requireSemver, strictPreid, useAutomaticFromRef, resolveRepositoryTags, projectRoot, }) {
93
93
  // If user provided a from ref, resolve it to a SHA
94
94
  if (fromRef) {
95
95
  return await (0, git_1.getCommitHash)(fromRef);
@@ -104,9 +104,13 @@ async function resolveChangelogFromSHA({ fromRef, tagPattern, tagPatternValues,
104
104
  if (latestTag?.tag) {
105
105
  return await (0, git_1.getCommitHash)(latestTag.tag);
106
106
  }
107
- // Finally, if automatic from ref is enabled, use the first commit as a fallback
107
+ // Finally, if automatic from ref is enabled, use the first commit as a fallback.
108
+ // When a projectRoot is provided, scope the fallback to the project's first commit
109
+ // to avoid scanning the entire repo history for projects added after the repo was created.
108
110
  if (useAutomaticFromRef) {
109
- return await (0, git_1.getFirstGitCommit)();
111
+ return projectRoot
112
+ ? await (0, git_1.getFirstProjectCommit)(projectRoot)
113
+ : await (0, git_1.getFirstGitCommit)();
110
114
  }
111
115
  return null;
112
116
  }
@@ -1,5 +1,6 @@
1
1
  import { NxReleaseConfiguration } from '../../config/nx-json';
2
2
  import { ChangelogOptions } from './command-object';
3
+ import { NxReleaseConfig } from './config/config';
3
4
  import { ReleaseVersion } from './utils/shared';
4
5
  export interface NxReleaseChangelogResult {
5
6
  workspaceChangelog?: {
@@ -17,5 +18,11 @@ export interface NxReleaseChangelogResult {
17
18
  }
18
19
  export type { ChangelogChange } from './changelog/version-plan-utils';
19
20
  export type PostGitTask = (latestCommit: string) => Promise<void>;
21
+ /**
22
+ * Determines whether a changelog configuration will actually produce any output.
23
+ * A changelog config is effectively enabled when it would produce a changelog file
24
+ * or create a remote release.
25
+ */
26
+ export declare function isChangelogEffectivelyEnabled(config?: NxReleaseConfig['changelog']['workspaceChangelog'] | NxReleaseConfig['groups'][string]['changelog']): boolean | undefined;
20
27
  export declare const releaseChangelogCLIHandler: (args: ChangelogOptions) => Promise<number>;
21
28
  export declare function createAPI(overrideReleaseConfig: NxReleaseConfiguration, ignoreNxJsonConfig: boolean): (args: ChangelogOptions) => Promise<NxReleaseChangelogResult>;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.releaseChangelogCLIHandler = void 0;
4
+ exports.isChangelogEffectivelyEnabled = isChangelogEffectivelyEnabled;
4
5
  exports.createAPI = createAPI;
5
6
  const tslib_1 = require("tslib");
6
7
  const pc = tslib_1.__importStar(require("picocolors"));
@@ -35,6 +36,17 @@ const resolve_changelog_renderer_1 = require("./utils/resolve-changelog-renderer
35
36
  const resolve_nx_json_error_message_1 = require("./utils/resolve-nx-json-error-message");
36
37
  const shared_1 = require("./utils/shared");
37
38
  const version_plan_utils_2 = require("./utils/version-plan-utils");
39
+ /**
40
+ * Determines whether a changelog configuration will actually produce any output.
41
+ * A changelog config is effectively enabled when it would produce a changelog file
42
+ * or create a remote release.
43
+ */
44
+ function isChangelogEffectivelyEnabled(config) {
45
+ if (!config) {
46
+ return false;
47
+ }
48
+ return config.file !== false || config.createRelease !== false;
49
+ }
38
50
  const releaseChangelogCLIHandler = (args) => (0, handle_errors_1.handleErrors)(args.verbose, () => createAPI({}, false)(args));
39
51
  exports.releaseChangelogCLIHandler = releaseChangelogCLIHandler;
40
52
  function createAPI(overrideReleaseConfig, ignoreNxJsonConfig) {
@@ -100,13 +112,8 @@ function createAPI(overrideReleaseConfig, ignoreNxJsonConfig) {
100
112
  process.env.NX_RELEASE_INTERNAL_SUPPRESS_FILTER_LOG !== 'true') {
101
113
  output_1.output.note(releaseGraph.filterLog);
102
114
  }
103
- let rawVersionPlans = await (0, version_plans_1.readRawVersionPlans)();
104
- if (args.deleteVersionPlans === undefined) {
105
- // default to deleting version plans in this command instead of after versioning
106
- args.deleteVersionPlans = true;
107
- }
108
- const changelogGenerationEnabled = !!nxReleaseConfig.changelog.workspaceChangelog ||
109
- Object.values(nxReleaseConfig.groups).some((g) => g.changelog);
115
+ const changelogGenerationEnabled = isChangelogEffectivelyEnabled(nxReleaseConfig.changelog.workspaceChangelog) ||
116
+ Object.values(nxReleaseConfig.groups).some((g) => isChangelogEffectivelyEnabled(g.changelog));
110
117
  if (!changelogGenerationEnabled) {
111
118
  output_1.output.warn({
112
119
  title: `Changelogs are disabled. No changelog entries will be generated`,
@@ -116,6 +123,11 @@ function createAPI(overrideReleaseConfig, ignoreNxJsonConfig) {
116
123
  });
117
124
  return {};
118
125
  }
126
+ let rawVersionPlans = await (0, version_plans_1.readRawVersionPlans)();
127
+ if (args.deleteVersionPlans === undefined) {
128
+ // default to deleting version plans in this command instead of after versioning
129
+ args.deleteVersionPlans = true;
130
+ }
119
131
  const useAutomaticFromRef = nxReleaseConfig.changelog?.automaticFromRef || args.firstRelease;
120
132
  /**
121
133
  * For determining the versions to use within changelog files, there are a few different possibilities:
@@ -214,7 +226,7 @@ function createAPI(overrideReleaseConfig, ignoreNxJsonConfig) {
214
226
  fromSHACache.set('workspace', fromSHA);
215
227
  }
216
228
  // Helper function to get cached from SHA or resolve and cache it
217
- const getCachedFromSHA = async (cacheKey, pattern, templateValues, preid, checkAllBranchesWhen, requireSemver, strictPreid) => {
229
+ const getCachedFromSHA = async (cacheKey, pattern, templateValues, preid, checkAllBranchesWhen, requireSemver, strictPreid, projectRoot) => {
218
230
  if (fromSHACache.has(cacheKey)) {
219
231
  return fromSHACache.get(cacheKey);
220
232
  }
@@ -228,6 +240,7 @@ function createAPI(overrideReleaseConfig, ignoreNxJsonConfig) {
228
240
  requireSemver,
229
241
  strictPreid,
230
242
  useAutomaticFromRef,
243
+ projectRoot,
231
244
  });
232
245
  fromSHACache.set(cacheKey, sha);
233
246
  return sha;
@@ -296,7 +309,7 @@ function createAPI(overrideReleaseConfig, ignoreNxJsonConfig) {
296
309
  const fromSHA = await getCachedFromSHA(projectCacheKey, releaseGroup.releaseTag.pattern, {
297
310
  projectName: project.name,
298
311
  releaseGroupName: releaseGroup.name,
299
- }, projectsPreid[project.name], releaseGroup.releaseTag.checkAllBranchesWhen, releaseGroup.releaseTag.requireSemver, releaseGroup.releaseTag.strictPreid);
312
+ }, projectsPreid[project.name], releaseGroup.releaseTag.checkAllBranchesWhen, releaseGroup.releaseTag.requireSemver, releaseGroup.releaseTag.strictPreid, project.data.root);
300
313
  let commits;
301
314
  let fromRef = fromSHA;
302
315
  if (!fromRef && useAutomaticFromRef) {
@@ -84,19 +84,6 @@ function createAPI(overrideReleaseConfig, ignoreNxJsonConfig) {
84
84
  });
85
85
  // Suppress the filter log for the changelog command as it would have already been printed by the version command
86
86
  process.env.NX_RELEASE_INTERNAL_SUPPRESS_FILTER_LOG = 'true';
87
- const changelogResult = await releaseChangelog({
88
- ...args,
89
- // Re-use existing release graph
90
- releaseGraph,
91
- versionData: projectsVersionData,
92
- version: workspaceVersion,
93
- stageChanges: shouldStage,
94
- gitCommit: false,
95
- gitTag: false,
96
- gitPush: false,
97
- createRelease: false,
98
- deleteVersionPlans: false,
99
- });
100
87
  await (0, version_plans_1.setResolvedVersionPlansOnGroups)(rawVersionPlans, releaseGraph.releaseGroups, Object.keys(projectGraph.nodes), args.verbose);
101
88
  // Validate version plans against the filter after resolution
102
89
  const versionPlanValidationError = (0, version_plan_utils_1.validateResolvedVersionPlansAgainstFilter)(releaseGraph.releaseGroups, releaseGraph.releaseGroupToFilteredProjects);
@@ -150,6 +137,27 @@ function createAPI(overrideReleaseConfig, ignoreNxJsonConfig) {
150
137
  verbose: args.verbose,
151
138
  });
152
139
  }
140
+ // Check if any changelog generation is actually enabled before calling releaseChangelog,
141
+ // to avoid expensive operations (project graph recreation, git log, etc.) when changelogs are disabled
142
+ const changelogGenerationEnabled = (0, changelog_1.isChangelogEffectivelyEnabled)(nxReleaseConfig.changelog.workspaceChangelog) ||
143
+ releaseGraph.releaseGroups.some((g) => (0, changelog_1.isChangelogEffectivelyEnabled)(g.changelog));
144
+ // Run changelog generation before git commit/tag so that changelog files are
145
+ // included in the same commit as the version bump
146
+ const changelogResult = changelogGenerationEnabled
147
+ ? await releaseChangelog({
148
+ ...args,
149
+ // Re-use existing release graph
150
+ releaseGraph,
151
+ versionData: projectsVersionData,
152
+ version: workspaceVersion,
153
+ stageChanges: shouldStage,
154
+ gitCommit: false,
155
+ gitTag: false,
156
+ gitPush: false,
157
+ createRelease: false,
158
+ deleteVersionPlans: false,
159
+ })
160
+ : undefined;
153
161
  if (shouldCommit) {
154
162
  output_1.output.logSingleLine(`Committing changes with git`);
155
163
  const commitMessage = nxReleaseConfig.git.commitMessage;
@@ -190,51 +198,53 @@ function createAPI(overrideReleaseConfig, ignoreNxJsonConfig) {
190
198
  });
191
199
  hasPushedChanges = true;
192
200
  }
193
- let latestCommit;
194
- if (shouldCreateWorkspaceRemoteRelease &&
195
- changelogResult.workspaceChangelog) {
196
- const remoteReleaseClient = await (0, remote_release_client_1.createRemoteReleaseClient)(
197
- // shouldCreateWorkspaceRemoteRelease() ensures that the createRelease property exists and is not false
198
- nxReleaseConfig.changelog.workspaceChangelog
199
- .createRelease);
200
- if (!hasPushedChanges) {
201
- throw new Error(`It is not possible to create a ${remoteReleaseClient.remoteReleaseProviderName} release for the workspace without pushing the changes to the remote, please ensure that you have not disabled git push in your nx release config`);
202
- }
203
- output_1.output.logSingleLine(`Creating ${remoteReleaseClient.remoteReleaseProviderName} Release`);
204
- latestCommit = await (0, git_1.getCommitHash)('HEAD');
205
- await remoteReleaseClient.createOrUpdateRelease(changelogResult.workspaceChangelog.releaseVersion, changelogResult.workspaceChangelog.contents, latestCommit, { dryRun: args.dryRun });
206
- }
207
- for (const releaseGroupName of releaseGraph.sortedReleaseGroups) {
208
- const releaseGroup = releaseGraph.releaseGroups.find((g) => g.name === releaseGroupName);
209
- if (!releaseGroup) {
210
- continue;
211
- }
212
- const shouldCreateProjectRemoteReleases = shouldCreateRemoteRelease(releaseGroup.changelog);
213
- if (shouldCreateProjectRemoteReleases &&
214
- changelogResult.projectChangelogs) {
201
+ if (changelogResult) {
202
+ let latestCommit;
203
+ if (shouldCreateWorkspaceRemoteRelease &&
204
+ changelogResult.workspaceChangelog) {
215
205
  const remoteReleaseClient = await (0, remote_release_client_1.createRemoteReleaseClient)(
216
- // shouldCreateProjectRemoteReleases() ensures that the createRelease property exists and is not false
217
- releaseGroup.changelog
206
+ // shouldCreateWorkspaceRemoteRelease() ensures that the createRelease property exists and is not false
207
+ nxReleaseConfig.changelog.workspaceChangelog
218
208
  .createRelease);
219
- const projects = args.projects?.length
220
- ? // If the user has passed a list of projects, we need to use the filtered list of projects within the release group
221
- Array.from(releaseGraph.releaseGroupToFilteredProjects.get(releaseGroup))
222
- : // Otherwise, we use the full list of projects within the release group
223
- releaseGroup.projects;
224
- const projectNodes = projects.map((name) => projectGraph.nodes[name]);
225
- for (const project of projectNodes) {
226
- const changelog = changelogResult.projectChangelogs[project.name];
227
- if (!changelog) {
228
- continue;
229
- }
230
- if (!hasPushedChanges) {
231
- throw new Error(`It is not possible to create a ${remoteReleaseClient.remoteReleaseProviderName} release for the project without pushing the changes to the remote, please ensure that you have not disabled git push in your nx release config`);
232
- }
233
- output_1.output.logSingleLine(`Creating ${remoteReleaseClient.remoteReleaseProviderName} Release`);
234
- if (!latestCommit) {
235
- latestCommit = await (0, git_1.getCommitHash)('HEAD');
209
+ if (!hasPushedChanges) {
210
+ throw new Error(`It is not possible to create a ${remoteReleaseClient.remoteReleaseProviderName} release for the workspace without pushing the changes to the remote, please ensure that you have not disabled git push in your nx release config`);
211
+ }
212
+ output_1.output.logSingleLine(`Creating ${remoteReleaseClient.remoteReleaseProviderName} Release`);
213
+ latestCommit = await (0, git_1.getCommitHash)('HEAD');
214
+ await remoteReleaseClient.createOrUpdateRelease(changelogResult.workspaceChangelog.releaseVersion, changelogResult.workspaceChangelog.contents, latestCommit, { dryRun: args.dryRun });
215
+ }
216
+ for (const releaseGroupName of releaseGraph.sortedReleaseGroups) {
217
+ const releaseGroup = releaseGraph.releaseGroups.find((g) => g.name === releaseGroupName);
218
+ if (!releaseGroup) {
219
+ continue;
220
+ }
221
+ const shouldCreateProjectRemoteReleases = shouldCreateRemoteRelease(releaseGroup.changelog);
222
+ if (shouldCreateProjectRemoteReleases &&
223
+ changelogResult.projectChangelogs) {
224
+ const remoteReleaseClient = await (0, remote_release_client_1.createRemoteReleaseClient)(
225
+ // shouldCreateProjectRemoteReleases() ensures that the createRelease property exists and is not false
226
+ releaseGroup.changelog
227
+ .createRelease);
228
+ const projects = args.projects?.length
229
+ ? // If the user has passed a list of projects, we need to use the filtered list of projects within the release group
230
+ Array.from(releaseGraph.releaseGroupToFilteredProjects.get(releaseGroup))
231
+ : // Otherwise, we use the full list of projects within the release group
232
+ releaseGroup.projects;
233
+ const projectNodes = projects.map((name) => projectGraph.nodes[name]);
234
+ for (const project of projectNodes) {
235
+ const changelog = changelogResult.projectChangelogs[project.name];
236
+ if (!changelog) {
237
+ continue;
238
+ }
239
+ if (!hasPushedChanges) {
240
+ throw new Error(`It is not possible to create a ${remoteReleaseClient.remoteReleaseProviderName} release for the project without pushing the changes to the remote, please ensure that you have not disabled git push in your nx release config`);
241
+ }
242
+ output_1.output.logSingleLine(`Creating ${remoteReleaseClient.remoteReleaseProviderName} Release`);
243
+ if (!latestCommit) {
244
+ latestCommit = await (0, git_1.getCommitHash)('HEAD');
245
+ }
246
+ await remoteReleaseClient.createOrUpdateRelease(changelog.releaseVersion, changelog.contents, latestCommit, { dryRun: args.dryRun });
236
247
  }
237
- await remoteReleaseClient.createOrUpdateRelease(changelog.releaseVersion, changelog.contents, latestCommit, { dryRun: args.dryRun });
238
248
  }
239
249
  }
240
250
  }
@@ -118,3 +118,9 @@ export declare function parseVersionPlanCommit(commit: RawGitCommit): {
118
118
  export declare function parseGitCommit(commit: RawGitCommit): GitCommit | null;
119
119
  export declare function getCommitHash(ref: string): Promise<string>;
120
120
  export declare function getFirstGitCommit(): Promise<string>;
121
+ /**
122
+ * Returns the parent of the first commit that touched the given project root,
123
+ * so that `from..HEAD` ranges include the project's creation commit.
124
+ * Falls back to getFirstGitCommit() if the project history cannot be determined.
125
+ */
126
+ export declare function getFirstProjectCommit(projectRoot: string): Promise<string>;
@@ -15,6 +15,7 @@ exports.parseVersionPlanCommit = parseVersionPlanCommit;
15
15
  exports.parseGitCommit = parseGitCommit;
16
16
  exports.getCommitHash = getCommitHash;
17
17
  exports.getFirstGitCommit = getFirstGitCommit;
18
+ exports.getFirstProjectCommit = getFirstProjectCommit;
18
19
  /**
19
20
  * Special thanks to changelogen for the original inspiration for many of these utilities:
20
21
  * https://github.com/unjs/changelogen
@@ -542,6 +543,38 @@ async function getFirstGitCommit() {
542
543
  throw new Error(`Unable to find first commit in git history`);
543
544
  }
544
545
  }
546
+ /**
547
+ * Returns the parent of the first commit that touched the given project root,
548
+ * so that `from..HEAD` ranges include the project's creation commit.
549
+ * Falls back to getFirstGitCommit() if the project history cannot be determined.
550
+ */
551
+ async function getFirstProjectCommit(projectRoot) {
552
+ try {
553
+ const result = (await (0, exec_command_1.execCommand)('git', [
554
+ 'rev-list',
555
+ '--reverse',
556
+ 'HEAD',
557
+ '--first-parent',
558
+ '--',
559
+ projectRoot,
560
+ ])).trim();
561
+ const firstCommit = result.split('\n')[0];
562
+ if (firstCommit) {
563
+ // Return the parent so the creation commit is included in from..to ranges
564
+ try {
565
+ return (await (0, exec_command_1.execCommand)('git', ['rev-parse', `${firstCommit}~1`])).trim();
566
+ }
567
+ catch {
568
+ // No parent (project was added in the repo's very first commit)
569
+ return firstCommit;
570
+ }
571
+ }
572
+ }
573
+ catch {
574
+ // fall through to fallback
575
+ }
576
+ return getFirstGitCommit();
577
+ }
545
578
  async function getGitRoot() {
546
579
  try {
547
580
  return (await (0, exec_command_1.execCommand)('git', ['rev-parse', '--show-toplevel'])).trim();