nx/packages/create-nx-workspace/bin/create-nx-workspace.ts
2022-10-11 14:30:28 -04:00

1053 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { exec } 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;
nxCloud: boolean;
allPrompts: boolean;
packageManager: PackageManager;
defaultBase: string;
ci: string;
skipGit: boolean;
commit: {
message: string;
name: string;
email: string;
};
};
enum Preset {
Apps = 'apps',
Empty = 'empty', // same as apps, deprecated
Core = 'core', // same as npm, deprecated
NPM = 'npm',
TS = 'ts',
WebComponents = 'web-components',
Angular = 'angular',
AngularWithNest = 'angular-nest',
React = 'react',
ReactWithExpress = 'react-express',
ReactNative = 'react-native',
Expo = 'expo',
NextJs = 'next',
Nest = 'nest',
Express = 'express',
}
const presetOptions: { name: Preset; message: string }[] = [
{
name: Preset.Apps,
message:
'apps [an empty workspace with no plugins with a layout that works best for building apps]',
},
{
name: Preset.TS,
message:
'ts [an empty workspace with the JS/TS plugin preinstalled]',
},
{
name: Preset.React,
message: 'react [a workspace with a single React application]',
},
{
name: Preset.Angular,
message:
'angular [a workspace with a single Angular application]',
},
{
name: Preset.NextJs,
message:
'next.js [a workspace with a single Next.js application]',
},
{
name: Preset.Nest,
message: 'nest [a workspace with a single Nest application]',
},
{
name: Preset.Express,
message:
'express [a workspace with a single Express application]',
},
{
name: Preset.WebComponents,
message:
'web components [a workspace with a single app built using web components]',
},
{
name: Preset.ReactNative,
message:
'react-native [a workspace with a single React Native application]',
},
{
name: Preset.Expo,
message: 'expo [a workspace with a single Expo application]',
},
{
name: Preset.ReactWithExpress,
message:
'react-express [a workspace with a full stack application (React + Express)]',
},
{
name: Preset.AngularWithNest,
message:
'angular-nest [a workspace with a full stack application (Angular + Nest)]',
},
];
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('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,
} = 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,
}
);
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;
const thirdPartyPreset = await determineThirdPartyPackage(argv);
if (thirdPartyPreset) {
preset = thirdPartyPreset;
name = await determineRepoName(argv);
appName = '';
style = null;
} else {
if (!argv.preset) {
if ((await determineMonorepoStyle()) === 'package-based') {
preset = 'npm';
} else {
preset = await determinePreset(argv);
}
} else {
preset = argv.preset;
}
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,
cli,
nxCloud,
packageManager,
defaultBase,
ci,
});
} 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 determineMonorepoStyle(): Promise<string> {
return enquirer
.prompt([
{
name: 'MonorepoStyle',
message: `Package-based or integrated monorepo?`,
type: 'select',
choices: [
{
name: 'package-based',
message:
'Create a package-based monorepo with Yarn, NPM or PNPM. Nx makes it fast, but stays out of your way.',
},
{
name: 'integrated',
message:
'Create an integrated monorepo using Nxs plugin system. Focus on shipping code, not fixing your tooling.',
},
],
},
])
.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;
});
}
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.Angular:
case Preset.AngularWithNest: {
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
) {
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.Angular, Preset.AngularWithNest].includes(preset)) {
choices.push({
name: 'styl',
message: 'Stylus(.styl) [ http://stylus-lang.com ]',
});
}
if ([Preset.ReactWithExpress, Preset.React, 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 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} --collection=@nrwl/workspace/generators.json --cli=${cli}`;
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 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 });
}
}
);
});
}
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.React:
case Preset.ReactWithExpress:
case Preset.NextJs:
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/react-tutorial/01-create-application`],
});
break;
case Preset.Angular:
case Preset.AngularWithNest:
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/angular-tutorial/01-create-application`],
});
break;
case Preset.Nest:
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/node-tutorial/01-create-application`],
});
break;
}
}