import * as enquirer from 'enquirer'; import * as yargs from 'yargs'; import * as chalk from 'chalk'; import { CreateWorkspaceOptions } from '../src/create-workspace-options'; import { createWorkspace } from '../src/create-workspace'; import { isKnownPreset, Preset } from '../src/utils/preset/preset'; import { presetOptions } from '../src/utils/preset/preset-options'; import { output } from '../src/utils/output'; import { nxVersion } from '../src/utils/nx/nx-version'; import { pointToTutorialAndCourse } from '../src/utils/preset/point-to-tutorial-and-course'; import { yargsDecorator } from './decorator'; import { getThirdPartyPreset } from '../src/utils/preset/get-third-party-preset'; import { Framework, frameworkList } from './types/framework-list'; import { Bundler, bundlerList } from './types/bundler-list'; import { determineCI, determineDefaultBase, determineNxCloud, determinePackageManager, } from '../src/internal-utils/prompts'; import { withAllPrompts, withCI, withGitOptions, withNxCloud, withOptions, withPackageManager, } from '../src/internal-utils/yargs-options'; import { showNxWarning } from '../src/utils/nx/show-nx-warning'; import { printNxCloudSuccessMessage } from '../src/utils/nx/nx-cloud'; import { messages, recordStat } from '../src/utils/nx/ab-testing'; interface Arguments extends CreateWorkspaceOptions { preset: string; appName: string; style: string; framework: Framework; standaloneApi: boolean; docker: boolean; nextAppDir: boolean; routing: boolean; bundler: Bundler; } export const commandsObject: yargs.Argv = yargs .wrap(yargs.terminalWidth()) .parserConfiguration({ 'strip-dashed': true, 'dot-notation': true, }) .command( // this is the default and only command '$0 [name] [options]', 'Create a new Nx workspace', (yargs) => withOptions( yargs .option('name', { describe: chalk.dim`Workspace name (e.g. org name)`, type: 'string', }) .option('preset', { describe: chalk.dim`Customizes the initial content of your workspace. Default presets include: [${Object.values( Preset ) .map((p) => `"${p}"`) .join( ', ' )}]. To build your own see https://nx.dev/packages/nx-plugin#preset`, type: 'string', }) .option('appName', { describe: chalk.dim`The name of the application when a preset with pregenerated app is selected`, type: 'string', }) .option('interactive', { describe: chalk.dim`Enable interactive mode with presets`, type: 'boolean', default: true, }) .option('style', { describe: chalk.dim`Style option to be used when a preset with pregenerated app is selected`, type: 'string', }) .option('standaloneApi', { describe: chalk.dim`Use Standalone Components if generating an Angular app`, type: 'boolean', }) .option('routing', { describe: chalk.dim`Add a routing setup when a preset with pregenerated app is selected`, type: 'boolean', }) .option('bundler', { describe: chalk.dim`Bundler to be used to build the application`, choices: bundlerList, type: 'string', }) .option('framework', { describe: chalk.dim`Framework option to be used when the node-server preset is selected`, choices: frameworkList, type: 'string', }) .option('docker', { describe: chalk.dim`Generate a Dockerfile with your node-server`, type: 'boolean', }) .option('nextAppDir', { describe: chalk.dim`Add Experimental app/ layout for next.js`, type: 'boolean', }), withNxCloud, withCI, withAllPrompts, withPackageManager, withGitOptions ), async (argv: yargs.ArgumentsCamelCase) => { await main(argv).catch((error) => { const { version } = require('../package.json'); output.error({ title: `Something went wrong! v${version}`, }); throw error; }); }, [normalizeArgsMiddleware as yargs.MiddlewareFunction<{}>] ) .help('help', chalk.dim`Show help`) .updateLocale(yargsDecorator) .version( 'version', chalk.dim`Show version`, nxVersion ) as yargs.Argv; async function main(parsedArgs: yargs.Arguments) { output.log({ title: `Creating your v${nxVersion} workspace.`, bodyLines: [ 'To make sure the command works reliably in all environments, and that the preset is applied correctly,', `Nx will run "${parsedArgs.packageManager} install" several times. Please wait.`, ], }); const workspaceInfo = await createWorkspace( parsedArgs.preset, parsedArgs ); showNxWarning(parsedArgs.name); await recordStat({ nxVersion, command: 'create-nx-workspace', useCloud: parsedArgs.nxCloud, meta: messages.codeOfSelectedPromptMessage('nxCloudCreation'), }); if (parsedArgs.nxCloud && workspaceInfo.nxCloudInfo) { printNxCloudSuccessMessage(workspaceInfo.nxCloudInfo); } if (isKnownPreset(parsedArgs.preset)) { pointToTutorialAndCourse(parsedArgs.preset as Preset); } else { output.log({ title: `Successfully applied preset: ${parsedArgs.preset}`, }); } } /** * This function is used to normalize the arguments passed to the command. * It would: * - normalize the preset. * @param argv user arguments */ async function normalizeArgsMiddleware( argv: yargs.Arguments ): Promise { try { let name, appName, style, preset, framework, bundler, docker, nextAppDir, routing, standaloneApi; output.log({ title: "Let's create a new workspace [https://nx.dev/getting-started/intro]", }); let thirdPartyPreset: string | null; try { thirdPartyPreset = await getThirdPartyPreset(argv.preset); } catch (e) { output.error({ title: `Could not find preset "${argv.preset}"`, }); process.exit(1); } if (thirdPartyPreset) { preset = thirdPartyPreset; name = await determineRepoName(argv); appName = ''; style = null; } else { if (!argv.preset) { const monorepoStyle = await determineMonorepoStyle(); if (monorepoStyle === 'package-based') { preset = 'npm'; } else if (monorepoStyle === 'react') { preset = await determineReactFramework(argv); } else if (monorepoStyle === 'angular') { preset = Preset.AngularStandalone; } else if (monorepoStyle === 'node-standalone') { preset = Preset.NodeStandalone; } else { // when choose integrated monorepo, further prompt for preset preset = await determinePreset(argv); } } else if (argv.preset === 'react') { preset = await monorepoOrStandalone('react'); } else if (argv.preset === 'angular') { preset = await monorepoOrStandalone('angular'); } else { preset = argv.preset; } if ( preset === Preset.ReactStandalone || preset === Preset.AngularStandalone || preset === Preset.NodeStandalone || preset === Preset.NextJsStandalone ) { appName = argv.appName ?? argv.name ?? (await determineAppName(preset, argv)); name = argv.name ?? appName; if (preset === Preset.NodeStandalone) { framework = await determineFramework(argv); if (framework !== 'none') { docker = await determineDockerfile(argv); } } if (preset === Preset.NextJsStandalone) { nextAppDir = await isNextAppDir(argv); } if (preset === Preset.ReactStandalone) { bundler = await determineBundler(argv); } if (preset === Preset.AngularStandalone) { standaloneApi = argv.standaloneApi ?? (argv.interactive ? await determineStandaloneApi(argv) : false); routing = argv.routing ?? (argv.interactive ? await determineRouting(argv) : true); } } else { name = await determineRepoName(argv); appName = await determineAppName(preset as Preset, argv); if (preset === Preset.ReactMonorepo) { bundler = await determineBundler(argv); } if (preset === Preset.AngularMonorepo) { standaloneApi = argv.standaloneApi ?? (argv.interactive ? await determineStandaloneApi(argv) : false); routing = argv.routing ?? (argv.interactive ? await determineRouting(argv) : true); } } style = await determineStyle(preset as Preset, argv); } const packageManager = await determinePackageManager(argv); const defaultBase = await determineDefaultBase(argv); const nxCloud = await determineNxCloud(argv); const ci = await determineCI(argv, nxCloud); Object.assign(argv, { name, preset, appName, style, standaloneApi, routing, framework, nxCloud, packageManager, defaultBase, ci, bundler, docker, nextAppDir, }); } catch (e) { console.error(e); process.exit(1); } } async function determineRepoName( parsedArgs: yargs.Arguments ): Promise { const repoName: string = parsedArgs._[0] ? parsedArgs._[0].toString() : parsedArgs.name; if (repoName) { return Promise.resolve(repoName); } const a = await enquirer.prompt<{ RepoName: string }>([ { name: 'RepoName', message: `Repository name `, type: 'input', }, ]); if (!a.RepoName) { output.error({ title: 'Invalid repository name', bodyLines: [`Repository name cannot be empty`], }); process.exit(1); } return a.RepoName; } async function monorepoOrStandalone(preset: string): Promise { const a = await enquirer.prompt<{ MonorepoOrStandalone: string }>([ { name: 'MonorepoOrStandalone', message: `--preset=${preset} has been replaced with the following:`, type: 'autocomplete', choices: [ { name: preset + '-standalone', message: `${preset}-standalone: a standalone ${preset} application.`, }, { name: preset + '-monorepo', message: `${preset}-monorepo: a monorepo with the apps and libs folders.`, }, ], }, ]); if (!a.MonorepoOrStandalone) { output.error({ title: 'Invalid selection', }); process.exit(1); } return a.MonorepoOrStandalone; } async function determineMonorepoStyle(): Promise { const a = await enquirer.prompt<{ MonorepoStyle: string }>([ { name: 'MonorepoStyle', message: `Choose what to create `, type: 'autocomplete', choices: [ { name: 'package-based', message: 'Package-based monorepo: Nx makes it fast, but lets you run things your way.', }, { name: 'integrated', message: 'Integrated monorepo: Nx configures your favorite frameworks and lets you focus on shipping features.', }, { name: 'react', message: 'Standalone React app: Nx configures a React app with an optional framework (e.g. Next.js).', }, { name: 'angular', message: 'Standalone Angular app: Nx configures Jest, ESLint and Cypress.', }, { name: 'node-standalone', message: 'Standalone Node app: Nx configures a framework (e.g. Express), esbuild, ESlint and Jest.', }, ], }, ]); if (!a.MonorepoStyle) { output.error({ title: 'Invalid monorepo style', }); process.exit(1); } return a.MonorepoStyle; } async function determinePreset(parsedArgs: any): Promise { if (parsedArgs.preset) { if (Object.values(Preset).indexOf(parsedArgs.preset) === -1) { output.error({ title: 'Invalid preset', bodyLines: [ `It must be one of the following:`, '', ...Object.values(Preset), ], }); process.exit(1); } else { return Promise.resolve(parsedArgs.preset); } } return enquirer .prompt<{ preset: Preset }>([ { name: 'preset', message: `What to create in the new workspace `, initial: 'empty' as any, type: 'autocomplete', choices: presetOptions, }, ]) .then((a: { preset: Preset }) => a.preset); } async function determineAppName( preset: Preset, parsedArgs: yargs.Arguments ): Promise { if ( preset === Preset.Apps || preset === Preset.Core || preset === Preset.TS || preset === Preset.Empty || preset === Preset.NPM ) { return Promise.resolve(''); } if (parsedArgs.appName) { return Promise.resolve(parsedArgs.appName); } return enquirer .prompt<{ AppName: string }>([ { name: 'AppName', message: `Application name `, type: 'input', }, ]) .then((a) => { if (!a.AppName) { output.error({ title: 'Invalid name', bodyLines: [`Name cannot be empty`], }); process.exit(1); } return a.AppName; }); } async function determineFramework( parsedArgs: yargs.Arguments ): Promise { const frameworkChoices = [ { name: 'express', message: 'Express [https://expressjs.com/]', }, { name: 'fastify', message: 'Fastify [https://www.fastify.io/]', }, { name: 'koa', message: 'Koa [https://koajs.com/]', }, { name: 'nest', message: 'NestJs [https://nestjs.com/]', }, { name: 'none', message: 'None', }, ]; if (!parsedArgs.framework) { return enquirer .prompt<{ framework: Framework }>([ { message: 'What framework should be used?', type: 'autocomplete', name: 'framework', choices: frameworkChoices, }, ]) .then((a) => a.framework); } const foundFramework = frameworkChoices .map(({ name }) => name) .indexOf(parsedArgs.framework); if (foundFramework < 0) { output.error({ title: 'Invalid framework', bodyLines: [ `It must be one of the following:`, '', ...frameworkChoices.map(({ name }) => name), ], }); process.exit(1); } return Promise.resolve(parsedArgs.framework); } async function determineStandaloneApi( parsedArgs: yargs.Arguments ): Promise { if (parsedArgs.standaloneApi === undefined) { return enquirer .prompt<{ standaloneApi: 'Yes' | 'No' }>([ { name: 'standaloneApi', message: 'Would you like to use Standalone Components in your application?', type: 'autocomplete', choices: [ { name: 'Yes', }, { name: 'No', }, ], initial: 'No' as any, }, ]) .then((a) => a.standaloneApi === 'Yes'); } return parsedArgs.standaloneApi; } async function determineDockerfile( parsedArgs: yargs.Arguments ): Promise { if (parsedArgs.docker === undefined) { return enquirer .prompt<{ docker: 'Yes' | 'No' }>([ { name: 'docker', message: 'Would you like to generate a Dockerfile? [https://docs.docker.com/]', type: 'autocomplete', choices: [ { name: 'Yes', hint: 'I want to generate a Dockerfile', }, { name: 'No', }, ], initial: 'No' as any, }, ]) .then((a) => a.docker === 'Yes'); } else { return Promise.resolve(parsedArgs.docker); } } async function determineReactFramework(parsedArgs: yargs.Arguments) { if (parsedArgs.framework) { return parsedArgs.framework === 'next.js' ? Preset.NextJsStandalone : Preset.ReactStandalone; } return enquirer .prompt<{ framework: 'none' | 'next.js' }>([ { name: 'framework', message: 'What framework would you like to use?', type: 'autocomplete', choices: [ { name: 'none', message: 'None', hint: 'I only want React', }, { name: 'next.js', message: 'Next.js [https://nextjs.org/]', }, ], initial: 'none' as any, }, ]) .then((choice) => choice.framework === 'next.js' ? Preset.NextJsStandalone : Preset.ReactStandalone ); } async function isNextAppDir(parsedArgs: yargs.Arguments) { if (parsedArgs.nextAppDir === undefined) { return enquirer .prompt<{ appDir: 'Yes' | 'No' }>([ { name: 'appDir', message: 'Do you want to use experimental app/ in this project?', type: 'autocomplete', choices: [ { name: 'No', }, { name: 'Yes', }, ], initial: 'No' as any, }, ]) .then((choice) => choice.appDir === 'Yes'); } else { return Promise.resolve(parsedArgs.nextAppDir); } } async function determineStyle( preset: Preset, parsedArgs: yargs.Arguments ): Promise { if ( preset === Preset.Apps || preset === Preset.Core || preset === Preset.TS || preset === Preset.Empty || preset === Preset.NPM || preset === Preset.Nest || preset === Preset.Express || preset === Preset.ReactNative || preset === Preset.Expo || preset === Preset.NodeStandalone ) { return Promise.resolve(null); } const choices = [ { name: 'css', message: 'CSS', }, { name: 'scss', message: 'SASS(.scss) [ http://sass-lang.com ]', }, { name: 'less', message: 'LESS [ http://lesscss.org ]', }, ]; if ( [ Preset.ReactMonorepo, Preset.ReactStandalone, Preset.NextJs, Preset.NextJsStandalone, ].includes(preset) ) { choices.push( { name: 'styled-components', message: 'styled-components [ https://styled-components.com ]', }, { name: '@emotion/styled', message: 'emotion [ https://emotion.sh ]', }, { name: 'styled-jsx', message: 'styled-jsx [ https://www.npmjs.com/package/styled-jsx ]', } ); } if (!parsedArgs.style) { return enquirer .prompt<{ style: string }>([ { name: 'style', message: `Default stylesheet format `, initial: 'css' as any, type: 'autocomplete', choices: choices, }, ]) .then((a: { style: string }) => a.style); } const foundStyle = choices.find((choice) => choice.name === parsedArgs.style); if (foundStyle === undefined) { output.error({ title: 'Invalid style', bodyLines: [ `It must be one of the following:`, '', ...choices.map((choice) => choice.name), ], }); process.exit(1); } return Promise.resolve(parsedArgs.style); } async function determineRouting( parsedArgs: yargs.Arguments ): Promise { if (!parsedArgs.routing) { return enquirer .prompt<{ routing: 'Yes' | 'No' }>([ { name: 'routing', message: 'Would you like to add routing?', type: 'autocomplete', choices: [ { name: 'Yes', }, { name: 'No', }, ], initial: 'Yes' as any, }, ]) .then((a) => a.routing === 'Yes'); } return parsedArgs.routing; } async function determineBundler( parsedArgs: yargs.Arguments ): Promise { const choices = [ { name: 'vite', message: 'Vite [ https://vitejs.dev/ ]', }, { name: 'webpack', message: 'Webpack [ https://webpack.js.org/ ]', }, { name: 'rspack', message: 'Rspack [ https://www.rspack.dev/ ]', }, ]; if (!parsedArgs.bundler) { return enquirer .prompt<{ bundler: Bundler }>([ { name: 'bundler', message: `Bundler to be used to build the application`, initial: 'vite' as any, type: 'autocomplete', choices: choices, }, ]) .then((a) => a.bundler); } const foundBundler = choices.find( (choice) => choice.name === parsedArgs.bundler ); if (foundBundler === undefined) { output.error({ title: 'Invalid bundler', bodyLines: [ `It must be one of the following:`, '', ...choices.map((choice) => choice.name), ], }); process.exit(1); } return parsedArgs.bundler; }