feat(core): add an option to seperate the output of show with provide… (#23172)

Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
This commit is contained in:
Daniel Santiago 2024-05-07 19:55:41 +02:00 committed by GitHub
parent 5edc64af92
commit d9a97120f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 484 additions and 85 deletions

View File

@ -145,6 +145,12 @@ Type: `string`
Show only projects that match a given pattern.
##### sep
Type: `string`
Outputs projects with the specified seperator
##### type
Type: `string`

View File

@ -145,6 +145,12 @@ Type: `string`
Show only projects that match a given pattern.
##### sep
Type: `string`
Outputs projects with the specified seperator
##### type
Type: `string`

View File

@ -12,17 +12,18 @@ export interface NxShowArgs {
}
export type ShowProjectsOptions = NxShowArgs & {
exclude: string;
files: string;
uncommitted: any;
untracked: any;
base: string;
head: string;
affected: boolean;
type: ProjectGraphProjectNode['type'];
projects: string[];
withTarget: string[];
verbose: boolean;
exclude?: string[];
files?: string;
uncommitted?: any;
untracked?: any;
base?: string;
head?: string;
affected?: boolean;
type?: ProjectGraphProjectNode['type'];
projects?: string[];
withTarget?: string[];
verbose?: boolean;
sep?: string;
};
export type ShowProjectOptions = NxShowArgs & {
@ -90,11 +91,17 @@ const showProjectsCommand: CommandModule<NxShowArgs, ShowProjectsOptions> = {
description: 'Select only projects of the given type',
choices: ['app', 'lib', 'e2e'],
})
.option('sep', {
type: 'string',
description: 'Outputs projects with the specified seperator',
})
.implies('untracked', 'affected')
.implies('uncommitted', 'affected')
.implies('files', 'affected')
.implies('base', 'affected')
.implies('head', 'affected')
.conflicts('sep', 'json')
.conflicts('json', 'sep')
.example(
'$0 show projects --projects "apps/*"',
'Show all projects in the apps directory'
@ -119,7 +126,9 @@ const showProjectsCommand: CommandModule<NxShowArgs, ShowProjectsOptions> = {
return handleErrors(
args.verbose ?? process.env.NX_VERBOSE_LOGGING === 'true',
async () => {
return (await import('./show')).showProjectsHandler(args);
const { showProjectsHandler } = await import('./projects');
await showProjectsHandler(args);
process.exit(0);
}
);
},
@ -145,21 +154,23 @@ const showProjectCommand: CommandModule<NxShowArgs, ShowProjectOptions> = {
description:
'Prints additional information about the commands (e.g., stack traces)',
})
.check((argv) => {
if (argv.web) {
argv.json = false;
}
return true;
})
.conflicts('json', 'web')
.conflicts('web', 'json')
.example(
'$0 show project my-app',
'View project information for my-app in JSON format'
)
.example(
'$0 show project my-app --web',
'View project information for my-app in the browser'
),
handler: (args) => {
return handleErrors(
args.verbose ?? process.env.NX_VERBOSE_LOGGING === 'true',
async () => {
return (await import('./show')).showProjectHandler(args);
const { showProjectHandler } = await import('./project');
await showProjectHandler(args);
process.exit(0);
}
);
},

View File

@ -0,0 +1,67 @@
import { output } from '../../utils/output';
import { createProjectGraphAsync } from '../../project-graph/project-graph';
import { ShowProjectOptions } from './command-object';
import { generateGraph } from '../graph/graph';
export async function showProjectHandler(
args: ShowProjectOptions
): Promise<void> {
const graph = await createProjectGraphAsync();
const node = graph.nodes[args.projectName];
if (!node) {
console.log(`Could not find project ${args.projectName}`);
process.exit(1);
}
if (args.json) {
console.log(JSON.stringify(node.data));
} else if (args.web) {
await generateGraph(
{
view: 'project-details',
focus: node.name,
watch: true,
open: true,
},
[]
);
} else {
const chalk = require('chalk') as typeof import('chalk');
const logIfExists = (label, key: keyof typeof node['data']) => {
if (node.data[key]) {
console.log(`${chalk.bold(label)}: ${node.data[key]}`);
}
};
logIfExists('Name', 'name');
logIfExists('Root', 'root');
logIfExists('Source Root', 'sourceRoot');
logIfExists('Tags', 'tags');
logIfExists('Implicit Dependencies', 'implicitDependencies');
const targets = Object.entries(node.data.targets ?? {});
const maxTargetNameLength = Math.max(...targets.map(([t]) => t.length));
const maxExecutorNameLength = Math.max(
...targets.map(([, t]) => t?.executor?.length ?? 0)
);
if (targets.length > 0) {
console.log(`${chalk.bold('Targets')}: `);
for (const [target, targetConfig] of targets) {
console.log(
`- ${chalk.bold((target + ':').padEnd(maxTargetNameLength + 2))} ${(
targetConfig?.executor ?? ''
).padEnd(maxExecutorNameLength + 2)} ${(() => {
const configurations = Object.keys(
targetConfig.configurations ?? {}
);
if (configurations.length) {
return chalk.dim(configurations.join(', '));
}
return '';
})()}`
);
}
}
}
await output.drain();
}

View File

@ -0,0 +1,371 @@
import type {
ProjectGraph,
ProjectGraphProjectNode,
} from '../../config/project-graph';
import type { ProjectConfiguration } from '../../config/workspace-json-project-json';
import { showProjectsHandler } from './projects';
let graph: ProjectGraph = {
nodes: {},
dependencies: {},
externalNodes: {},
};
jest.mock('../../project-graph/project-graph', () => ({
...(jest.requireActual(
'../../project-graph/project-graph'
) as typeof import('../../project-graph/project-graph')),
createProjectGraphAsync: jest
.fn()
.mockImplementation(() => Promise.resolve(graph)),
}));
describe('show projects', () => {
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should print out projects with provided seperator value', async () => {
graph = new GraphBuilder()
.addProjectConfiguration(
{
root: 'proj1',
name: 'proj1',
},
'app'
)
.addProjectConfiguration(
{
root: 'proj2',
name: 'proj2',
},
'lib'
)
.addProjectConfiguration(
{
root: 'proj3',
name: 'proj3',
},
'lib'
)
.build();
await showProjectsHandler({
sep: ',',
});
expect(console.log).toHaveBeenCalledWith('proj1,proj2,proj3');
});
it('should default to printing one project per line', async () => {
graph = new GraphBuilder()
.addProjectConfiguration(
{
root: 'proj1',
name: 'proj1',
},
'app'
)
.addProjectConfiguration(
{
root: 'proj2',
name: 'proj2',
},
'lib'
)
.addProjectConfiguration(
{
root: 'proj3',
name: 'proj3',
},
'lib'
)
.build();
await showProjectsHandler({});
expect(console.log).toHaveBeenCalledWith('proj1');
expect(console.log).toHaveBeenCalledWith('proj2');
expect(console.log).toHaveBeenCalledWith('proj3');
expect(console.log).toHaveBeenCalledTimes(3);
});
it('should print out projects in json format', async () => {
graph = new GraphBuilder()
.addProjectConfiguration(
{
root: 'proj1',
name: 'proj1',
},
'app'
)
.addProjectConfiguration(
{
root: 'proj2',
name: 'proj2',
},
'lib'
)
.addProjectConfiguration(
{
root: 'proj3',
name: 'proj3',
},
'lib'
)
.build();
await showProjectsHandler({
json: true,
});
expect(console.log).toHaveBeenCalledWith('["proj1","proj2","proj3"]');
});
it('should filter projects by type', async () => {
graph = new GraphBuilder()
.addProjectConfiguration(
{
root: 'proj1',
name: 'proj1',
},
'app'
)
.addProjectConfiguration(
{
root: 'proj2',
name: 'proj2',
},
'lib'
)
.addProjectConfiguration(
{
root: 'proj3',
name: 'proj3',
},
'lib'
)
.build();
await showProjectsHandler({
type: 'lib',
});
expect(console.log).toHaveBeenCalledWith('proj2');
expect(console.log).toHaveBeenCalledWith('proj3');
expect(console.log).toHaveBeenCalledTimes(2);
});
it('should filter projects by name', async () => {
graph = new GraphBuilder()
.addProjectConfiguration(
{
root: 'proj1',
name: 'proj1',
},
'app'
)
.addProjectConfiguration(
{
root: 'proj2',
name: 'proj2',
},
'lib'
)
.addProjectConfiguration(
{
root: 'proj3',
name: 'proj3',
},
'lib'
)
.build();
await showProjectsHandler({
projects: ['proj1', 'proj3'],
});
expect(console.log).toHaveBeenCalledWith('proj1');
expect(console.log).toHaveBeenCalledWith('proj3');
expect(console.log).toHaveBeenCalledTimes(2);
});
it('should exclude projects by name', async () => {
graph = new GraphBuilder()
.addProjectConfiguration(
{
root: 'proj1',
name: 'proj1',
},
'app'
)
.addProjectConfiguration(
{
root: 'proj2',
name: 'proj2',
},
'lib'
)
.addProjectConfiguration(
{
root: 'proj3',
name: 'proj3',
},
'lib'
)
.build();
await showProjectsHandler({
exclude: ['proj1', 'proj3'],
});
expect(console.log).toHaveBeenCalledWith('proj2');
expect(console.log).toHaveBeenCalledTimes(1);
});
it('should find projects with wildcard', async () => {
graph = new GraphBuilder()
.addProjectConfiguration(
{
root: 'proj1',
name: 'proj1',
},
'app'
)
.addProjectConfiguration(
{
root: 'proj2',
name: 'proj2',
},
'lib'
)
.addProjectConfiguration(
{
root: 'proj3',
name: 'proj3',
},
'lib'
)
.build();
await showProjectsHandler({
projects: ['*1'],
});
expect(console.log).toHaveBeenCalledWith('proj1');
expect(console.log).toHaveBeenCalledTimes(1);
});
it('should find projects with specific tag', async () => {
graph = new GraphBuilder()
.addProjectConfiguration(
{
root: 'proj1',
name: 'proj1',
tags: ['tag1'],
},
'app'
)
.addProjectConfiguration(
{
root: 'proj2',
name: 'proj2',
tags: ['tag2'],
},
'lib'
)
.addProjectConfiguration(
{
root: 'proj3',
name: 'proj3',
tags: ['tag1'],
},
'lib'
)
.build();
await showProjectsHandler({
projects: ['tag:tag1'],
});
expect(console.log).toHaveBeenCalledWith('proj1');
expect(console.log).toHaveBeenCalledWith('proj3');
expect(console.log).toHaveBeenCalledTimes(2);
});
it('should list projects with specific target', async () => {
graph = new GraphBuilder()
.addProjectConfiguration(
{
root: 'proj1',
name: 'proj1',
targets: {
build: {
executor: 'build',
},
},
},
'app'
)
.addProjectConfiguration(
{
root: 'proj2',
name: 'proj2',
targets: {
build: {
executor: 'build',
},
},
},
'lib'
)
.addProjectConfiguration(
{
root: 'proj3',
name: 'proj3',
targets: {
test: {
executor: 'test',
},
},
},
'lib'
)
.build();
await showProjectsHandler({
withTarget: ['build'],
});
expect(console.log).toHaveBeenCalledWith('proj1');
expect(console.log).toHaveBeenCalledWith('proj2');
expect(console.log).toHaveBeenCalledTimes(2);
});
});
class GraphBuilder {
nodes: Record<string, ProjectGraphProjectNode> = {};
addProjectConfiguration(
project: ProjectConfiguration,
type: ProjectGraph['nodes'][string]['type']
) {
this.nodes[project.name] = {
name: project.name,
type,
data: { ...project },
};
return this;
}
build(): ProjectGraph {
return {
nodes: this.nodes,
dependencies: {},
externalNodes: {},
};
}
}

