Skip to content

Commit

Permalink
feat(core): add watch command (#13664)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cammisuli committed Dec 13, 2022
1 parent 2f4435b commit a1d9c46
Show file tree
Hide file tree
Showing 8 changed files with 526 additions and 2 deletions.
74 changes: 74 additions & 0 deletions docs/generated/cli/watch.md
@@ -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
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
Expand Up @@ -1425,6 +1425,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
@@ -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
Expand Up @@ -74,7 +74,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 @@ -1006,7 +1006,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
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
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

1 comment on commit a1d9c46

@vercel
Copy link

@vercel vercel bot commented on a1d9c46 Dec 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-dev-git-master-nrwl.vercel.app
nx.dev
nx-dev-nrwl.vercel.app
nx-five.vercel.app

Please sign in to comment.