This PR fixes some inconsistencies when generating a TS solution workspace (e.g. `npx create-nx-workspace --workspace`). If the user chooses `None` stack (or `--preset=ts`), then we continue to default to no Prettier (and ESLint, etc.). However, for React, Vue, and Node, we want the defaults to be useful for users to run lint, test, etc. Thus, they are now updated the default does not opt out of any tooling, but users can still choose no to any of them. <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior The defaults for React, Vue, Node are inconsistent (e2e runner is there by default, but not unit test runner, etc.) ## Expected Behavior Defaults are consistent and useful, especially for tutorials. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
1496 lines
41 KiB
TypeScript
1496 lines
41 KiB
TypeScript
import * as enquirer from 'enquirer';
|
|
import * as yargs from 'yargs';
|
|
import * as chalk from 'chalk';
|
|
|
|
import { CreateWorkspaceOptions } from '../src/create-workspace-options';
|
|
import { createWorkspace } from '../src/create-workspace';
|
|
import { isKnownPreset, Preset } from '../src/utils/preset/preset';
|
|
import { CLIErrorMessageConfig, output } from '../src/utils/output';
|
|
import { nxVersion } from '../src/utils/nx/nx-version';
|
|
|
|
import { yargsDecorator } from './decorator';
|
|
import { getPackageNameFromThirdPartyPreset } from '../src/utils/preset/get-third-party-preset';
|
|
import {
|
|
determineDefaultBase,
|
|
determineIfGitHubWillBeUsed,
|
|
determineNxCloud,
|
|
determinePackageManager,
|
|
} from '../src/internal-utils/prompts';
|
|
import {
|
|
withAllPrompts,
|
|
withGitOptions,
|
|
withUseGitHub,
|
|
withNxCloud,
|
|
withOptions,
|
|
withPackageManager,
|
|
} from '../src/internal-utils/yargs-options';
|
|
import { messages, recordStat } from '../src/utils/nx/ab-testing';
|
|
import { mapErrorToBodyLines } from '../src/utils/error-utils';
|
|
import { existsSync } from 'fs';
|
|
import { isCI } from '../src/utils/ci/is-ci';
|
|
import { printSocialInformation } from '../src/utils/social-information';
|
|
|
|
interface BaseArguments extends CreateWorkspaceOptions {
|
|
preset: Preset;
|
|
linter?: 'none' | 'eslint';
|
|
formatter?: 'none' | 'prettier';
|
|
workspaces?: boolean;
|
|
}
|
|
|
|
interface NoneArguments extends BaseArguments {
|
|
stack: 'none';
|
|
workspaceType?: 'package-based' | 'integrated' | 'standalone';
|
|
js?: boolean;
|
|
appName?: string | undefined;
|
|
}
|
|
|
|
interface ReactArguments extends BaseArguments {
|
|
stack: 'react';
|
|
workspaceType: 'standalone' | 'integrated';
|
|
appName: string;
|
|
framework: 'none' | 'next' | 'remix';
|
|
style: string;
|
|
bundler: 'webpack' | 'vite' | 'rspack';
|
|
nextAppDir: boolean;
|
|
nextSrcDir: boolean;
|
|
unitTestRunner: 'none' | 'jest' | 'vitest';
|
|
e2eTestRunner: 'none' | 'cypress' | 'playwright';
|
|
}
|
|
|
|
interface AngularArguments extends BaseArguments {
|
|
stack: 'angular';
|
|
workspaceType: 'standalone' | 'integrated';
|
|
appName: string;
|
|
style: string;
|
|
routing: boolean;
|
|
standaloneApi: boolean;
|
|
unitTestRunner: 'none' | 'jest' | 'vitest';
|
|
e2eTestRunner: 'none' | 'cypress' | 'playwright';
|
|
bundler: 'webpack' | 'esbuild';
|
|
ssr: boolean;
|
|
serverRouting: boolean;
|
|
prefix: string;
|
|
}
|
|
|
|
interface VueArguments extends BaseArguments {
|
|
stack: 'vue';
|
|
workspaceType: 'standalone' | 'integrated';
|
|
appName: string;
|
|
framework: 'none' | 'nuxt';
|
|
style: string;
|
|
unitTestRunner: 'none' | 'vitest';
|
|
e2eTestRunner: 'none' | 'cypress' | 'playwright';
|
|
}
|
|
|
|
interface NodeArguments extends BaseArguments {
|
|
stack: 'node';
|
|
workspaceType: 'standalone' | 'integrated';
|
|
appName: string;
|
|
framework: 'none' | 'express' | 'fastify' | 'koa' | 'nest';
|
|
docker: boolean;
|
|
unitTestRunner: 'none' | 'jest';
|
|
}
|
|
|
|
interface UnknownStackArguments extends BaseArguments {
|
|
stack: 'unknown';
|
|
}
|
|
|
|
type Arguments =
|
|
| NoneArguments
|
|
| ReactArguments
|
|
| AngularArguments
|
|
| VueArguments
|
|
| NodeArguments
|
|
| UnknownStackArguments;
|
|
|
|
export const commandsObject: yargs.Argv<Arguments> = yargs
|
|
.wrap(yargs.terminalWidth())
|
|
.parserConfiguration({
|
|
'strip-dashed': true,
|
|
'dot-notation': true,
|
|
})
|
|
.command<Arguments>(
|
|
// this is the default and only command
|
|
'$0 [name] [options]',
|
|
'Create a new Nx workspace',
|
|
(yargs) =>
|
|
withOptions(
|
|
yargs
|
|
.option('name', {
|
|
describe: chalk.dim`Workspace name (e.g. org name).`,
|
|
type: 'string',
|
|
})
|
|
.option('preset', {
|
|
// 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
|
|
)
|
|
.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 app.`,
|
|
type: 'boolean',
|
|
default: true,
|
|
})
|
|
.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: false,
|
|
})
|
|
.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',
|
|
}),
|
|
withNxCloud,
|
|
withUseGitHub,
|
|
withAllPrompts,
|
|
withPackageManager,
|
|
withGitOptions
|
|
),
|
|
|
|
async function handler(argv: yargs.ArgumentsCamelCase<Arguments>) {
|
|
await main(argv).catch((error) => {
|
|
const { version } = require('../package.json');
|
|
output.error({
|
|
title: `Something went wrong! v${version}`,
|
|
});
|
|
throw error;
|
|
});
|
|
},
|
|
[normalizeArgsMiddleware] as yargs.MiddlewareFunction<{}>[]
|
|
)
|
|
.help('help', chalk.dim`Show help`)
|
|
.updateLocale(yargsDecorator)
|
|
.version(
|
|
'version',
|
|
chalk.dim`Show version`,
|
|
nxVersion
|
|
) as yargs.Argv<Arguments>;
|
|
|
|
async function main(parsedArgs: yargs.Arguments<Arguments>) {
|
|
output.log({
|
|
title: `Creating your v${nxVersion} workspace.`,
|
|
});
|
|
|
|
const workspaceInfo = await createWorkspace<Arguments>(
|
|
parsedArgs.preset,
|
|
parsedArgs
|
|
);
|
|
|
|
await recordStat({
|
|
nxVersion,
|
|
command: 'create-nx-workspace',
|
|
useCloud: parsedArgs.nxCloud !== 'skip',
|
|
meta: [
|
|
messages.codeOfSelectedPromptMessage('setupCI'),
|
|
messages.codeOfSelectedPromptMessage('setupNxCloud'),
|
|
parsedArgs.nxCloud,
|
|
],
|
|
});
|
|
|
|
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: yargs.Arguments<Arguments>
|
|
): Promise<void> {
|
|
output.log({
|
|
title:
|
|
"Let's create a new workspace [https://nx.dev/getting-started/intro]",
|
|
});
|
|
|
|
try {
|
|
argv.name = await determineFolder(argv);
|
|
if (!argv.preset || isKnownPreset(argv.preset)) {
|
|
argv.stack = await determineStack(argv);
|
|
const presetOptions = await determinePresetOptions(argv);
|
|
Object.assign(argv, presetOptions);
|
|
} else {
|
|
try {
|
|
getPackageNameFromThirdPartyPreset(argv.preset);
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
output.error({
|
|
title: `Could not find preset "${argv.preset}"`,
|
|
bodyLines: mapErrorToBodyLines(e),
|
|
});
|
|
} else {
|
|
console.error(e);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
const packageManager = await determinePackageManager(argv);
|
|
const defaultBase = await determineDefaultBase(argv);
|
|
const nxCloud =
|
|
argv.skipGit === true ? 'skip' : await determineNxCloud(argv);
|
|
const useGitHub =
|
|
nxCloud === 'skip'
|
|
? undefined
|
|
: nxCloud === 'github' || (await determineIfGitHubWillBeUsed(nxCloud));
|
|
Object.assign(argv, {
|
|
nxCloud,
|
|
useGitHub,
|
|
packageManager,
|
|
defaultBase,
|
|
});
|
|
} catch (e) {
|
|
console.error(e);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function invariant(
|
|
predicate: string | number | boolean,
|
|
message: CLIErrorMessageConfig
|
|
): asserts predicate is NonNullable<string | number> | true {
|
|
if (!predicate) {
|
|
output.error(message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function determineFolder(
|
|
parsedArgs: yargs.Arguments<Arguments>
|
|
): Promise<string> {
|
|
const folderName: string = parsedArgs._[0]
|
|
? parsedArgs._[0].toString()
|
|
: parsedArgs.name;
|
|
if (folderName) return folderName;
|
|
const reply = await enquirer.prompt<{ folderName: string }>([
|
|
{
|
|
name: 'folderName',
|
|
message: `Where would you like to create your workspace?`,
|
|
initial: 'org',
|
|
type: 'input',
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
},
|
|
]);
|
|
|
|
invariant(reply.folderName, {
|
|
title: 'Invalid folder name',
|
|
bodyLines: [`Folder name cannot be empty`],
|
|
});
|
|
|
|
invariant(!existsSync(reply.folderName), {
|
|
title: 'That folder is already taken',
|
|
});
|
|
|
|
return reply.folderName;
|
|
}
|
|
|
|
async function determineStack(
|
|
parsedArgs: yargs.Arguments<Arguments>
|
|
): Promise<'none' | 'react' | 'angular' | 'vue' | 'node' | 'unknown'> {
|
|
if (parsedArgs.preset) {
|
|
switch (parsedArgs.preset) {
|
|
case Preset.Angular:
|
|
case Preset.AngularStandalone:
|
|
case Preset.AngularMonorepo:
|
|
return 'angular';
|
|
case Preset.React:
|
|
case Preset.ReactStandalone:
|
|
case Preset.ReactMonorepo:
|
|
case Preset.NextJs:
|
|
case Preset.NextJsStandalone:
|
|
case Preset.RemixStandalone:
|
|
case Preset.RemixMonorepo:
|
|
case Preset.ReactNative:
|
|
case Preset.Expo:
|
|
return 'react';
|
|
case Preset.Vue:
|
|
case Preset.VueStandalone:
|
|
case Preset.VueMonorepo:
|
|
case Preset.Nuxt:
|
|
case Preset.NuxtStandalone:
|
|
return 'vue';
|
|
case Preset.Nest:
|
|
case Preset.NodeStandalone:
|
|
case Preset.NodeMonorepo:
|
|
case Preset.Express:
|
|
return 'node';
|
|
case Preset.Apps:
|
|
case Preset.NPM:
|
|
case Preset.TS:
|
|
case Preset.TsStandalone:
|
|
return 'none';
|
|
case Preset.WebComponents:
|
|
default:
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
const { stack } = await enquirer.prompt<{
|
|
stack: 'none' | 'react' | 'angular' | 'node' | 'vue';
|
|
}>([
|
|
{
|
|
name: 'stack',
|
|
message: `Which stack do you want to use?`,
|
|
type: 'autocomplete',
|
|
choices: [
|
|
{
|
|
name: `none`,
|
|
message:
|
|
process.env.NX_ADD_PLUGINS !== 'false' &&
|
|
process.env.NX_ADD_TS_PLUGIN !== 'false'
|
|
? `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: yargs.Arguments<Arguments>
|
|
): Promise<Partial<Arguments>> {
|
|
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: {
|
|
formatter?: 'none' | 'prettier';
|
|
interactive?: boolean;
|
|
},
|
|
opts?: { preferPrettier?: boolean }
|
|
) {
|
|
if (args.formatter) return args.formatter;
|
|
const reply = await enquirer.prompt<{ prettier: 'Yes' | 'No' }>([
|
|
{
|
|
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 || isCI(),
|
|
},
|
|
]);
|
|
return reply.prettier === 'Yes' ? 'prettier' : 'none';
|
|
}
|
|
|
|
async function determineLinterOptions(
|
|
args: { interactive?: boolean },
|
|
opts?: { preferEslint?: boolean }
|
|
) {
|
|
const reply = await enquirer.prompt<{ eslint: 'Yes' | 'No' }>([
|
|
{
|
|
name: 'eslint',
|
|
message: `Would you like to use ESLint?`,
|
|
type: 'autocomplete',
|
|
choices: [
|
|
{
|
|
name: 'Yes',
|
|
},
|
|
{
|
|
name: 'No',
|
|
},
|
|
],
|
|
initial: opts?.preferEslint ? 0 : 1,
|
|
skip: !args.interactive || isCI(),
|
|
},
|
|
]);
|
|
return reply.eslint === 'Yes' ? 'eslint' : 'none';
|
|
}
|
|
|
|
async function determineNoneOptions(
|
|
parsedArgs: yargs.Arguments<NoneArguments>
|
|
): Promise<Partial<NoneArguments>> {
|
|
if (
|
|
(!parsedArgs.preset || parsedArgs.preset === Preset.TS) &&
|
|
process.env.NX_ADD_PLUGINS !== 'false' &&
|
|
process.env.NX_ADD_TS_PLUGIN !== 'false'
|
|
) {
|
|
return {
|
|
preset: Preset.TS,
|
|
formatter: await determineFormatterOptions(parsedArgs),
|
|
};
|
|
} else {
|
|
let preset: Preset;
|
|
let workspaceType:
|
|
| 'package-based'
|
|
| 'standalone'
|
|
| 'integrated'
|
|
| undefined = undefined;
|
|
let appName: string | undefined = undefined;
|
|
let js: boolean | undefined;
|
|
|
|
if (parsedArgs.preset) {
|
|
preset = parsedArgs.preset;
|
|
} else {
|
|
workspaceType = await determinePackageBasedOrIntegratedOrStandalone();
|
|
if (workspaceType === 'standalone') {
|
|
preset = Preset.TsStandalone;
|
|
} else if (workspaceType === 'integrated') {
|
|
preset = Preset.Apps;
|
|
} else {
|
|
preset = Preset.NPM;
|
|
}
|
|
}
|
|
|
|
if (preset === Preset.TS) {
|
|
return { preset, formatter: 'prettier' };
|
|
}
|
|
|
|
if (parsedArgs.js !== undefined) {
|
|
js = parsedArgs.js;
|
|
} else if (preset === Preset.TsStandalone) {
|
|
// Only standalone TS preset generates a default package, so we need to provide --js and --appName options.
|
|
appName = parsedArgs.name;
|
|
const reply = await enquirer.prompt<{ ts: 'Yes' | 'No' }>([
|
|
{
|
|
name: 'ts',
|
|
message: `Would you like to use TypeScript with this project?`,
|
|
type: 'autocomplete',
|
|
choices: [
|
|
{
|
|
name: 'Yes',
|
|
},
|
|
{
|
|
name: 'No',
|
|
},
|
|
],
|
|
initial: 0,
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
},
|
|
]);
|
|
js = reply.ts === 'No';
|
|
}
|
|
|
|
return { preset, js, appName };
|
|
}
|
|
}
|
|
|
|
async function determineReactOptions(
|
|
parsedArgs: yargs.Arguments<ReactArguments>
|
|
): Promise<Partial<ReactArguments>> {
|
|
let preset: Preset;
|
|
let style: undefined | string = undefined;
|
|
let appName: string;
|
|
let bundler: undefined | 'webpack' | 'vite' | 'rspack' = undefined;
|
|
let unitTestRunner: undefined | 'none' | 'jest' | 'vitest' = undefined;
|
|
let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined;
|
|
let nextAppDir = false;
|
|
let nextSrcDir = false;
|
|
let linter: undefined | 'none' | 'eslint';
|
|
let formatter: undefined | 'none' | 'prettier';
|
|
|
|
const workspaces = parsedArgs.workspaces ?? false;
|
|
|
|
if (parsedArgs.preset && parsedArgs.preset !== Preset.React) {
|
|
preset = parsedArgs.preset;
|
|
if (
|
|
preset === Preset.ReactStandalone ||
|
|
preset === Preset.NextJsStandalone ||
|
|
preset === Preset.RemixStandalone
|
|
) {
|
|
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 === 'nextjs') {
|
|
if (isStandalone) {
|
|
preset = Preset.NextJsStandalone;
|
|
} else {
|
|
preset = Preset.NextJs;
|
|
}
|
|
} else if (framework === 'remix') {
|
|
if (isStandalone) {
|
|
preset = Preset.RemixStandalone;
|
|
} else {
|
|
preset = Preset.RemixMonorepo;
|
|
}
|
|
} else if (framework === 'react-native') {
|
|
preset = Preset.ReactNative;
|
|
} else if (framework === 'expo') {
|
|
preset = Preset.Expo;
|
|
} else {
|
|
if (isStandalone) {
|
|
preset = Preset.ReactStandalone;
|
|
} else {
|
|
preset = Preset.ReactMonorepo;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (preset === Preset.ReactStandalone || preset === Preset.ReactMonorepo) {
|
|
bundler = await determineReactBundler(parsedArgs);
|
|
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
|
|
preferVitest: bundler === 'vite',
|
|
});
|
|
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
|
|
} else if (preset === Preset.NextJs || preset === Preset.NextJsStandalone) {
|
|
nextAppDir = await determineNextAppDir(parsedArgs);
|
|
nextSrcDir = await determineNextSrcDir(parsedArgs);
|
|
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
|
|
exclude: 'vitest',
|
|
});
|
|
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
|
|
} else if (
|
|
preset === Preset.RemixMonorepo ||
|
|
preset === Preset.RemixStandalone
|
|
) {
|
|
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
|
|
preferVitest: true,
|
|
});
|
|
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
|
|
} else if (preset === Preset.ReactNative || preset === Preset.Expo) {
|
|
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
|
|
exclude: 'vitest',
|
|
});
|
|
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
|
|
}
|
|
|
|
if (parsedArgs.style) {
|
|
style = parsedArgs.style;
|
|
} else if (
|
|
preset === Preset.ReactStandalone ||
|
|
preset === Preset.ReactMonorepo ||
|
|
preset === Preset.NextJs ||
|
|
preset === Preset.NextJsStandalone
|
|
) {
|
|
const reply = await enquirer.prompt<{ style: string }>([
|
|
{
|
|
name: 'style',
|
|
message: `Default stylesheet format`,
|
|
initial: 0,
|
|
type: 'autocomplete',
|
|
skip: !parsedArgs.interactive || 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,
|
|
linter,
|
|
formatter,
|
|
workspaces,
|
|
};
|
|
}
|
|
|
|
async function determineVueOptions(
|
|
parsedArgs: yargs.Arguments<VueArguments>
|
|
): Promise<Partial<VueArguments>> {
|
|
let preset: Preset;
|
|
let style: undefined | string = undefined;
|
|
let appName: string;
|
|
let unitTestRunner: undefined | 'none' | 'vitest' = undefined;
|
|
let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined;
|
|
let linter: undefined | 'none' | 'eslint';
|
|
let formatter: undefined | 'none' | 'prettier';
|
|
|
|
const workspaces = parsedArgs.workspaces ?? false;
|
|
|
|
if (parsedArgs.preset && parsedArgs.preset !== Preset.Vue) {
|
|
preset = parsedArgs.preset;
|
|
if (preset === Preset.VueStandalone || preset === 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.NuxtStandalone;
|
|
} else {
|
|
preset = Preset.Nuxt;
|
|
}
|
|
} else {
|
|
if (workspaceType === 'standalone') {
|
|
preset = Preset.VueStandalone;
|
|
} else {
|
|
preset = Preset.VueMonorepo;
|
|
}
|
|
}
|
|
}
|
|
|
|
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
|
|
exclude: 'jest',
|
|
});
|
|
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
|
|
|
|
if (parsedArgs.style) {
|
|
style = parsedArgs.style;
|
|
} else {
|
|
const reply = await enquirer.prompt<{ style: string }>([
|
|
{
|
|
name: 'style',
|
|
message: `Default stylesheet format`,
|
|
initial: 0,
|
|
type: 'autocomplete',
|
|
skip: !parsedArgs.interactive || 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: yargs.Arguments<AngularArguments>
|
|
): Promise<Partial<AngularArguments>> {
|
|
let preset: Preset;
|
|
let style: string;
|
|
let appName: string;
|
|
let unitTestRunner: undefined | 'none' | 'jest' | 'vitest' = undefined;
|
|
let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined;
|
|
let bundler: undefined | 'webpack' | 'esbuild' = undefined;
|
|
let ssr: undefined | boolean = undefined;
|
|
let serverRouting: undefined | boolean = undefined;
|
|
|
|
const standaloneApi = parsedArgs.standaloneApi;
|
|
const routing = parsedArgs.routing;
|
|
const prefix = parsedArgs.prefix;
|
|
|
|
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`)) {
|
|
output.error({
|
|
title: `Failed to create a workspace.`,
|
|
bodyLines: [
|
|
`The provided "${prefix}" prefix is invalid. It must be a valid HTML selector.`,
|
|
],
|
|
});
|
|
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (parsedArgs.preset && parsedArgs.preset !== Preset.Angular) {
|
|
preset = parsedArgs.preset;
|
|
|
|
if (preset === Preset.AngularStandalone) {
|
|
appName = parsedArgs.name;
|
|
} else {
|
|
appName = await determineAppName(parsedArgs);
|
|
}
|
|
} else {
|
|
const workspaceType = await determineStandaloneOrMonorepo();
|
|
|
|
if (workspaceType === 'standalone') {
|
|
preset = Preset.AngularStandalone;
|
|
appName = parsedArgs.name;
|
|
} else {
|
|
preset = Preset.AngularMonorepo;
|
|
appName = await determineAppName(parsedArgs);
|
|
}
|
|
}
|
|
|
|
if (parsedArgs.bundler) {
|
|
bundler = parsedArgs.bundler;
|
|
} else {
|
|
const reply = await enquirer.prompt<{ bundler: 'esbuild' | 'webpack' }>([
|
|
{
|
|
name: 'bundler',
|
|
message: `Which bundler would you like to use?`,
|
|
type: 'autocomplete',
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
choices: [
|
|
{
|
|
name: 'esbuild',
|
|
message: 'esbuild [ https://esbuild.github.io/ ]',
|
|
},
|
|
{
|
|
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<{ style: string }>([
|
|
{
|
|
name: 'style',
|
|
message: `Default stylesheet format`,
|
|
initial: 0,
|
|
type: 'autocomplete',
|
|
skip: !parsedArgs.interactive || 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<{ ssr: 'Yes' | 'No' }>([
|
|
{
|
|
name: 'ssr',
|
|
message:
|
|
'Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)?',
|
|
type: 'autocomplete',
|
|
choices: [{ name: 'Yes' }, { name: 'No' }],
|
|
initial: 1,
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
},
|
|
]);
|
|
ssr = reply.ssr === 'Yes';
|
|
}
|
|
|
|
if (parsedArgs.serverRouting !== undefined) {
|
|
serverRouting = parsedArgs.serverRouting;
|
|
} else if (ssr && bundler === 'esbuild') {
|
|
const reply = await enquirer.prompt<{ serverRouting: 'Yes' | 'No' }>([
|
|
{
|
|
name: 'serverRouting',
|
|
message:
|
|
'Would you like to use the Server Routing and App Engine APIs (Developer Preview) for this server application?',
|
|
type: 'autocomplete',
|
|
choices: [{ name: 'Yes' }, { name: 'No' }],
|
|
initial: 1,
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
},
|
|
]);
|
|
serverRouting = reply.serverRouting === 'Yes';
|
|
} else {
|
|
serverRouting = false;
|
|
}
|
|
|
|
unitTestRunner = await determineUnitTestRunner(parsedArgs);
|
|
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
|
|
|
|
return {
|
|
preset,
|
|
style,
|
|
appName,
|
|
standaloneApi,
|
|
routing,
|
|
unitTestRunner,
|
|
e2eTestRunner,
|
|
bundler,
|
|
ssr,
|
|
serverRouting,
|
|
prefix,
|
|
};
|
|
}
|
|
|
|
async function determineNodeOptions(
|
|
parsedArgs: yargs.Arguments<NodeArguments>
|
|
): Promise<Partial<NodeArguments>> {
|
|
let preset: Preset;
|
|
let appName: string;
|
|
let framework: 'express' | 'fastify' | 'koa' | 'nest' | 'none';
|
|
let docker: boolean;
|
|
let linter: undefined | 'none' | 'eslint';
|
|
let formatter: undefined | 'none' | 'prettier';
|
|
let unitTestRunner: undefined | 'none' | 'jest' = undefined;
|
|
const workspaces = parsedArgs.workspaces ?? false;
|
|
|
|
if (parsedArgs.preset) {
|
|
preset = parsedArgs.preset;
|
|
|
|
if (
|
|
preset === Preset.Nest ||
|
|
preset === Preset.Express ||
|
|
preset === Preset.NodeMonorepo
|
|
) {
|
|
appName = await determineAppName(parsedArgs);
|
|
} else {
|
|
appName = parsedArgs.name;
|
|
}
|
|
|
|
if (preset === Preset.NodeStandalone || preset === Preset.NodeMonorepo) {
|
|
framework = await determineNodeFramework(parsedArgs);
|
|
} else {
|
|
framework = 'none';
|
|
}
|
|
} else {
|
|
framework = await determineNodeFramework(parsedArgs);
|
|
|
|
const workspaceType = workspaces
|
|
? 'monorepo'
|
|
: await determineStandaloneOrMonorepo();
|
|
if (workspaceType === 'standalone') {
|
|
preset = Preset.NodeStandalone;
|
|
appName = parsedArgs.name;
|
|
} else {
|
|
preset = Preset.NodeMonorepo;
|
|
appName = await determineAppName(parsedArgs);
|
|
}
|
|
}
|
|
|
|
if (parsedArgs.docker !== undefined) {
|
|
docker = parsedArgs.docker;
|
|
} else {
|
|
const reply = await enquirer.prompt<{ docker: 'Yes' | 'No' }>([
|
|
{
|
|
name: 'docker',
|
|
message:
|
|
'Would you like to generate a Dockerfile? [https://docs.docker.com/]',
|
|
type: 'autocomplete',
|
|
skip: !parsedArgs.interactive || 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(): Promise<
|
|
'package-based' | 'integrated' | 'standalone'
|
|
> {
|
|
const { workspaceType } = await enquirer.prompt<{
|
|
workspaceType: 'standalone' | 'integrated' | 'package-based';
|
|
}>([
|
|
{
|
|
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, {
|
|
title: 'Invalid workspace type',
|
|
bodyLines: [
|
|
`It must be one of the following: standalone, integrated. Got ${workspaceType}`,
|
|
],
|
|
});
|
|
|
|
return workspaceType;
|
|
}
|
|
|
|
async function determineStandaloneOrMonorepo(): Promise<
|
|
'integrated' | 'standalone'
|
|
> {
|
|
const { workspaceType } = await enquirer.prompt<{
|
|
workspaceType: 'standalone' | 'integrated';
|
|
}>([
|
|
{
|
|
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, {
|
|
title: 'Invalid workspace type',
|
|
bodyLines: [
|
|
`It must be one of the following: standalone, integrated. Got ${workspaceType}`,
|
|
],
|
|
});
|
|
|
|
return workspaceType;
|
|
}
|
|
|
|
async function determineAppName(
|
|
parsedArgs: yargs.Arguments<
|
|
ReactArguments | AngularArguments | NodeArguments | VueArguments
|
|
>
|
|
): Promise<string> {
|
|
if (parsedArgs.appName) return parsedArgs.appName;
|
|
|
|
const { appName } = await enquirer.prompt<{ appName: string }>([
|
|
{
|
|
name: 'appName',
|
|
message: `Application name`,
|
|
type: 'input',
|
|
initial: parsedArgs.name,
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
},
|
|
]);
|
|
invariant(appName, {
|
|
title: 'Invalid name',
|
|
bodyLines: [`Name cannot be empty`],
|
|
});
|
|
return appName;
|
|
}
|
|
|
|
async function determineReactFramework(
|
|
parsedArgs: yargs.Arguments<ReactArguments>
|
|
): Promise<'none' | 'nextjs' | 'remix' | 'expo' | 'react-native'> {
|
|
const reply = await enquirer.prompt<{
|
|
framework: 'none' | 'nextjs' | 'remix' | 'expo' | 'react-native';
|
|
}>([
|
|
{
|
|
name: 'framework',
|
|
message: 'What framework would you like to use?',
|
|
type: 'autocomplete',
|
|
choices: [
|
|
{
|
|
name: 'none',
|
|
message: 'None',
|
|
hint: ' I only want react and react-dom',
|
|
},
|
|
{
|
|
name: 'nextjs',
|
|
message: 'Next.js [ https://nextjs.org/ ]',
|
|
},
|
|
{
|
|
name: 'remix',
|
|
message: 'Remix [ https://remix.run/ ]',
|
|
},
|
|
{
|
|
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: yargs.Arguments<ReactArguments>
|
|
): Promise<'webpack' | 'vite' | 'rspack'> {
|
|
if (parsedArgs.bundler) return parsedArgs.bundler;
|
|
const reply = await enquirer.prompt<{
|
|
bundler: 'webpack' | 'vite' | 'rspack';
|
|
}>([
|
|
{
|
|
name: 'bundler',
|
|
message: `Which bundler would you like to use?`,
|
|
type: 'autocomplete',
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
choices: [
|
|
{
|
|
name: 'vite',
|
|
message: 'Vite [ https://vitejs.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: yargs.Arguments<ReactArguments>
|
|
): Promise<boolean> {
|
|
if (parsedArgs.nextAppDir !== undefined) return parsedArgs.nextAppDir;
|
|
const reply = await enquirer.prompt<{ nextAppDir: 'Yes' | 'No' }>([
|
|
{
|
|
name: 'nextAppDir',
|
|
message: 'Would you like to use the App Router (recommended)?',
|
|
type: 'autocomplete',
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
choices: [
|
|
{
|
|
name: 'Yes',
|
|
},
|
|
{
|
|
name: 'No',
|
|
},
|
|
],
|
|
initial: 0,
|
|
},
|
|
]);
|
|
return reply.nextAppDir === 'Yes';
|
|
}
|
|
|
|
async function determineNextSrcDir(
|
|
parsedArgs: yargs.Arguments<ReactArguments>
|
|
): Promise<boolean> {
|
|
if (parsedArgs.nextSrcDir !== undefined) return parsedArgs.nextSrcDir;
|
|
const reply = await enquirer.prompt<{ nextSrcDir: 'Yes' | 'No' }>([
|
|
{
|
|
name: 'nextSrcDir',
|
|
message: 'Would you like to use the src/ directory?',
|
|
type: 'autocomplete',
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
choices: [
|
|
{
|
|
name: 'Yes',
|
|
},
|
|
{
|
|
name: 'No',
|
|
},
|
|
],
|
|
initial: 0,
|
|
},
|
|
]);
|
|
return reply.nextSrcDir === 'Yes';
|
|
}
|
|
|
|
async function determineVueFramework(
|
|
parsedArgs: yargs.Arguments<VueArguments>
|
|
): Promise<'none' | 'nuxt'> {
|
|
if (!!parsedArgs.framework) return parsedArgs.framework;
|
|
const reply = await enquirer.prompt<{
|
|
framework: 'none' | 'nuxt';
|
|
}>([
|
|
{
|
|
name: 'framework',
|
|
message: 'What framework would you like to use?',
|
|
type: 'autocomplete',
|
|
skip: !parsedArgs.interactive || 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: yargs.Arguments<NodeArguments>
|
|
): Promise<'express' | 'fastify' | 'koa' | 'nest' | 'none'> {
|
|
if (!!parsedArgs.framework) return parsedArgs.framework;
|
|
const reply = await enquirer.prompt<{
|
|
framework: 'express' | 'fastify' | 'koa' | 'nest' | 'none';
|
|
}>([
|
|
{
|
|
message: 'What framework should be used?',
|
|
type: 'autocomplete',
|
|
name: 'framework',
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
choices: [
|
|
{
|
|
name: 'none',
|
|
message: 'None',
|
|
},
|
|
{
|
|
name: 'express',
|
|
message: 'Express [ https://expressjs.com/ ]',
|
|
},
|
|
{
|
|
name: 'fastify',
|
|
message: 'Fastify [ https://www.fastify.dev/ ]',
|
|
},
|
|
{
|
|
name: 'koa',
|
|
message: 'Koa [ https://koajs.com/ ]',
|
|
},
|
|
{
|
|
name: 'nest',
|
|
message: 'NestJs [ https://nestjs.com/ ]',
|
|
},
|
|
],
|
|
initial: 0,
|
|
},
|
|
]);
|
|
return reply.framework;
|
|
}
|
|
|
|
async function determineUnitTestRunner<T extends 'none' | 'jest' | 'vitest'>(
|
|
parsedArgs: yargs.Arguments<{
|
|
bundler?: 'vite' | string;
|
|
unitTestRunner?: T;
|
|
workspaces?: boolean;
|
|
}>,
|
|
options?: {
|
|
exclude?: 'jest' | 'vitest';
|
|
preferVitest?: boolean;
|
|
}
|
|
): Promise<T | undefined> {
|
|
if (parsedArgs.unitTestRunner) {
|
|
return parsedArgs.unitTestRunner;
|
|
} else if (!parsedArgs.workspaces) {
|
|
return undefined;
|
|
}
|
|
|
|
const reply = await enquirer.prompt<{
|
|
unitTestRunner: 'none' | 'jest' | 'vitest';
|
|
}>([
|
|
{
|
|
message: 'Which unit test runner would you like to use?',
|
|
type: 'autocomplete',
|
|
name: 'unitTestRunner',
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
choices: [
|
|
{
|
|
name: 'none',
|
|
message: 'None',
|
|
},
|
|
{
|
|
name: 'jest',
|
|
message: 'Jest [ https://jestjs.io/ ]',
|
|
},
|
|
{
|
|
name: 'vitest',
|
|
message: 'Vitest [ https://vitest.dev/ ]',
|
|
},
|
|
]
|
|
.filter((t) => !options?.exclude || options.exclude !== t.name)
|
|
.sort((a, b) => {
|
|
if (a.name === 'none') return 1;
|
|
if (b.name === 'none') return -1;
|
|
if (options?.preferVitest && a.name === 'vitest') return -1;
|
|
if (options?.preferVitest && b.name === 'vitest') return 1;
|
|
return 0;
|
|
}),
|
|
initial: 0, // This should be either vite or jest
|
|
},
|
|
]);
|
|
|
|
return reply.unitTestRunner as T;
|
|
}
|
|
|
|
async function determineE2eTestRunner(
|
|
parsedArgs: yargs.Arguments<{
|
|
e2eTestRunner?: 'none' | 'cypress' | 'playwright';
|
|
}>
|
|
): Promise<'none' | 'cypress' | 'playwright'> {
|
|
if (parsedArgs.e2eTestRunner) return parsedArgs.e2eTestRunner;
|
|
const reply = await enquirer.prompt<{
|
|
e2eTestRunner: 'none' | 'cypress' | 'playwright';
|
|
}>([
|
|
{
|
|
message: 'Test runner to use for end to end (E2E) tests',
|
|
type: 'autocomplete',
|
|
name: 'e2eTestRunner',
|
|
skip: !parsedArgs.interactive || isCI(),
|
|
choices: [
|
|
{
|
|
name: 'playwright',
|
|
message: 'Playwright [ https://playwright.dev/ ]',
|
|
},
|
|
{
|
|
name: 'cypress',
|
|
message: 'Cypress [ https://www.cypress.io/ ]',
|
|
},
|
|
{
|
|
name: 'none',
|
|
message: 'None',
|
|
},
|
|
],
|
|
initial: 0,
|
|
},
|
|
]);
|
|
return reply.e2eTestRunner;
|
|
}
|