feat(misc): prompt for unit test runner when creating a workspace using --workspaces flag (#29743)

## Current Behavior

Creating a new workspace does not prompt for the unit test runner.

## Expected Behavior

Creating a new workspace should prompt for the unit test runner.

For now, this new behavior will be behind the `--workspaces` flag.

## Related Issue(s)

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2025-01-24 16:03:57 +01:00 committed by GitHub
parent 86798a286e
commit 8d9234b385
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 153 additions and 17 deletions

View File

@ -44,6 +44,7 @@ Install `create-nx-workspace` globally to invoke the command directly, or use `n
| `--ssr` | boolean | Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application. |
| `--standaloneApi` | boolean | Use Standalone Components if generating an Angular app. (Default: `true`) |
| `--style` | string | Stylesheet type to be used with certain stacks. |
| `--unitTestRunner` | `jest`, `vitest`, `none` | Test runner to use for unit tests. |
| `--useGitHub` | boolean | Will you be using GitHub as your git hosting provider? (Default: `false`) |
| `--version` | boolean | Show version number. |
| `--workspaces` | boolean | Use package manager workspaces. (Default: `false`) |

View File

@ -44,6 +44,7 @@ Install `create-nx-workspace` globally to invoke the command directly, or use `n
| `--ssr` | boolean | Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application. |
| `--standaloneApi` | boolean | Use Standalone Components if generating an Angular app. (Default: `true`) |
| `--style` | string | Stylesheet type to be used with certain stacks. |
| `--unitTestRunner` | `jest`, `vitest`, `none` | Test runner to use for unit tests. |
| `--useGitHub` | boolean | Will you be using GitHub as your git hosting provider? (Default: `false`) |
| `--version` | boolean | Show version number. |
| `--workspaces` | boolean | Use package manager workspaces. (Default: `false`) |

View File

@ -70,6 +70,11 @@
"type": "boolean",
"default": true
},
"unitTestRunner": {
"description": "The tool to use for running unit tests.",
"type": "string",
"enum": ["jest", "vitest", "none"]
},
"e2eTestRunner": {
"description": "The tool to use for running e2e tests.",
"type": "string",

View File

@ -87,6 +87,11 @@
"type": "boolean",
"default": true
},
"unitTestRunner": {
"description": "The tool to use for running unit tests.",
"type": "string",
"enum": ["jest", "vitest", "none"]
},
"e2eTestRunner": {
"description": "The tool to use for running e2e tests.",
"type": "string",

View File

@ -223,7 +223,8 @@ describe('create-nx-workspace', () => {
});
expectNoAngularDevkit();
expectNoTsJestInJestConfig(wsName);
checkFilesExist('vitest.workspace.ts');
checkFilesDoNotExist('jest.config.ts');
const packageJson = readJson('package.json');
expect(packageJson.devDependencies['@nx/vite']).toBeDefined(); // vite should be default bundler
expectCodeIsFormatted();

View File

@ -39,7 +39,6 @@ export async function normalizeOptions(
inlineTemplate: false,
skipTests: options.unitTestRunner === UnitTestRunner.None,
skipFormat: false,
unitTestRunner: UnitTestRunner.Jest,
e2eTestRunner: E2eTestRunner.Playwright,
linter: Linter.EsLint,
strict: true,
@ -59,5 +58,6 @@ export async function normalizeOptions(
!options.rootProject ? appProjectRoot : appProjectName
),
ssr: options.ssr ?? false,
unitTestRunner: options.unitTestRunner ?? UnitTestRunner.Jest,
};
}

View File

