Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add watch command #13664

Merged
merged 11 commits into from
Dec 13, 2022
74 changes: 74 additions & 0 deletions docs/generated/cli/watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: 'watch - CLI command'
description: 'Watch for changes within projects, and execute commands'
---

# watch

Watch for changes within projects, and execute commands

## Usage

```terminal
nx watch
```

Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`.

### Examples

Watch the "app" project and echo the project name and the files that changed:

```terminal
nx watch --projects=app -- echo \$NX_PROJECT_NAME \$NX_FILE_CHANGES
```

Watch "app1" and "app2" and echo the project name whenever a specified project or its dependencies change:

```terminal
nx watch --projects=app1,app2 --includeDependencies -- echo \$NX_PROJECT_NAME
```

Watch all projects (including newly created projects) in the workspace:

```terminal
nx watch --all -- echo \$NX_PROJECT_NAME
```

## Options

### all

Type: `boolean`

Watch all projects.

### help

Type: `boolean`

Show help

### includeDependentProjects

Type: `boolean`

When watching selected projects, include dependent projects as well.

### projects

Type: `string`

Projects to watch (comma delimited).

### verbose

Type: `boolean`

Run watch mode in verbose mode, where commands are logged before execution.

### version

Type: `boolean`

Show version number
7 changes: 7 additions & 0 deletions docs/generated/packages/nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@
"id": "exec",
"file": "generated/cli/exec",
"content": "---\ntitle: 'exec - CLI command'\ndescription: 'Executes any command as if it was a target on the project'\n---\n\n# exec\n\nExecutes any command as if it was a target on the project\n\n## Usage\n\n```terminal\nnx exec\n```\n\nInstall `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`.\n\n## Options\n\n### configuration\n\nType: `string`\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: `array`\n\nDefault: `[]`\n\nExclude certain projects from being processed\n\n### nx-bail\n\nType: `boolean`\n\nDefault: `false`\n\nStop command execution after the first failed task\n\n### nx-ignore-cycles\n\nType: `boolean`\n\nDefault: `false`\n\nIgnore cycles in the task graph\n\n### output-style\n\nType: `string`\n\nChoices: [dynamic, static, stream, stream-without-prefixes, compact]\n\nDefines how Nx emits outputs tasks logs\n\n### parallel\n\nType: `string`\n\nMax number of parallel processes [default is 3]\n\n### project\n\nType: `string`\n\nTarget project\n\n### runner\n\nType: `string`\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: `boolean`\n\nDefault: `false`\n\nRerun the tasks even when the results are available in the cache\n\n### target\n\nType: `string`\n\nTask to run for affected projects\n\n### verbose\n\nType: `boolean`\n\nDefault: `false`\n\nPrints additional information about the commands (e.g., stack traces)\n\n### version\n\nType: `boolean`\n\nShow version number\n"
},
{
"name": "watch",
"id": "watch",
"tags": ["workspace-watching"],
"file": "generated/cli/watch",
"content": "---\ntitle: 'watch - CLI command'\ndescription: 'Watch for changes within projects, and execute commands'\n---\n\n# watch\n\nWatch for changes within projects, and execute commands\n\n## Usage\n\n```terminal\nnx watch\n```\n\nInstall `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`.\n\n### Examples\n\nWatch the \"app\" project and echo the project name and the files that changed:\n\n```terminal\n nx watch --projects=app -- echo \\$NX_PROJECT_NAME \\$NX_FILE_CHANGES\n```\n\nWatch \"app1\" and \"app2\" and echo the project name whenever a specified project or its dependencies change:\n\n```terminal\n nx watch --projects=app1,app2 --includeDependencies -- echo \\$NX_PROJECT_NAME\n```\n\nWatch all projects (including newly created projects) in the workspace:\n\n```terminal\n nx watch --all -- echo \\$NX_PROJECT_NAME\n```\n\n## Options\n\n### all\n\nType: `boolean`\n\nWatch all projects.\n\n### help\n\nType: `boolean`\n\nShow help\n\n### includeDependentProjects\n\nType: `boolean`\n\nWhen watching selected projects, include dependent projects as well.\n\n### projects\n\nType: `string`\n\nProjects to watch (comma delimited).\n\n### verbose\n\nType: `boolean`\n\nRun watch mode in verbose mode, where commands are logged before execution.\n\n### version\n\nType: `boolean`\n\nShow version number\n"
}
],
"generators": [],
Expand Down
6 changes: 6 additions & 0 deletions docs/map.json
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,12 @@
"name": "exec",
"id": "exec",
"file": "generated/cli/exec"
},
{
"name": "watch",
"id": "watch",
"tags": ["workspace-watching"],
"file": "generated/cli/watch"
}
]
},
Expand Down
152 changes: 152 additions & 0 deletions e2e/nx-misc/src/watch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
cleanupProject,
createFile,
newProject,
runCLI,
uniq,
getPackageManagerCommand,
tmpProjPath,
getStrippedEnvironmentVariables,
updateJson,
isVerbose,
} from '@nrwl/e2e/utils';
import { exec, spawn } from 'child_process';

describe('Nx Commands', () => {
let proj1 = uniq('proj1');
let proj2 = uniq('proj2');
let proj3 = uniq('proj3');
beforeAll(() => {
newProject({ packageManager: 'npm' });
runCLI(`generate @nrwl/workspace:lib ${proj1}`);
runCLI(`generate @nrwl/workspace:lib ${proj2}`);
runCLI(`generate @nrwl/workspace:lib ${proj3}`);
});

afterAll(() => cleanupProject());

it('should watch for project changes', async () => {
const getOutput = await runWatch(
`--projects=${proj1} -- echo \\$NX_PROJECT_NAME`
);
createFile(`libs/${proj1}/newfile.txt`, 'content');
createFile(`libs/${proj2}/newfile.txt`, 'content');
createFile(`libs/${proj1}/newfile2.txt`, 'content');
createFile(`libs/${proj3}/newfile2.txt`, 'content');
createFile(`newfile2.txt`, 'content');

expect(await getOutput()).toEqual([proj1]);
});

it('should watch for all projects and output the project name', async () => {
const getOutput = await runWatch(`--all -- echo \\$NX_PROJECT_NAME`);
createFile(`libs/${proj1}/newfile.txt`, 'content');
createFile(`libs/${proj2}/newfile.txt`, 'content');
createFile(`libs/${proj1}/newfile2.txt`, 'content');
createFile(`libs/${proj3}/newfile2.txt`, 'content');
createFile(`newfile2.txt`, 'content');

expect(await getOutput()).toEqual([proj1, proj2, proj3]);
});

it('should watch for all project changes and output the file name changes', async () => {
const getOutput = await runWatch(`--all -- echo \\$NX_FILE_CHANGES`);
createFile(`libs/${proj1}/newfile.txt`, 'content');
createFile(`libs/${proj2}/newfile.txt`, 'content');
createFile(`libs/${proj1}/newfile2.txt`, 'content');
createFile(`newfile2.txt`, 'content');

expect(await getOutput()).toEqual([
`libs/${proj1}/newfile.txt libs/${proj1}/newfile2.txt libs/${proj2}/newfile.txt`,
]);
});

it('should watch for global workspace file changes', async () => {
const getOutput = await runWatch(
`--all --includeGlobalWorkspaceFiles -- echo \\$NX_FILE_CHANGES`
);
createFile(`libs/${proj1}/newfile.txt`, 'content');
createFile(`libs/${proj2}/newfile.txt`, 'content');
createFile(`libs/${proj1}/newfile2.txt`, 'content');
createFile(`newfile2.txt`, 'content');

expect(await getOutput()).toEqual([
`libs/${proj1}/newfile.txt libs/${proj1}/newfile2.txt libs/${proj2}/newfile.txt newfile2.txt`,
]);
});

it('should watch selected projects only', async () => {
const getOutput = await runWatch(
`--projects=${proj1},${proj3} -- echo \\$NX_PROJECT_NAME`
);
createFile(`libs/${proj1}/newfile.txt`, 'content');
createFile(`libs/${proj2}/newfile.txt`, 'content');
createFile(`libs/${proj1}/newfile2.txt`, 'content');
createFile(`libs/${proj3}/newfile2.txt`, 'content');
createFile(`newfile2.txt`, 'content');

expect(await getOutput()).toEqual([proj1, proj3]);
});

it('should watch projects including their dependencies', async () => {
updateJson(`libs/${proj3}/project.json`, (json) => {
json.implicitDependencies = [proj1];
return json;
});

const getOutput = await runWatch(
`--projects=${proj3} --includeDependentProjects -- echo \\$NX_PROJECT_NAME`
);
createFile(`libs/${proj1}/newfile.txt`, 'content');
createFile(`libs/${proj2}/newfile.txt`, 'content');
createFile(`libs/${proj1}/newfile2.txt`, 'content');
createFile(`libs/${proj3}/newfile2.txt`, 'content');
createFile(`newfile2.txt`, 'content');

expect(await getOutput()).toEqual([proj3, proj1]);
});
});

async function wait(timeout = 200) {
return new Promise<void>((res) => {
setTimeout(() => {
res();
}, timeout);
});
}

async function runWatch(command: string) {
const output = [];
const pm = getPackageManagerCommand();
const runCommand = `npx -c 'nx watch --verbose ${command}'`;
isVerbose() && console.log(runCommand);
return new Promise<(timeout?: number) => Promise<string[]>>((resolve) => {
const p = spawn(runCommand, {
cwd: tmpProjPath(),
env: {
CI: 'true',
...getStrippedEnvironmentVariables(),
FORCE_COLOR: 'false',
},
shell: true,
stdio: 'pipe',
});

p.stdout?.on('data', (data) => {
const s = data.toString().trim();
isVerbose() && console.log(s);
if (s.includes('watch process waiting')) {
resolve(async (timeout = 6000) => {
await wait(timeout);
p.kill();
return output;
});
} else {
if (s.length == 0 || s.includes('NX')) {
return;
}
output.push(s);
}
});
});
}
4 changes: 2 additions & 2 deletions e2e/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const e2eRoot = isCI
? dirSync({ prefix: 'nx-e2e-' }).name
: '/tmp/nx-e2e';

function isVerbose() {
export function isVerbose() {
return (
process.env.NX_VERBOSE_LOGGING === 'true' ||
process.argv.includes('--verbose')
Expand Down Expand Up @@ -1012,7 +1012,7 @@ export async function expectJestTestsToPass(
expect(results.combinedOutput).toContain('Test Suites: 1 passed, 1 total');
}

function getStrippedEnvironmentVariables() {
export function getStrippedEnvironmentVariables() {
const strippedVariables = new Set(['NX_TASK_TARGET_PROJECT']);
return Object.fromEntries(
Object.entries(process.env).filter(
Expand Down
19 changes: 19 additions & 0 deletions packages/nx/src/command-line/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,23 @@ export const examples: Record<string, Example[]> = {
'Create a dedicated commit for each successfully completed migration. You can customize the prefix used for each commit by additionally setting --commit-prefix="PREFIX_HERE "',
},
],
watch: [
{
command:
'watch --projects=app -- echo \\$NX_PROJECT_NAME \\$NX_FILE_CHANGES',
description:
'Watch the "app" project and echo the project name and the files that changed',
},
{
command:
'watch --projects=app1,app2 --includeDependencies -- echo \\$NX_PROJECT_NAME',
description:
'Watch "app1" and "app2" and echo the project name whenever a specified project or its dependencies change',
},
{
command: 'watch --all -- echo \\$NX_PROJECT_NAME',
description:
'Watch all projects (including newly created projects) in the workspace',
},
],
};
65 changes: 65 additions & 0 deletions packages/nx/src/command-line/nx-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { examples } from './examples';
import { workspaceRoot } from '../utils/workspace-root';
import { getPackageManagerCommand } from '../utils/package-manager';
import { writeJsonFile } from '../utils/fileutils';
import { WatchArguments } from './watch';

// Ensure that the output takes up the available width of the terminal.
yargs.wrap(yargs.terminalWidth());
Expand Down Expand Up @@ -381,6 +382,15 @@ export const commandsObject = yargs
process.exit(0);
},
})
.command({
command: 'watch',
describe: 'Watch for changes within projects, and execute commands',
builder: (yargs) =>
linkToNxDevAndExamples(withWatchOptions(yargs), 'watch'),
handler: async (args) => {
await import('./watch').then((m) => m.watch(args as WatchArguments));
},
})
.help()
.version(nxVersion);

Expand Down Expand Up @@ -927,6 +937,61 @@ function withMigrationOptions(yargs: yargs.Argv) {
});
}

function withWatchOptions(yargs: yargs.Argv) {
return yargs
.parserConfiguration({
'strip-dashed': true,
'populate--': true,
})
.option('projects', {
type: 'array',
string: true,
coerce: parseCSV,
description: 'Projects to watch (comma delimited).',
})
.option('all', {
type: 'boolean',
description: 'Watch all projects.',
})
.option('includeDependentProjects', {
type: 'boolean',
description:
'When watching selected projects, include dependent projects as well.',
alias: 'd',
})
.option('includeGlobalWorkspaceFiles', {
type: 'boolean',
description:
'Include global workspace files that are not part of a project. For example, the root eslint, or tsconfig file.',
alias: 'g',
hidden: true,
})
.option('command', { type: 'string', hidden: true })
.option('verbose', {
type: 'boolean',
description:
'Run watch mode in verbose mode, where commands are logged before execution.',
})
.conflicts({
all: 'projects',
})
.check((args) => {
if (!args.all && !args.projects) {
throw Error('Please specify either --all or --projects');
}

return true;
})
.middleware((args) => {
const { '--': doubledash } = args;
if (doubledash && Array.isArray(doubledash)) {
args.command = (doubledash as string[]).join(' ');
} else {
throw Error('No command specified for watch mode.');
}
}, true);
}

function parseCSV(args: string[]) {
if (!args) {
return args;
Expand Down
Loading