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 { CLIErrorMessageConfig, 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 { 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'; import { existsSync } from 'fs'; interface BaseArguments extends CreateWorkspaceOptions { preset: Preset; } interface NoneArguments extends BaseArguments { stack: 'none'; workspaceType: 'package-based' | 'integrated'; } interface TsArguments extends BaseArguments { stack: 'ts'; workspaceType: 'standalone' | 'integrated'; js: boolean; } interface ReactArguments extends BaseArguments { stack: 'react'; workspaceType: 'standalone' | 'integrated'; appName: string; framework: 'none' | 'next'; style: string; bundler: 'webpack' | 'vite' | 'rspack'; nextAppDir: boolean; } interface AngularArguments extends BaseArguments { stack: 'angular'; workspaceType: 'standalone' | 'integrated'; appName: string; style: string; routing: boolean; standaloneApi: boolean; } interface NodeArguments extends BaseArguments { stack: 'node'; workspaceType: 'standalone' | 'integrated'; appName: string; framework: 'express' | 'fastify' | 'koa' | 'nest'; docker: boolean; } interface UnknownStackArguments extends BaseArguments { stack: 'unknown'; } type Arguments = | NoneArguments | TsArguments | ReactArguments | AngularArguments | NodeArguments | UnknownStackArguments; 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/plugins/recipes/create-preset`, type: 'string', }) .option('interactive', { describe: chalk.dim`Enable interactive mode with presets`, type: 'boolean', default: true, }) .option('workspaceType', { describe: chalk.dim`The type of workspace to create`, choices: ['integrated', 'package-based', 'standalone'], type: 'string', }) .option('appName', { describe: chalk.dim`The name of the app when using a monorepo with certain stacks`, type: 'string', }) .option('style', { describe: chalk.dim`Stylesheet type to be used with certain stacks`, 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 for an Angular app`, type: 'boolean', }) .option('bundler', { describe: chalk.dim`Bundler to be used to build the app`, type: 'string', }) .option('framework', { describe: chalk.dim`Framework option to be used with certain stacks`, type: 'string', }) .option('docker', { describe: chalk.dim`Generate a Dockerfile for the Node API`, type: 'boolean', }) .option('nextAppDir', { describe: chalk.dim`Enable the App Router for Next.js`, type: 'boolean', }), withNxCloud, withCI, withAllPrompts, withPackageManager, withGitOptions ), async function handler(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 { output.log({ title: "Let's create a new workspace [https://nx.dev/getting-started/intro]", }); try { let thirdPartyPreset: string | null; // Node options let docker: boolean; try { thirdPartyPreset = await getThirdPartyPreset(argv.preset); } catch (e) { output.error({ title: `Could not find preset "${argv.preset}"`, }); process.exit(1); } argv.name = await determineFolder(argv); if (thirdPartyPreset) { Object.assign(argv, { preset: thirdPartyPreset, appName: '', style: '', }); } else { argv.stack = await determineStack(argv); const presetOptions = await determinePresetOptions(argv); Object.assign(argv, presetOptions); } const packageManager = await determinePackageManager(argv); const defaultBase = await determineDefaultBase(argv); const nxCloud = await determineNxCloud(argv); const ci = await determineCI(argv, nxCloud); Object.assign(argv, { nxCloud, packageManager, defaultBase, ci, }); } catch (e) { console.error(e); process.exit(1); } } function invariant( predicate: string | number | boolean, message: CLIErrorMessageConfig ): asserts predicate is NonNullable | true { if (!predicate) { output.error(message); process.exit(1); } } async function determineFolder( parsedArgs: yargs.Arguments ): Promise { const folderName: string = parsedArgs._[0] ? parsedArgs._[0].toString() : parsedArgs.name; if (folderName) return folderName; const reply = await enquirer.prompt<{ folderName: string }>([ { name: 'folderName', message: `Where would you like to create your workspace?`, initial: 'org', type: 'input', }, ]); invariant(reply.folderName, { title: 'Invalid folder name', bodyLines: [`Folder name cannot be empty`], }); invariant(!existsSync(reply.folderName), { title: 'That folder is already taken', }); return reply.folderName; } async function determineStack( parsedArgs: yargs.Arguments ): Promise<'none' | 'ts' | 'react' | 'angular' | 'node' | 'unknown'> { if (parsedArgs.preset) { switch (parsedArgs.preset) { case Preset.Angular: case Preset.AngularStandalone: case Preset.AngularMonorepo: return 'angular'; case Preset.React: case Preset.ReactStandalone: case Preset.ReactMonorepo: case Preset.NextJs: case Preset.NextJsStandalone: return 'react'; case Preset.Nest: case Preset.NodeStandalone: case Preset.Express: return 'node'; case Preset.Core: case Preset.Empty: case Preset.Apps: case Preset.NPM: return 'none'; case Preset.TS: case Preset.TsStandalone: return 'ts'; case Preset.WebComponents: case Preset.ReactNative: case Preset.Expo: default: return 'unknown'; } } const { stack } = await enquirer.prompt<{ stack: 'none' | 'ts' | 'react' | 'angular' | 'node'; }>([ { name: 'stack', message: `Which stack do you want to use?`, type: 'autocomplete', choices: [ { name: `none`, message: `None: Configures a minimal structure without specific frameworks or technologies.`, }, { name: `ts`, message: `TS/JS: Configures a TypeScript/JavaScript package without specific frameworks or platforms.`, }, { name: `react`, message: `React: Configures a React app with your framework of choice.`, }, { name: `angular`, message: `Angular: Configures a Angular app with modern tooling.`, }, { name: `node`, message: `Node: Configures a Node API with your framework of choice.`, }, ], }, ]); return stack; } async function determinePresetOptions( parsedArgs: yargs.Arguments ): Promise> { switch (parsedArgs.stack) { case 'none': return determineNoneOptions(parsedArgs); case 'ts': return determineTsOptions(parsedArgs); case 'react': return determineReactOptions(parsedArgs); case 'angular': return determineAngularOptions(parsedArgs); case 'node': return determineNodeOptions(parsedArgs); default: return parsedArgs; } } async function determineNoneOptions( parsedArgs: yargs.Arguments ): Promise> { if (parsedArgs.preset) return parsedArgs; const { workspaceType } = await enquirer.prompt<{ workspaceType: 'package-based' | 'integrated'; }>([ { type: 'autocomplete', name: 'workspaceType', message: `Package-based or integrated?`, initial: 'package-based' as any, choices: [ { name: 'package-based', message: 'Package-based: Nx makes it fast, but lets you run things your way.', }, { name: 'integrated', message: 'Integrated: Nx creates a workspace structure most suitable for building apps.', }, ], }, ]); if (workspaceType === 'integrated') { return { preset: Preset.Apps, }; } else { return { preset: Preset.NPM, }; } } async function determineTsOptions( parsedArgs: yargs.Arguments ): Promise> { let preset: Preset; let workspaceType: 'standalone' | 'integrated' | undefined = undefined; let appName: string | undefined = undefined; let js = false; if (parsedArgs.preset) { preset = parsedArgs.preset; } else { workspaceType = await determineStandAloneOrMonorepo(); preset = workspaceType === 'standalone' ? Preset.TsStandalone : Preset.TS; } if (parsedArgs.js !== undefined) { js = parsedArgs.js; } else if (preset === Preset.TsStandalone) { // Only standalone TS preset generates a default package, so we need to provide --js and --appName options. appName = parsedArgs.name; const reply = await enquirer.prompt<{ ts: 'Yes' | 'No' }>([ { name: 'ts', message: `Would you like to use TypeScript with this project?`, type: 'autocomplete', choices: [ { name: 'Yes', }, { name: 'No', }, ], initial: 'Yes' as any, }, ]); js = reply.ts === 'No'; } return { preset, js, appName }; } async function determineReactOptions( parsedArgs: yargs.Arguments ): Promise> { let preset: Preset; let style: undefined | string = undefined; let appName: string; let bundler: undefined | 'webpack' | 'vite' | 'rspack' = undefined; let nextAppDir = false; if (parsedArgs.preset) { preset = parsedArgs.preset; if ( preset === Preset.ReactStandalone || preset === Preset.NextJsStandalone ) { appName = parsedArgs.appName ?? parsedArgs.name; } else { appName = await determineAppName(parsedArgs); } } else { const framework = await determineReactFramework(parsedArgs); // React Native and Expo only support integrated monorepos for now. // TODO(jack): Add standalone support for React Native and Expo. const workspaceType = framework === 'react-native' || framework === 'expo' ? 'integrated' : await determineStandAloneOrMonorepo(); if (workspaceType === 'standalone') { appName = parsedArgs.name; } else { appName = await determineAppName(parsedArgs); } if (framework === 'nextjs') { if (workspaceType === 'standalone') { preset = Preset.NextJsStandalone; } else { preset = Preset.NextJs; } } else if (framework === 'react-native') { preset = Preset.ReactNative; } else if (framework === 'expo') { preset = Preset.Expo; } else { if (workspaceType === 'standalone') { preset = Preset.ReactStandalone; } else { preset = Preset.ReactMonorepo; } } } if (preset === Preset.ReactStandalone || preset === Preset.ReactMonorepo) { bundler = await determineReactBundler(parsedArgs); } else if (preset === Preset.NextJs || preset === Preset.NextJsStandalone) { nextAppDir = await determineNextAppDir(parsedArgs); } if (parsedArgs.style) { style = parsedArgs.style; } else if ( preset === Preset.ReactStandalone || preset === Preset.ReactMonorepo || preset === Preset.NextJs || preset === Preset.NextJsStandalone ) { const reply = await enquirer.prompt<{ style: string }>([ { name: 'style', message: `Default stylesheet format`, initial: 'css' as any, type: 'autocomplete', choices: [ { name: 'css', message: 'CSS', }, { name: 'scss', message: 'SASS(.scss) [ http://sass-lang.com ]', }, { name: 'less', message: 'LESS [ http://lesscss.org ]', }, { 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 ]', }, ], }, ]); style = reply.style; } return { preset, style, appName, bundler, nextAppDir }; } async function determineAngularOptions( parsedArgs: yargs.Arguments ): Promise> { let preset: Preset; let style: string; let appName: string; let standaloneApi: boolean; let routing: boolean; if (parsedArgs.preset) { preset = parsedArgs.preset; if (preset === Preset.AngularStandalone) { appName = parsedArgs.name; } else { appName = await determineAppName(parsedArgs); } } else { const workspaceType = await determineStandAloneOrMonorepo(); if (workspaceType === 'standalone') { preset = Preset.AngularStandalone; appName = parsedArgs.name; } else { preset = Preset.AngularMonorepo; appName = await determineAppName(parsedArgs); } } if (parsedArgs.style) { style = parsedArgs.style; } else { const reply = await enquirer.prompt<{ style: string }>([ { name: 'style', message: `Default stylesheet format`, initial: 'css' as any, type: 'autocomplete', choices: [ { name: 'css', message: 'CSS', }, { name: 'scss', message: 'SASS(.scss) [ http://sass-lang.com ]', }, { name: 'less', message: 'LESS [ http://lesscss.org ]', }, ], }, ]); style = reply.style; } if (parsedArgs.standaloneApi !== undefined) { standaloneApi = parsedArgs.standaloneApi; } else { const reply = await enquirer.prompt<{ standaloneApi: 'Yes' | 'No' }>([ { name: 'standaloneApi', message: 'Would you like to use Standalone Components in your application?', type: 'autocomplete', choices: [ { name: 'No', }, { name: 'Yes', }, ], initial: 'No' as any, }, ]); standaloneApi = reply.standaloneApi === 'Yes'; } if (parsedArgs.routing !== undefined) { routing = parsedArgs.routing; } else { const reply = await 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, }, ]); routing = reply.routing === 'Yes'; } return { preset, style, appName, standaloneApi, routing }; } async function determineNodeOptions( parsedArgs: yargs.Arguments ): Promise> { let preset: Preset; let appName: string; let framework: 'express' | 'fastify' | 'koa' | 'nest' | 'none'; let docker: boolean; if (parsedArgs.preset) { preset = parsedArgs.preset; if ( preset === Preset.Nest || preset === Preset.Express || preset === Preset.NodeMonorepo ) { appName = await determineAppName(parsedArgs); } else { appName = parsedArgs.name; } if (preset === Preset.NodeStandalone || preset === Preset.NodeMonorepo) { framework = await determineNodeFramework(parsedArgs); } else { framework = 'none'; } } else { framework = await determineNodeFramework(parsedArgs); const workspaceType = await determineStandAloneOrMonorepo(); if (workspaceType === 'standalone') { preset = Preset.NodeStandalone; appName = parsedArgs.name; } else { preset = Preset.NodeMonorepo; appName = await determineAppName(parsedArgs); } } if (parsedArgs.docker !== undefined) { docker = parsedArgs.docker; } else { const reply = await 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, }, ]); docker = reply.docker === 'Yes'; } return { preset, appName, framework, docker, }; } async function determineStandAloneOrMonorepo(): Promise< 'integrated' | 'standalone' > { const { workspaceType } = await enquirer.prompt<{ workspaceType: 'standalone' | 'integrated'; }>([ { type: 'autocomplete', name: 'workspaceType', message: `Standalone project or integrated monorepo?`, initial: 'standalone' as any, choices: [ { name: 'standalone', message: 'Standalone: Nx creates a single project and makes it fast.', }, { name: 'integrated', message: 'Integrated Monorepo: Nx creates a monorepo that contains multiple projects.', }, ], }, ]); invariant(workspaceType, { title: 'Invalid workspace type', bodyLines: [ `It must be one of the following: standalone, integrated. Got ${workspaceType}`, ], }); return workspaceType; } async function determineAppName( parsedArgs: yargs.Arguments ): Promise { if (parsedArgs.appName) return parsedArgs.appName; const { appName } = await enquirer.prompt<{ appName: string }>([ { name: 'appName', message: `Application name`, type: 'input', initial: parsedArgs.name, }, ]); invariant(appName, { title: 'Invalid name', bodyLines: [`Name cannot be empty`], }); return appName; } async function determineReactFramework( parsedArgs: yargs.Arguments ): Promise<'none' | 'nextjs' | 'expo' | 'react-native'> { const reply = await enquirer.prompt<{ framework: 'none' | 'nextjs' | 'expo' | 'react-native'; }>([ { name: 'framework', message: 'What framework would you like to use?', type: 'autocomplete', choices: [ { name: 'none', message: 'None', hint: ' I only want react and react-dom', }, { name: 'nextjs', message: 'Next.js [ https://nextjs.org/ ]', }, { name: 'expo', message: 'Expo [ https://expo.io/ ]', }, { name: 'react-native', message: 'React Native [ https://reactnative.dev/ ]', }, ], initial: 'none' as any, }, ]); return reply.framework; } async function determineReactBundler( parsedArgs: yargs.Arguments ): Promise<'webpack' | 'vite' | 'rspack'> { if (parsedArgs.bundler) return parsedArgs.bundler; const reply = await enquirer.prompt<{ bundler: 'webpack' | 'vite' | 'rspack'; }>([ { name: 'bundler', message: `Which bundler would you like to use?`, type: 'autocomplete', choices: [ { name: 'vite', message: 'Vite [ https://vitejs.dev/ ]', }, { name: 'webpack', message: 'Webpack [ https://webpack.js.org/ ]', }, { name: 'rspack', message: 'Rspack [ https://www.rspack.dev/ ]', }, ], }, ]); return reply.bundler; } async function determineNextAppDir( parsedArgs: yargs.Arguments ): Promise { if (parsedArgs.nextAppDir !== undefined) return parsedArgs.nextAppDir; const reply = await enquirer.prompt<{ nextAppDir: 'Yes' | 'No' }>([ { name: 'nextAppDir', message: 'Would you like to use the App Router (recommended)?', type: 'autocomplete', choices: [ { name: 'Yes', }, { name: 'No', }, ], initial: 'Yes' as any, }, ]); return reply.nextAppDir === 'Yes'; } async function determineNodeFramework( parsedArgs: yargs.Arguments ): Promise<'express' | 'fastify' | 'koa' | 'nest' | 'none'> { if (parsedArgs.framework) return parsedArgs.framework; const reply = await enquirer.prompt<{ framework: 'express' | 'fastify' | 'koa' | 'nest' | 'none'; }>([ { message: 'What framework should be used?', type: 'autocomplete', name: 'framework', choices: [ { name: 'none', message: 'None', }, { 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/ ]', }, ], }, ]); return reply.framework; }