create-nx-workspace
Version:
1,385 lines (1,384 loc) • 53.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.commandsObject = void 0;
exports.validateWorkspaceName = validateWorkspaceName;
const enquirer = require("enquirer");
const yargs = require("yargs");
const chalk = require("chalk");
const create_workspace_options_1 = require("../src/create-workspace-options");
const create_workspace_1 = require("../src/create-workspace");
const preset_1 = require("../src/utils/preset/preset");
const output_1 = require("../src/utils/output");
const nx_version_1 = require("../src/utils/nx/nx-version");
const decorator_1 = require("./decorator");
const get_third_party_preset_1 = require("../src/utils/preset/get-third-party-preset");
const package_manager_1 = require("../src/utils/package-manager");
const prompts_1 = require("../src/internal-utils/prompts");
const yargs_options_1 = require("../src/internal-utils/yargs-options");
const ab_testing_1 = require("../src/utils/nx/ab-testing");
const error_utils_1 = require("../src/utils/error-utils");
const fs_1 = require("fs");
const is_ci_1 = require("../src/utils/ci/is-ci");
function extractErrorFile(error) {
if (!error.stack)
return undefined;
const lines = error.stack.split('\n');
// Find the first line with a file path (typically line 1 after the error message)
const fileLine = lines.find((line) => line.includes('at ') && line.includes('/'));
if (!fileLine)
return undefined;
// Extract just the file path portion
const match = fileLine.match(/\(([^)]+)\)/) || fileLine.match(/at\s+(.+)/);
return match?.[1]?.trim();
}
// For template-based CNW we want to know if user picked empty vs react vs angular etc.
let chosenTemplate;
// Also track old custom presets so we know which ones users want.
let chosenPreset;
// Track whether user opted into cloud or not for SIGINT handler.
let useCloud;
// For stats
let packageManager;
exports.commandsObject = 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) => (0, yargs_options_1.withOptions)(yargs
.option('name', {
describe: chalk.dim `Workspace name (e.g. org name).`,
type: 'string',
})
.option('preset', {
// This describe is hard to auto-fix because of the loop in the code.
// eslint-disable-next-line @nx/workspace/valid-command-object
describe: chalk.dim `Customizes the initial content of your workspace. Default presets include: [${Object.values(preset_1.Preset)
.map((p) => `"${p}"`)
.join(', ')}]. To build your own see https://nx.dev/extending-nx/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',
default: true,
})
.option('routing', {
describe: chalk.dim `Add a routing setup for an Angular or React app.`,
type: 'boolean',
default: true,
})
.option('useReactRouter', {
describe: chalk.dim `Generate a Server-Side Rendered (SSR) React app using React Router.`,
type: 'boolean',
})
.option('bundler', {
describe: chalk.dim `Bundler to be used to build the app.`,
type: 'string',
})
.option('workspaces', {
describe: chalk.dim `Use package manager workspaces.`,
type: 'boolean',
default: true,
})
.option('useProjectJson', {
describe: chalk.dim `Use a 'project.json' file for the Nx configuration instead of a 'package.json' file. This defaults to 'true' when '--no-workspaces' is used. Otherwise, it defaults to 'false'.`,
type: 'boolean',
})
.option('formatter', {
describe: chalk.dim `Code formatter to use.`,
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',
})
.option('nextSrcDir', {
describe: chalk.dim `Generate a 'src/' directory for Next.js.`,
type: 'boolean',
})
.option('e2eTestRunner', {
describe: chalk.dim `Test runner to use for end to end (E2E) tests.`,
choices: ['playwright', 'cypress', 'none'],
type: 'string',
})
.option('unitTestRunner', {
describe: chalk.dim `Test runner to use for unit tests.`,
choices: ['jest', 'vitest', 'none'],
type: 'string',
})
.option('ssr', {
describe: chalk.dim `Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.`,
type: 'boolean',
})
.option('prefix', {
describe: chalk.dim `Prefix to use for Angular component and directive selectors.`,
type: 'string',
})
.option('zoneless', {
describe: chalk.dim `Generate an application that does not use 'zone.js'.`,
type: 'boolean',
default: true,
})
.option('aiAgents', {
describe: chalk.dim `List of AI agents to configure.`,
type: 'array',
choices: [...create_workspace_options_1.supportedAgents],
}), yargs_options_1.withNxCloud, yargs_options_1.withUseGitHub, yargs_options_1.withAllPrompts, yargs_options_1.withPackageManager, yargs_options_1.withGitOptions), async function handler(argv) {
await main(argv).catch(async (error) => {
const { version } = require('../package.json');
// Record error stat for telemetry
const errorCode = error instanceof error_utils_1.CnwError ? error.code : 'UNKNOWN';
const errorMessage = error instanceof Error ? error.message : String(error);
const errorFile = error instanceof Error ? extractErrorFile(error) : undefined;
useCloud = argv.nxCloud !== 'skip';
await (0, ab_testing_1.recordStat)({
nxVersion: nx_version_1.nxVersion,
command: 'create-nx-workspace',
useCloud,
meta: {
type: 'error',
flowVariant: (0, ab_testing_1.getFlowVariant)(),
errorCode,
errorMessage,
errorFile: errorFile ?? '',
template: chosenTemplate ?? '',
preset: chosenPreset ?? '',
nodeVersion: process.versions.node ?? '',
packageManager: packageManager ?? '',
},
});
if (error instanceof error_utils_1.CnwError) {
output_1.output.error({
title: `Failed to create workspace`,
bodyLines: error.message.split('\n').filter((line) => line.trim()),
});
}
else {
output_1.output.error({
title: `Failed to create workspace (v${version})`,
bodyLines: (0, error_utils_1.mapErrorToBodyLines)(error),
});
}
process.exit(1);
});
}, [normalizeArgsMiddleware])
.help('help', chalk.dim `Show help`)
.updateLocale(decorator_1.yargsDecorator)
.version('version', chalk.dim `Show version`, nx_version_1.nxVersion);
// Node 24 has stricter readline behavior, and enquirer is not checking for closed state
// when invoking operations, thus you get an ERR_USE_AFTER_CLOSE error.
process.on('uncaughtException', (error) => {
if (error &&
typeof error === 'object' &&
'code' in error &&
error['code'] === 'ERR_USE_AFTER_CLOSE')
return;
throw error;
});
// Handle Ctrl+C gracefully - show helpful message if workspace was already created
process.on('SIGINT', async () => {
const { directory, connectUrl } = (0, create_workspace_1.getInterruptedWorkspaceState)();
if (directory) {
console.log(''); // New line after ^C
output_1.output.log({
title: 'Workspace creation interrupted',
bodyLines: [
`Your workspace was created at: ${directory}`,
'',
'To complete the setup:',
' 1. Ensure your repo is pushed (e.g. https://github.com/new)',
connectUrl
? ` 2. Connect to Nx Cloud: ${connectUrl}`
: ' 2. Connect to Nx Cloud: Run "nx connect"',
],
});
}
process.exit(130); // Standard exit code for SIGINT
});
let rawArgs;
async function main(parsedArgs) {
output_1.output.log({
title: `Creating your v${nx_version_1.nxVersion} workspace.`,
});
const workspaceInfo = await (0, create_workspace_1.createWorkspace)(parsedArgs.preset, parsedArgs, rawArgs);
await (0, ab_testing_1.recordStat)({
nxVersion: nx_version_1.nxVersion,
command: 'create-nx-workspace',
useCloud: parsedArgs.nxCloud !== 'skip',
meta: {
type: 'complete',
flowVariant: (0, ab_testing_1.getFlowVariant)(),
setupCIPrompt: ab_testing_1.messages.codeOfSelectedPromptMessage('setupCI'),
setupCloudPrompt: ab_testing_1.messages.codeOfSelectedPromptMessage('setupNxCloudV2') ||
ab_testing_1.messages.codeOfSelectedPromptMessage('setupNxCloud'),
nxCloudArg: parsedArgs.nxCloud ?? '',
nxCloudArgRaw: rawArgs.nxCloud ?? '',
pushedToVcs: workspaceInfo.pushedToVcs ?? '',
template: chosenTemplate ?? '',
preset: chosenPreset ?? '',
connectUrl: workspaceInfo.connectUrl ?? '',
nodeVersion: process.versions.node,
packageManager: packageManager ?? '',
},
});
if (parsedArgs.nxCloud && workspaceInfo.nxCloudInfo) {
process.stdout.write(workspaceInfo.nxCloudInfo);
}
// if (isKnownPreset(parsedArgs.preset)) {
// printSocialInformation();
// } 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) {
rawArgs = { ...argv };
output_1.output.log({
title: "Let's create a new workspace [https://nx.dev/getting-started/intro]",
});
argv.workspaces ??= true;
argv.useProjectJson ??= !argv.workspaces;
await (0, ab_testing_1.recordStat)({
nxVersion: nx_version_1.nxVersion,
command: 'create-nx-workspace',
useCloud: argv.nxCloud !== 'skip',
meta: {
type: 'start',
flowVariant: (0, ab_testing_1.getFlowVariant)(),
nodeVersion: process.versions.node,
},
});
try {
argv.name = await determineFolder(argv);
const template = await (0, prompts_1.determineTemplate)(argv);
chosenTemplate = template;
if (template !== 'custom') {
// Template flow - respects CLI arg, otherwise uses detected package manager (from invoking command)
argv.template = template;
const aiAgents = await (0, prompts_1.determineAiAgents)(argv);
const nxCloud = argv.skipGit === true ? 'skip' : await (0, prompts_1.determineNxCloudV2)(argv);
const completionMessageKey = nxCloud === 'skip'
? undefined
: ab_testing_1.messages.completionMessageOfSelectedPrompt('setupNxCloudV2');
packageManager = argv.packageManager ?? (0, package_manager_1.detectInvokedPackageManager)();
Object.assign(argv, {
nxCloud,
useGitHub: nxCloud !== 'skip',
completionMessageKey,
packageManager,
defaultBase: 'main',
aiAgents,
});
await (0, ab_testing_1.recordStat)({
nxVersion: nx_version_1.nxVersion,
command: 'create-nx-workspace',
useCloud: nxCloud !== 'skip',
meta: {
type: 'precreate',
flowVariant: (0, ab_testing_1.getFlowVariant)(),
template: chosenTemplate,
preset: '',
nodeVersion: process.versions.node ?? '',
packageManager,
},
});
}
else {
// Preset flow - existing behavior
if (!argv.preset || (0, preset_1.isKnownPreset)(argv.preset)) {
argv.stack = await determineStack(argv);
const presetOptions = await determinePresetOptions(argv);
Object.assign(argv, presetOptions);
}
else {
try {
(0, get_third_party_preset_1.getPackageNameFromThirdPartyPreset)(argv.preset);
}
catch (e) {
const message = e instanceof Error ? e.message : String(e);
throw new error_utils_1.CnwError('INVALID_PRESET', `Could not find preset "${argv.preset}": ${message}`);
}
}
packageManager = await (0, prompts_1.determinePackageManager)(argv);
const aiAgents = await (0, prompts_1.determineAiAgents)(argv);
const defaultBase = await (0, prompts_1.determineDefaultBase)(argv);
// Check if CLI arg was provided (use rawArgs to check original input)
const cliNxCloudArgProvided = rawArgs.nxCloud !== undefined;
let nxCloud;
let useGitHub;
let completionMessageKey;
if (argv.skipGit === true) {
nxCloud = 'skip';
useGitHub = undefined;
}
else if (cliNxCloudArgProvided) {
// CLI arg provided: use existing flow (CI provider selection if needed)
nxCloud = await (0, prompts_1.determineNxCloud)(argv);
useGitHub =
nxCloud === 'skip'
? undefined
: nxCloud === 'github' || (await (0, prompts_1.determineIfGitHubWillBeUsed)(argv));
}
else {
// No CLI arg: use simplified prompt (same as template flow)
nxCloud = await (0, prompts_1.determineNxCloudV2)(argv);
useGitHub = nxCloud !== 'skip';
completionMessageKey =
nxCloud === 'skip'
? undefined
: ab_testing_1.messages.completionMessageOfSelectedPrompt('setupNxCloudV2');
}
Object.assign(argv, {
nxCloud,
useGitHub,
completionMessageKey,
packageManager,
defaultBase,
aiAgents,
});
chosenPreset = argv.preset;
await (0, ab_testing_1.recordStat)({
nxVersion: nx_version_1.nxVersion,
command: 'create-nx-workspace',
useCloud: nxCloud !== 'skip',
meta: {
type: 'precreate',
flowVariant: (0, ab_testing_1.getFlowVariant)(),
template: '',
preset: chosenPreset ?? '',
nodeVersion: process.versions.node ?? '',
packageManager,
},
});
}
}
catch (e) {
if (e instanceof error_utils_1.CnwError) {
throw e;
}
const message = e instanceof Error ? e.message : String(e);
throw new error_utils_1.CnwError('UNKNOWN', message);
}
}
function invariant(predicate, errorCode, message) {
if (!predicate) {
throw new error_utils_1.CnwError(errorCode, message);
}
}
function validateWorkspaceName(name) {
const pattern = /^[a-zA-Z]/;
if (!pattern.test(name)) {
output_1.output.error({
title: 'Invalid workspace name',
bodyLines: [
`The workspace name "${name}" is invalid.`,
`Workspace names must start with a letter.`,
`Examples of valid names: myapp, MyApp, my-app, my_app`,
],
});
process.exit(1);
}
}
async function determineFolder(parsedArgs) {
const folderName = parsedArgs._[0]
? parsedArgs._[0].toString()
: parsedArgs.name;
if (folderName) {
validateWorkspaceName(folderName);
// If directory exists, either re-prompt (interactive) or error (non-interactive)
if ((0, fs_1.existsSync)(folderName)) {
if (parsedArgs.interactive && !(0, is_ci_1.isCI)()) {
output_1.output.warn({
title: `Directory ${folderName} already exists.`,
});
// Re-prompt for a new folder name
return promptForFolder(parsedArgs);
}
throw new error_utils_1.CnwError('DIRECTORY_EXISTS', `The directory '${folderName}' already exists. Choose a different name or remove the existing directory.`);
}
return folderName;
}
return promptForFolder(parsedArgs);
}
async function promptForFolder(parsedArgs) {
const reply = await enquirer.prompt([
{
name: 'folderName',
message: `Where would you like to create your workspace?`,
initial: 'org',
type: 'input',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
validate: (value) => {
if (!value) {
return 'Folder name cannot be empty';
}
if (!/^[a-zA-Z]/.test(value)) {
return 'Workspace name must start with a letter';
}
if ((0, fs_1.existsSync)(value)) {
return `The directory '${value}' already exists`;
}
return true;
},
},
]);
// Fallback invariants in case validate is bypassed (e.g., in CI or non-interactive mode)
invariant(reply.folderName, 'INVALID_FOLDER_NAME', 'Folder name cannot be empty');
validateWorkspaceName(reply.folderName);
invariant(!(0, fs_1.existsSync)(reply.folderName), 'DIRECTORY_EXISTS', `The directory '${reply.folderName}' already exists. Choose a different name or remove the existing directory.`);
return reply.folderName;
}
async function determineStack(parsedArgs) {
if (parsedArgs.preset) {
switch (parsedArgs.preset) {
case preset_1.Preset.Angular:
case preset_1.Preset.AngularStandalone:
case preset_1.Preset.AngularMonorepo:
return 'angular';
case preset_1.Preset.React:
case preset_1.Preset.ReactStandalone:
case preset_1.Preset.ReactMonorepo:
case preset_1.Preset.NextJs:
case preset_1.Preset.NextJsStandalone:
case preset_1.Preset.ReactNative:
case preset_1.Preset.Expo:
return 'react';
case preset_1.Preset.Vue:
case preset_1.Preset.VueStandalone:
case preset_1.Preset.VueMonorepo:
case preset_1.Preset.Nuxt:
case preset_1.Preset.NuxtStandalone:
return 'vue';
case preset_1.Preset.Nest:
case preset_1.Preset.NodeStandalone:
case preset_1.Preset.NodeMonorepo:
case preset_1.Preset.Express:
return 'node';
case preset_1.Preset.Apps:
case preset_1.Preset.NPM:
case preset_1.Preset.TS:
case preset_1.Preset.TsStandalone:
return 'none';
case preset_1.Preset.WebComponents:
default:
return 'unknown';
}
}
const { stack } = await enquirer.prompt([
{
name: 'stack',
message: `Which stack do you want to use?`,
type: 'autocomplete',
choices: [
{
name: `none`,
message: process.env.NX_ADD_PLUGINS !== 'false' && parsedArgs.workspaces
? `None: Configures a TypeScript/JavaScript monorepo.`
: `None: Configures a TypeScript/JavaScript project with minimal structure.`,
},
{
name: `react`,
message: `React: Configures a React application with your framework of choice.`,
},
{
name: `vue`,
message: `Vue: Configures a Vue application with your framework of choice.`,
},
{
name: `angular`,
message: `Angular: Configures a Angular application with modern tooling.`,
},
{
name: `node`,
message: `Node: Configures a Node API application with your framework of choice.`,
},
],
},
]);
return stack;
}
async function determinePresetOptions(parsedArgs) {
switch (parsedArgs.stack) {
case 'none':
return determineNoneOptions(parsedArgs);
case 'react':
return determineReactOptions(parsedArgs);
case 'angular':
return determineAngularOptions(parsedArgs);
case 'vue':
return determineVueOptions(parsedArgs);
case 'node':
return determineNodeOptions(parsedArgs);
default:
return parsedArgs;
}
}
async function determineFormatterOptions(args, opts) {
if (args.formatter)
return args.formatter;
const reply = await enquirer.prompt([
{
name: 'prettier',
message: `Would you like to use Prettier for code formatting?`,
type: 'autocomplete',
choices: [
{
name: 'Yes',
},
{
name: 'No',
},
],
initial: opts?.preferPrettier ? 0 : 1,
skip: !args.interactive || (0, is_ci_1.isCI)(),
},
]);
return reply.prettier === 'Yes' ? 'prettier' : 'none';
}
async function determineLinterOptions(args, opts) {
const reply = await enquirer.prompt([
{
name: 'eslint',
message: `Would you like to use ESLint?`,
type: 'autocomplete',
choices: [
{
name: 'Yes',
},
{
name: 'No',
},
],
initial: opts?.preferEslint ? 0 : 1,
skip: !args.interactive || (0, is_ci_1.isCI)(),
},
]);
return reply.eslint === 'Yes' ? 'eslint' : 'none';
}
async function determineNoneOptions(parsedArgs) {
if ((!parsedArgs.preset || parsedArgs.preset === preset_1.Preset.TS) &&
process.env.NX_ADD_PLUGINS !== 'false' &&
parsedArgs.workspaces) {
return {
preset: preset_1.Preset.TS,
formatter: await determineFormatterOptions(parsedArgs),
};
}
else {
let preset;
let workspaceType = undefined;
let appName = undefined;
let js;
if (parsedArgs.preset) {
preset = parsedArgs.preset;
}
else {
workspaceType = await determinePackageBasedOrIntegratedOrStandalone();
if (workspaceType === 'standalone') {
preset = preset_1.Preset.TsStandalone;
}
else if (workspaceType === 'integrated') {
preset = preset_1.Preset.Apps;
}
else {
preset = preset_1.Preset.NPM;
}
}
if (preset === preset_1.Preset.TS) {
return { preset, formatter: 'prettier' };
}
if (parsedArgs.js !== undefined) {
js = parsedArgs.js;
}
else if (preset === preset_1.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([
{
name: 'ts',
message: `Would you like to use TypeScript with this project?`,
type: 'autocomplete',
choices: [
{
name: 'Yes',
},
{
name: 'No',
},
],
initial: 0,
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
},
]);
js = reply.ts === 'No';
}
return { preset, js, appName };
}
}
async function determineReactOptions(parsedArgs) {
let preset;
let style = undefined;
let appName;
let bundler = undefined;
let unitTestRunner = undefined;
let e2eTestRunner = undefined;
let useReactRouter = false;
let routing = true;
let nextAppDir = false;
let nextSrcDir = false;
let linter;
let formatter;
const workspaces = parsedArgs.workspaces;
if (parsedArgs.preset && parsedArgs.preset !== preset_1.Preset.React) {
preset = parsedArgs.preset;
if (preset === preset_1.Preset.ReactStandalone ||
preset === preset_1.Preset.NextJsStandalone) {
appName = parsedArgs.appName ?? parsedArgs.name;
}
else {
appName = await determineAppName(parsedArgs);
}
}
else {
const framework = await determineReactFramework(parsedArgs);
const isStandalone = workspaces || framework === 'react-native' || framework === 'expo'
? false
: (await determineStandaloneOrMonorepo()) === 'standalone';
if (isStandalone) {
appName = parsedArgs.name;
}
else {
appName = await determineAppName(parsedArgs);
}
if (framework === 'next') {
if (isStandalone) {
preset = preset_1.Preset.NextJsStandalone;
}
else {
preset = preset_1.Preset.NextJs;
}
}
else if (framework === 'react-native') {
preset = preset_1.Preset.ReactNative;
}
else if (framework === 'expo') {
preset = preset_1.Preset.Expo;
}
else {
useReactRouter = await determineReactRouter(parsedArgs);
if (isStandalone) {
preset = preset_1.Preset.ReactStandalone;
}
else {
preset = preset_1.Preset.ReactMonorepo;
}
}
}
if (preset === preset_1.Preset.ReactStandalone || preset === preset_1.Preset.ReactMonorepo) {
bundler = useReactRouter ? 'vite' : await determineReactBundler(parsedArgs);
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
preferVitest: bundler === 'vite',
});
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
}
else if (preset === preset_1.Preset.NextJs || preset === preset_1.Preset.NextJsStandalone) {
nextAppDir = await determineNextAppDir(parsedArgs);
nextSrcDir = await determineNextSrcDir(parsedArgs);
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
exclude: 'vitest',
});
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
}
else if (preset === preset_1.Preset.ReactNative || preset === preset_1.Preset.Expo) {
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
exclude: 'vitest',
});
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
}
if (parsedArgs.style) {
style = parsedArgs.style;
}
else if (preset === preset_1.Preset.ReactStandalone ||
preset === preset_1.Preset.ReactMonorepo ||
preset === preset_1.Preset.NextJs ||
preset === preset_1.Preset.NextJsStandalone) {
const reply = await enquirer.prompt([
{
name: 'style',
message: `Default stylesheet format`,
initial: 0,
type: 'autocomplete',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
choices: [
{
name: 'css',
message: 'CSS',
},
{
name: 'scss',
message: 'SASS(.scss) [ https://sass-lang.com ]',
},
{
name: 'less',
message: 'LESS [ https://lesscss.org ]',
},
{
name: 'tailwind',
message: 'tailwind [ https://tailwindcss.com ]',
},
{
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;
}
if (workspaces) {
linter = await determineLinterOptions(parsedArgs, { preferEslint: true });
formatter = await determineFormatterOptions(parsedArgs, {
preferPrettier: true,
});
}
else {
linter = 'eslint';
formatter = 'prettier';
}
return {
preset,
style,
appName,
bundler,
nextAppDir,
nextSrcDir,
unitTestRunner,
e2eTestRunner,
useReactRouter,
routing,
linter,
formatter,
workspaces,
};
}
async function determineVueOptions(parsedArgs) {
let preset;
let style = undefined;
let appName;
let unitTestRunner = undefined;
let e2eTestRunner = undefined;
let linter;
let formatter;
const workspaces = parsedArgs.workspaces;
if (parsedArgs.preset && parsedArgs.preset !== preset_1.Preset.Vue) {
preset = parsedArgs.preset;
if (preset === preset_1.Preset.VueStandalone || preset === preset_1.Preset.NuxtStandalone) {
appName = parsedArgs.appName ?? parsedArgs.name;
}
else {
appName = await determineAppName(parsedArgs);
}
}
else {
const framework = await determineVueFramework(parsedArgs);
const workspaceType = workspaces
? 'monorepo'
: await determineStandaloneOrMonorepo();
if (workspaceType === 'standalone') {
appName = parsedArgs.appName ?? parsedArgs.name;
}
else {
appName = await determineAppName(parsedArgs);
}
if (framework === 'nuxt') {
if (workspaceType === 'standalone') {
preset = preset_1.Preset.NuxtStandalone;
}
else {
preset = preset_1.Preset.Nuxt;
}
}
else {
if (workspaceType === 'standalone') {
preset = preset_1.Preset.VueStandalone;
}
else {
preset = preset_1.Preset.VueMonorepo;
}
}
}
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
exclude: 'jest',
});
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
if (parsedArgs.style) {
style = parsedArgs.style;
}
else {
const reply = await enquirer.prompt([
{
name: 'style',
message: `Default stylesheet format`,
initial: 0,
type: 'autocomplete',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
choices: [
{
name: 'css',
message: 'CSS',
},
{
name: 'scss',
message: 'SASS(.scss) [ https://sass-lang.com ]',
},
{
name: 'less',
message: 'LESS [ https://lesscss.org ]',
},
{
name: 'none',
message: 'None',
},
],
},
]);
style = reply.style;
}
if (workspaces) {
linter = await determineLinterOptions(parsedArgs, { preferEslint: true });
formatter = await determineFormatterOptions(parsedArgs, {
preferPrettier: true,
});
}
else {
linter = 'eslint';
formatter = 'prettier';
}
return {
preset,
style,
appName,
unitTestRunner,
e2eTestRunner,
linter,
formatter,
workspaces,
};
}
async function determineAngularOptions(parsedArgs) {
let preset;
let style;
let appName;
let unitTestRunner = undefined;
let e2eTestRunner = undefined;
let bundler = undefined;
let ssr = undefined;
const standaloneApi = parsedArgs.standaloneApi;
const routing = parsedArgs.routing;
const prefix = parsedArgs.prefix;
const zoneless = parsedArgs.zoneless;
if (prefix) {
// https://github.com/angular/angular-cli/blob/main/packages/schematics/angular/utility/validation.ts#L11-L14
const htmlSelectorRegex = /^[a-zA-Z][.0-9a-zA-Z]*((:?-[0-9]+)*|(:?-[a-zA-Z][.0-9a-zA-Z]*(:?-[0-9]+)*)*)$/;
// validate whether component/directive selectors will be valid with the provided prefix
if (!htmlSelectorRegex.test(`${prefix}-placeholder`)) {
throw new error_utils_1.CnwError('ANGULAR_PREFIX_INVALID', `The provided "${prefix}" prefix is invalid. It must be a valid HTML selector.`);
}
}
if (parsedArgs.preset && parsedArgs.preset !== preset_1.Preset.Angular) {
preset = parsedArgs.preset;
if (preset === preset_1.Preset.AngularStandalone) {
appName = parsedArgs.name;
}
else {
appName = await determineAppName(parsedArgs);
}
}
else {
const workspaceType = await determineStandaloneOrMonorepo();
if (workspaceType === 'standalone') {
preset = preset_1.Preset.AngularStandalone;
appName = parsedArgs.name;
}
else {
preset = preset_1.Preset.AngularMonorepo;
appName = await determineAppName(parsedArgs);
}
}
if (parsedArgs.bundler) {
bundler = parsedArgs.bundler;
}
else {
const reply = await enquirer.prompt([
{
name: 'bundler',
message: `Which bundler would you like to use?`,
type: 'autocomplete',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
choices: [
{
name: 'esbuild',
message: 'esbuild [ https://esbuild.github.io/ ]',
},
{
name: 'rspack',
message: 'Rspack [ https://rspack.dev/ ]',
},
{
name: 'webpack',
message: 'Webpack [ https://webpack.js.org/ ]',
},
],
initial: 0,
},
]);
bundler = reply.bundler;
}
if (parsedArgs.style) {
style = parsedArgs.style;
}
else {
const reply = await enquirer.prompt([
{
name: 'style',
message: `Default stylesheet format`,
initial: 0,
type: 'autocomplete',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
choices: [
{
name: 'css',
message: 'CSS',
},
{
name: 'scss',
message: 'SASS(.scss) [ https://sass-lang.com ]',
},
{
name: 'less',
message: 'LESS [ https://lesscss.org ]',
},
],
},
]);
style = reply.style;
}
if (parsedArgs.ssr !== undefined) {
ssr = parsedArgs.ssr;
}
else {
const reply = await enquirer.prompt([
{
name: 'ssr',
message: `Do you want to enable Server-Side Rendering (SSR)${bundler !== 'rspack'
? ' and Static Site Generation (SSG/Prerendering)?'
: '?'}`,
type: 'autocomplete',
choices: [{ name: 'Yes' }, { name: 'No' }],
initial: 1,
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
},
]);
ssr = reply.ssr === 'Yes';
}
if (parsedArgs.unitTestRunner) {
unitTestRunner = parsedArgs.unitTestRunner;
}
else if (!parsedArgs.workspaces) {
unitTestRunner = undefined;
}
else {
unitTestRunner = await enquirer
.prompt([
{
message: 'Which unit test runner would you like to use?',
type: 'autocomplete',
name: 'unitTestRunner',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
choices: [
{
name: 'vitest-angular',
message: 'Vitest & Angular [ https://vitest.dev/ & https://angular.dev ]',
},
{
name: 'vitest-analog',
message: 'Vitest & Analog [ https://vitest.dev/ & https://analogjs.org/ ]',
},
{
name: 'jest',
message: 'Jest [ https://jestjs.io/ ]',
},
{
name: 'none',
message: 'None',
},
],
initial: 0,
},
])
.then((r) => r.unitTestRunner);
}
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
return {
preset,
style,
appName,
standaloneApi,
routing,
unitTestRunner,
e2eTestRunner,
bundler,
ssr,
prefix,
zoneless,
};
}
async function determineNodeOptions(parsedArgs) {
let preset;
let appName;
let framework;
let docker;
let linter;
let formatter;
let unitTestRunner = undefined;
const workspaces = parsedArgs.workspaces;
if (parsedArgs.preset) {
preset = parsedArgs.preset;
if (preset === preset_1.Preset.Nest ||
preset === preset_1.Preset.Express ||
preset === preset_1.Preset.NodeMonorepo) {
appName = await determineAppName(parsedArgs);
}
else {
appName = parsedArgs.name;
}
if (preset === preset_1.Preset.NodeStandalone || preset === preset_1.Preset.NodeMonorepo) {
framework = await determineNodeFramework(parsedArgs);
}
else {
framework = 'none';
}
}
else {
framework = await determineNodeFramework(parsedArgs);
const workspaceType = workspaces
? 'monorepo'
: await determineStandaloneOrMonorepo();
if (workspaceType === 'standalone') {
preset = preset_1.Preset.NodeStandalone;
appName = parsedArgs.name;
}
else {
preset = preset_1.Preset.NodeMonorepo;
appName = await determineAppName(parsedArgs);
}
}
if (parsedArgs.docker !== undefined) {
docker = parsedArgs.docker;
}
else {
const reply = await enquirer.prompt([
{
name: 'docker',
message: 'Would you like to generate a Dockerfile? [https://docs.docker.com/]',
type: 'autocomplete',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
choices: [
{
name: 'Yes',
hint: 'I want to generate a Dockerfile',
},
{
name: 'No',
},
],
initial: 1,
},
]);
docker = reply.docker === 'Yes';
}
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
exclude: 'vitest',
});
if (workspaces) {
linter = await determineLinterOptions(parsedArgs, { preferEslint: true });
formatter = await determineFormatterOptions(parsedArgs, {
preferPrettier: true,
});
}
else {
linter = 'eslint';
formatter = 'prettier';
}
return {
preset,
appName,
framework,
docker,
linter,
formatter,
workspaces,
unitTestRunner,
};
}
async function determinePackageBasedOrIntegratedOrStandalone() {
const { workspaceType } = await enquirer.prompt([
{
type: 'autocomplete',
name: 'workspaceType',
message: `Package-based monorepo, integrated monorepo, or standalone project?`,
initial: 0,
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 creates a monorepo that contains multiple projects.',
},
{
name: 'standalone',
message: 'Standalone: Nx creates a single project and makes it fast.',
},
],
},
]);
invariant(workspaceType, 'INVALID_WORKSPACE_TYPE', `Invalid workspace type. It must be one of the following: standalone, integrated. Got ${workspaceType}`);
return workspaceType;
}
async function determineStandaloneOrMonorepo() {
const { workspaceType } = await enquirer.prompt([
{
type: 'autocomplete',
name: 'workspaceType',
message: `Integrated monorepo, or standalone project?`,
initial: 1,
choices: [
{
name: 'integrated',
message: 'Integrated Monorepo: Nx creates a monorepo that contains multiple projects.',
},
{
name: 'standalone',
message: 'Standalone: Nx creates a single project and makes it fast.',
},
],
},
]);
invariant(workspaceType, 'INVALID_WORKSPACE_TYPE', `Invalid workspace type. It must be one of the following: standalone, integrated. Got ${workspaceType}`);
return workspaceType;
}
async function determineAppName(parsedArgs) {
if (parsedArgs.appName)
return parsedArgs.appName;
const { appName } = await enquirer.prompt([
{
name: 'appName',
message: `Application name`,
type: 'input',
initial: parsedArgs.name,
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
},
]);
invariant(appName, 'INVALID_APP_NAME', 'App name cannot be empty');
return appName;
}
async function determineReactFramework(parsedArgs) {
if (parsedArgs.framework) {
return parsedArgs.framework;
}
if (!parsedArgs.interactive) {
return 'none';
}
const reply = await enquirer.prompt([
{
name: 'framework',
message: 'What framework would you like to use?',
type: 'autocomplete',
choices: [
{
name: 'none',
message: 'None',
hint: ' I only want react, react-dom or react-router',
},
{
name: 'next',
message: 'Next.js [ https://nextjs.org/ ]',
},
{
name: 'expo',
message: 'Expo [ https://expo.io/ ]',
},
{
name: 'react-native',
message: 'React Native [ https://reactnative.dev/ ]',
},
],
initial: 0,
},
]);
return reply.framework;
}
async function determineReactBundler(parsedArgs) {
if (parsedArgs.bundler)
return parsedArgs.bundler;
const reply = await enquirer.prompt([
{
name: 'bundler',
message: `Which bundler would you like to use?`,
type: 'autocomplete',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
choices: [
{
name: 'vite',
message: 'Vite [ https://vite.dev/ ]',
},
{
name: 'webpack',
message: 'Webpack [ https://webpack.js.org/ ]',
},
{
name: 'rspack',
message: 'Rspack [ https://www.rspack.dev/ ]',
},
],
initial: 0,
},
]);
return reply.bundler;
}
async function determineNextAppDir(parsedArgs) {
if (parsedArgs.nextAppDir !== undefined)
return parsedArgs.nextAppDir;
const reply = await enquirer.prompt([
{
name: 'nextAppDir',
message: 'Would you like to use the App Router (recommended)?',
type: 'autocomplete',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
choices: [
{
name: 'Yes',
},
{
name: 'No',
},
],
initial: 0,
},
]);
return reply.nextAppDir === 'Yes';
}
async function determineNextSrcDir(parsedArgs) {
if (parsedArgs.nextSrcDir !== undefined)
return parsedArgs.nextSrcDir;
const reply = await enquirer.prompt([
{
name: 'nextSrcDir',
message: 'Would you like to use the src/ directory?',
type: 'autocomplete',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
choices: [
{
name: 'Yes',
},
{
name: 'No',
},
],
initial: 0,
},
]);
return reply.nextSrcDir === 'Yes';
}
async function determineVueFramework(parsedArgs) {
if (!!parsedArgs.framework)
return parsedArgs.framework;
const reply = await enquirer.prompt([
{
name: 'framework',
message: 'What framework would you like to use?',
type: 'autocomplete',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
choices: [
{
name: 'none',
message: 'None',
hint: ' I only want Vue',
},
{
name: 'nuxt',
message: 'Nuxt [ https://nuxt.com/ ]',
},
],
initial: 0,
},
]);
return reply.framework;
}
async function determineNodeFramework(parsedArgs) {
if (!!parsedArgs.framework)
return parsedArgs.framework;
const reply = await enquirer.prompt([
{
message: 'What framework should be used?',
type: 'autocomplete',
name: 'framework',
skip: !parsedArgs.interactive || (0, is_ci_1.isCI)(),
choices: [
{
name: 'none',
message: 'None',
},
{
name: 'express',
message: 'Express [ https://expressjs.com/ ]',
},
{
name: 'fastify',
message: 'Fastify [ https://www.fastify.dev/ ]',