Skip to content

Commit a1d9c46

Browse files
authored
feat(core): add watch command (#13664)
1 parent 2f4435b commit a1d9c46

File tree

8 files changed

+526
-2
lines changed

8 files changed

+526
-2
lines changed

docs/generated/cli/watch.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
title: 'watch - CLI command'
3+
description: 'Watch for changes within projects, and execute commands'
4+
---
5+
6+
# watch
7+
8+
Watch for changes within projects, and execute commands
9+
10+
## Usage
11+
12+
```terminal
13+
nx watch
14+
```
15+
16+
Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`.
17+
18+
### Examples
19+
20+
Watch the "app" project and echo the project name and the files that changed:
21+
22+
```terminal
23+
nx watch --projects=app -- echo \$NX_PROJECT_NAME \$NX_FILE_CHANGES
24+
```
25+
26+
Watch "app1" and "app2" and echo the project name whenever a specified project or its dependencies change:
27+
28+
```terminal
29+
nx watch --projects=app1,app2 --includeDependencies -- echo \$NX_PROJECT_NAME
30+
```
31+
32+
Watch all projects (including newly created projects) in the workspace:
33+
34+
```terminal
35+
nx watch --all -- echo \$NX_PROJECT_NAME
36+
```
37+
38+
## Options
39+
40+
### all
41+
42+
Type: `boolean`
43+
44+
Watch all projects.
45+
46+
### help
47+
48+
Type: `boolean`
49+
50+
Show help
51+
52+
### includeDependentProjects
53+
54+
Type: `boolean`
55+
56+
When watching selected projects, include dependent projects as well.
57+
58+
### projects
59+
60+
Type: `string`
61+
62+
Projects to watch (comma delimited).
63+
64+
### verbose
65+
66+
Type: `boolean`
67+
68+
Run watch mode in verbose mode, where commands are logged before execution.
69+
70+
### version
71+
72+
Type: `boolean`
73+
74+
Show version number

docs/generated/packages/nx.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@
154154
"id": "exec",
155155
"file": "generated/cli/exec",
156156
"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"
157+
},
158+
{
159+
"name": "watch",
160+
"id": "watch",
161+
"tags": ["workspace-watching"],
162+
"file": "generated/cli/watch",
163+
"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"
157164
}
158165
],
159166
"generators": [],

docs/map.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,12 @@
14251425
"name": "exec",
14261426
"id": "exec",
14271427
"file": "generated/cli/exec"
1428+
},
1429+
{
1430+
"name": "watch",
1431+
"id": "watch",
1432+
"tags": ["workspace-watching"],
1433+
"file": "generated/cli/watch"
14281434
}
14291435
]
14301436
},

e2e/nx-misc/src/watch.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import {
2+
cleanupProject,
3+
createFile,
4+
newProject,
5+
runCLI,
6+
uniq,
7+
getPackageManagerCommand,
8+
tmpProjPath,
9+
getStrippedEnvironmentVariables,
10+
updateJson,
11+
isVerbose,
12+
} from '@nrwl/e2e/utils';
13+
import { exec, spawn } from 'child_process';
14+
15+
describe('Nx Commands', () => {
16+
let proj1 = uniq('proj1');
17+
let proj2 = uniq('proj2');
18+
let proj3 = uniq('proj3');
19+
beforeAll(() => {
20+
newProject({ packageManager: 'npm' });
21+
runCLI(`generate @nrwl/workspace:lib ${proj1}`);
22+
runCLI(`generate @nrwl/workspace:lib ${proj2}`);
23+
runCLI(`generate @nrwl/workspace:lib ${proj3}`);
24+
});
25+
26+
afterAll(() => cleanupProject());
27+
28+
it('should watch for project changes', async () => {
29+
const getOutput = await runWatch(
30+
`--projects=${proj1} -- echo \\$NX_PROJECT_NAME`
31+
);
32+
createFile(`libs/${proj1}/newfile.txt`, 'content');
33+
createFile(`libs/${proj2}/newfile.txt`, 'content');
34+
createFile(`libs/${proj1}/newfile2.txt`, 'content');
35+
createFile(`libs/${proj3}/newfile2.txt`, 'content');
36+
createFile(`newfile2.txt`, 'content');
37+
38+
expect(await getOutput()).toEqual([proj1]);
39+
});
40+
41+
it('should watch for all projects and output the project name', async () => {
42+
const getOutput = await runWatch(`--all -- echo \\$NX_PROJECT_NAME`);
43+
createFile(`libs/${proj1}/newfile.txt`, 'content');
44+
createFile(`libs/${proj2}/newfile.txt`, 'content');
45+
createFile(`libs/${proj1}/newfile2.txt`, 'content');
46+
createFile(`libs/${proj3}/newfile2.txt`, 'content');
47+
createFile(`newfile2.txt`, 'content');
48+
49+
expect(await getOutput()).toEqual([proj1, proj2, proj3]);
50+
});
51+
52+
it('should watch for all project changes and output the file name changes', async () => {
53+
const getOutput = await runWatch(`--all -- echo \\$NX_FILE_CHANGES`);
54+
createFile(`libs/${proj1}/newfile.txt`, 'content');
55+
createFile(`libs/${proj2}/newfile.txt`, 'content');
56+
createFile(`libs/${proj1}/newfile2.txt`, 'content');
57+
createFile(`newfile2.txt`, 'content');
58+
59+
expect(await getOutput()).toEqual([
60+
`libs/${proj1}/newfile.txt libs/${proj1}/newfile2.txt libs/${proj2}/newfile.txt`,
61+
]);
62+
});
63+
64+
it('should watch for global workspace file changes', async () => {
65+
const getOutput = await runWatch(
66+
`--all --includeGlobalWorkspaceFiles -- echo \\$NX_FILE_CHANGES`
67+
);
68+
createFile(`libs/${proj1}/newfile.txt`, 'content');
69+
createFile(`libs/${proj2}/newfile.txt`, 'content');
70+
createFile(`libs/${proj1}/newfile2.txt`, 'content');
71+
createFile(`newfile2.txt`, 'content');
72+
73+
expect(await getOutput()).toEqual([
74+
`libs/${proj1}/newfile.txt libs/${proj1}/newfile2.txt libs/${proj2}/newfile.txt newfile2.txt`,
75+
]);
76+
});
77+
78+
it('should watch selected projects only', async () => {
79+
const getOutput = await runWatch(
80+
`--projects=${proj1},${proj3} -- echo \\$NX_PROJECT_NAME`
81+
);
82+
createFile(`libs/${proj1}/newfile.txt`, 'content');
83+
createFile(`libs/${proj2}/newfile.txt`, 'content');
84+
createFile(`libs/${proj1}/newfile2.txt`, 'content');
85+
createFile(`libs/${proj3}/newfile2.txt`, 'content');
86+
createFile(`newfile2.txt`, 'content');
87+
88+
expect(await getOutput()).toEqual([proj1, proj3]);
89+
});
90+
91+
it('should watch projects including their dependencies', async () => {
92+
updateJson(`libs/${proj3}/project.json`, (json) => {
93+
json.implicitDependencies = [proj1];
94+
return json;
95+
});
96+
97+
const getOutput = await runWatch(
98+
`--projects=${proj3} --includeDependentProjects -- echo \\$NX_PROJECT_NAME`
99+
);
100+
createFile(`libs/${proj1}/newfile.txt`, 'content');
101+
createFile(`libs/${proj2}/newfile.txt`, 'content');
102+
createFile(`libs/${proj1}/newfile2.txt`, 'content');
103+
createFile(`libs/${proj3}/newfile2.txt`, 'content');
104+
createFile(`newfile2.txt`, 'content');
105+
106+
expect(await getOutput()).toEqual([proj3, proj1]);
107+
});
108+
});
109+
110+
async function wait(timeout = 200) {
111+
return new Promise<void>((res) => {
112+
setTimeout(() => {
113+
res();
114+
}, timeout);
115+
});
116+
}
117+
118+
async function runWatch(command: string) {
119+
const output = [];
120+
const pm = getPackageManagerCommand();
121+
const runCommand = `npx -c 'nx watch --verbose ${command}'`;
122+
isVerbose() && console.log(runCommand);
123+
return new Promise<(timeout?: number) => Promise<string[]>>((resolve) => {
124+
const p = spawn(runCommand, {
125+
cwd: tmpProjPath(),
126+
env: {
127+
CI: 'true',
128+
...getStrippedEnvironmentVariables(),
129+
FORCE_COLOR: 'false',
130+
},
131+
shell: true,
132+
stdio: 'pipe',
133+
});
134+
135+
p.stdout?.on('data', (data) => {
136+
const s = data.toString().trim();
137+
isVerbose() && console.log(s);
138+
if (s.includes('watch process waiting')) {
139+
resolve(async (timeout = 6000) => {
140+
await wait(timeout);
141+
p.kill();
142+
return output;
143+
});
144+
} else {
145+
if (s.length == 0 || s.includes('NX')) {
146+
return;
147+
}
148+
output.push(s);
149+
}
150+
});
151+
});
152+
}

e2e/utils/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const e2eRoot = isCI
7474
? dirSync({ prefix: 'nx-e2e-' }).name
7575
: '/tmp/nx-e2e';
7676

77-
function isVerbose() {
77+
export function isVerbose() {
7878
return (
7979
process.env.NX_VERBOSE_LOGGING === 'true' ||
8080
process.argv.includes('--verbose')
@@ -1006,7 +1006,7 @@ export async function expectJestTestsToPass(
10061006
expect(results.combinedOutput).toContain('Test Suites: 1 passed, 1 total');
10071007
}
10081008

1009-
function getStrippedEnvironmentVariables() {
1009+
export function getStrippedEnvironmentVariables() {
10101010
const strippedVariables = new Set(['NX_TASK_TARGET_PROJECT']);
10111011
return Object.fromEntries(
10121012
Object.entries(process.env).filter(

packages/nx/src/command-line/examples.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,23 @@ export const examples: Record<string, Example[]> = {
340340
'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 "',
341341
},
342342
],
343+
watch: [
344+
{
345+
command:
346+
'watch --projects=app -- echo \\$NX_PROJECT_NAME \\$NX_FILE_CHANGES',
347+
description:
348+
'Watch the "app" project and echo the project name and the files that changed',
349+
},
350+
{
351+
command:
352+
'watch --projects=app1,app2 --includeDependencies -- echo \\$NX_PROJECT_NAME',
353+
description:
354+
'Watch "app1" and "app2" and echo the project name whenever a specified project or its dependencies change',
355+
},
356+
{
357+
command: 'watch --all -- echo \\$NX_PROJECT_NAME',
358+
description:
359+
'Watch all projects (including newly created projects) in the workspace',
360+
},
361+
],
343362
};

packages/nx/src/command-line/nx-commands.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { examples } from './examples';
77
import { workspaceRoot } from '../utils/workspace-root';
88
import { getPackageManagerCommand } from '../utils/package-manager';
99
import { writeJsonFile } from '../utils/fileutils';
10+
import { WatchArguments } from './watch';
1011

1112
// Ensure that the output takes up the available width of the terminal.
1213
yargs.wrap(yargs.terminalWidth());
@@ -381,6 +382,15 @@ export const commandsObject = yargs
381382
process.exit(0);
382383
},
383384
})
385+
.command({
386+
command: 'watch',
387+
describe: 'Watch for changes within projects, and execute commands',
388+
builder: (yargs) =>
389+
linkToNxDevAndExamples(withWatchOptions(yargs), 'watch'),
390+
handler: async (args) => {
391+
await import('./watch').then((m) => m.watch(args as WatchArguments));
392+
},
393+
})
384394
.help()
385395
.version(nxVersion);
386396

@@ -927,6 +937,61 @@ function withMigrationOptions(yargs: yargs.Argv) {
927937
});
928938
}
929939

940+
function withWatchOptions(yargs: yargs.Argv) {
941+
return yargs
942+
.parserConfiguration({
943+
'strip-dashed': true,
944+
'populate--': true,
945+
})
946+
.option('projects', {
947+
type: 'array',
948+
string: true,
949+
coerce: parseCSV,
950+
description: 'Projects to watch (comma delimited).',
951+
})
952+
.option('all', {
953+
type: 'boolean',
954+
description: 'Watch all projects.',
955+
})
956+
.option('includeDependentProjects', {
957+
type: 'boolean',
958+
description:
959+
'When watching selected projects, include dependent projects as well.',
960+
alias: 'd',
961+
})
962+
.option('includeGlobalWorkspaceFiles', {
963+
type: 'boolean',
964+
description:
965+
'Include global workspace files that are not part of a project. For example, the root eslint, or tsconfig file.',
966+
alias: 'g',
967+
hidden: true,
968+
})
969+
.option('command', { type: 'string', hidden: true })
970+
.option('verbose', {
971+
type: 'boolean',
972+
description:
973+
'Run watch mode in verbose mode, where commands are logged before execution.',
974+
})
975+
.conflicts({
976+
all: 'projects',
977+
})
978+
.check((args) => {
979+
if (!args.all && !args.projects) {
980+
throw Error('Please specify either --all or --projects');
981+
}
982+
983+
return true;
984+
})
985+
.middleware((args) => {
986+
const { '--': doubledash } = args;
987+
if (doubledash && Array.isArray(doubledash)) {
988+
args.command = (doubledash as string[]).join(' ');
989+
} else {
990+
throw Error('No command specified for watch mode.');
991+
}
992+
}, true);
993+
}
994+
930995
function parseCSV(args: string[]) {
931996
if (!args) {
932997
return args;

0 commit comments

Comments
 (0)