@ -53,6 +53,7 @@ interface ReactArguments extends BaseArguments {
bundler: 'webpack' | 'vite' | 'rspack';
nextAppDir: boolean;
nextSrcDir: boolean;
unitTestRunner: 'none' | 'jest' | 'vitest';
e2eTestRunner: 'none' | 'cypress' | 'playwright';
}
@ -63,6 +64,7 @@ interface AngularArguments extends BaseArguments {
style: string;
routing: boolean;
standaloneApi: boolean;
unitTestRunner: 'none' | 'jest' | 'vitest';
e2eTestRunner: 'none' | 'cypress' | 'playwright';
bundler: 'webpack' | 'esbuild';
ssr: boolean;
@ -76,6 +78,7 @@ interface VueArguments extends BaseArguments {
appName: string;
framework: 'none' | 'nuxt';
style: string;
unitTestRunner: 'none' | 'vitest';
e2eTestRunner: 'none' | 'cypress' | 'playwright';
}
@ -83,8 +86,9 @@ interface NodeArguments extends BaseArguments {
stack: 'node';
workspaceType: 'standalone' | 'integrated';
appName: string;
framework: 'express' | 'fastify' | 'koa' | 'nest';
framework: 'none' | 'express' | 'fastify' | 'koa' | 'nest';
docker: boolean;
unitTestRunner: 'none' | 'jest';
}
interface UnknownStackArguments extends BaseArguments {
@ -190,6 +194,11 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
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',
@ -568,11 +577,12 @@ async function determineNoneOptions(
async function determineReactOptions(
parsedArgs: yargs.Arguments<ReactArguments>
): Promise<Partial<Arguments>> {
): 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;
@ -633,17 +643,29 @@ async function determineReactOptions(
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 ||
preset === Preset.ReactNative ||
preset === Preset.Expo
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);
}
@ -715,6 +737,7 @@ async function determineReactOptions(
bundler,
nextAppDir,
nextSrcDir,
unitTestRunner,
e2eTestRunner,
linter,
formatter,
@ -724,10 +747,11 @@ async function determineReactOptions(
async function determineVueOptions(
parsedArgs: yargs.Arguments<VueArguments>
): Promise<Partial<Arguments>> {
): 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';
@ -768,6 +792,9 @@ async function determineVueOptions(
}
}
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
exclude: 'jest',
});
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
if (parsedArgs.style) {
@ -815,6 +842,7 @@ async function determineVueOptions(
preset,
style,
appName,
unitTestRunner,
e2eTestRunner,
linter,
formatter,
@ -824,10 +852,11 @@ async function determineVueOptions(
async function determineAngularOptions(
parsedArgs: yargs.Arguments<AngularArguments>
): Promise<Partial<Arguments>> {
): 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;
@ -965,6 +994,7 @@ async function determineAngularOptions(
serverRouting = false;
}
unitTestRunner = await determineUnitTestRunner(parsedArgs);
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
return {
@ -973,6 +1003,7 @@ async function determineAngularOptions(
appName,
standaloneApi,
routing,
unitTestRunner,
e2eTestRunner,
bundler,
ssr,
@ -983,14 +1014,14 @@ async function determineAngularOptions(
async function determineNodeOptions(
parsedArgs: yargs.Arguments<NodeArguments>
): Promise<Partial<Arguments>> {
): 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) {
@ -1051,6 +1082,10 @@ async function determineNodeOptions(
docker = reply.docker === 'Yes';
}
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
exclude: 'vitest',
});
if (workspaces) {
linter = await determineLinterOptions(parsedArgs);
formatter = await determineFormatterOptions(parsedArgs);
@ -1067,6 +1102,7 @@ async function determineNodeOptions(
linter,
formatter,
workspaces,
unitTestRunner,
};
}
@ -1358,6 +1394,60 @@ async function determineNodeFramework(
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,
},
]);
return reply.unitTestRunner as T;
}
async function determineE2eTestRunner(
parsedArgs: yargs.Arguments<{
e2eTestRunner?: 'none' | 'cypress' | 'playwright';

View File

@ -88,6 +88,9 @@ export function generatePreset(host: Tree, opts: NormalizedSchema) {
: null,
parsedArgs.interactive ? '--interactive=true' : '--interactive=false',
opts.routing !== undefined ? `--routing=${opts.routing}` : null,
opts.unitTestRunner !== undefined
? `--unitTestRunner=${opts.unitTestRunner}`
: null,
opts.e2eTestRunner !== undefined
? `--e2eTestRunner=${opts.e2eTestRunner}`
: null,

View File

@ -32,6 +32,7 @@ interface Schema {
standaloneApi?: boolean;
routing?: boolean;
packageManager?: PackageManager;
unitTestRunner?: 'jest' | 'vitest' | 'none';
e2eTestRunner?: 'cypress' | 'playwright' | 'detox' | 'jest' | 'none';
ssr?: boolean;
serverRouting?: boolean;

View File

@ -73,6 +73,11 @@
"type": "boolean",
"default": true
},
"unitTestRunner": {
"description": "The tool to use for running unit tests.",
"type": "string",
"enum": ["jest", "vitest", "none"]
},
"e2eTestRunner": {
"description": "The tool to use for running e2e tests.",
"type": "string",

View File

@ -34,6 +34,7 @@ async function createPreset(tree: Tree, options: Schema) {
standalone: options.standaloneApi,
routing: options.routing,
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
unitTestRunner: options.unitTestRunner,
bundler: options.bundler,
ssr: options.ssr,
serverRouting: options.serverRouting,
@ -54,6 +55,7 @@ async function createPreset(tree: Tree, options: Schema) {
rootProject: true,
standalone: options.standaloneApi,
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
unitTestRunner: options.unitTestRunner,
bundler: options.bundler,
ssr: options.ssr,
serverRouting: options.serverRouting,
@ -71,6 +73,9 @@ async function createPreset(tree: Tree, options: Schema) {
linter: options.linter,
bundler: options.bundler ?? 'webpack',
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
unitTestRunner:
options.unitTestRunner ??
(options.bundler === 'vite' ? 'vitest' : 'jest'),
addPlugin,
nxCloudToken: options.nxCloudToken,
useTsSolution: options.workspaces,
@ -80,15 +85,18 @@ async function createPreset(tree: Tree, options: Schema) {
const { applicationGenerator: reactApplicationGenerator } = require('@nx' +
'/react');
const bundler = options.bundler ?? 'vite';
return reactApplicationGenerator(tree, {
name: options.name,
directory: '.',
style: options.style,
linter: options.linter,
rootProject: true,
bundler: options.bundler ?? 'vite',
bundler,
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
unitTestRunner: options.bundler === 'vite' ? 'vitest' : 'jest',
unitTestRunner:
options.unitTestRunner ?? (bundler === 'vite' ? 'vitest' : 'jest'),
addPlugin,
nxCloudToken: options.nxCloudToken,
formatter: options.formatter,
@ -102,7 +110,7 @@ async function createPreset(tree: Tree, options: Schema) {
directory: join('apps', options.name),
linter: options.linter,
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
unitTestRunner: 'vitest',
unitTestRunner: options.unitTestRunner ?? 'vitest',
addPlugin,
nxCloudToken: options.nxCloudToken,
useTsSolution: options.workspaces,
@ -118,7 +126,7 @@ async function createPreset(tree: Tree, options: Schema) {
linter: options.linter,
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
rootProject: true,
unitTestRunner: 'vitest',
unitTestRunner: options.unitTestRunner ?? 'vitest',
addPlugin,
nxCloudToken: options.nxCloudToken,
formatter: options.formatter,
@ -133,6 +141,7 @@ async function createPreset(tree: Tree, options: Schema) {
style: options.style,
linter: options.linter,
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
unitTestRunner: options.unitTestRunner,
addPlugin,
nxCloudToken: options.nxCloudToken,
useTsSolution: options.workspaces,
@ -149,7 +158,7 @@ async function createPreset(tree: Tree, options: Schema) {
linter: options.linter,
rootProject: true,
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
unitTestRunner: 'vitest',
unitTestRunner: options.unitTestRunner ?? 'vitest',
addPlugin,
nxCloudToken: options.nxCloudToken,
});
@ -163,6 +172,7 @@ async function createPreset(tree: Tree, options: Schema) {
style: options.style,
linter: options.linter,
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
unitTestRunner: options.unitTestRunner,
addPlugin,
nxCloudToken: options.nxCloudToken,
useTsSolution: options.workspaces,
@ -179,7 +189,7 @@ async function createPreset(tree: Tree, options: Schema) {
linter: options.linter,
rootProject: true,
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
unitTestRunner: 'vitest',
unitTestRunner: options.unitTestRunner ?? 'vitest',
addPlugin,
nxCloudToken: options.nxCloudToken,
});
@ -195,6 +205,7 @@ async function createPreset(tree: Tree, options: Schema) {
appDir: options.nextAppDir,
src: options.nextSrcDir,
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
unitTestRunner: options.unitTestRunner,
addPlugin,
useTsSolution: options.workspaces,
formatter: options.formatter,
@ -210,6 +221,7 @@ async function createPreset(tree: Tree, options: Schema) {
appDir: options.nextAppDir,
src: options.nextSrcDir,
e2eTestRunner: options.e2eTestRunner ?? 'playwright',
unitTestRunner: options.unitTestRunner,
rootProject: true,
addPlugin,
formatter: options.formatter,
@ -237,6 +249,7 @@ async function createPreset(tree: Tree, options: Schema) {
directory: join('apps', options.name),
linter: options.linter,
e2eTestRunner: options.e2eTestRunner ?? 'jest',
unitTestRunner: options.unitTestRunner,
addPlugin,
useTsSolution: options.workspaces,
formatter: options.formatter,
@ -250,6 +263,7 @@ async function createPreset(tree: Tree, options: Schema) {
directory: join('apps', options.name),
linter: options.linter,
e2eTestRunner: options.e2eTestRunner ?? 'jest',
unitTestRunner: options.unitTestRunner,
addPlugin,
useTsSolution: options.workspaces,
formatter: options.formatter,
@ -262,6 +276,7 @@ async function createPreset(tree: Tree, options: Schema) {
directory: join('apps', options.name),
linter: options.linter,
e2eTestRunner: options.e2eTestRunner ?? 'detox',
unitTestRunner: options.unitTestRunner,
addPlugin,
nxCloudToken: options.nxCloudToken,
bundler: options.bundler ?? 'webpack',
@ -275,6 +290,7 @@ async function createPreset(tree: Tree, options: Schema) {
directory: join('apps', options.name),
linter: options.linter,
e2eTestRunner: options.e2eTestRunner ?? 'detox',
unitTestRunner: options.unitTestRunner,
addPlugin,
nxCloudToken: options.nxCloudToken,
useTsSolution: options.workspaces,
@ -314,6 +330,7 @@ async function createPreset(tree: Tree, options: Schema) {
docker: options.docker,
rootProject: true,
e2eTestRunner: options.e2eTestRunner ?? 'jest',
unitTestRunner: options.unitTestRunner,
addPlugin,
});
} else if (options.preset === Preset.NodeMonorepo) {
@ -329,6 +346,7 @@ async function createPreset(tree: Tree, options: Schema) {
docker: options.docker,
rootProject: false,
e2eTestRunner: options.e2eTestRunner ?? 'jest',
unitTestRunner: options.unitTestRunner,
addPlugin,
useTsSolution: options.workspaces,
formatter: options.formatter,

View File

@ -17,6 +17,7 @@ export interface Schema {
nextSrcDir?: boolean;
routing?: boolean;
standaloneApi?: boolean;
unitTestRunner?: 'jest' | 'vitest' | 'none';
e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none';
js?: boolean;
ssr?: boolean;

View File

@ -90,6 +90,11 @@
"type": "boolean",
"default": true
},
"unitTestRunner": {
"description": "The tool to use for running unit tests.",
"type": "string",
"enum": ["jest", "vitest", "none"]
},
"e2eTestRunner": {
"description": "The tool to use for running e2e tests.",
"type": "string",