feat(core): add --affected to show only affected projects (#16970)

This commit is contained in:
Craigory Coppola 2023-05-13 01:48:35 -04:00 committed by GitHub
parent c71027fbfb
commit b20e906f00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 428 additions and 193 deletions

View File

@ -55,12 +55,6 @@ Open the project graph of the workspace in the browser, highlight the projects a
## Options
### all
Type: `boolean`
All projects
### base
Type: `string`

View File

@ -73,11 +73,11 @@ Use the currently executing project name in your command.:
## Options
### all
### ~~all~~
Type: `boolean`
All projects
**Deprecated:** Use `nx run-many` instead
### base

View File

@ -21,7 +21,7 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`
Type: `boolean`
All projects
Format all projects
### base

View File

@ -21,7 +21,7 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`
Type: `boolean`
All projects
Format all projects
### base

View File

@ -49,12 +49,6 @@ Prints the tasks.target.project property from the print-affected output:
## Options
### all
Type: `boolean`
All projects
### base
Type: `string`

View File

@ -10,11 +10,31 @@ Show information about the workspace (e.g., list of projects)
## Usage
```shell
nx show <object>
nx show
```
Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`.
### Examples
Show all projects in the workspace:
```shell
nx show projects
```
Show affected projects in the workspace:
```shell
nx show projects --affected
```
Show affected projects in the workspace, excluding end-to-end projects:
```shell
nx show projects --affected --exclude *-e2e
```
## Options
### help
@ -23,14 +43,74 @@ Type: `boolean`
Show help
### object
Choices: [projects]
What to show (e.g., projects)
### version
Type: `boolean`
Show version number
## Subcommands
### projects
Show a list of projects in the workspace
```shell
nx show projects
```
#### Options
##### affected
Type: `boolean`
Show only affected projects
##### base
Type: `string`
Base of the current branch (usually main)
##### exclude
Type: `string`
Exclude certain projects from being processed
##### files
Type: `string`
Change the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas or spaces
##### head
Type: `string`
Latest commit of the current branch (usually HEAD)
##### help
Type: `boolean`
Show help
##### uncommitted
Type: `boolean`
Uncommitted changes
##### untracked
Type: `boolean`
Untracked changes
##### version
Type: `boolean`
Show version number

View File

@ -55,12 +55,6 @@ Open the project graph of the workspace in the browser, highlight the projects a
## Options
### all
Type: `boolean`
All projects
### base
Type: `string`

View File

@ -73,11 +73,11 @@ Use the currently executing project name in your command.:
## Options
### all
### ~~all~~
Type: `boolean`
All projects
**Deprecated:** Use `nx run-many` instead
### base

View File

@ -21,7 +21,7 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`
Type: `boolean`
All projects
Format all projects
### base

View File

@ -21,7 +21,7 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`
Type: `boolean`
All projects
Format all projects
### base

View File

@ -49,12 +49,6 @@ Prints the tasks.target.project property from the print-affected output:
## Options
### all
Type: `boolean`
All projects
### base
Type: `string`

View File

@ -10,11 +10,31 @@ Show information about the workspace (e.g., list of projects)
## Usage
```shell
nx show <object>
nx show
```
Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`.
### Examples
Show all projects in the workspace:
```shell
nx show projects
```
Show affected projects in the workspace:
```shell
nx show projects --affected
```
Show affected projects in the workspace, excluding end-to-end projects:
```shell
nx show projects --affected --exclude *-e2e
```
## Options
### help
@ -23,14 +43,74 @@ Type: `boolean`
Show help
### object
Choices: [projects]
What to show (e.g., projects)
### version
Type: `boolean`
Show version number
## Subcommands
### projects
Show a list of projects in the workspace
```shell
nx show projects
```
#### Options
##### affected
Type: `boolean`
Show only affected projects
##### base
Type: `string`
Base of the current branch (usually main)
##### exclude
Type: `string`
Exclude certain projects from being processed
##### files
Type: `string`
Change the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas or spaces
##### head
Type: `string`
Latest commit of the current branch (usually HEAD)
##### help
Type: `boolean`
Show help
##### uncommitted
Type: `boolean`
Uncommitted changes
##### untracked
Type: `boolean`
Untracked changes
##### version
Type: `boolean`
Show version number

View File

@ -65,46 +65,46 @@ describe('Nx Affected and Graph Tests', () => {
);
const affectedApps = runCLI(
`print-affected --files="libs/${mylib}/src/index.ts" --select projects`
`show projects --affected --files="libs/${mylib}/src/index.ts"`
);
expect(affectedApps).toContain(myapp);
expect(affectedApps).not.toContain(myapp2);
const implicitlyAffectedApps = runCLI(
'print-affected --select projects --files="tsconfig.base.json"'
'show projects --affected --files="tsconfig.base.json"'
);
expect(implicitlyAffectedApps).toContain(myapp);
expect(implicitlyAffectedApps).toContain(myapp2);
const noAffectedApps = runCLI(
'print-affected --select projects --files="README.md"'
'show projects --affected projects --files="README.md"'
);
expect(noAffectedApps).not.toContain(myapp);
expect(noAffectedApps).not.toContain(myapp2);
const affectedLibs = runCLI(
`print-affected --select projects --files="libs/${mylib}/src/index.ts"`
`show projects --affected --files="libs/${mylib}/src/index.ts"`
);
expect(affectedLibs).toContain(mypublishablelib);
expect(affectedLibs).toContain(mylib);
expect(affectedLibs).not.toContain(mylib2);
const implicitlyAffectedLibs = runCLI(
'print-affected --select projects --files="tsconfig.base.json"'
'show projects --affected --files="tsconfig.base.json"'
);
expect(implicitlyAffectedLibs).toContain(mypublishablelib);
expect(implicitlyAffectedLibs).toContain(mylib);
expect(implicitlyAffectedLibs).toContain(mylib2);
const noAffectedLibsNonExistentFile = runCLI(
'print-affected --select projects --files="tsconfig.json"'
'show projects --affected --files="tsconfig.json"'
);
expect(noAffectedLibsNonExistentFile).not.toContain(mypublishablelib);
expect(noAffectedLibsNonExistentFile).not.toContain(mylib);
expect(noAffectedLibsNonExistentFile).not.toContain(mylib2);
const noAffectedLibs = runCLI(
'print-affected --select projects --files="README.md"'
'show projects --affected --files="README.md"'
);
expect(noAffectedLibs).not.toContain(mypublishablelib);
expect(noAffectedLibs).not.toContain(mylib);

View File

@ -205,7 +205,7 @@
"lines-and-columns": "~2.0.3",
"loader-utils": "2.0.3",
"magic-string": "~0.26.2",
"markdown-factory": "^0.0.3",
"markdown-factory": "^0.0.5",
"memfs": "^3.0.1",
"metro-resolver": "^0.74.1",
"mini-css-extract-plugin": "~2.4.7",

View File

@ -1,4 +1,4 @@
import { CommandModule } from 'yargs';
import { boolean, CommandModule, middleware } from 'yargs';
import { linkToNxDevAndExamples } from '../yargs-utils/documentation';
import {
withAffectedOptions,
@ -19,7 +19,18 @@ export const yargsAffectedCommand: CommandModule = {
withRunOptions(
withOutputStyleOption(withTargetAndConfigurationOption(yargs))
)
),
)
.option('all', {
type: 'boolean',
deprecated: 'Use `nx run-many` instead',
})
.middleware((args) => {
if (args.all !== undefined) {
throw new Error(
"The '--all' option has been removed for `nx affected`. Use 'nx run-many' instead."
);
}
}),
'affected'
),
handler: async (args) =>

View File

@ -339,6 +339,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 "',
},
],
show: [
{
command: 'show projects',
description: 'Show all projects in the workspace',
},
{
command: 'show projects --affected',
description: 'Show affected projects in the workspace',
},
{
command: 'show projects --affected --exclude *-e2e',
description:
'Show affected projects in the workspace, excluding end-to-end projects',
},
],
watch: [
{
command:

View File

@ -39,6 +39,10 @@ function withFormatOptions(yargs: Argv): Argv {
type: 'string',
coerce: parseCSV,
})
.option('all', {
describe: 'Format all projects',
type: 'boolean',
})
.conflicts({
all: 'projects',
});

View File

@ -1,45 +1,47 @@
import { Argv, CommandModule } from 'yargs';
import { CommandModule } from 'yargs';
import { withAffectedOptions } from '../yargs-utils/shared-options';
import { ShowProjectOptions } from './show';
const validObjectTypes = ['projects'] as const;
type NxObject = typeof validObjectTypes[number];
interface ShowCommandArguments {
object: NxObject;
}
export const yargsShowCommand: CommandModule<
ShowCommandArguments,
ShowCommandArguments
> = {
command: 'show <object>',
export const yargsShowCommand: CommandModule = {
command: 'show',
describe: 'Show information about the workspace (e.g., list of projects)',
builder: (yargs) => withShowOptions(yargs),
builder: (yargs) =>
yargs
.command(showProjectsCommand)
.demandCommand()
.example(
'$0 show projects',
'Show a list of all projects in the workspace'
)
.example(
'$0 show projects --affected',
'Show affected projects in the workspace'
)
.example(
'$0 show projects --affected --exclude *-e2e',
'Show affected projects in the workspace, excluding end-to-end projects'
),
handler: async (args) => {
if (!validObjectTypes.includes(args.object)) {
}
await import('./show').then((m) => m.show(args));
process.exit(0);
// Noop, yargs will error if not in a subcommand.
},
};
function withShowOptions(yargs: Argv) {
return yargs
.positional('object', {
describe: 'What to show (e.g., projects)',
choices: ['projects'],
required: true,
const showProjectsCommand: CommandModule<
Record<string, unknown>,
ShowProjectOptions
> = {
command: 'projects',
describe: 'Show a list of projects in the workspace',
builder: (yargs) =>
withAffectedOptions(yargs)
.option('affected', {
type: 'boolean',
description: 'Show only affected projects',
})
.coerce({
object: (arg) => {
if (validObjectTypes.includes(arg)) {
return arg;
} else {
throw new Error(
`Invalid object type: ${arg}. Valid object types are: ${validObjectTypes.join(
', '
)}`
);
}
},
});
}
.implies('untracked', 'affected')
.implies('uncommitted', 'affected')
.implies('files', 'affected')
.implies('base', 'affected')
.implies('head', 'affected'),
handler: (args) => import('./show').then((m) => m.showProjectsHandler(args)),
};

View File

@ -1,13 +1,74 @@
import { filterAffected } from '../../project-graph/affected/affected-project-graph';
import {
calculateFileChanges,
readNxJson,
} from '../../project-graph/file-utils';
import {
NxArgs,
parseFiles,
splitArgsIntoNxArgsAndOverrides,
} from '../../utils/command-line-utils';
import { createProjectGraphAsync } from '../../project-graph/project-graph';
import { NxJsonConfiguration } from '../../config/nx-json';
import { ProjectGraph } from '../../config/project-graph';
import { findMatchingProjects } from '../../utils/find-matching-projects';
export async function show(args: { object: 'projects' }): Promise<void> {
if (args.object == 'projects') {
const graph = await createProjectGraphAsync();
const projects = Object.keys(graph.nodes).join('\n');
export type ShowProjectOptions = {
exclude: string;
files: string;
uncommitted: any;
untracked: any;
base: string;
head: string;
affected: boolean;
};
export async function showProjectsHandler(
args: ShowProjectOptions
): Promise<void> {
let graph = await createProjectGraphAsync();
const nxJson = readNxJson();
const { nxArgs } = splitArgsIntoNxArgsAndOverrides(
args,
'affected',
{
printWarnings: false,
},
nxJson
);
if (args.affected) {
graph = await getAffectedGraph(nxArgs, nxJson, graph);
}
const selectedProjects = new Set(Object.keys(graph.nodes));
if (args.exclude) {
const excludedProjects = findMatchingProjects(nxArgs.exclude, graph.nodes);
for (const excludedProject of excludedProjects) {
selectedProjects.delete(excludedProject);
}
}
const projects = Array.from(selectedProjects).join('\n');
if (projects.length) {
console.log(projects);
}
} else {
throw new Error(`Unrecognized option: ${args.object}`);
process.exit(0);
}
function getAffectedGraph(
nxArgs: NxArgs,
nxJson: NxJsonConfiguration<'*' | string[]>,
graph: ProjectGraph
) {
return filterAffected(
graph,
calculateFileChanges(
parseFiles(nxArgs).files,
graph.allWorkspaceFiles,
nxArgs
),
nxJson
);
}

View File

@ -1,6 +1,6 @@
import { Argv } from 'yargs';
export function withExcludeOption(yargs: Argv): Argv {
export function withExcludeOption(yargs: Argv) {
return yargs.option('exclude', {
describe: 'Exclude certain projects from being processed',
type: 'string',
@ -8,7 +8,7 @@ export function withExcludeOption(yargs: Argv): Argv {
});
}
export function withRunOptions(yargs: Argv): Argv {
export function withRunOptions(yargs: Argv) {
return withExcludeOption(yargs)
.option('parallel', {
describe: 'Max number of parallel processes [default is 3]',
@ -74,7 +74,7 @@ export function withRunOptions(yargs: Argv): Argv {
export function withTargetAndConfigurationOption(
yargs: Argv,
demandOption = true
): Argv {
) {
return withConfiguration(yargs).option('targets', {
describe: 'Tasks to run for affected projects',
type: 'string',
@ -95,7 +95,7 @@ export function withConfiguration(yargs: Argv) {
});
}
export function withAffectedOptions(yargs: Argv): Argv {
export function withAffectedOptions(yargs: Argv) {
return withExcludeOption(yargs)
.parserConfiguration({
'strip-dashed': true,
@ -112,17 +112,10 @@ export function withAffectedOptions(yargs: Argv): Argv {
.option('uncommitted', {
describe: 'Uncommitted changes',
type: 'boolean',
default: undefined,
})
.option('untracked', {
describe: 'Untracked changes',
type: 'boolean',
default: undefined,
})
.option('all', {
describe: 'All projects',
type: 'boolean',
default: undefined,
})
.option('base', {
describe: 'Base of the current branch (usually main)',
@ -145,14 +138,13 @@ export function withAffectedOptions(yargs: Argv): Argv {
.group(['files', 'uncommitted', 'untracked'], 'or using:')
.implies('head', 'base')
.conflicts({
files: ['uncommitted', 'untracked', 'base', 'head', 'all'],
untracked: ['uncommitted', 'files', 'base', 'head', 'all'],
uncommitted: ['files', 'untracked', 'base', 'head', 'all'],
all: ['files', 'untracked', 'uncommitted', 'base', 'head'],
files: ['uncommitted', 'untracked', 'base', 'head'],
untracked: ['uncommitted', 'files', 'base', 'head'],
uncommitted: ['files', 'untracked', 'base', 'head'],
});
}
export function withRunManyOptions(yargs: Argv): Argv {
export function withRunManyOptions(yargs: Argv) {
return withRunOptions(yargs)
.parserConfiguration({
'strip-dashed': true,
@ -185,7 +177,7 @@ export function withOverrides(args: any): any {
export function withOutputStyleOption(
yargs: Argv,
choices = ['dynamic', 'static', 'stream', 'stream-without-prefixes']
): Argv {
) {
return yargs.option('output-style', {
describe: 'Defines how Nx emits outputs tasks logs',
type: 'string',
@ -193,7 +185,7 @@ export function withOutputStyleOption(
});
}
export function withDepGraphOptions(yargs: Argv): Argv {
export function withDepGraphOptions(yargs: Argv) {
return yargs
.option('file', {
describe:

View File

@ -111,21 +111,6 @@ export function splitArgsIntoNxArgsAndOverrides(
});
}
if (
!nxArgs.files &&
!nxArgs.uncommitted &&
!nxArgs.untracked &&
!nxArgs.base &&
!nxArgs.head &&
!nxArgs.all &&
overrides._ &&
overrides._.length >= 2
) {
throw new Error(
`Nx no longer supports using positional arguments for base and head. Please use --base and --head instead.`
);
}
// Allow setting base and head via environment variables (lower priority then direct command arguments)
if (!nxArgs.base && process.env.NX_BASE) {
nxArgs.base = process.env.NX_BASE;

8
pnpm-lock.yaml generated
View File

@ -665,8 +665,8 @@ devDependencies:
specifier: ~0.26.2
version: 0.26.2
markdown-factory:
specifier: ^0.0.3
version: 0.0.3
specifier: ^0.0.5
version: 0.0.5
memfs:
specifier: ^3.0.1
version: 3.4.7
@ -17673,8 +17673,8 @@ packages:
resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==}
dev: true
/markdown-factory@0.0.3:
resolution: {integrity: sha512-y7XffnQ61exHdRdFmvp9MeJBGOpUIYJEdiNgj25VCCivA/oXs615ppGqH01lnG8zcZ9cEEPW1jIi1y/Xu4URPA==}
/markdown-factory@0.0.5:
resolution: {integrity: sha512-76bovWfQkv0Pd4qrq3h5sTiimoBUIi3sKac6CUKGPLgEhghrPGBgdXRhVH9sjDpbbyCspRPg940WVrcA9h+XPg==}
dev: true
/markdown-to-jsx@7.2.0(react@18.2.0):

View File

@ -1,11 +1,13 @@
import * as chalk from 'chalk';
import { readFileSync } from 'fs';
import { readJsonSync } from 'fs-extra';
import { codeBlock, h1, h2, h3, lines } from 'markdown-factory';
import { join } from 'path';
import { register as registerTsConfigPaths } from 'tsconfig-paths';
import { examples } from '../../../packages/nx/src/command-line/examples';
import {
formatDeprecated,
formatDescription,
generateMarkdownFile,
generateOptionsMarkdown,
getCommands,
@ -39,39 +41,52 @@ export async function generateCliDocumentation(
);
function generateMarkdown(command: ParsedCommand) {
let template = `
let templateLines = [
`
---
title: "${command.name} - CLI command"
description: "${command.description}"
---
# ${command.name}
${formatDeprecated(command.description, command.deprecated)}
## Usage
\`\`\`shell
nx ${command.commandString}
\`\`\`
Install \`nx\` globally to invoke the command directly using \`nx\`, or use \`npx nx\`, \`yarn nx\`, or \`pnpm nx\`.\n`;
---`,
h1(command.name),
formatDescription(command.description, command.deprecated),
h2('Usage'),
codeBlock(`nx ${command.commandString}`, 'shell'),
'Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`.',
];
if (examples[command.name] && examples[command.name].length > 0) {
template += `\n### Examples\n`;
templateLines.push(h3('Examples'));
examples[command.name].forEach((example) => {
template += `${example.description}:\n\`\`\`shell\n nx ${example.command}\n\`\`\`\n`;
templateLines.push(
example.description + ':',
codeBlock(` nx ${example.command}`, 'shell')
);
});
}
template += generateOptionsMarkdown(command);
templateLines.push(generateOptionsMarkdown(command));
if (command.subcommands?.length) {
templateLines.push(h2('Subcommands'));
for (const subcommand of command.subcommands) {
templateLines.push(
h3(subcommand.name),
formatDescription(subcommand.description, subcommand.deprecated),
codeBlock(
`nx ${command.commandString} ${subcommand.commandString}`,
'shell'
),
generateOptionsMarkdown(subcommand, 2)
);
}
}
return {
name: command.name
.replace(':', '-')
.replace(' ', '-')
.replace(/[\]\[.]+/gm, ''),
template,
template: lines(templateLines),
};
}

View File

@ -1,4 +1,5 @@
import { outputFileSync } from 'fs-extra';
import { bold, h, lines as mdLines, strikethrough } from 'markdown-factory';
import { join } from 'path';
import { format, resolveConfig } from 'prettier';
@ -65,7 +66,7 @@ export async function formatWithPrettier(filePath: string, content: string) {
return format(content, options);
}
export function formatDeprecated(
export function formatDescription(
description: string,
deprecated: boolean | string
) {
@ -73,12 +74,8 @@ export function formatDeprecated(
return description;
}
return deprecated === true
? `**Deprecated:** ${description}`
: `
**Deprecated:** ${deprecated}
${description}
`;
? `${bold('Deprecated:')} ${description}`
: mdLines(`${bold('Deprecated:')} ${deprecated}`, description);
}
export function getCommands(command: any) {
@ -87,9 +84,12 @@ export function getCommands(command: any) {
export interface ParsedCommandOption {
name: string;
type: string;
description: string;
default: string;
deprecated: boolean | string;
hidden: boolean;
choices?: string[];
}
export interface ParsedCommand {
@ -98,6 +98,7 @@ export interface ParsedCommand {
description: string;
deprecated: string;
options?: Array<ParsedCommandOption>;
subcommands?: Array<ParsedCommand>;
}
const YargsTypes = ['array', 'count', 'string', 'boolean', 'number'];
@ -142,6 +143,12 @@ export async function parseCommand(
);
return acc;
}, {});
const subcommands = await Promise.all(
Object.entries(getCommands(builder)).map(
([subCommandName, subCommandConfig]) =>
parseCommand(subCommandName, subCommandConfig)
)
);
return {
name,
@ -160,39 +167,44 @@ export async function parseCommand(
deprecated: builderDeprecatedOptions[key],
hidden: builderOptions.hiddenOptions.includes(key),
})) || null,
subcommands,
};
}
export function generateOptionsMarkdown(command: any): string {
let response = '';
export function generateOptionsMarkdown(
command: ParsedCommand,
extraHeadingLevels = 0
): string {
const lines: string[] = [];
if (Array.isArray(command.options) && !!command.options.length) {
response += '\n## Options\n';
lines.push(h(2 + extraHeadingLevels, 'Options'));
command.options
.sort((a: any, b: any) => sortAlphabeticallyFunction(a.name, b.name))
.filter(({ hidden }: any) => !hidden)
.forEach((option: any) => {
response += `\n### ${
option.deprecated ? `~~${option.name}~~` : option.name
}\n`;
.sort((a, b) => sortAlphabeticallyFunction(a.name, b.name))
.filter(({ hidden }) => !hidden)
.forEach((option) => {
lines.push(
h(
3 + extraHeadingLevels,
option.deprecated ? strikethrough(option.name) : option.name
)
);
if (option.type !== undefined && option.type !== '') {
response += `\nType: \`${option.type}\`\n`;
lines.push(`Type: \`${option.type}\``);
}
if (option.choices !== undefined) {
const choices = option.choices
.map((c: any) => JSON.stringify(c).replace(/"/g, ''))
.join(', ');
response += `\nChoices: [${choices}]\n`;
lines.push(`Choices: [${choices}]`);
}
if (option.default !== undefined && option.default !== '') {
response += `\nDefault: \`${JSON.stringify(option.default).replace(
/"/g,
''
)}\`\n`;
lines.push(
`Default: \`${JSON.stringify(option.default).replace(/"/g, '')}\``
);
}
response +=
'\n' + formatDeprecated(option.description, option.deprecated);
lines.push(formatDescription(option.description, option.deprecated));
});
}
return response;
return mdLines(lines);
}