nx/packages/create-nx-workspace/bin/create-nx-workspace.ts
2023-01-20 08:18:32 -05:00

1320 lines
34 KiB
TypeScript

import { exec, spawn } from 'child_process';
import { writeFileSync } from 'fs';
import * as enquirer from 'enquirer';
import * as path from 'path';
import { join } from 'path';
import { dirSync } from 'tmp';
import * as yargs from 'yargs';
import { showNxWarning, unparse } from './shared';
import { output } from './output';
import * as ora from 'ora';
import {
detectInvokedPackageManager,
generatePackageManagerFiles,
getPackageManagerCommand,
getPackageManagerVersion,
PackageManager,
packageManagerList,
} from './package-manager';
import { validateNpmPackage } from './validate-npm-package';
import { deduceDefaultBase } from './default-base';
import { getFileName, stringifyCollection } from './utils';
import { yargsDecorator } from './decorator';
import { ciList } from './ci';
import { initializeGitRepo } from './git';
import { messages, recordStat } from './ab-testing';
import chalk = require('chalk');
type Arguments = {
name: string;
preset: string;
appName: string;
cli: string;
style: string;
framework: string;
nxCloud: boolean;
allPrompts: boolean;
packageManager: PackageManager;
defaultBase: string;
ci: string;
skipGit: boolean;
commit: {
message: string;
name: string;
email: string;
};
bundler: 'vite' | 'webpack';
};
enum Preset {
Apps = 'apps',
Empty = 'empty', // same as apps, deprecated
Core = 'core', // same as npm, deprecated
NPM = 'npm',
TS = 'ts',
WebComponents = 'web-components',
AngularMonorepo = 'angular-monorepo',
AngularStandalone = 'angular-standalone',
ReactMonorepo = 'react-monorepo',
ReactStandalone = 'react-standalone',
ReactNative = 'react-native',
Expo = 'expo',
NextJs = 'next',
Nest = 'nest',
Express = 'express',
React = 'react',
Angular = 'angular',
NodeServer = 'node-server',
}
const presetOptions: { name: Preset; message: string }[] = [
{
name: Preset.Apps,
message:
'apps [an empty monorepo with no plugins with a layout that works best for building apps]',
},
{
name: Preset.TS,
message:
'ts [an empty monorepo with the JS/TS plugin preinstalled]',
},
{
name: Preset.ReactMonorepo,
message: 'react [a monorepo with a single React application]',
},
{
name: Preset.AngularMonorepo,
message: 'angular [a monorepo with a single Angular application]',
},
{
name: Preset.NextJs,
message: 'next.js [a monorepo with a single Next.js application]',
},
{
name: Preset.Nest,
message: 'nest [a monorepo with a single Nest application]',
},
{
name: Preset.ReactNative,
message:
'react-native [a monorepo with a single React Native application]',
},
{
name: Preset.NodeServer,
message:
'node [a standalone repo with a single Node Server e.g. Express]',
},
];
const nxVersion = require('../package.json').version;
const tsVersion = 'TYPESCRIPT_VERSION'; // This gets replaced with the typescript version in the root package.json during build
const prettierVersion = 'PRETTIER_VERSION'; // This gets replaced with the prettier version in the root package.json during build
export const commandsObject: yargs.Argv<Arguments> = 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) =>
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',
})
.option('cli', {
describe: chalk.dim`CLI to power the Nx workspace`,
choices: ['nx', 'angular'],
type: 'string',
})
.option('style', {
describe: chalk.dim`Style option to be used when a preset with pregenerated app is selected`,
type: 'string',
})
.option('bundler', {
describe: chalk.dim`Bundler to be used to build the application`,
type: 'string',
})
.option('framework', {
describe: chalk.dim`Framework option to be used when the node-server preset is selected`,
type: 'string',
})
.option('nxCloud', {
describe: chalk.dim(messages.getPromptMessage('nxCloudCreation')),
type: 'boolean',
})
.option('ci', {
describe: chalk.dim`Generate a CI workflow file`,
choices: ciList,
defaultDescription: '',
type: 'string',
})
.option('allPrompts', {
alias: 'a',
describe: chalk.dim`Show all prompts`,
type: 'boolean',
default: false,
})
.option('packageManager', {
alias: 'pm',
describe: chalk.dim`Package manager to use`,
choices: [...packageManagerList].sort(),
defaultDescription: 'npm',
type: 'string',
})
.option('defaultBase', {
defaultDescription: 'main',
describe: chalk.dim`Default base to use for new projects`,
type: 'string',
})
.option('skipGit', {
describe: chalk.dim`Skip initializing a git repository.`,
type: 'boolean',
default: false,
alias: 'g',
})
.option('commit.name', {
describe: chalk.dim`Name of the committer`,
type: 'string',
})
.option('commit.email', {
describe: chalk.dim`E-mail of the committer`,
type: 'string',
})
.option('commit.message', {
describe: chalk.dim`Commit message`,
type: 'string',
default: 'Initial commit',
}),
async (argv: yargs.ArgumentsCamelCase<Arguments>) => {
await main(argv).catch((error) => {
const { version } = require('../package.json');
output.error({
title: `Something went wrong! v${version}`,
});
throw error;
});
},
[getConfiguration]
)
.help('help', chalk.dim`Show help`)
.updateLocale(yargsDecorator)
.version(
'version',
chalk.dim`Show version`,
nxVersion
) as yargs.Argv<Arguments>;
async function main(parsedArgs: yargs.Arguments<Arguments>) {
const {
name,
cli,
preset,
appName,
style,
nxCloud,
packageManager,
defaultBase,
ci,
skipGit,
commit,
framework,
} = parsedArgs;
output.log({
title: `Nx is 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 "${packageManager} install" several times. Please wait.`,
],
});
const tmpDir = await createSandbox(packageManager);
const directory = await createApp(
tmpDir,
name,
packageManager as PackageManager,
{
...parsedArgs,
cli,
preset,
appName,
style,
nxCloud,
defaultBase,
framework,
}
);
if (!isKnownPreset(parsedArgs.preset)) {
await createPreset(parsedArgs, packageManager as PackageManager, directory);
}
let nxCloudInstallRes;
if (nxCloud) {
nxCloudInstallRes = await setupNxCloud(
name,
packageManager as PackageManager
);
}
if (ci) {
await setupCI(
name,
ci,
packageManager as PackageManager,
nxCloud && nxCloudInstallRes.code === 0
);
}
if (!skipGit) {
try {
await initializeGitRepo(directory, { defaultBase, commit });
} catch (e) {
output.error({
title: 'Could not initialize git repository',
bodyLines: [e.message],
});
}
}
showNxWarning(name);
pointToTutorialAndCourse(preset as Preset);
if (nxCloud && nxCloudInstallRes.code === 0) {
printNxCloudSuccessMessage(nxCloudInstallRes.stdout);
}
await recordStat({
nxVersion,
command: 'create-nx-workspace',
useCloud: nxCloud,
meta: messages.codeOfSelectedPromptMessage('nxCloudCreation'),
});
}
async function getConfiguration(
argv: yargs.Arguments<Arguments>
): Promise<void> {
try {
let name, appName, style, preset, framework, bundler;
output.log({
title:
"Let's create a new workspace [https://nx.dev/getting-started/intro]",
});
const thirdPartyPreset = await determineThirdPartyPackage(argv);
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 = Preset.ReactStandalone;
} else if (monorepoStyle === 'angular') {
preset = Preset.AngularStandalone;
} else if (monorepoStyle === 'node-server') {
preset = Preset.NodeServer;
} else {
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.NodeServer
) {
appName =
argv.appName ?? argv.name ?? (await determineAppName(preset, argv));
name = argv.name ?? appName;
if (preset === Preset.NodeServer) {
framework = await determineFramework(preset, argv);
}
if (preset === Preset.ReactStandalone) {
bundler = await determineBundler(argv);
}
} else {
name = await determineRepoName(argv);
appName = await determineAppName(preset, argv);
}
style = await determineStyle(preset, argv);
}
const cli = await determineCli(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,
framework,
cli,
nxCloud,
packageManager,
defaultBase,
ci,
bundler,
});
} catch (e) {
console.error(e);
process.exit(1);
}
}
function determineRepoName(
parsedArgs: yargs.Arguments<Arguments>
): Promise<string> {
const repoName: string = parsedArgs._[0]
? parsedArgs._[0].toString()
: parsedArgs.name;
if (repoName) {
return Promise.resolve(repoName);
}
return enquirer
.prompt([
{
name: 'RepoName',
message: `Repository name `,
type: 'input',
},
])
.then((a: { RepoName: string }) => {
if (!a.RepoName) {
output.error({
title: 'Invalid repository name',
bodyLines: [`Repository name cannot be empty`],
});
process.exit(1);
}
return a.RepoName;
});
}
function monorepoOrStandalone(preset: string): Promise<string> {
return enquirer
.prompt([
{
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.`,
},
],
},
])
.then((a: { MonorepoOrStandalone: string }) => {
if (!a.MonorepoOrStandalone) {
output.error({
title: 'Invalid selection',
});
process.exit(1);
}
return a.MonorepoOrStandalone;
});
}
function determineMonorepoStyle(): Promise<string> {
return enquirer
.prompt([
{
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 Vite, Vitest, ESLint, and Cypress.',
},
{
name: 'angular',
message:
'Standalone Angular app: Nx configures Jest, ESLint and Cypress.',
},
{
name: 'node-server',
message:
'Standalone Node Server app: Nx configures your framework of choice (e.g. Express) along with esbuild, Eslint and Jest.',
},
],
},
])
.then((a: { MonorepoStyle: string }) => {
if (!a.MonorepoStyle) {
output.error({
title: 'Invalid monorepo style',
});
process.exit(1);
}
return a.MonorepoStyle;
});
}
async function determinePackageManager(
parsedArgs: yargs.Arguments<Arguments>
): Promise<PackageManager> {
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: 'autocomplete',
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<Arguments>
): Promise<string> {
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<Arguments>) {
if (preset && !isKnownPreset(preset)) {
const packageName = preset.match(/.+@/)
? preset[0] + preset.substring(1).split('@')[0]
: preset;
const validateResult = validateNpmPackage(packageName);
if (validateResult.validForNewPackages) {
return Promise.resolve(preset);
} else {
//! Error here
output.error({
title: 'Invalid preset npm package',
bodyLines: [
`There was an error with the preset npm package you provided:`,
'',
...validateResult.errors,
],
});
process.exit(1);
}
} else {
return Promise.resolve(null);
}
}
async function determinePreset(parsedArgs: any): Promise<Preset> {
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([
{
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<Arguments>
): Promise<string> {
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([
{
name: 'AppName',
message: `Application name `,
type: 'input',
},
])
.then((a: { AppName: string }) => {
if (!a.AppName) {
output.error({
title: 'Invalid name',
bodyLines: [`Name cannot be empty`],
});
process.exit(1);
}
return a.AppName;
});
}
async function determineFramework(
preset: Preset,
parsedArgs: yargs.Arguments<Arguments>
): Promise<string> {
if (preset !== Preset.NodeServer) {
return Promise.resolve('');
}
const frameworkChoices = ['express', 'koa', 'fastify', 'connect'];
if (!parsedArgs.framework) {
return enquirer
.prompt([
{
message: 'What framework should be used?',
type: 'select',
name: 'framework',
choices: frameworkChoices,
},
])
.then((a: { framework: string }) => a.framework);
}
const foundFramework = frameworkChoices.indexOf(parsedArgs.framework);
if (foundFramework < 0) {
output.error({
title: 'Invalid framwork',
bodyLines: [
`It must be one of the following:`,
'',
...frameworkChoices.map((choice) => choice),
],
});
process.exit(1);
}
return Promise.resolve(parsedArgs.framework);
}
function isValidCli(cli: string): cli is 'angular' | 'nx' {
return ['nx', 'angular'].indexOf(cli) !== -1;
}
async function determineCli(
preset: Preset,
parsedArgs: yargs.Arguments<Arguments>
): Promise<'nx' | 'angular'> {
if (parsedArgs.cli) {
if (!isValidCli(parsedArgs.cli)) {
output.error({
title: 'Invalid cli',
bodyLines: [`It must be one of the following:`, '', 'nx', 'angular'],
});
process.exit(1);
}
return Promise.resolve(parsedArgs.cli);
}
switch (preset) {
case Preset.AngularMonorepo: {
return Promise.resolve('angular');
}
default: {
return Promise.resolve('nx');
}
}
}
async function determineStyle(
preset: Preset,
parsedArgs: yargs.Arguments<Arguments>
): Promise<string> {
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.NodeServer
) {
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.AngularMonorepo, Preset.AngularStandalone].includes(preset)) {
choices.push({
name: 'styl',
message: 'Stylus(.styl) [ http://stylus-lang.com ]',
});
}
if (
[Preset.ReactMonorepo, Preset.ReactStandalone, Preset.NextJs].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([
{
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 determineBundler(
parsedArgs: yargs.Arguments<Arguments>
): Promise<'vite' | 'webpack'> {
const choices = [
{
name: 'vite',
message: 'Vite [ https://vitejs.dev/ ]',
},
{
name: 'webpack',
message: 'Webpack [ https://webpack.js.org/ ]',
},
];
if (!parsedArgs.bundler) {
return enquirer
.prompt([
{
name: 'bundler',
message: `Bundler to be used to build the application`,
initial: 'vite' as any,
type: 'autocomplete',
choices: choices,
},
])
.then((a: { bundler: 'vite' | 'webpack' }) => 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 Promise.resolve(parsedArgs.bundler);
}
async function determineNxCloud(
parsedArgs: yargs.Arguments<Arguments>
): Promise<boolean> {
if (parsedArgs.nxCloud === undefined) {
return enquirer
.prompt([
{
name: 'NxCloud',
message: messages.getPromptMessage('nxCloudCreation'),
type: 'autocomplete',
choices: [
{
name: 'Yes',
hint: 'I want faster builds',
},
{
name: 'No',
},
],
initial: 'Yes' as any,
},
])
.then((a: { NxCloud: 'Yes' | 'No' }) => a.NxCloud === 'Yes');
} else {
return parsedArgs.nxCloud;
}
}
async function determineCI(
parsedArgs: yargs.Arguments<Arguments>,
nxCloud: boolean
): Promise<string> {
if (!nxCloud) {
if (parsedArgs.ci) {
output.warn({
title: 'Invalid CI value',
bodyLines: [
`CI option only works when Nx Cloud is enabled.`,
`The value provided will be ignored.`,
],
});
}
return '';
}
if (parsedArgs.ci) {
return parsedArgs.ci;
}
if (parsedArgs.allPrompts) {
return (
enquirer
.prompt([
{
name: 'CI',
message: `CI workflow file to generate? `,
type: 'autocomplete',
initial: '' as any,
choices: [
{ message: 'none', name: '' },
{ message: 'GitHub Actions', name: 'github' },
{ message: 'Circle CI', name: 'circleci' },
{ message: 'Azure DevOps', name: 'azure' },
],
},
])
// enquirer ignores name and value if they are falsy and takes
// first field that has a truthy value, so wee need to explicitly
// check for none
.then((a: { CI: string }) => (a.CI !== 'none' ? a.CI : ''))
);
}
return '';
}
async function createSandbox(packageManager: PackageManager) {
const installSpinner = ora(
`Installing dependencies with ${packageManager}`
).start();
const { install } = getPackageManagerCommand(packageManager);
const tmpDir = dirSync().name;
try {
writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({
dependencies: {
'@nrwl/workspace': nxVersion,
nx: nxVersion,
typescript: tsVersion,
prettier: prettierVersion,
},
license: 'MIT',
})
);
generatePackageManagerFiles(tmpDir, packageManager);
await execAndWait(install, tmpDir);
installSpinner.succeed();
} catch (e) {
installSpinner.fail();
output.error({
title: `Nx failed to install dependencies`,
bodyLines: mapErrorToBodyLines(e),
});
process.exit(1);
} finally {
installSpinner.stop();
}
return tmpDir;
}
async function createApp(
tmpDir: string,
name: string,
packageManager: PackageManager,
parsedArgs: any
): Promise<string> {
const { _, cli, ...restArgs } = parsedArgs;
// Ensure to use packageManager for args
// if it's not already passed in from previous process
if (!restArgs.packageManager) {
restArgs.packageManager = packageManager;
}
const args = unparse(restArgs).join(' ');
const pmc = getPackageManagerCommand(packageManager);
const command = `new ${name} ${args}`;
const workingDir = process.cwd().replace(/\\/g, '/');
let nxWorkspaceRoot = `"${workingDir}"`;
// If path contains spaces there is a problem in Windows for npm@6.
// In this case we have to escape the wrapping quotes.
if (
process.platform === 'win32' &&
/\s/.test(nxWorkspaceRoot) &&
packageManager === 'npm'
) {
const pmVersion = +getPackageManagerVersion(packageManager).split('.')[0];
if (pmVersion < 7) {
nxWorkspaceRoot = `\\"${nxWorkspaceRoot.slice(1, -1)}\\"`;
}
}
let workspaceSetupSpinner = ora(
`Creating your workspace in ${getFileName(name)}`
).start();
try {
const fullCommand = `${pmc.exec} nx ${command} --nxWorkspaceRoot=${nxWorkspaceRoot}`;
await execAndWait(fullCommand, tmpDir);
workspaceSetupSpinner.succeed(
`Nx has successfully created the workspace: ${getFileName(name)}.`
);
} catch (e) {
workspaceSetupSpinner.fail();
output.error({
title: `Nx failed to create a workspace.`,
bodyLines: mapErrorToBodyLines(e),
});
process.exit(1);
} finally {
workspaceSetupSpinner.stop();
}
return join(workingDir, getFileName(name));
}
async function createPreset(
parsedArgs: yargs.Arguments<Arguments>,
packageManager: PackageManager,
directory: string
): Promise<void> {
const {
_,
cli,
skipGit,
ci,
commit,
allPrompts,
nxCloud,
preset,
...restArgs
} = parsedArgs;
const args = unparse(restArgs).join(' ');
const pmc = getPackageManagerCommand(packageManager);
const workingDir = process.cwd().replace(/\\/g, '/');
let nxWorkspaceRoot = `"${workingDir}"`;
// If path contains spaces there is a problem in Windows for npm@6.
// In this case we have to escape the wrapping quotes.
if (
process.platform === 'win32' &&
/\s/.test(nxWorkspaceRoot) &&
packageManager === 'npm'
) {
const pmVersion = +getPackageManagerVersion(packageManager).split('.')[0];
if (pmVersion < 7) {
nxWorkspaceRoot = `\\"${nxWorkspaceRoot.slice(1, -1)}\\"`;
}
}
const command = `g ${preset}:preset ${args}`;
try {
const [exec, ...args] = pmc.exec.split(' ');
args.push(
'nx',
`--nxWorkspaceRoot=${nxWorkspaceRoot}`,
...command.split(' ')
);
await spawnAndWait(exec, args, directory);
output.log({
title: `Successfully applied preset: ${preset}.`,
});
} catch (e) {
output.error({
title: `Failed to apply preset: ${preset}`,
bodyLines: ['See above'],
});
process.exit(1);
}
}
async function setupNxCloud(name: string, packageManager: PackageManager) {
const nxCloudSpinner = ora(`Setting up NxCloud`).start();
try {
const pmc = getPackageManagerCommand(packageManager);
const res = await execAndWait(
`${pmc.exec} nx g @nrwl/nx-cloud:init --no-analytics --installationSource=create-nx-workspace`,
path.join(process.cwd(), getFileName(name))
);
nxCloudSpinner.succeed('NxCloud has been set up successfully');
return res;
} catch (e) {
nxCloudSpinner.fail();
output.error({
title: `Nx failed to setup NxCloud`,
bodyLines: mapErrorToBodyLines(e),
});
process.exit(1);
} finally {
nxCloudSpinner.stop();
}
}
async function setupCI(
name: string,
ci: string,
packageManager: PackageManager,
nxCloudSuccessfullyInstalled: boolean
) {
if (!nxCloudSuccessfullyInstalled) {
output.error({
title: `CI workflow generation skipped`,
bodyLines: [
`Nx Cloud was not installed`,
`The autogenerated CI workflow requires Nx Cloud to be set-up.`,
],
});
}
const ciSpinner = ora(`Generating CI workflow`).start();
try {
const pmc = getPackageManagerCommand(packageManager);
const res = await execAndWait(
`${pmc.exec} nx g @nrwl/workspace:ci-workflow --ci=${ci}`,
path.join(process.cwd(), getFileName(name))
);
ciSpinner.succeed('CI workflow has been generated successfully');
return res;
} catch (e) {
ciSpinner.fail();
output.error({
title: `Nx failed to generate CI workflow`,
bodyLines: mapErrorToBodyLines(e),
});
process.exit(1);
} finally {
ciSpinner.stop();
}
}
function printNxCloudSuccessMessage(nxCloudOut: string) {
const bodyLines = nxCloudOut
.split('Distributed caching via Nx Cloud has been enabled')[1]
.trim();
output.note({
title: `Distributed caching via Nx Cloud has been enabled`,
bodyLines: bodyLines.split('\n').map((r) => r.trim()),
});
}
function mapErrorToBodyLines(error: {
logMessage: string;
code: number;
logFile: string;
}): string[] {
if (error.logMessage?.split('\n').filter((line) => !!line).length === 1) {
// print entire log message only if it's only a single message
return [`Error: ${error.logMessage}`];
}
const lines = [`Exit code: ${error.code}`, `Log file: ${error.logFile}`];
if (process.env.NX_VERBOSE_LOGGING) {
lines.push(`Error: ${error.logMessage}`);
}
return lines;
}
function execAndWait(command: string, cwd: string) {
return new Promise((res, rej) => {
exec(
command,
{ cwd, env: { ...process.env, NX_DAEMON: 'false' } },
(error, stdout, stderr) => {
if (error) {
const logFile = path.join(cwd, 'error.log');
writeFileSync(logFile, `${stdout}\n${stderr}`);
rej({ code: error.code, logFile, logMessage: stderr });
} else {
res({ code: 0, stdout });
}
}
);
});
}
/**
* Use spawn only for interactive shells
*/
function spawnAndWait(command: string, args: string[], cwd: string) {
return new Promise((res, rej) => {
const childProcess = spawn(command, args, {
cwd,
stdio: 'inherit',
env: { ...process.env, NX_DAEMON: 'false' },
});
childProcess.on('exit', (code) => {
if (code !== 0) {
rej({ code: code });
} else {
res({ code: 0 });
}
});
});
}
function pointToTutorialAndCourse(preset: Preset) {
const title = `First time using Nx? Check out this interactive Nx tutorial.`;
switch (preset) {
case Preset.Empty:
case Preset.NPM:
case Preset.Apps:
case Preset.Core:
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/core-tutorial/01-create-blog`],
});
break;
case Preset.TS:
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/getting-started/nx-and-typescript`],
});
break;
case Preset.ReactStandalone:
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/getting-started/react-standalone-tutorial`],
});
break;
case Preset.ReactMonorepo:
case Preset.NextJs:
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/react-tutorial/1-code-generation`],
});
break;
case Preset.AngularStandalone:
output.addVerticalSeparator();
output.note({
title,
bodyLines: [
`https://nx.dev/getting-started/angular-standalone-tutorial`,
],
});
break;
case Preset.AngularMonorepo:
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/angular-tutorial/1-code-generation`],
});
break;
case Preset.Express:
case Preset.NodeServer:
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/node-tutorial/1-code-generation`],
});
break;
}
}