diff --git a/packages/nx/src/command-line/generate.ts b/packages/nx/src/command-line/generate.ts index b813f60f52..d9e63f7004 100644 --- a/packages/nx/src/command-line/generate.ts +++ b/packages/nx/src/command-line/generate.ts @@ -1,23 +1,26 @@ +import * as chalk from 'chalk'; +import { prompt } from 'enquirer'; +import { readJsonFile } from 'nx/src/utils/fileutils'; + +import { readNxJson } from '../config/configuration'; +import { NxJsonConfiguration } from '../config/nx-json'; +import { ProjectsConfigurations } from '../config/workspace-json-project-json'; +import { Workspaces } from '../config/workspaces'; +import { FileChange, flushChanges, FsTree } from '../generators/tree'; +import { + createProjectGraphAsync, + readProjectsConfigurationFromProjectGraph, +} from '../project-graph/project-graph'; +import { logger } from '../utils/logger'; import { combineOptionsForGenerator, handleErrors, Options, Schema, } from '../utils/params'; -import { Workspaces } from '../config/workspaces'; -import { FileChange, flushChanges, FsTree } from '../generators/tree'; -import { logger } from '../utils/logger'; -import * as chalk from 'chalk'; -import { workspaceRoot } from '../utils/workspace-root'; -import { NxJsonConfiguration } from '../config/nx-json'; +import { getLocalWorkspacePlugins } from '../utils/plugins/local-plugins'; import { printHelp } from '../utils/print-help'; -import { prompt } from 'enquirer'; -import { readJsonFile } from 'nx/src/utils/fileutils'; -import { - createProjectGraphAsync, - readProjectsConfigurationFromProjectGraph, -} from '../project-graph/project-graph'; -import { readNxJson } from '../config/configuration'; +import { workspaceRoot } from '../utils/workspace-root'; export interface GenerateOptions { collectionName: string; @@ -44,18 +47,21 @@ export function printChanges(fileChanges: FileChange[]) { async function promptForCollection( generatorName: string, ws: Workspaces, - interactive: boolean -) { + interactive: boolean, + projectsConfiguration: ProjectsConfigurations +): Promise { const packageJson = readJsonFile(`${workspaceRoot}/package.json`); - const collections = Array.from( + const localPlugins = getLocalWorkspacePlugins(projectsConfiguration); + + const installedCollections = Array.from( new Set([ ...Object.keys(packageJson.dependencies || {}), ...Object.keys(packageJson.devDependencies || {}), ]) ); - const choicesMap = new Set(); - for (const collectionName of collections) { + const choicesMap = new Set(); + for (const collectionName of installedCollections) { try { const { resolvedCollectionName, normalizedGeneratorName } = ws.readGenerator(collectionName, generatorName); @@ -64,14 +70,44 @@ async function promptForCollection( } catch {} } - const choices = Array.from(choicesMap); - + const choicesFromLocalPlugins: { + name: string; + message: string; + value: string; + }[] = []; + for (const [name] of localPlugins) { + try { + const { resolvedCollectionName, normalizedGeneratorName } = + ws.readGenerator(name, generatorName); + const value = `${resolvedCollectionName}:${normalizedGeneratorName}`; + if (!choicesMap.has(value)) { + choicesFromLocalPlugins.push({ + name: value, + message: chalk.bold(value), + value, + }); + } + } catch {} + } + if (choicesFromLocalPlugins.length) { + choicesFromLocalPlugins[choicesFromLocalPlugins.length - 1].message += '\n'; + } + const choices = ( + choicesFromLocalPlugins as ( + | string + | { + name: string; + message: string; + value: string; + } + )[] + ).concat(...choicesMap); if (choices.length === 1) { - return choices[0]; + return typeof choices[0] === 'string' ? choices[0] : choices[0].value; } else if (!interactive && choices.length > 1) { - throwInvalidInvocation(choices); + throwInvalidInvocation(Array.from(choicesMap)); } else if (interactive && choices.length > 1) { - const noneOfTheAbove = `None of the above`; + const noneOfTheAbove = `\nNone of the above`; choices.push(noneOfTheAbove); let { generator, customCollection } = await prompt<{ generator: string; @@ -81,7 +117,8 @@ async function promptForCollection( name: 'generator', message: `Which generator would you like to use?`, type: 'autocomplete', - choices, + // enquirer's typings are incorrect here... It supports (string | Choice)[], but is typed as (string[] | Choice[]) + choices: choices as string[], }, { name: 'customCollection', @@ -135,7 +172,8 @@ async function convertToGenerateOptions( generatorOptions: { [p: string]: any }, ws: Workspaces, defaultCollectionName: string, - mode: 'generate' | 'new' + mode: 'generate' | 'new', + projectsConfiguration?: ProjectsConfigurations ): Promise { let collectionName: string | null = null; let generatorName: string | null = null; @@ -152,7 +190,8 @@ async function convertToGenerateOptions( const generatorString = await promptForCollection( generatorDescriptor, ws, - interactive + interactive, + projectsConfiguration ); const parsedGeneratorString = parseGeneratorString(generatorString); collectionName = parsedGeneratorString.collection; @@ -297,7 +336,8 @@ export async function generate(cwd: string, args: { [k: string]: any }) { args, ws, readDefaultCollection(nxJson), - 'generate' + 'generate', + projectsConfiguration ); const { normalizedGeneratorName, schema, implementationFactory, aliases } = ws.readGenerator(opts.collectionName, opts.generatorName); diff --git a/packages/nx/src/command-line/list.ts b/packages/nx/src/command-line/list.ts index 0a776dd098..1a3065b98f 100644 --- a/packages/nx/src/command-line/list.ts +++ b/packages/nx/src/command-line/list.ts @@ -9,6 +9,14 @@ import { listInstalledPlugins, listPluginCapabilities, } from '../utils/plugins'; +import { + getLocalWorkspacePlugins, + listLocalWorkspacePlugins, +} from '../utils/plugins/local-plugins'; +import { + createProjectGraphAsync, + readProjectsConfigurationFromProjectGraph, +} from '../project-graph/project-graph'; export interface ListArgs { /** The name of an installed plugin to query */ @@ -36,12 +44,18 @@ export async function listHandler(args: ListArgs): Promise { return []; }); + const projectGraph = await createProjectGraphAsync(); + const localPlugins = getLocalWorkspacePlugins( + readProjectsConfigurationFromProjectGraph(projectGraph) + ); const installedPlugins = getInstalledPluginsFromPackageJson( workspaceRoot, corePlugins, communityPlugins ); + + listLocalWorkspacePlugins(localPlugins); listInstalledPlugins(installedPlugins); listCorePlugins(installedPlugins, corePlugins); listCommunityPlugins(installedPlugins, communityPlugins); diff --git a/packages/nx/src/command-line/report.ts b/packages/nx/src/command-line/report.ts index aff19b70f7..30a970a93b 100644 --- a/packages/nx/src/command-line/report.ts +++ b/packages/nx/src/command-line/report.ts @@ -8,6 +8,11 @@ import { } from '../utils/package-manager'; import { readJsonFile } from '../utils/fileutils'; import { PackageJson, readModulePackageJson } from '../utils/package-json'; +import { getLocalWorkspacePlugins } from '../utils/plugins/local-plugins'; +import { + createProjectGraphAsync, + readProjectsConfigurationFromProjectGraph, +} from '../project-graph/project-graph'; export const packagesWeCareAbout = [ 'nx', @@ -49,7 +54,7 @@ export const patternsWeIgnoreInCommunityReport: Array = [ * Must be run within an Nx workspace * */ -export function reportHandler() { +export async function reportHandler() { const pm = detectPackageManager(); const pmVersion = getPackageManagerVersion(pm); @@ -66,6 +71,22 @@ export function reportHandler() { bodyLines.push('---------------------------------------'); + try { + const projectGraph = await createProjectGraphAsync(); + bodyLines.push('Local workspace plugins:'); + const plugins = getLocalWorkspacePlugins( + readProjectsConfigurationFromProjectGraph(projectGraph) + ).keys(); + for (const plugin of plugins) { + bodyLines.push(`\t ${chalk.green(plugin)}`); + } + bodyLines.push(...plugins); + } catch { + bodyLines.push('Unable to construct project graph'); + } + + bodyLines.push('---------------------------------------'); + const communityPlugins = findInstalledCommunityPlugins(); bodyLines.push('Community plugins:'); communityPlugins.forEach((p) => { diff --git a/packages/nx/src/utils/nx-plugin.ts b/packages/nx/src/utils/nx-plugin.ts index c85a855e93..ec1009a7f6 100644 --- a/packages/nx/src/utils/nx-plugin.ts +++ b/packages/nx/src/utils/nx-plugin.ts @@ -6,7 +6,11 @@ import { Workspaces } from '../config/workspaces'; import { workspaceRoot } from './workspace-root'; import { readJsonFile } from '../utils/fileutils'; -import { PackageJson, readModulePackageJson } from './package-json'; +import { + PackageJson, + readModulePackageJson, + readModulePackageJsonWithoutFallbacks, +} from './package-json'; import { registerTsProject } from './register'; import { ProjectConfiguration, @@ -118,7 +122,7 @@ export function readPluginPackageJson( json: PackageJson; } { try { - const result = readModulePackageJson(pluginName, paths); + const result = readModulePackageJsonWithoutFallbacks(pluginName, paths); return { json: result.packageJson, path: result.path, diff --git a/packages/nx/src/utils/package-json.ts b/packages/nx/src/utils/package-json.ts index 9ee9877676..c55f500746 100644 --- a/packages/nx/src/utils/package-json.ts +++ b/packages/nx/src/utils/package-json.ts @@ -106,6 +106,41 @@ export function buildTargetFromScript( }; } +/** + * Uses `require.resolve` to read the package.json for a module. + * + * This will fail if the module doesn't export package.json + * + * @returns package json contents and path + */ +export function readModulePackageJsonWithoutFallbacks( + moduleSpecifier: string, + requirePaths = [workspaceRoot] +): { + packageJson: PackageJson; + path: string; +} { + const packageJsonPath: string = require.resolve( + `${moduleSpecifier}/package.json`, + { + paths: requirePaths, + } + ); + const packageJson: PackageJson = readJsonFile(packageJsonPath); + + return { + path: packageJsonPath, + packageJson, + }; +} + +/** + * Reads the package.json file for a specified module. + * + * Includes a fallback that accounts for modules that don't export package.json + * + * @returns package json contents and path + */ export function readModulePackageJson( moduleSpecifier: string, requirePaths = [workspaceRoot] @@ -117,14 +152,13 @@ export function readModulePackageJson( let packageJson: PackageJson; try { - packageJsonPath = require.resolve(`${moduleSpecifier}/package.json`, { - paths: requirePaths, - }); - packageJson = readJsonFile(packageJsonPath); + ({ path: packageJsonPath, packageJson } = + readModulePackageJsonWithoutFallbacks(moduleSpecifier, requirePaths)); } catch { const entryPoint = require.resolve(moduleSpecifier, { paths: requirePaths, }); + let moduleRootPath = dirname(entryPoint); packageJsonPath = join(moduleRootPath, 'package.json'); diff --git a/packages/nx/src/utils/plugins/local-plugins.ts b/packages/nx/src/utils/plugins/local-plugins.ts new file mode 100644 index 0000000000..94fd105d26 --- /dev/null +++ b/packages/nx/src/utils/plugins/local-plugins.ts @@ -0,0 +1,70 @@ +import * as chalk from 'chalk'; +import { output } from '../output'; +import type { CommunityPlugin, CorePlugin, PluginCapabilities } from './models'; +import { getPluginCapabilities } from './plugin-capabilities'; +import { hasElements } from './shared'; +import { readJsonFile } from '../fileutils'; +import { PackageJson, readModulePackageJson } from '../package-json'; +import { ProjectsConfigurations } from 'nx/src/config/workspace-json-project-json'; +import { join } from 'path'; +import { workspaceRoot } from '../workspace-root'; +import { existsSync } from 'fs'; +import { ExecutorsJson, GeneratorsJson } from 'nx/src/config/misc-interfaces'; + +export function getLocalWorkspacePlugins( + projectsConfiguration: ProjectsConfigurations +): Map { + const plugins: Map = new Map(); + for (const project of Object.values(projectsConfiguration.projects)) { + const packageJsonPath = join(workspaceRoot, project.root, 'package.json'); + if (existsSync(packageJsonPath)) { + const packageJson: PackageJson = readJsonFile(packageJsonPath); + const capabilities: Partial = {}; + const generatorsPath = packageJson.generators ?? packageJson.schematics; + const executorsPath = packageJson.executors ?? packageJson.builders; + if (generatorsPath) { + const file = readJsonFile( + join(workspaceRoot, project.root, generatorsPath) + ); + capabilities.generators = file.generators ?? file.schematics; + } + if (executorsPath) { + const file = readJsonFile( + join(workspaceRoot, project.root, executorsPath) + ); + capabilities.executors = file.executors ?? file.builders; + } + if (capabilities.executors || capabilities.generators) { + plugins.set(packageJson.name, { + executors: capabilities.executors ?? {}, + generators: capabilities.generators ?? {}, + name: packageJson.name, + }); + } + } + } + + return plugins; +} + +export function listLocalWorkspacePlugins( + installedPlugins: Map +) { + const bodyLines: string[] = []; + + for (const [, p] of installedPlugins) { + const capabilities = []; + if (hasElements(p.executors)) { + capabilities.push('executors'); + } + if (hasElements(p.generators)) { + capabilities.push('generators'); + } + bodyLines.push(`${chalk.bold(p.name)} (${capabilities.join()})`); + } + + output.log({ + title: `Local workspace plugins:`, + bodyLines: bodyLines, + }); +} diff --git a/packages/nx/src/utils/plugins/plugin-capabilities.ts b/packages/nx/src/utils/plugins/plugin-capabilities.ts index 12b7c502ab..e0671fef8e 100644 --- a/packages/nx/src/utils/plugins/plugin-capabilities.ts +++ b/packages/nx/src/utils/plugins/plugin-capabilities.ts @@ -6,7 +6,7 @@ import type { PluginCapabilities } from './models'; import { hasElements } from './shared'; import { readJsonFile } from '../fileutils'; import { getPackageManagerCommand } from '../package-manager'; -import { readModulePackageJson } from '../package-json'; +import { readPluginPackageJson } from '../nx-plugin'; function tryGetCollection( packageJsonPath: string, @@ -30,8 +30,8 @@ export function getPluginCapabilities( pluginName: string ): PluginCapabilities | null { try { - const { packageJson, path: packageJsonPath } = - readModulePackageJson(pluginName); + const { json: packageJson, path: packageJsonPath } = + readPluginPackageJson(pluginName); return { name: pluginName, generators: