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 ## Options
### all
Type: `boolean`
All projects
### base ### base
Type: `string` Type: `string`

View File

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

View File

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

View File

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

View File

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

View File

@ -10,11 +10,31 @@ Show information about the workspace (e.g., list of projects)
## Usage ## Usage
```shell ```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`. 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 ## Options
### help ### help
@ -23,14 +43,74 @@ Type: `boolean`
Show help Show help
### object
Choices: [projects]
What to show (e.g., projects)
### version ### version
Type: `boolean` Type: `boolean`
Show version number 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

@ -5,9 +5,9 @@ description: 'Runs a workspace generator from the tools/generators directory'
# workspace-generator # workspace-generator
**Deprecated:** Use a local plugin instead. See: https://nx.dev/deprecated/workspace-generators **Deprecated:** Use a local plugin instead. See: https://nx.dev/deprecated/workspace-generators
Runs a workspace generator from the tools/generators directory Runs a workspace generator from the tools/generators directory
## Usage ## Usage

View File

@ -5,9 +5,9 @@ description: 'Lint nx specific workspace files (nx.json, workspace.json)'
# workspace-lint # workspace-lint
**Deprecated:** workspace-lint is deprecated, and will be removed in v17. The checks it used to perform are no longer relevant. **Deprecated:** workspace-lint is deprecated, and will be removed in v17. The checks it used to perform are no longer relevant.
Lint nx specific workspace files (nx.json, workspace.json) Lint nx specific workspace files (nx.json, workspace.json)
## Usage ## Usage

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,11 +10,31 @@ Show information about the workspace (e.g., list of projects)
## Usage ## Usage
```shell ```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`. 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 ## Options
### help ### help
@ -23,14 +43,74 @@ Type: `boolean`
Show help Show help
### object
Choices: [projects]
What to show (e.g., projects)
### version ### version
Type: `boolean` Type: `boolean`
Show version number 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

@ -5,9 +5,9 @@ description: 'Runs a workspace generator from the tools/generators directory'
# workspace-generator # workspace-generator
**Deprecated:** Use a local plugin instead. See: https://nx.dev/deprecated/workspace-generators **Deprecated:** Use a local plugin instead. See: https://nx.dev/deprecated/workspace-generators
Runs a workspace generator from the tools/generators directory Runs a workspace generator from the tools/generators directory
## Usage ## Usage

View File

@ -5,9 +5,9 @@ description: 'Lint nx specific workspace files (nx.json, workspace.json)'
# workspace-lint # workspace-lint
**Deprecated:** workspace-lint is deprecated, and will be removed in v17. The checks it used to perform are no longer relevant. **Deprecated:** workspace-lint is deprecated, and will be removed in v17. The checks it used to perform are no longer relevant.
Lint nx specific workspace files (nx.json, workspace.json) Lint nx specific workspace files (nx.json, workspace.json)
## Usage ## Usage

View File