View File

@ -18,8 +18,7 @@ import {
splitArgsIntoNxArgsAndOverrides,
} from '../../utils/command-line-utils';
import { findMatchingProjects } from '../../utils/find-matching-projects';
import { ShowProjectOptions, ShowProjectsOptions } from './command-object';
import { generateGraph } from '../graph/graph';
import { ShowProjectsOptions } from './command-object';
export async function showProjectsHandler(
args: ShowProjectsOptions
@ -75,76 +74,15 @@ export async function showProjectsHandler(
if (args.json) {
console.log(JSON.stringify(Array.from(selectedProjects)));
} else if (args.sep) {
console.log(Array.from(selectedProjects.values()).join(args.sep));
} else {
for (const project of selectedProjects) {
console.log(project);
}
}
await output.drain();
process.exit(0);
}
export async function showProjectHandler(
args: ShowProjectOptions
): Promise<void> {
const graph = await createProjectGraphAsync();
const node = graph.nodes[args.projectName];
if (!node) {
console.log(`Could not find project ${args.projectName}`);
process.exit(1);
}
if (args.json) {
console.log(JSON.stringify(node.data));
} else if (args.web) {
await generateGraph(
{
view: 'project-details',
focus: node.name,
watch: true,
open: true,
},
[]
);
} else {
const chalk = require('chalk') as typeof import('chalk');
const logIfExists = (label, key: keyof typeof node['data']) => {
if (node.data[key]) {
console.log(`${chalk.bold(label)}: ${node.data[key]}`);
}
};
logIfExists('Name', 'name');
logIfExists('Root', 'root');
logIfExists('Source Root', 'sourceRoot');
logIfExists('Tags', 'tags');
logIfExists('Implicit Dependencies', 'implicitDependencies');
const targets = Object.entries(node.data.targets ?? {});
const maxTargetNameLength = Math.max(...targets.map(([t]) => t.length));
const maxExecutorNameLength = Math.max(
...targets.map(([, t]) => t?.executor?.length ?? 0)
);
if (targets.length > 0) {
console.log(`${chalk.bold('Targets')}: `);
for (const [target, targetConfig] of targets) {
console.log(
`- ${chalk.bold((target + ':').padEnd(maxTargetNameLength + 2))} ${(
targetConfig?.executor ?? ''
).padEnd(maxExecutorNameLength + 2)} ${(() => {
const configurations = Object.keys(
targetConfig.configurations ?? {}
);
if (configurations.length) {
return chalk.dim(configurations.join(', '));
}
return '';
})()}`
);
}
}
}
process.exit(0);
}
function getGraphNodesMatchingPatterns(