Skip to content

Commit

Permalink
fix(run): only defer to Nx when targetDefaults are defined in nx.json
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Oct 2, 2022
1 parent d68f19a commit 127f90c
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 23 deletions.
4 changes: 2 additions & 2 deletions packages/run/README.md
Expand Up @@ -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

Expand All @@ -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`).
> **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`).
@@ -0,0 +1,4 @@
{
"version": "1.0.0",
"useNx": true
}
@@ -0,0 +1,16 @@
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"useDaemonProcess": false,
"cacheableOperations": ["my-cacheable-script"]
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build", "prebuild"]
}
}
}
@@ -0,0 +1,3 @@
{
"name": "powered-by-nx"
}
@@ -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"
}
}
@@ -0,0 +1,10 @@
{
"name": "package-2",
"version": "1.0.0",
"scripts": {
"fail": "exit 1"
},
"dependencies": {
"package-1": "^1.0.0"
}
}
@@ -0,0 +1,10 @@
{
"name": "package-3",
"version": "1.0.0",
"scripts": {
"my-script": "echo package-3"
},
"devDependencies": {
"package-2": "^1.0.0"
}
}
@@ -0,0 +1,10 @@
{
"name": "package-4",
"version": "1.0.0",
"scripts": {
"my-cacheable-script": "echo cacheable"
},
"dependencies": {
"package-1": "^0.0.0"
}
}
@@ -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.'
);
});
});
});
9 changes: 4 additions & 5 deletions packages/run/src/__tests__/run-command.spec.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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.'
);
});

Expand Down Expand Up @@ -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');
});
Expand All @@ -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');
});
Expand Down
45 changes: 29 additions & 16 deletions packages/run/src/run-command.ts
Expand Up @@ -252,17 +252,22 @@ export class RunCommand extends Command<RunCommandOption & FilterOptions> {

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
Expand All @@ -278,41 +283,49 @@ export class RunCommand extends Command<RunCommandOption & FilterOptions> {
* 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,
verbose: this.options.verbose,
__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 };
Expand Down

0 comments on commit 127f90c

Please sign in to comment.