@ -65,46 +65,46 @@ describe('Nx Affected and Graph Tests', () => {
); );
const affectedApps = runCLI( 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).toContain(myapp);
expect(affectedApps).not.toContain(myapp2); expect(affectedApps).not.toContain(myapp2);
const implicitlyAffectedApps = runCLI( 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(myapp);
expect(implicitlyAffectedApps).toContain(myapp2); expect(implicitlyAffectedApps).toContain(myapp2);
const noAffectedApps = runCLI( 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(myapp);
expect(noAffectedApps).not.toContain(myapp2); expect(noAffectedApps).not.toContain(myapp2);
const affectedLibs = runCLI( 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(mypublishablelib);
expect(affectedLibs).toContain(mylib); expect(affectedLibs).toContain(mylib);
expect(affectedLibs).not.toContain(mylib2); expect(affectedLibs).not.toContain(mylib2);
const implicitlyAffectedLibs = runCLI( 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(mypublishablelib);
expect(implicitlyAffectedLibs).toContain(mylib); expect(implicitlyAffectedLibs).toContain(mylib);
expect(implicitlyAffectedLibs).toContain(mylib2); expect(implicitlyAffectedLibs).toContain(mylib2);
const noAffectedLibsNonExistentFile = runCLI( 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(mypublishablelib);
expect(noAffectedLibsNonExistentFile).not.toContain(mylib); expect(noAffectedLibsNonExistentFile).not.toContain(mylib);
expect(noAffectedLibsNonExistentFile).not.toContain(mylib2); expect(noAffectedLibsNonExistentFile).not.toContain(mylib2);
const noAffectedLibs = runCLI( 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(mypublishablelib);
expect(noAffectedLibs).not.toContain(mylib); expect(noAffectedLibs).not.toContain(mylib);

View File

@ -205,7 +205,7 @@
"lines-and-columns": "~2.0.3", "lines-and-columns": "~2.0.3",
"loader-utils": "2.0.3", "loader-utils": "2.0.3",
"magic-string": "~0.26.2", "magic-string": "~0.26.2",
"markdown-factory": "^0.0.3", "markdown-factory": "^0.0.5",
"memfs": "^3.0.1", "memfs": "^3.0.1",
"metro-resolver": "^0.74.1", "metro-resolver": "^0.74.1",
"mini-css-extract-plugin": "~2.4.7", "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 { linkToNxDevAndExamples } from '../yargs-utils/documentation';
import { import {
withAffectedOptions, withAffectedOptions,
@ -19,7 +19,18 @@ export const yargsAffectedCommand: CommandModule = {
withRunOptions( withRunOptions(
withOutputStyleOption(withTargetAndConfigurationOption(yargs)) 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' 'affected'
), ),
handler: async (args) => 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 "', '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: [ watch: [
{ {
command: command:

View File

@ -39,6 +39,10 @@ function withFormatOptions(yargs: Argv): Argv {
type: 'string', type: 'string',
coerce: parseCSV, coerce: parseCSV,
}) })
.option('all', {
describe: 'Format all projects',
type: 'boolean',
})
.conflicts({ .conflicts({
all: 'projects', 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; export const yargsShowCommand: CommandModule = {
type NxObject = typeof validObjectTypes[number]; command: 'show',
interface ShowCommandArguments {
object: NxObject;
}
export const yargsShowCommand: CommandModule<
ShowCommandArguments,
ShowCommandArguments
> = {
command: 'show <object>',
describe: 'Show information about the workspace (e.g., list of projects)', 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) => { handler: async (args) => {
if (!validObjectTypes.includes(args.object)) { // Noop, yargs will error if not in a subcommand.
}
await import('./show').then((m) => m.show(args));
process.exit(0);
}, },
}; };
function withShowOptions(yargs: Argv) { const showProjectsCommand: CommandModule<
return yargs Record<string, unknown>,
.positional('object', { ShowProjectOptions
describe: 'What to show (e.g., projects)', > = {
choices: ['projects'], command: 'projects',
required: true, describe: 'Show a list of projects in the workspace',
}) builder: (yargs) =>
.coerce({ withAffectedOptions(yargs)
object: (arg) => { .option('affected', {
if (validObjectTypes.includes(arg)) { type: 'boolean',
return arg; description: 'Show only affected projects',
} else { })
throw new Error( .implies('untracked', 'affected')
`Invalid object type: ${arg}. Valid object types are: ${validObjectTypes.join( .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 { 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> { export type ShowProjectOptions = {
if (args.object == 'projects') { exclude: string;
const graph = await createProjectGraphAsync(); files: string;
const projects = Object.keys(graph.nodes).join('\n'); uncommitted: any;
if (projects.length) { untracked: any;
console.log(projects); base: string;
} head: string;
} else { affected: boolean;
throw new Error(`Unrecognized option: ${args.object}`); };
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);
}
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'; import { Argv } from 'yargs';
export function withExcludeOption(yargs: Argv): Argv { export function withExcludeOption(yargs: Argv) {
return yargs.option('exclude', { return yargs.option('exclude', {
describe: 'Exclude certain projects from being processed', describe: 'Exclude certain projects from being processed',
type: 'string', 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) return withExcludeOption(yargs)
.option('parallel', { .option('parallel', {
describe: 'Max number of parallel processes [default is 3]', describe: 'Max number of parallel processes [default is 3]',
@ -74,7 +74,7 @@ export function withRunOptions(yargs: Argv): Argv {
export function withTargetAndConfigurationOption( export function withTargetAndConfigurationOption(
yargs: Argv, yargs: Argv,
demandOption = true demandOption = true
): Argv { ) {
return withConfiguration(yargs).option('targets', { return withConfiguration(yargs).option('targets', {
describe: 'Tasks to run for affected projects', describe: 'Tasks to run for affected projects',
type: 'string', 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) return withExcludeOption(yargs)
.parserConfiguration({ .parserConfiguration({
'strip-dashed': true, 'strip-dashed': true,
@ -112,17 +112,10 @@ export function withAffectedOptions(yargs: Argv): Argv {
.option('uncommitted', { .option('uncommitted', {
describe: 'Uncommitted changes', describe: 'Uncommitted changes',
type: 'boolean', type: 'boolean',
default: undefined,
}) })
.option('untracked', { .option('untracked', {
describe: 'Untracked changes', describe: 'Untracked changes',
type: 'boolean', type: 'boolean',
default: undefined,
})
.option('all', {
describe: 'All projects',
type: 'boolean',
default: undefined,
}) })
.option('base', { .option('base', {
describe: 'Base of the current branch (usually main)', describe: 'Base of the current branch (usually main)',
@ -145,14 +138,13 @@ export function withAffectedOptions(yargs: Argv): Argv {
.group(['files', 'uncommitted', 'untracked'], 'or using:') .group(['files', 'uncommitted', 'untracked'], 'or using:')
.implies('head', 'base') .implies('head', 'base')
.conflicts({ .conflicts({
files: ['uncommitted', 'untracked', 'base', 'head', 'all'], files: ['uncommitted', 'untracked', 'base', 'head'],
untracked: ['uncommitted', 'files', 'base', 'head', 'all'], untracked: ['uncommitted', 'files', 'base', 'head'],
uncommitted: ['files', 'untracked', 'base', 'head', 'all'], uncommitted: ['files', 'untracked', 'base', 'head'],
all: ['files', 'untracked', 'uncommitted', 'base', 'head'],
}); });
} }
export function withRunManyOptions(yargs: Argv): Argv { export function withRunManyOptions(yargs: Argv) {
return withRunOptions(yargs) return withRunOptions(yargs)
.parserConfiguration({ .parserConfiguration({
'strip-dashed': true, 'strip-dashed': true,
@ -185,7 +177,7 @@ export function withOverrides(args: any): any {
export function withOutputStyleOption( export function withOutputStyleOption(
yargs: Argv, yargs: Argv,
choices = ['dynamic', 'static', 'stream', 'stream-without-prefixes'] choices = ['dynamic', 'static', 'stream', 'stream-without-prefixes']
): Argv { ) {
return yargs.option('output-style', { return yargs.option('output-style', {
describe: 'Defines how Nx emits outputs tasks logs', describe: 'Defines how Nx emits outputs tasks logs',
type: 'string', type: 'string',
@ -193,7 +185,7 @@ export function withOutputStyleOption(
}); });
} }
export function withDepGraphOptions(yargs: Argv): Argv { export function withDepGraphOptions(yargs: Argv) {
return yargs return yargs
.option('file', { .option('file', {
describe: 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) // Allow setting base and head via environment variables (lower priority then direct command arguments)
if (!nxArgs.base && process.env.NX_BASE) { if (!nxArgs.base && process.env.NX_BASE) {
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 specifier: ~0.26.2
version: 0.26.2 version: 0.26.2
markdown-factory: markdown-factory:
specifier: ^0.0.3 specifier: ^0.0.5
version: 0.0.3 version: 0.0.5
memfs: memfs:
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.4.7 version: 3.4.7
@ -17673,8 +17673,8 @@ packages:
resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==}
dev: true dev: true
/markdown-factory@0.0.3: /markdown-factory@0.0.5:
resolution: {integrity: sha512-y7XffnQ61exHdRdFmvp9MeJBGOpUIYJEdiNgj25VCCivA/oXs615ppGqH01lnG8zcZ9cEEPW1jIi1y/Xu4URPA==} resolution: {integrity: sha512-76bovWfQkv0Pd4qrq3h5sTiimoBUIi3sKac6CUKGPLgEhghrPGBgdXRhVH9sjDpbbyCspRPg940WVrcA9h+XPg==}
dev: true dev: true
/markdown-to-jsx@7.2.0(react@18.2.0): /markdown-to-jsx@7.2.0(react@18.2.0):

View File

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

View File

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