diff --git a/docs/generated/cli/create-nx-workspace.md b/docs/generated/cli/create-nx-workspace.md new file mode 100644 index 0000000000000..5f0fa095daeb5 --- /dev/null +++ b/docs/generated/cli/create-nx-workspace.md @@ -0,0 +1,80 @@ +--- +title: 'create-nx-workspace - CLI command' +description: 'Create a new Nx workspace' +--- + +# create-nx-workspace + +Create a new Nx workspace + +## Usage + +```bash +create-nx-workspace [name] [options] +``` + +Install `create-nx-workspace` globally to invoke the command directly, or use `npx create-nx-workspace`, `yarn create nx-workspace`, or `pnpx create-nx-workspace`. + +## Options + +### allPrompts + +Default: `false` + +Show all prompts + +### appName + +The name of the application when a preset with pregenerated app is selected + +### cli + +Choices: `["nx", "angular"]` + +CLI to power the Nx workspace + +### defaultBase + +Default: `main` + +Default base to use for new projects + +### help + +Show help + +### interactive + +Enable interactive mode with presets + +### name + +Workspace name (e.g. org name) + +### nxCloud + +Default: `true` + +Use Nx Cloud + +### packageManager + +Default: `npm` + +Choices: `["npm", "pnpm", "yarn"]` + +Package manager to use + +### preset + +Choices: `["apps", "empty", "core", "npm", "ts", "web-components", "angular", "angular-nest", "react", "react-express", "react-native", "next", "nest", "express"]` + +Customizes the initial content of your workspace. To build your own see https://nx.dev/nx-plugin/overview#preset + +### style + +Style option to be used when a preset with pregenerated app is selected + +### version + +Show version number diff --git a/docs/map.json b/docs/map.json index f1442ad72f8d3..7581facd178cd 100644 --- a/docs/map.json +++ b/docs/map.json @@ -360,6 +360,11 @@ "name": "CLI", "id": "cli", "itemList": [ + { + "name": "create-nx-workspace", + "id": "create-nx-workspace", + "file": "generated/cli/create-nx-workspace" + }, { "name": "generate", "id": "generate", diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts index 9b82c4e2e95ae..328238ec370f0 100644 --- a/e2e/utils/index.ts +++ b/e2e/utils/index.ts @@ -10,6 +10,7 @@ import { copySync, createFileSync, ensureDirSync, + existsSync, moveSync, readdirSync, readFileSync, @@ -28,7 +29,15 @@ import chalk = require('chalk'); import isCI = require('is-ci'); import treeKill = require('tree-kill'); import { Workspaces } from '../../packages/nx/src/shared/workspace'; -import { detectPackageManager } from '../../packages/create-nx-workspace/bin/package-manager'; +import { PackageManager } from 'nx/src/utils/package-manager'; + +export function detectPackageManager(dir: string = ''): PackageManager { + return existsSync(join(dir, 'yarn.lock')) + ? 'yarn' + : existsSync(join(dir, 'pnpm-lock.yaml')) + ? 'pnpm' + : 'npm'; +} const kill = require('kill-port'); export const isWindows = require('is-windows'); diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index f9657b34890c0..b3817d3aeddda 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -1,24 +1,37 @@ -#!/usr/bin/env node - import { exec } from 'child_process'; import { writeFileSync } from 'fs'; import * as enquirer from 'enquirer'; import * as path from 'path'; import { dirSync } from 'tmp'; -import * as yargsParser from 'yargs-parser'; +import * as yargs from 'yargs'; import { showNxWarning, unparse } from './shared'; import { output } from './output'; import * as ora from 'ora'; - import { detectInvokedPackageManager, getPackageManagerCommand, getPackageManagerVersion, PackageManager, + packageManagerList, } from './package-manager'; import { validateNpmPackage } from './validate-npm-package'; - -export enum Preset { +import { deduceDefaultBase } from './default-base'; +import { stringifyCollection } from './utils'; +import { yargsDecorator } from './decorator'; + +type Arguments = { + name: string; + preset: string; + appName: string; + cli: string; + style: string; + nxCloud: boolean; + allPrompts: boolean; + packageManager: string; + defaultBase: string; +}; + +enum Preset { Apps = 'apps', Empty = 'empty', // same as apps, deprecated Core = 'core', @@ -101,41 +114,93 @@ const cliVersion = 'NX_VERSION'; const nxVersion = 'NX_VERSION'; const prettierVersion = 'PRETTIER_VERSION'; -const parsedArgs: any = yargsParser(process.argv.slice(2), { - string: [ - 'name', - 'cli', - 'preset', - 'appName', - 'style', - 'defaultBase', - 'packageManager', - ], - alias: { - packageManager: 'pm', - }, - boolean: ['help', 'interactive', 'nxCloud'], - default: { - interactive: false, - }, - configuration: { +export const commandsObject: yargs.Argv = yargs + .wrap(yargs.terminalWidth()) + .parserConfiguration({ 'strip-dashed': true, - 'strip-aliased': true, - }, -}) as any; - -if (parsedArgs.help) { - showHelp(); - process.exit(0); -} - -(async function main() { - const packageManager: PackageManager = - parsedArgs.packageManager || detectInvokedPackageManager(); - - const { name, cli, preset, appName, style, nxCloud } = await getConfiguration( - parsedArgs - ); + 'dot-notation': false, + }) + .command( + // this is the default and only command + '$0 [name] [options]', + 'Create a new Nx workspace', + (yargs) => + yargs + .option('name', { + describe: `Workspace name (e.g. org name)`, + type: 'string', + }) + .option('preset', { + describe: `Customizes the initial content of your workspace. To build your own see https://nx.dev/nx-plugin/overview#preset`, + choices: Object.values(Preset), + type: 'string', + }) + .option('appName', { + describe: `The name of the application when a preset with pregenerated app is selected`, + type: 'string', + }) + .option('interactive', { + describe: `Enable interactive mode with presets`, + type: 'boolean', + }) + .option('cli', { + describe: `CLI to power the Nx workspace`, + choices: ['nx', 'angular'], + type: 'string', + }) + .option('style', { + describe: `Style option to be used when a preset with pregenerated app is selected`, + type: 'string', + }) + .option('nxCloud', { + describe: `Use Nx Cloud`, + defaultDescription: 'true', + type: 'boolean', + }) + .option('allPrompts', { + alias: 'a', + describe: `Show all prompts`, + type: 'boolean', + default: false, + }) + .option('packageManager', { + alias: 'pm', + describe: `Package manager to use`, + choices: [...packageManagerList].sort(), + defaultDescription: 'npm', + type: 'string', + }) + .option('defaultBase', { + defaultDescription: 'main', + describe: `Default base to use for new projects`, + type: 'string', + }), + async (argv: yargs.ArgumentsCamelCase) => { + await main(argv).catch((error) => { + const { version } = require('../package.json'); + output.error({ + title: `Something went wrong! v${version}`, + }); + throw error; + }); + }, + [getConfiguration] + ) + .help('help') + .updateLocale(yargsDecorator) + .version(nxVersion) as yargs.Argv; + +async function main(parsedArgs: yargs.Arguments) { + const { + name, + cli, + preset, + appName, + style, + nxCloud, + packageManager, + defaultBase, + } = parsedArgs; output.log({ title: `Nx is creating your v${cliVersion} workspace.`, @@ -147,101 +212,73 @@ if (parsedArgs.help) { const tmpDir = await createSandbox(packageManager); - await createApp(tmpDir, name, packageManager, { + await createApp(tmpDir, name, packageManager as PackageManager, { ...parsedArgs, cli, preset, appName, style, nxCloud, + defaultBase, }); let nxCloudInstallRes; if (nxCloud) { - nxCloudInstallRes = await setupNxCloud(name, packageManager); + nxCloudInstallRes = await setupNxCloud( + name, + packageManager as PackageManager + ); } showNxWarning(name); - pointToTutorialAndCourse(preset); + pointToTutorialAndCourse(preset as Preset); if (nxCloud && nxCloudInstallRes.code === 0) { printNxCloudSuccessMessage(nxCloudInstallRes.stdout); } -})().catch((error) => { - const { version } = require('../package.json'); - output.error({ - title: `Something went wrong! v${version}`, - }); - throw error; -}); - -function showHelp() { - const options = Object.values(Preset) - .map((preset) => `"${preset}"`) - .join(', '); - - console.log(` - Usage: create-nx-workspace [options] [new workspace options] - - Create a new Nx workspace - - Options: - - name Workspace name (e.g., org name) - - preset Customizes the initial content of your workspace (options: ${options}). To build your own see https://nx.dev/nx-plugin/overview#preset - - appName The name of the application created by some presets - - cli CLI to power the Nx workspace (options: "nx", "angular") - - style Default style option to be used when a non-empty preset is selected - options: ("css", "scss", "less") plus ("styl") for all non-Angular and ("styled-components", "@emotion/styled", "styled-jsx") for React, Next.js - - interactive Enable interactive mode when using presets (boolean) - - packageManager Package manager to use (alias: "pm") - options: ("npm", "yarn", "pnpm") - - defaultBase Name of the main branch (default: "main") - - nx-cloud Use Nx Cloud (boolean) -`); } -async function getConfiguration(parsedArgs) { +async function getConfiguration( + argv: yargs.Arguments +): Promise { try { let style, appName; - const name = await determineWorkspaceName(parsedArgs); - let preset = await determineThirdPartyPackage(parsedArgs); + const name = await determineWorkspaceName(argv); + let preset = await determineThirdPartyPackage(argv); if (!preset) { - preset = await determinePreset(parsedArgs); - appName = await determineAppName(preset, parsedArgs); - style = await determineStyle(preset, parsedArgs); + preset = await determinePreset(argv); + appName = await determineAppName(preset, argv); + style = await determineStyle(preset, argv); } - const cli = await determineCli(preset, parsedArgs); - const nxCloud = await askAboutNxCloud(parsedArgs); + const cli = await determineCli(preset, argv); + const packageManager = await determinePackageManager(argv); + const defaultBase = await determineDefaultBase(argv); + const nxCloud = await determineNxCloud(argv); - return { + Object.assign(argv, { name, preset, appName, style, cli, nxCloud, - }; + packageManager, + defaultBase, + }); } catch (e) { console.error(e); process.exit(1); } } -function determineWorkspaceName(parsedArgs: any): Promise { +function determineWorkspaceName( + parsedArgs: yargs.Arguments +): Promise { const workspaceName: string = parsedArgs._[0] - ? parsedArgs._[0] + ? parsedArgs._[0].toString() : parsedArgs.name; if (workspaceName) { @@ -268,8 +305,85 @@ function determineWorkspaceName(parsedArgs: any): Promise { }); } -async function determineThirdPartyPackage({ preset }) { - if (preset && Object.values(Preset).indexOf(preset) === -1) { +async function determinePackageManager( + parsedArgs: yargs.Arguments +): Promise { + const packageManager: string = parsedArgs.packageManager; + + if (packageManager) { + if (packageManagerList.includes(packageManager as PackageManager)) { + return Promise.resolve(packageManager as PackageManager); + } + output.error({ + title: 'Invalid package manager', + bodyLines: [ + `Package manager must be one of ${stringifyCollection([ + ...packageManagerList, + ])}`, + ], + }); + process.exit(1); + } + + if (parsedArgs.allPrompts) { + return enquirer + .prompt([ + { + name: 'PackageManager', + message: `Which package manager to use `, + initial: 'npm' as any, + type: 'select', + choices: [ + { name: 'npm', message: 'NPM' }, + { name: 'yarn', message: 'Yarn' }, + { name: 'pnpm', message: 'PNPM' }, + ], + }, + ]) + .then((a: { PackageManager }) => a.PackageManager); + } + + return Promise.resolve(detectInvokedPackageManager()); +} + +async function determineDefaultBase( + parsedArgs: yargs.Arguments +): Promise { + if (parsedArgs.defaultBase) { + return Promise.resolve(parsedArgs.defaultBase); + } + if (parsedArgs.allPrompts) { + return enquirer + .prompt([ + { + name: 'DefaultBase', + message: `Main branch name `, + initial: `main`, + type: 'input', + }, + ]) + .then((a: { DefaultBase: string }) => { + if (!a.DefaultBase) { + output.error({ + title: 'Invalid branch name', + bodyLines: [`Branch name cannot be empty`], + }); + process.exit(1); + } + return a.DefaultBase; + }); + } + return Promise.resolve(deduceDefaultBase()); +} + +function isKnownPreset(preset: string): preset is Preset { + return Object.values(Preset).includes(preset as Preset); +} + +async function determineThirdPartyPackage({ + preset, +}: yargs.Arguments) { + if (preset && !isKnownPreset(preset)) { const packageName = preset.match(/.+@/) ? preset[0] + preset.substring(1).split('@')[0] : preset; @@ -293,7 +407,7 @@ async function determineThirdPartyPackage({ preset }) { } } -function determinePreset(parsedArgs: any): Promise { +async function determinePreset(parsedArgs: any): Promise { if (parsedArgs.preset) { if (Object.values(Preset).indexOf(parsedArgs.preset) === -1) { output.error({ @@ -323,7 +437,10 @@ function determinePreset(parsedArgs: any): Promise { .then((a: { Preset: Preset }) => a.Preset); } -function determineAppName(preset: Preset, parsedArgs: any): Promise { +async function determineAppName( + preset: Preset, + parsedArgs: yargs.Arguments +): Promise { if ( preset === Preset.Apps || preset === Preset.Core || @@ -358,12 +475,16 @@ function determineAppName(preset: Preset, parsedArgs: any): Promise { }); } -function determineCli( +function isValidCli(cli: string): cli is 'angular' | 'nx' { + return ['nx', 'angular'].indexOf(cli) !== -1; +} + +async function determineCli( preset: Preset, - parsedArgs: any + parsedArgs: yargs.Arguments ): Promise<'nx' | 'angular'> { if (parsedArgs.cli) { - if (['nx', 'angular'].indexOf(parsedArgs.cli) === -1) { + if (!isValidCli(parsedArgs.cli)) { output.error({ title: 'Invalid cli', bodyLines: [`It must be one of the following:`, '', 'nx', 'angular'], @@ -384,7 +505,10 @@ function determineCli( } } -function determineStyle(preset: Preset, parsedArgs: any) { +async function determineStyle( + preset: Preset, + parsedArgs: yargs.Arguments +) { if ( preset === Preset.Apps || preset === Preset.Core || @@ -625,7 +749,7 @@ function execAndWait(command: string, cwd: string) { }); } -async function askAboutNxCloud(parsedArgs: any) { +async function determineNxCloud(parsedArgs: yargs.Arguments) { if (parsedArgs.nxCloud === undefined) { return enquirer .prompt([ diff --git a/packages/create-nx-workspace/bin/decorator.ts b/packages/create-nx-workspace/bin/decorator.ts new file mode 100644 index 0000000000000..6e1f2f8aececa --- /dev/null +++ b/packages/create-nx-workspace/bin/decorator.ts @@ -0,0 +1,6 @@ +import * as chalk from 'chalk'; + +export const yargsDecorator = { + 'Options:': `${chalk.bold('Options')}:`, + 'Positionals:': `${chalk.bold('Positionals')}:`, +}; diff --git a/packages/create-nx-workspace/bin/default-base.ts b/packages/create-nx-workspace/bin/default-base.ts new file mode 100644 index 0000000000000..e94f951707b1d --- /dev/null +++ b/packages/create-nx-workspace/bin/default-base.ts @@ -0,0 +1,17 @@ +import { execSync } from 'child_process'; + +/* + * Because we don't want to depend on @nrwl/workspace (to speed up the workspace creation) + * we duplicate the helper functions from @nrwl/workspace in this file. + */ +export function deduceDefaultBase(): string { + const nxDefaultBase = 'main'; + try { + return ( + execSync('git config --get init.defaultBranch').toString().trim() || + nxDefaultBase + ); + } catch { + return nxDefaultBase; + } +} diff --git a/packages/create-nx-workspace/bin/index.ts b/packages/create-nx-workspace/bin/index.ts new file mode 100644 index 0000000000000..0f88bbe6c12e1 --- /dev/null +++ b/packages/create-nx-workspace/bin/index.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { commandsObject } from './create-nx-workspace'; + +commandsObject.argv; diff --git a/packages/create-nx-workspace/bin/package-manager.ts b/packages/create-nx-workspace/bin/package-manager.ts index d86247bad26f6..1500fdd3481d4 100644 --- a/packages/create-nx-workspace/bin/package-manager.ts +++ b/packages/create-nx-workspace/bin/package-manager.ts @@ -7,7 +7,7 @@ import { join } from 'path'; * we duplicate the helper functions from @nrwl/workspace in this file. */ -const packageManagerList = ['pnpm', 'yarn', 'npm'] as const; +export const packageManagerList = ['pnpm', 'yarn', 'npm'] as const; export type PackageManager = typeof packageManagerList[number]; @@ -109,7 +109,7 @@ export function detectInvokedPackageManager(): PackageManager { if (!invoker) { return detectedPackageManager; } - + console.log(invoker.path); for (const pkgManager of packageManagerList) { if (invoker.path.includes(pkgManager)) { detectedPackageManager = pkgManager; diff --git a/packages/create-nx-workspace/bin/utils.ts b/packages/create-nx-workspace/bin/utils.ts new file mode 100644 index 0000000000000..c455fe1b2eed0 --- /dev/null +++ b/packages/create-nx-workspace/bin/utils.ts @@ -0,0 +1,3 @@ +export function stringifyCollection(items: string[]): string { + return items.map((item) => `"${item}"`).join(', '); +} diff --git a/packages/create-nx-workspace/package.json b/packages/create-nx-workspace/package.json index 6f676b2110199..a89077d070571 100644 --- a/packages/create-nx-workspace/package.json +++ b/packages/create-nx-workspace/package.json @@ -19,7 +19,7 @@ "CLI" ], "bin": { - "create-nx-workspace": "./bin/create-nx-workspace.js" + "create-nx-workspace": "./bin/index.js" }, "author": "Victor Savkin", "license": "MIT", @@ -29,7 +29,7 @@ "homepage": "https://nx.dev", "dependencies": { "tmp": "~0.2.1", - "yargs-parser": "21.0.1", + "yargs": "^17.4.0", "enquirer": "~2.3.6", "flat": "^5.0.2", "chalk": "4.1.0", diff --git a/packages/workspace/src/utilities/default-base.ts b/packages/workspace/src/utilities/default-base.ts index a6844bddd145d..3ead8e25b72f2 100644 --- a/packages/workspace/src/utilities/default-base.ts +++ b/packages/workspace/src/utilities/default-base.ts @@ -1,6 +1,5 @@ import { execSync } from 'child_process'; -// TODO (v13): Update to main export function deduceDefaultBase(): string { const nxDefaultBase = 'main'; try { diff --git a/scripts/documentation/documentation.ts b/scripts/documentation/documentation.ts index e5f844907204e..33f17c0cb581f 100644 --- a/scripts/documentation/documentation.ts +++ b/scripts/documentation/documentation.ts @@ -1,6 +1,9 @@ import * as chalk from 'chalk'; import { execSync } from 'child_process'; +import { removeSync } from 'fs-extra'; +import { join } from 'path'; import { generateCLIDocumentation } from './generate-cli-data'; +import { generateCNWocumentation } from './generate-cnw-documentation'; import { generateDevkitDocumentation } from './generate-devkit-documentation'; import { generatePackageSchemas } from './package-schemas/generatePackageSchemas'; @@ -9,7 +12,16 @@ async function generate() { console.log(`${chalk.blue('i')} Generating Documentation`); generatePackageSchemas(); generateDevkitDocumentation(); - await generateCLIDocumentation(); + + const commandsOutputDirectory = join( + __dirname, + '../../docs/', + 'generated', + 'cli' + ); + removeSync(commandsOutputDirectory); + await generateCNWocumentation(commandsOutputDirectory); + await generateCLIDocumentation(commandsOutputDirectory); console.log(`\n${chalk.green('✓')} Generated Documentation\n`); } catch (e) { diff --git a/scripts/documentation/generate-cli-data.ts b/scripts/documentation/generate-cli-data.ts index 1a1216e88a4d8..178252913b8b7 100644 --- a/scripts/documentation/generate-cli-data.ts +++ b/scripts/documentation/generate-cli-data.ts @@ -1,16 +1,19 @@ import * as chalk from 'chalk'; import { readFileSync } from 'fs'; -import { readJsonSync, removeSync } from 'fs-extra'; +import { readJsonSync } from 'fs-extra'; import { join } from 'path'; -import { dedent } from 'tslint/lib/utils'; import { formatDeprecated, generateMarkdownFile, + generateOptionsMarkdown, + getCommands, + parseCommand, + ParsedCommand, sortAlphabeticallyFunction, } from './utils'; import { register as registerTsConfigPaths } from 'tsconfig-paths'; - import { examples } from '../../packages/nx/src/command-line/examples'; +import { dedent } from 'tslint/lib/utils'; const importFresh = require('import-fresh'); @@ -24,21 +27,9 @@ const sharedCommands = [ 'test', ]; -interface ParsedCommandOption { - name: string; - description: string; - default: string; - deprecated: boolean | string; -} - -interface ParsedCommand { - name: string; - commandString: string; - description: string; - options?: Array; -} - -export async function generateCLIDocumentation() { +export async function generateCLIDocumentation( + commandsOutputDirectory: string +) { /** * For certain commands, they will output dynamic data at runtime in a real workspace, * so we leverage an envrionment variable to inform the logic of the context that we @@ -57,65 +48,6 @@ export async function generateCLIDocumentation() { '../../packages/nx/src/command-line/nx-commands' ); - const commandsOutputDirectory = join( - __dirname, - '../../docs/', - 'generated', - 'cli' - ); - removeSync(commandsOutputDirectory); - - function getCommands(command) { - return command - .getInternalMethods() - .getCommandInstance() - .getCommandHandlers(); - } - async function parseCommandInstance( - name: string, - command: any - ): Promise { - // It is not a function return a strip down version of the command - if ( - !( - command.builder && - command.builder.constructor && - command.builder.call && - command.builder.apply - ) - ) { - return { - name, - commandString: command.original, - description: command.description, - }; - } - // Show all the options we can get from yargs - const builder = await command.builder( - importFresh('yargs')().getInternalMethods().reset() - ); - const builderDescriptions = builder - .getInternalMethods() - .getUsageInstance() - .getDescriptions(); - const builderDefaultOptions = builder.getOptions().default; - const builderDeprecatedOptions = builder.getDeprecatedOptions(); - return { - name, - description: command.description, - commandString: command.original, - options: - Object.keys(builderDescriptions).map((key) => ({ - name: key, - description: builderDescriptions[key] - ? builderDescriptions[key].replace('__yargsString__:', '') - : '', - default: builderDefaultOptions[key], - deprecated: builderDeprecatedOptions[key], - })) || null, - }; - } - function generateMarkdown(command: ParsedCommand) { let template = dedent` --- @@ -146,25 +78,7 @@ nx ${command.commandString} }); } - if (Array.isArray(command.options) && !!command.options.length) { - template += '\n## Options'; - - command.options - .sort((a, b) => sortAlphabeticallyFunction(a.name, b.name)) - .forEach((option) => { - template += dedent` - ### ${option.deprecated ? `~~${option.name}~~` : option.name} - ${ - option.default === undefined || option.default === '' - ? '' - : `Default: \`${option.default}\`\n` - } - `; - template += dedent` - ${formatDeprecated(option.description, option.deprecated)} - `; - }); - } + template += generateOptionsMarkdown(command); return { name: command.name @@ -181,7 +95,7 @@ nx ${command.commandString} Object.keys(nxCommands) .filter((name) => !sharedCommands.includes(name)) .filter((name) => nxCommands[name].description) - .map((name) => parseCommandInstance(name, nxCommands[name])) + .map((name) => parseCommand(name, nxCommands[name])) .map(async (command) => generateMarkdown(await command)) .map(async (templateObject) => generateMarkdownFile(commandsOutputDirectory, await templateObject) diff --git a/scripts/documentation/generate-cnw-documentation.ts b/scripts/documentation/generate-cnw-documentation.ts new file mode 100644 index 0000000000000..af41677da20b1 --- /dev/null +++ b/scripts/documentation/generate-cnw-documentation.ts @@ -0,0 +1,65 @@ +import * as chalk from 'chalk'; +import { dedent } from 'tslint/lib/utils'; +import { + generateMarkdownFile, + getCommands, + parseCommand, + ParsedCommand, + sortAlphabeticallyFunction, + formatDeprecated, + generateOptionsMarkdown, +} from './utils'; +const importFresh = require('import-fresh'); + +export async function generateCNWocumentation(commandsOutputDirectory: string) { + process.env.NX_GENERATE_DOCS_PROCESS = 'true'; + + console.log( + `\n${chalk.blue( + 'i' + )} Generating Documentation for Create Nx Workspace Command` + ); + + const { commandsObject } = importFresh( + '../../packages/create-nx-workspace/bin/create-nx-workspace' + ); + + const command = getCommands(commandsObject)['$0']; + const parsedCommand = await parseCommand('create-nx-workspace', command); + const markdown = generateMarkdown(parsedCommand); + generateMarkdownFile(commandsOutputDirectory, markdown); + + delete process.env.NX_GENERATE_DOCS_PROCESS; + + console.log( + `${chalk.green( + '✓' + )} Generated Documentation for Create Nx Workspace Command` + ); +} + +function generateMarkdown(command: ParsedCommand) { + let template = dedent` + --- + title: "${command.name} - CLI command" + description: "${command.description}" + --- + # ${command.name} + + ${command.description} + + ## Usage + + \`\`\`bash + ${command.commandString} + \`\`\` + + Install \`create-nx-workspace\` globally to invoke the command directly, or use \`npx create-nx-workspace\`, \`yarn create nx-workspace\`, or \`pnpx create-nx-workspace\`.\n`; + + template += generateOptionsMarkdown(command); + + return { + name: command.name, + template, + }; +} diff --git a/scripts/documentation/utils.ts b/scripts/documentation/utils.ts index b7c73c26ac2a4..aa817a59813ea 100644 --- a/scripts/documentation/utils.ts +++ b/scripts/documentation/utils.ts @@ -1,7 +1,9 @@ import { outputFileSync, readJsonSync } from 'fs-extra'; import { join } from 'path'; import { format, resolveConfig } from 'prettier'; +import { dedent } from 'tslint/lib/utils'; const stripAnsi = require('strip-ansi'); +const importFresh = require('import-fresh'); export function sortAlphabeticallyFunction(a: string, b: string): number { const nameA = a.toUpperCase(); // ignore upper and lowercase @@ -99,7 +101,106 @@ export function formatDeprecated( ? `**Deprecated:** ${description}` : ` **Deprecated:** ${deprecated} - + ${description} `; } + +export function getCommands(command) { + return command.getInternalMethods().getCommandInstance().getCommandHandlers(); +} + +export interface ParsedCommandOption { + name: string; + description: string; + default: string; + deprecated: boolean | string; +} + +export interface ParsedCommand { + name: string; + commandString: string; + description: string; + options?: Array; +} + +export async function parseCommand( + name: string, + command: any +): Promise { + // It is not a function return a strip down version of the command + if ( + !( + command.builder && + command.builder.constructor && + command.builder.call && + command.builder.apply + ) + ) { + return { + name, + commandString: command.original, + description: command.description, + }; + } + + // Show all the options we can get from yargs + const builder = await command.builder( + importFresh('yargs')().getInternalMethods().reset() + ); + const builderDescriptions = builder + .getInternalMethods() + .getUsageInstance() + .getDescriptions(); + const builderDefaultOptions = builder.getOptions().default; + const builderAutomatedOptions = builder.getOptions().defaultDescription; + const builderDeprecatedOptions = builder.getDeprecatedOptions(); + const builderOptionsChoices = builder.getOptions().choices; + + return { + name, + description: command.description, + commandString: command.original.replace('$0', name), + options: + Object.keys(builderDescriptions).map((key) => ({ + name: key, + description: builderDescriptions[key] + ? builderDescriptions[key].replace('__yargsString__:', '') + : '', + default: builderDefaultOptions[key] ?? builderAutomatedOptions[key], + choices: builderOptionsChoices[key], + deprecated: builderDeprecatedOptions[key], + })) || null, + }; +} + +export function generateOptionsMarkdown(command): string { + let response = ''; + if (Array.isArray(command.options) && !!command.options.length) { + response += '\n## Options'; + + command.options + .sort((a, b) => sortAlphabeticallyFunction(a.name, b.name)) + .forEach((option) => { + response += dedent` + ### ${option.deprecated ? `~~${option.name}~~` : option.name} + ${ + option.default === undefined || option.default === '' + ? '' + : `Default: \`${option.default}\`\n` + }`; + response += dedent` + ${ + option.choices === undefined + ? '' + : `Choices: \`[${option.choices + .map((c) => `"${c}"`) + .join(', ')}]\`\n` + }`; + response += dedent` + ${formatDeprecated(option.description, option.deprecated)} + `; + }); + } + return response; +}