#!/usr/bin/env node // we can import from '@nrwl/workspace' because it will require typescript import { output } from '@nrwl/workspace/src/utils/output'; import { getPackageManagerExecuteCommand } from '@nrwl/workspace/src/utils/detect-package-manager'; import { execSync } from 'child_process'; import { writeFileSync } from 'fs'; import * as inquirer from 'inquirer'; import * as path from 'path'; import { dirSync } from 'tmp'; import * as yargsParser from 'yargs-parser'; import { determinePackageManager, showNxWarning } from './shared'; enum Preset { Empty = 'empty', OSS = 'oss', WebComponents = 'web-components', Angular = 'angular', AngularWithNest = 'angular-nest', React = 'react', ReactWithExpress = 'react-express', NextJs = 'next', Nest = 'nest', } const presetOptions = [ { value: Preset.Empty, name: 'empty [an empty workspace with a layout that works best for building apps]', }, { value: Preset.React, name: 'react [a workspace with a single React application]', }, { value: Preset.Angular, name: 'angular [a workspace with a single Angular application]', }, { value: Preset.NextJs, name: 'next.js [a workspace with a single Next.js application]', }, { value: Preset.Nest, name: 'nest [a workspace with a single Nest application]', }, { value: 'web-components', name: 'web components [a workspace with a single app built using web components]', }, { value: Preset.ReactWithExpress, name: 'react-express [a workspace with a full stack application (React + Express)]', }, { value: Preset.AngularWithNest, name: 'angular-nest [a workspace with a full stack application (Angular + Nest)]', }, { value: 'oss', name: 'oss [an empty workspace with a layout that works best for open-source projects]', }, ]; const tsVersion = 'TYPESCRIPT_VERSION'; const cliVersion = 'NX_VERSION'; const nxVersion = 'NX_VERSION'; const angularCliVersion = 'ANGULAR_CLI_VERSION'; const prettierVersion = 'PRETTIER_VERSION'; const parsedArgs = yargsParser(process.argv, { string: ['cli', 'preset', 'appName', 'style', 'linter', 'defaultBase'], alias: { appName: 'app-name', nxCloud: 'nx-cloud', defaultBase: 'default-base', }, boolean: ['help', 'interactive', 'nxCloud'], }); if (parsedArgs.help) { showHelp(); process.exit(0); } const packageManager = determinePackageManager(); determineWorkspaceName(parsedArgs).then((name) => { determinePreset(parsedArgs).then((preset) => { return determineAppName(preset, parsedArgs).then((appName) => { return determineStyle(preset, parsedArgs).then((style) => { return determineCli(preset, parsedArgs).then((cli) => { return determineLinter(cli, parsedArgs).then((linter) => { return askAboutNxCloud(parsedArgs).then((cloud) => { const tmpDir = createSandbox(packageManager); createApp( tmpDir, cli, parsedArgs, name, preset, appName, style, linter, cloud, parsedArgs.interactive, parsedArgs.defaultBase ); showNxWarning(name); pointToTutorialAndCourse(preset); }); }); }); }); }); }); }); 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 What to create in a new workspace (options: ${options}) 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", "styl", "less") for React/Next.js also ("styled-components", "@emotion/styled") linter Default linter. Options: "eslint", "tslint". interactive Enable interactive mode when using presets (boolean) nx-cloud Use Nx Cloud (boolean) [new workspace options] any 'new workspace' options `); } function determineWorkspaceName(parsedArgs: any): Promise { const workspaceName: string = parsedArgs._[2]; if (workspaceName) { return Promise.resolve(workspaceName); } return inquirer .prompt([ { name: 'WorkspaceName', message: `Workspace name (e.g., org name) `, type: 'string', }, ]) .then((a) => { if (!a.WorkspaceName) { output.error({ title: 'Invalid workspace name', bodyLines: [`Workspace name cannot be empty`], }); process.exit(1); } return a.WorkspaceName; }); } 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 inquirer .prompt([ { name: 'Preset', message: `What to create in the new workspace`, default: 'empty', type: 'list', choices: presetOptions, }, ]) .then((a: { Preset: Preset }) => a.Preset); } function determineAppName(preset: Preset, parsedArgs: any): Promise { if (preset === Preset.Empty || preset === Preset.OSS) { return Promise.resolve(''); } if (parsedArgs.appName) { return Promise.resolve(parsedArgs.appName); } return inquirer .prompt([ { name: 'AppName', message: `Application name `, type: 'string', }, ]) .then((a) => { if (!a.AppName) { output.error({ title: 'Invalid name', bodyLines: [`Name cannot be empty`], }); process.exit(1); } return a.AppName; }); } function determineCli(preset: Preset, parsedArgs: any) { const angular = { package: '@angular/cli', version: angularCliVersion, command: 'ng', }; const nx = { package: '@nrwl/tao', version: cliVersion, command: 'tao', }; if (parsedArgs.cli) { if (['nx', 'angular'].indexOf(parsedArgs.cli) === -1) { output.error({ title: 'Invalid cli', bodyLines: [`It must be one of the following:`, '', 'nx', 'angular'], }); process.exit(1); } return Promise.resolve(parsedArgs.cli === 'angular' ? angular : nx); } switch (preset) { case Preset.Angular: case Preset.AngularWithNest: { return Promise.resolve(angular); } case Preset.OSS: case Preset.WebComponents: case Preset.React: case Preset.ReactWithExpress: case Preset.NextJs: case Preset.Nest: { return Promise.resolve(nx); } default: { return inquirer .prompt([ { name: 'CLI', message: `CLI to power the Nx workspace `, default: 'nx', type: 'list', choices: [ { value: 'nx', name: 'Nx [Recommended for all applications (React, Node, etc..)]', }, { value: 'angular', name: 'Angular CLI [Recommended for Angular only workspaces]', }, ], }, ]) .then((a: { CLI: string }) => (a.CLI === 'angular' ? angular : nx)); } } } function determineStyle(preset: Preset, parsedArgs: any) { if ( preset === Preset.Empty || preset === Preset.OSS || preset === Preset.Nest ) { return Promise.resolve(null); } const choices = [ { value: 'css', name: 'CSS', }, { value: 'scss', name: 'SASS(.scss) [ http://sass-lang.com ]', }, { value: 'styl', name: 'Stylus(.styl)[ http://stylus-lang.com ]', }, { value: 'less', name: 'LESS [ http://lesscss.org ]', }, ]; if ([Preset.ReactWithExpress, Preset.React, Preset.NextJs].includes(preset)) { choices.push( { value: 'styled-components', name: 'styled-components [ https://styled-components.com ]', }, { value: '@emotion/styled', name: 'emotion [ https://emotion.sh ]', }, { value: 'styled-jsx', name: 'styled-jsx [ https://www.npmjs.com/package/styled-jsx ]', } ); } if (!parsedArgs.style) { return inquirer .prompt([ { name: 'style', message: `Default stylesheet format `, default: 'css', type: 'list', choices, }, ]) .then((a) => a.style); } const foundStyle = choices.find( (choice) => choice.value === parsedArgs.style ); if (foundStyle === undefined) { output.error({ title: 'Invalid style', bodyLines: [ `It must be one of the following:`, '', ...choices.map((choice) => choice.value), ], }); process.exit(1); } return Promise.resolve(parsedArgs.style); } function determineLinter(preset: Preset, parsedArgs: any) { if (!parsedArgs.linter) { if (preset === Preset.Angular || preset === Preset.AngularWithNest) { return inquirer .prompt([ { name: 'linter', message: `Default linter `, default: 'tslint', type: 'list', choices: [ { value: 'tslint', name: 'TSLint [ Used by Angular CLI ]', }, { value: 'eslint', name: 'ESLint [ Modern linting tool ]', }, ], }, ]) .then((a) => a.linter); } else { return Promise.resolve('eslint'); } } else { if (parsedArgs.linter !== 'eslint' && parsedArgs.linter !== 'tslint') { output.error({ title: 'Invalid linter', bodyLines: [`It must be one of the following:`, '', 'eslint', 'tslint'], }); process.exit(1); } else { return Promise.resolve(parsedArgs.linter); } } } function createSandbox(packageManager: string) { console.log(`Creating a sandbox with Nx...`); const tmpDir = dirSync().name; writeFileSync( path.join(tmpDir, 'package.json'), JSON.stringify({ dependencies: { '@nrwl/workspace': nxVersion, '@nrwl/tao': cliVersion, typescript: tsVersion, prettier: prettierVersion, }, license: 'MIT', }) ); execSync(`${packageManager} install --silent`, { cwd: tmpDir, stdio: [0, 1, 2], }); return tmpDir; } function createApp( tmpDir: string, cli: { command: string }, parsedArgs: any, name: string, preset: Preset, appName: string, style: string | null, linter: string, nxCloud: boolean, interactive: boolean, defaultBase: string ) { const filterArgs = [ '_', 'app-name', 'appName', 'cli', 'default-base', 'defaultBase', 'interactive', 'nx-cloud', 'nxCloud', 'preset', 'style', 'linter', ]; // These are the arguments that are passed to the schematic const args = Object.keys(parsedArgs) .filter((key) => !filterArgs.includes(key)) .map((key) => `--${key}=${parsedArgs[key]}`) .join(' '); const appNameArg = appName ? ` --appName="${appName}"` : ``; const styleArg = style ? ` --style="${style}"` : ``; const linterArg = ` --linter="${linter}"`; const nxCloudArg = nxCloud ? ` --nxCloud` : ``; const interactiveArg = interactive ? ` --interactive=true` : ` --interactive=false`; const defaultBaseArg = defaultBase ? ` --defaultBase="${defaultBase}"` : ``; const packageExec = getPackageManagerExecuteCommand(packageManager); const command = `new ${name} ${args} --preset="${preset}"${appNameArg}${styleArg}${linterArg}${nxCloudArg}${interactiveArg}${defaultBaseArg} --collection=@nrwl/workspace`; console.log(command); const collectionJsonPath = require.resolve( '@nrwl/workspace/collection.json', { paths: [tmpDir] } ); execSync( `${packageExec} tao ${command.replace( '--collection=@nrwl/workspace', `--collection=${collectionJsonPath}` )} --cli=${cli.command} --nxWorkspaceRoot=${process.cwd()}`, { stdio: [0, 1, 2], cwd: tmpDir, } ); if (nxCloud) { output.addVerticalSeparator(); execSync(`${packageExec} nx g @nrwl/nx-cloud:init --no-analytics`, { stdio: [0, 1, 2], cwd: path.join(process.cwd(), name), }); } } async function askAboutNxCloud(parsedArgs: any) { if (parsedArgs.nxCloud === undefined) { return inquirer .prompt([ { name: 'NxCloud', message: `Use Nx Cloud? (It's free and doesn't require registration.)`, type: 'list', choices: [ { value: 'yes', name: 'Yes [Faster builds, run details, Github integration. Learn more at https://nx.app]', }, { value: 'no', name: 'No', }, ], default: 'no', }, ]) .then((a: { NxCloud: 'yes' | 'no' }) => a.NxCloud === 'yes'); } else { return parsedArgs.nxCloud; } } function pointToTutorialAndCourse(preset: Preset) { const title = `First time using Nx? Check out this interactive Nx tutorial.`; switch (preset) { case Preset.React: case Preset.ReactWithExpress: case Preset.NextJs: output.addVerticalSeparator(); output.note({ title: title, bodyLines: [ `https://nx.dev/react/tutorial/01-create-application`, ...pointToCourse(), ], }); break; case Preset.Angular: case Preset.AngularWithNest: output.addVerticalSeparator(); output.note({ title: title, bodyLines: [ `https://nx.dev/angular/tutorial/01-create-application`, ...pointToCourse(), ], }); break; // TODO(Cammisuli): include this after the nx.dev site is updated with the node flavour // case Preset.Nest: // output.addVerticalSeparator(); // output.note({ // title, // bodyLines: [ // `https://nx.dev/node/tutorial/01-create-application`, // ...pointToCourse(), // ], // }); // break; } } function pointToCourse(): string[] { return [ ``, `Prefer watching videos? Check out this free Nx course on YouTube.`, `https://www.youtube.com/watch?v=2mYLe9Kp9VM&list=PLakNactNC1dH38AfqmwabvOszDmKriGco`, ]; }