feat(core): nx list enhancements and cleanup

This commit is contained in:
Wes Grimes 2020-03-16 16:42:22 -04:00 committed by Victor Savkin
parent ff55863727
commit bb12d7c6d6
16 changed files with 358 additions and 328 deletions

View File

@ -2,7 +2,7 @@ _[Please make sure you have read the submission guidelines before posting an PR]
# Community Plugin Submission # Community Plugin Submission
Thanks for submitting your Nx Plugin to our approved plugins list. Make sure to follow the following steps to ensure that your PR is approved in a timely manner. Thanks for submitting your Nx Plugin to our community plugins list. Make sure to follow these steps to ensure that your PR is approved in a timely manner.
## Steps to Submit Your Plugin ## Steps to Submit Your Plugin
- Use the following commit message template: `chore(core): nx plugin submission [PLUGIN_NAME]` - Use the following commit message template: `chore(core): nx plugin submission [PLUGIN_NAME]`

View File

@ -100,7 +100,7 @@ nx list @nrwl/web
This will list all the capabilities in the `@nrwl/web` collection. This will list all the capabilities in the `@nrwl/web` collection.
`nx list` will also output a list of Nrwl-approved plugins that you may want to consider adding to your workspace. Alongside the Nrwl core plugins `nx list` will also display some community plugins that you may want to consider adding to your workspace.
## Creating an application ## Creating an application

View File

