diff --git a/packages/run/README.md b/packages/run/README.md index d0a9907f..9faa7363 100644 --- a/packages/run/README.md +++ b/packages/run/README.md @@ -182,7 +182,7 @@ Nx will run tasks in an order and with a concurrency that it determines appropri **This behavior allows Nx to run tasks in the most efficient way possible, but it also means that some existing options for `lerna run` become obsolete as explained below.** -> **Note** when Lerna is set to use Nx and detects `nx.json` in the workspace, it will defer to Nx to detect task dependencies. Some options for `lerna run` will behave differently. See [Using Lerna (Powered by Nx) to Run Tasks](./recipes/using-lerna-powered-by-nx-to-run-tasks) for more details. +> **Note** when Lerna is set to use Nx and detects `nx.json` with `targetDefaults` in the workspace, it will defer to Nx to detect task dependencies. Some options for `lerna run` will behave differently. See [Using Lerna (Powered by Nx) to Run Tasks](./recipes/using-lerna-powered-by-nx-to-run-tasks) for more details. #### Obsolete Options when `useNx` is enabled @@ -207,4 +207,4 @@ This is no longer a problem when Lerna uses Nx to run tasks. Nx, utilizing its [ When used with Nx, `--ignore` will never cause `lerna run` to exclude any tasks that are deemed to be required by the Nx [task graph](https://nx.dev/concepts/mental-model#the-task-graph). -> **Tip** the effects on the options above will only apply if `nx.json` exists in the root. If `nx.json` does not exist and `useNx` is `true`, then they will behave just as they would with Lerna's base task runner (if `useNx` is `false`). \ No newline at end of file +> **Tip** the effects on the options above will only apply if `nx.json` exists in the root with the `targetDefaults` property defined. Otherwise, they will behave just as they would with Lerna's base task runner (if `useNx` is `false`). \ No newline at end of file diff --git a/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/lerna.json b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/lerna.json new file mode 100644 index 00000000..d6ec96fc --- /dev/null +++ b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/lerna.json @@ -0,0 +1,4 @@ +{ + "version": "1.0.0", + "useNx": true +} \ No newline at end of file diff --git a/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/nx.json b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/nx.json new file mode 100644 index 00000000..f3cca4d6 --- /dev/null +++ b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/nx.json @@ -0,0 +1,16 @@ +{ + "tasksRunnerOptions": { + "default": { + "runner": "nx/tasks-runners/default", + "options": { + "useDaemonProcess": false, + "cacheableOperations": ["my-cacheable-script"] + } + } + }, + "targetDefaults": { + "build": { + "dependsOn": ["^build", "prebuild"] + } + } +} diff --git a/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/package.json b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/package.json new file mode 100644 index 00000000..d5cdacd5 --- /dev/null +++ b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/package.json @@ -0,0 +1,3 @@ +{ + "name": "powered-by-nx" +} \ No newline at end of file diff --git a/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-1/package.json b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-1/package.json new file mode 100644 index 00000000..9978bcb2 --- /dev/null +++ b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-1/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-1", + "version": "1.0.0", + "scripts": { + "fail": "exit 1", + "my-script": "echo package-1", + "another-script:but-with-colons": "echo package-1-script-with-colons" + } +} \ No newline at end of file diff --git a/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-2/package.json b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-2/package.json new file mode 100644 index 00000000..bcae5573 --- /dev/null +++ b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-2/package.json @@ -0,0 +1,10 @@ +{ + "name": "package-2", + "version": "1.0.0", + "scripts": { + "fail": "exit 1" + }, + "dependencies": { + "package-1": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-3/package.json b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-3/package.json new file mode 100644 index 00000000..f12cd809 --- /dev/null +++ b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-3/package.json @@ -0,0 +1,10 @@ +{ + "name": "package-3", + "version": "1.0.0", + "scripts": { + "my-script": "echo package-3" + }, + "devDependencies": { + "package-2": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-4/package.json b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-4/package.json new file mode 100644 index 00000000..46f5ecee --- /dev/null +++ b/packages/run/src/__fixtures__/powered-by-nx-with-target-defaults/packages/package-4/package.json @@ -0,0 +1,10 @@ +{ + "name": "package-4", + "version": "1.0.0", + "scripts": { + "my-cacheable-script": "echo cacheable" + }, + "dependencies": { + "package-1": "^0.0.0" + } +} diff --git a/packages/run/src/__tests__/run-command-with-nx-no-target-defaults.spec.ts b/packages/run/src/__tests__/run-command-with-nx-no-target-defaults.spec.ts new file mode 100644 index 00000000..05970259 --- /dev/null +++ b/packages/run/src/__tests__/run-command-with-nx-no-target-defaults.spec.ts @@ -0,0 +1,72 @@ +jest.mock('../lib/npm-run-script'); + +jest.mock('@lerna-lite/core', () => ({ + ...(jest.requireActual('@lerna-lite/core') as any), // return the other real methods, below we'll mock only 2 of the methods + logOutput: jest.requireActual('../../../core/src/__mocks__/output').logOutput, + runTopologically: jest.requireActual('../../../core/src/utils/run-topologically').runTopologically, + QueryGraph: jest.requireActual('../../../core/src/utils/query-graph').QueryGraph, +})); + +// also point to the local run command so that all mocks are properly used even by the command-runner +jest.mock('@lerna-lite/run', () => jest.requireActual('../run-command')); + +// mocked modules +import { npmRunScript, npmRunScriptStreaming } from '../lib/npm-run-script'; +import cliRunCommands from '../../../cli/src/cli-commands/cli-run-commands'; + +// helpers +import { commandRunner, initFixtureFactory, loggingOutput, normalizeRelativeDir } from '@lerna-test/helpers'; +const lernaRun = commandRunner(cliRunCommands); +const initFixture = initFixtureFactory(__dirname); + +describe('RunCommand', () => { + (npmRunScript as jest.Mock).mockImplementation((script, { pkg }) => + Promise.resolve({ exitCode: 0, stdout: pkg.name }) + ); + (npmRunScriptStreaming as jest.Mock).mockImplementation(() => Promise.resolve({ exitCode: 0 })); + + afterEach(() => { + process.exitCode = undefined; + }); + + // this is a temporary set of tests, which will be replaced by verdacio-driven tests + // once the required setup is fully set up + describe('in a repo powered by Nx', () => { + let testDir; + let collectedOutput = ''; + let originalStdout; + + beforeAll(async () => { + testDir = await initFixture('powered-by-nx'); + process.env.NX_WORKSPACE_ROOT_PATH = testDir; + // @ts-ignore + jest.spyOn(process, 'exit').mockImplementation((code: any) => { + if (code !== 0) { + throw new Error(); + } + }); + originalStdout = process.stdout.write; + (process.stdout as any).write = (v) => { + collectedOutput = `${collectedOutput}\n${v}`; + }; + }); + + afterAll(() => { + process.stdout.write = originalStdout; + }); + + it('runs a script in packages', async () => { + collectedOutput = ''; + await lernaRun(testDir)('my-script'); + + expect(collectedOutput).toContain('package-1'); + expect(collectedOutput).toContain('package-3'); + expect(collectedOutput).toContain('Successfully ran target'); + + const logMessages = loggingOutput('verbose'); + expect(logMessages).toContain( + 'nx.json was not found or is missing targetDefaults. Task dependencies will not be automatically included.' + ); + }); + }); +}); diff --git a/packages/run/src/__tests__/run-command.spec.ts b/packages/run/src/__tests__/run-command.spec.ts index 252e5d6f..ccfe7c02 100644 --- a/packages/run/src/__tests__/run-command.spec.ts +++ b/packages/run/src/__tests__/run-command.spec.ts @@ -12,7 +12,6 @@ jest.mock('@lerna-lite/run', () => jest.requireActual('../run-command')); import fs from 'fs-extra'; import globby from 'globby'; -import { afterEach, afterAll } from 'jest-circus'; import yargParser from 'yargs-parser'; // make sure to import the output mock @@ -361,7 +360,7 @@ describe('RunCommand', () => { let originalStdout; beforeAll(async () => { - testDir = await initFixture('powered-by-nx'); + testDir = await initFixture('powered-by-nx-with-target-defaults'); process.env.NX_WORKSPACE_ROOT_PATH = testDir; // @ts-ignore jest.spyOn(process, 'exit').mockImplementation((code: any) => { @@ -410,7 +409,7 @@ describe('RunCommand', () => { const logMessages = loggingOutput('info'); expect(logMessages).toContain( - 'Using the "ignore" option when nx.json exists will exclude only tasks that are not determined to be required by Nx. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks#--ignore for details.' + 'Using the "ignore" option when nx.json has targetDefaults defined will exclude only tasks that are not determined to be required by Nx. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks#--ignore for details.' ); }); @@ -445,7 +444,7 @@ describe('RunCommand', () => { const [logMessage] = loggingOutput('warn'); expect(logMessage).toContain( - '"parallel", "sort", and "no-sort" are ignored when nx.json exists. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks for details.' + '"parallel", "sort", and "no-sort" are ignored when nx.json has targetDefaults defined. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks for details.' ); expect(collectedOutput).toContain('package-1'); }); @@ -457,7 +456,7 @@ describe('RunCommand', () => { const logMessages = loggingOutput('info'); expect(logMessages).toContain( - 'Using the "include-dependencies" option when nx.json exists will include both task dependencies detected by Nx and project dependencies detected by Lerna. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks#--include-dependencies for details.' + 'Using the "include-dependencies" option when nx.json has targetDefaults defined will include both task dependencies detected by Nx and project dependencies detected by Lerna. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks#--include-dependencies for details.' ); expect(collectedOutput).toContain('package-1'); }); diff --git a/packages/run/src/run-command.ts b/packages/run/src/run-command.ts index 9bfa3581..3c1a58aa 100644 --- a/packages/run/src/run-command.ts +++ b/packages/run/src/run-command.ts @@ -252,17 +252,22 @@ export class RunCommand extends Command { async prepNxOptions() { const nxJsonExists = existsSync(path.join(this.project.rootPath, 'nx.json')); + const { readNxJson } = await import('nx/src/config/configuration'); + const nxJson = readNxJson(); + const targetDependenciesAreDefined = + Object.keys(nxJson.targetDependencies || nxJson.targetDefaults || {}).length > 0; + const mimicLernaDefaultBehavior = !(nxJsonExists && targetDependenciesAreDefined); + const targetDependencies = - // prettier-ignore - this.toposort && !this.options.parallel && !nxJsonExists + this.toposort && !this.options.parallel && mimicLernaDefaultBehavior ? { - [this.script]: [ - { - projects: 'dependencies', - target: this.script, - }, - ], - } + [this.script]: [ + { + projects: 'dependencies', + target: this.script, + }, + ], + } : {}; // prettier-ignore @@ -278,7 +283,7 @@ export class RunCommand extends Command { * To match lerna's own behavior (via pMap's default concurrency), we set parallel to a very large number if * the flag has been set (we can't use Infinity because that would cause issues with the task runner). */ - parallel: this.options.parallel && !nxJsonExists ? 999 : this.concurrency, + parallel: this.options.parallel && mimicLernaDefaultBehavior ? 999 : this.concurrency, nxBail: this.bail, nxIgnoreCycles: !this.options.rejectCycles, skipNxCache: this.options.skipNxCache, @@ -286,33 +291,41 @@ export class RunCommand extends Command { __overrides__: this.args.map((t) => t.toString()), }; - if (nxJsonExists) { - this.logger.verbose(this.name, 'nx.json was found. Task dependencies will be automatically included.'); + if (!mimicLernaDefaultBehavior) { + this.logger.verbose( + this.name, + 'nx.json with targetDefaults was found. Task dependencies will be automatically included.' + ); if (this.options.parallel || this.options.sort !== undefined) { this.logger.warn( this.name, - `"parallel", "sort", and "no-sort" are ignored when nx.json exists. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks for details.` + `"parallel", "sort", and "no-sort" are ignored when nx.json has targetDefaults defined. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks for details.` ); } if (this.options.includeDependencies) { this.logger.info( this.name, - `Using the "include-dependencies" option when nx.json exists will include both task dependencies detected by Nx and project dependencies detected by Lerna. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks#--include-dependencies for details.` + `Using the "include-dependencies" option when nx.json has targetDefaults defined will include both task dependencies detected by Nx and project dependencies detected by Lerna. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks#--include-dependencies for details.` ); } if (this.options.ignore) { this.logger.info( this.name, - `Using the "ignore" option when nx.json exists will exclude only tasks that are not determined to be required by Nx. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks#--ignore for details.` + `Using the "ignore" option when nx.json has targetDefaults defined will exclude only tasks that are not determined to be required by Nx. See https://lerna.js.org/docs/recipes/using-lerna-powered-by-nx-to-run-tasks#--ignore for details.` ); } + } else { + this.logger.verbose( + this.name, + 'nx.json was not found or is missing targetDefaults. Task dependencies will not be automatically included.' + ); } const extraOptions = { - excludeTaskDependencies: !nxJsonExists, + excludeTaskDependencies: mimicLernaDefaultBehavior, }; return { targetDependencies, options, extraOptions };