@ -82,7 +82,7 @@ nx list @nrwl/web
This will list all the schematics in the `@nrwl/web` collection. This will list all the schematics in the `@nrwl/web` collection.
`nx list` will also output a list of Nrwl-approved plugins that you may want to consider adding to your workspace. `nx list` will also output a list of Nrwl core and community plugins that you may want to consider adding to your workspace.
> Visit the [CLI Commands](/react/guides/cli#cli-commands) section to see more available commands. > Visit the [CLI Commands](/react/guides/cli#cli-commands) section to see more available commands.

View File

@ -253,7 +253,7 @@ After that, you can then install your plugin like any other npm package,
### Listing your Nx Plugin ### Listing your Nx Plugin
Nx provides a utility (`nx list`) that lists all approved plugins. To submit your plugin, please follow the steps below: Nx provides a utility (`nx list`) that lists both core and community plugins. To submit your plugin, please follow the steps below:
- Fork the [Nx repo](https://github.com/nrwl/nx/fork) (if you haven't already) - Fork the [Nx repo](https://github.com/nrwl/nx/fork) (if you haven't already)
- Update the [`community/approved-plugins.json` file](https://github.com/nrwl/nx/blob/master/community/approved-plugins.json) with a new entry for your plugin that includes name, url and description - Update the [`community/approved-plugins.json` file](https://github.com/nrwl/nx/blob/master/community/approved-plugins.json) with a new entry for your plugin that includes name, url and description

View File

@ -82,7 +82,7 @@ nx list @nrwl/web
This will list all the schematics in the `@nrwl/web` collection. This will list all the schematics in the `@nrwl/web` collection.
`nx list` will also output a list of Nrwl-approved plugins that you may want to consider adding to your workspace. `nx list` will also output a list of Nrwl core and community plugins that you may want to consider adding to your workspace.
> Visit the [CLI Commands](/web/guides/cli#cli-commands) section to see more available commands. > Visit the [CLI Commands](/web/guides/cli#cli-commands) section to see more available commands.

View File

@ -1,3 +1,5 @@
import { packagesWeCareAbout } from '@nrwl/workspace/src/command-line/report';
import { renameSync } from 'fs';
import { import {
ensureProject, ensureProject,
forEachCli, forEachCli,
@ -9,8 +11,6 @@ import {
tmpProjPath, tmpProjPath,
updateFile updateFile
} from './utils'; } from './utils';
import { packagesWeCareAbout } from '@nrwl/workspace/src/command-line/report';
import { renameSync } from 'fs';
forEachCli('nx', () => { forEachCli('nx', () => {
describe('Help', () => { describe('Help', () => {
@ -91,8 +91,6 @@ forEachCli(() => {
// just check for some, not all // just check for some, not all
expect(listOutput).toContain('@nrwl/angular'); expect(listOutput).toContain('@nrwl/angular');
expect(listOutput).toContain('@schematics/angular');
expect(listOutput).toContain('@ngrx/store');
expect(listOutput).not.toContain('NX Also available'); expect(listOutput).not.toContain('NX Also available');
@ -118,7 +116,7 @@ forEachCli(() => {
// check for builders // check for builders
expect(listOutput).toContain('run-commands'); expect(listOutput).toContain('run-commands');
// look for uninstalled approved plugin // look for uninstalled core plugin
listOutput = runCommand('npm run nx -- list @nrwl/angular'); listOutput = runCommand('npm run nx -- list @nrwl/angular');
expect(listOutput).toContain( expect(listOutput).toContain(
@ -129,7 +127,7 @@ forEachCli(() => {
listOutput = runCommand('npm run nx -- list @wibble/fish'); listOutput = runCommand('npm run nx -- list @wibble/fish');
expect(listOutput).toContain( expect(listOutput).toContain(
'NX ERROR Could not find plugin @wibble/fish' 'NX NOTE @wibble/fish is not currently installed'
); );
// put back the @nrwl/angular module (or all the other e2e tests after this will fail) // put back the @nrwl/angular module (or all the other e2e tests after this will fail)

View File

@ -1,15 +1,15 @@
import yargs = require('yargs'); import yargs = require('yargs');
import { terminal } from '@angular-devkit/core';
import { appRootPath } from '../utils/app-root'; import { appRootPath } from '../utils/app-root';
import { listCommunityPlugins } from '../utils/community-plugins';
import { detectPackageManager } from '../utils/detect-package-manager';
import { output } from '../utils/output'; import { output } from '../utils/output';
import { import {
getPluginCapabilities, fetchCommunityPlugins,
getPluginVersion, fetchCorePlugins,
readCapabilitiesFromNodeModules getInstalledPluginsFromNodeModules,
} from '../utils/plugin-utils'; listCommunityPlugins,
import { approvedPlugins } from '../utils/plugins'; listCorePlugins,
listInstalledPlugins,
listPluginCapabilities
} from '../utils/plugins';
export interface YargsListArgs extends yargs.Arguments, ListArgs {} export interface YargsListArgs extends yargs.Arguments, ListArgs {}
@ -39,149 +39,21 @@ export const list = {
*/ */
async function listHandler(args: YargsListArgs) { async function listHandler(args: YargsListArgs) {
if (args.plugin) { if (args.plugin) {
listCapabilities(args.plugin); listPluginCapabilities(args.plugin);
} else { } else {
await listPlugins(); const corePlugins = await fetchCorePlugins();
} const communityPlugins = await fetchCommunityPlugins();
} const installedPlugins = getInstalledPluginsFromNodeModules(
appRootPath,
function getPackageManagerInstallCommand(): string { corePlugins,
let packageManager = detectPackageManager(); communityPlugins
let packageManagerInstallCommand = 'npm install --save-dev';
switch (packageManager) {
case 'yarn':
packageManagerInstallCommand = 'yarn add --dev';
break;
case 'pnpm':
packageManagerInstallCommand = 'pnpm install --save-dev';
break;
}
return packageManagerInstallCommand;
}
function hasElements(obj: any): boolean {
return obj && Object.values(obj).length > 0;
}
function listCapabilities(pluginName: string) {
const plugin = getPluginCapabilities(appRootPath, pluginName);
if (!plugin) {
const approvedPlugin = approvedPlugins.find(p => p.name === pluginName);
if (approvedPlugin) {
const installedPlugins = readCapabilitiesFromNodeModules(appRootPath);
let workspaceVersion = 'latest';
if (installedPlugins.some(x => x.name === '@nrwl/workspace')) {
workspaceVersion = getPluginVersion(appRootPath, '@nrwl/workspace');
}
output.note({
title: `${pluginName} is not currently installed`,
bodyLines: [
`Use "${getPackageManagerInstallCommand()} ${pluginName}@${workspaceVersion}" to add new capabilities`,
'',
`Visit ${terminal.bold(
approvedPlugin.link ? approvedPlugin.link : 'https://nx.dev/'
)} for more information`
]
});
} else {
output.error({
title: `Could not find plugin ${pluginName}`
});
}
return;
}
const hasBuilders = hasElements(plugin.builders);
const hasSchematics = hasElements(plugin.schematics);
if (!hasBuilders && !hasSchematics) {
output.warn({ title: `No capabilities found in ${pluginName}` });
return;
}
const bodyLines = [];
if (hasSchematics) {
bodyLines.push(terminal.bold(terminal.green('SCHEMATICS')));
bodyLines.push('');
bodyLines.push(
...Object.keys(plugin.schematics).map(
name =>
`${terminal.bold(name)} : ${plugin.schematics[name].description}`
)
); );
if (hasBuilders) { listInstalledPlugins(installedPlugins);
bodyLines.push(''); listCorePlugins(installedPlugins, corePlugins);
} listCommunityPlugins(installedPlugins, communityPlugins);
}
if (hasBuilders) { output.note({
bodyLines.push(terminal.bold(terminal.green('BUILDERS'))); title: `Use "nx list [plugin]" to find out more`
bodyLines.push('');
bodyLines.push(
...Object.keys(plugin.builders).map(
name => `${terminal.bold(name)} : ${plugin.builders[name].description}`
)
);
}
output.log({
title: `Capabilities in ${plugin.name}:`,
bodyLines
});
}
async function listPlugins() {
const installedPlugins = readCapabilitiesFromNodeModules(appRootPath);
// The following packages are present in any workspace. Hide them to avoid confusion.
const hide = [
'@angular-devkit/architect',
'@angular-devkit/build-ng-packagr',
'@angular-devkit/build-webpack',
'@angular-eslint/builder'
];
const filtered = installedPlugins.filter(p => hide.indexOf(p.name) === -1);
output.log({
title: `Installed plugins:`,
bodyLines: filtered.map(p => {
const capabilities = [];
if (hasElements(p.builders)) {
capabilities.push('builders');
}
if (hasElements(p.schematics)) {
capabilities.push('schematics');
}
return `${terminal.bold(p.name)} (${capabilities.join()})`;
})
});
const installedPluginsMap: Set<string> = new Set<string>(
installedPlugins.map(p => p.name)
);
const alsoAvailable = approvedPlugins.filter(
p => !installedPluginsMap.has(p.name)
);
if (alsoAvailable.length) {
output.log({
title: `Also available:`,
bodyLines: alsoAvailable.map(p => {
return `${terminal.bold(p.name)} (${p.capabilities})`;
})
}); });
} }
await listCommunityPlugins(installedPluginsMap);
output.note({
title: `Use "nx list [plugin]" to find out more`
});
} }

View File

@ -1,104 +0,0 @@
// Lifted in part from https://github.com/nrwl/angular-console
import { readdirSync } from 'fs';
import * as path from 'path';
import { readJsonFile } from './fileutils';
export interface Schematic {
factory: string;
schema: string;
description: string;
aliases: string;
hidden: boolean;
}
export interface Builder {
implementation: string;
schema: string;
description: string;
}
export interface PluginCapabilities {
name: string;
builders: { [name: string]: Builder };
schematics: { [name: string]: Schematic };
}
export function getPluginVersion(workspaceRoot: string, name: string): string {
try {
const packageJson = readJsonFile(
path.join(workspaceRoot, 'node_modules', name, 'package.json')
);
return packageJson.version;
} catch {
throw new Error(`Could not read package.json for module ${name}`);
}
}
export function readCapabilitiesFromNodeModules(
workspaceRoot: string
): Array<PluginCapabilities> {
const packages = listOfUnnestedNpmPackages(workspaceRoot);
return packages
.map(name => getPluginCapabilities(workspaceRoot, name))
.filter(x => x && !!(x.schematics || x.builders));
}
export function getPluginCapabilities(
workspaceRoot: string,
pluginName: string
): PluginCapabilities {
try {
const pluginPath = path.join(workspaceRoot, 'node_modules', pluginName);
const packageJson = readJsonFile(path.join(pluginPath, 'package.json'));
return {
name: pluginName,
schematics: tryGetCollection(
pluginPath,
packageJson.schematics,
'schematics'
),
builders: tryGetCollection(pluginPath, packageJson.builders, 'builders')
};
} catch {
return null;
}
}
function tryGetCollection<T>(
pluginPath: string,
jsonFile: string,
propName: string
): T {
if (!jsonFile) {
return null;
}
try {
return readJsonFile<T>(path.join(pluginPath, jsonFile))[propName];
} catch {
return null;
}
}
let packageList: string[] = [];
export function listOfUnnestedNpmPackages(
workspaceRoot: string,
requery: boolean = false
): string[] {
if (!requery && packageList.length > 0) {
return packageList;
}
const nodeModulesDir = path.join(workspaceRoot, 'node_modules');
readdirSync(nodeModulesDir).forEach(npmPackageOrScope => {
if (npmPackageOrScope.startsWith('@')) {
readdirSync(path.join(nodeModulesDir, npmPackageOrScope)).forEach(p => {
packageList.push(`${npmPackageOrScope}/${p}`);
});
} else {
packageList.push(npmPackageOrScope);
}
});
return packageList;
}

View File

@ -1,52 +0,0 @@
/**
* This file is used by `nx list` to display approved plugins
*/
export interface Plugin {
name: string;
capabilities: 'builders' | 'schematics' | 'builders,schematics';
link?: string;
}
export const approvedPlugins: Plugin[] = [
{
name: '@nrwl/angular',
capabilities: 'schematics'
},
{
name: '@nrwl/cypress',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/express',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/jest',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/nest',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/next',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/node',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/react',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/storybook',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/web',
capabilities: 'builders,schematics'
}
];

View File

@ -1,27 +1,27 @@
import { terminal } from '@angular-devkit/core'; import { terminal } from '@angular-devkit/core';
import axios from 'axios'; import axios from 'axios';
import { output } from './output'; import { output } from '../output';
import { CommunityPlugin, PluginCapabilities } from './models';
const APPROVED_PLUGINS_JSON_URL = const COMMUNITY_PLUGINS_JSON_URL =
'https://raw.githubusercontent.com/nrwl/nx/master/community/approved-plugins.json'; 'https://raw.githubusercontent.com/nrwl/nx/master/community/approved-plugins.json';
interface CommunityPlugin { export async function fetchCommunityPlugins(): Promise<CommunityPlugin[]> {
name: string;
url: string;
description: string;
}
async function fetchCommunityPlugins(): Promise<CommunityPlugin[]> {
const response = await axios.get<CommunityPlugin[]>( const response = await axios.get<CommunityPlugin[]>(
APPROVED_PLUGINS_JSON_URL COMMUNITY_PLUGINS_JSON_URL
); );
return response.data; return response.data;
} }
export async function listCommunityPlugins(installedPluginsMap: Set<string>) { export function listCommunityPlugins(
installedPlugins: PluginCapabilities[],
communityPlugins: CommunityPlugin[]
) {
try { try {
const communityPlugins = await fetchCommunityPlugins(); const installedPluginsMap: Set<string> = new Set<string>(
installedPlugins.map(p => p.name)
);
const availableCommunityPlugins = communityPlugins.filter( const availableCommunityPlugins = communityPlugins.filter(
p => !installedPluginsMap.has(p.name) p => !installedPluginsMap.has(p.name)

View File

@ -0,0 +1,87 @@
import { terminal } from '@angular-devkit/core';
import { output } from '../output';
import { CorePlugin, PluginCapabilities } from './models';
export function fetchCorePlugins() {
const corePlugins: CorePlugin[] = [
{
name: '@nrwl/angular',
capabilities: 'schematics'
},
{
name: '@nrwl/bazel',
capabilities: 'schematics'
},
{
name: '@nrwl/cypress',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/express',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/jest',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/linter',
capabilities: 'builders'
},
{
name: '@nrwl/nest',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/next',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/node',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/nx-plugin',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/react',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/storybook',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/web',
capabilities: 'builders,schematics'
},
{
name: '@nrwl/workspace',
capabilities: 'builders,schematics'
}
];
return corePlugins;
}
export function listCorePlugins(
installedPlugins: PluginCapabilities[],
corePlugins: CorePlugin[]
) {
const installedPluginsMap: Set<string> = new Set<string>(
installedPlugins.map(p => p.name)
);
const alsoAvailable = corePlugins.filter(
p => !installedPluginsMap.has(p.name)
);
if (alsoAvailable.length) {
output.log({
title: `Also available:`,
bodyLines: alsoAvailable.map(p => {
return `${terminal.bold(p.name)} (${p.capabilities})`;
})
});
}
}

View File

@ -0,0 +1,13 @@
export {
fetchCommunityPlugins,
listCommunityPlugins
} from './community-plugins';
export { fetchCorePlugins, listCorePlugins } from './core-plugins';
export {
getInstalledPluginsFromNodeModules,
listInstalledPlugins
} from './installed-plugins';
export {
getPluginCapabilities,
listPluginCapabilities
} from './plugin-capabilities';

View File

@ -0,0 +1,66 @@
import { terminal } from '@angular-devkit/core';
import { readdirSync } from 'fs';
import * as path from 'path';
import { output } from '../output';
import { CommunityPlugin, CorePlugin, PluginCapabilities } from './models';
import { getPluginCapabilities } from './plugin-capabilities';
import { hasElements } from './shared';
function getPackagesFromNodeModules(
workspaceRoot: string,
requery: boolean = false
): string[] {
let packageList: string[] = [];
if (!requery && packageList.length > 0) {
return packageList;
}
const nodeModulesDir = path.join(workspaceRoot, 'node_modules');
readdirSync(nodeModulesDir).forEach(npmPackageOrScope => {
if (npmPackageOrScope.startsWith('@')) {
readdirSync(path.join(nodeModulesDir, npmPackageOrScope)).forEach(p => {
packageList.push(`${npmPackageOrScope}/${p}`);
});
} else {
packageList.push(npmPackageOrScope);
}
});
return packageList;
}
export function getInstalledPluginsFromNodeModules(
workspaceRoot: string,
corePlugins: CorePlugin[],
communityPlugins: CommunityPlugin[]
): Array<PluginCapabilities> {
const corePluginNames = corePlugins.map(p => p.name);
const communityPluginNames = communityPlugins.map(p => p.name);
const packages = getPackagesFromNodeModules(workspaceRoot);
return packages
.filter(
name =>
corePluginNames.indexOf(name) > -1 ||
communityPluginNames.indexOf(name) > -1
)
.map(name => getPluginCapabilities(workspaceRoot, name))
.filter(x => x && !!(x.schematics || x.builders));
}
export function listInstalledPlugins(installedPlugins: PluginCapabilities[]) {
output.log({
title: `Installed plugins:`,
bodyLines: installedPlugins.map(p => {
const capabilities = [];
if (hasElements(p.builders)) {
capabilities.push('builders');
}
if (hasElements(p.schematics)) {
capabilities.push('schematics');
}
return `${terminal.bold(p.name)} (${capabilities.join()})`;
})
});
}

View File

@ -0,0 +1,31 @@
export interface PluginSchematic {
factory: string;
schema: string;
description: string;
aliases: string;
hidden: boolean;
}
export interface PluginBuilder {
implementation: string;
schema: string;
description: string;
}
export interface PluginCapabilities {
name: string;
builders: { [name: string]: PluginBuilder };
schematics: { [name: string]: PluginSchematic };
}
export interface CorePlugin {
name: string;
capabilities: 'builders' | 'schematics' | 'builders,schematics';
link?: string;
}
export interface CommunityPlugin {
name: string;
url: string;
description: string;
}

View File

@ -0,0 +1,114 @@
import { terminal } from '@angular-devkit/core';
import * as path from 'path';
import { appRootPath } from '../app-root';
import { detectPackageManager } from '../detect-package-manager';
import { readJsonFile } from '../fileutils';
import { output } from '../output';
import { PluginCapabilities } from './models';
import { hasElements } from './shared';
function getPackageManagerInstallCommand(): string {
let packageManager = detectPackageManager();
let packageManagerInstallCommand = 'npm install --save-dev';
switch (packageManager) {
case 'yarn':
packageManagerInstallCommand = 'yarn add --dev';
break;
case 'pnpm':
packageManagerInstallCommand = 'pnpm install --save-dev';
break;
}
return packageManagerInstallCommand;
}
function tryGetCollection<T>(
pluginPath: string,
jsonFile: string,
propName: string
): T {
if (!jsonFile) {
return null;
}
try {
return readJsonFile<T>(path.join(pluginPath, jsonFile))[propName];
} catch {
return null;
}
}
export function getPluginCapabilities(
workspaceRoot: string,
pluginName: string
): PluginCapabilities {
try {
const pluginPath = path.join(workspaceRoot, 'node_modules', pluginName);
const packageJson = readJsonFile(path.join(pluginPath, 'package.json'));
return {
name: pluginName,
schematics: tryGetCollection(
pluginPath,
packageJson.schematics,
'schematics'
),
builders: tryGetCollection(pluginPath, packageJson.builders, 'builders')
};
} catch {
return null;
}
}
export function listPluginCapabilities(pluginName: string) {
const plugin = getPluginCapabilities(appRootPath, pluginName);
if (!plugin) {
output.note({
title: `${pluginName} is not currently installed`,
bodyLines: [
`Use "${getPackageManagerInstallCommand()} ${pluginName}" to add new capabilities`
]
});
return;
}
const hasBuilders = hasElements(plugin.builders);
const hasSchematics = hasElements(plugin.schematics);
if (!hasBuilders && !hasSchematics) {
output.warn({ title: `No capabilities found in ${pluginName}` });
return;
}
const bodyLines = [];
if (hasSchematics) {
bodyLines.push(terminal.bold(terminal.green('SCHEMATICS')));
bodyLines.push('');
bodyLines.push(
...Object.keys(plugin.schematics).map(
name =>
`${terminal.bold(name)} : ${plugin.schematics[name].description}`
)
);
if (hasBuilders) {
bodyLines.push('');
}
}
if (hasBuilders) {
bodyLines.push(terminal.bold(terminal.green('BUILDERS')));
bodyLines.push('');
bodyLines.push(
...Object.keys(plugin.builders).map(
name => `${terminal.bold(name)} : ${plugin.builders[name].description}`
)
);
}
output.log({
title: `Capabilities in ${plugin.name}:`,
bodyLines
});
}

View File

@ -0,0 +1,5 @@
// Lifted in part from https://github.com/nrwl/angular-console
export function hasElements(obj: any): boolean {
return obj && Object.values(obj).length > 0;
}