fix(misc): create-nx-workspace errors should display properly (#15988)

This commit is contained in:
Craigory Coppola 2023-03-30 18:27:36 -04:00 committed by GitHub
parent d24d850494
commit 5d51ed9be5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 158 additions and 109 deletions

View File

@ -41,7 +41,7 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
'strip-dashed': true,
'dot-notation': true,
})
.command(
.command<Arguments>(
// this is the default and only command
'$0 [name] [options]',
'Create a new Nx workspace',
@ -152,7 +152,7 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
throw error;
});
},
[normalizeArgsMiddleware]
[normalizeArgsMiddleware as yargs.MiddlewareFunction<{}>]
)
.help('help', chalk.dim`Show help`)
.updateLocale(yargsDecorator)
@ -195,7 +195,7 @@ async function normalizeArgsMiddleware(
"Let's create a new workspace [https://nx.dev/getting-started/intro]",
});
let thirdPartyPreset: string;
let thirdPartyPreset: string | null;
try {
thirdPartyPreset = await getThirdPartyPreset(argv.preset);
} catch (e) {
@ -262,7 +262,7 @@ async function normalizeArgsMiddleware(
}
} else {
name = await determineRepoName(argv);
appName = await determineAppName(preset, argv);
appName = await determineAppName(preset as Preset, argv);
if (preset === Preset.ReactMonorepo) {
bundler = await determineBundler(argv);
}
@ -276,7 +276,7 @@ async function normalizeArgsMiddleware(
(argv.interactive ? await determineRouting(argv) : true);
}
}
style = await determineStyle(preset, argv);
style = await determineStyle(preset as Preset, argv);
}
const packageManager = await determinePackageManager(argv);
@ -439,7 +439,7 @@ async function determinePackageManager(
],
},
])
.then((a: { packageManager }) => a.packageManager);
.then((a) => a.packageManager);
}
return Promise.resolve(detectInvokedPackageManager());
@ -453,7 +453,7 @@ async function determineDefaultBase(
}
if (parsedArgs.allPrompts) {
return enquirer
.prompt([
.prompt<{ DefaultBase: string }>([
{
name: 'DefaultBase',
message: `Main branch name `,
@ -461,7 +461,7 @@ async function determineDefaultBase(
type: 'input',
},
])
.then((a: { DefaultBase: string }) => {
.then((a) => {
if (!a.DefaultBase) {
output.error({
title: 'Invalid branch name',
@ -524,14 +524,14 @@ async function determineAppName(
}
return enquirer
.prompt([
.prompt<{ AppName: string }>([
{
name: 'AppName',
message: `Application name `,
type: 'input',
},
])
.then((a: { AppName: string }) => {
.then((a) => {
if (!a.AppName) {
output.error({
title: 'Invalid name',
@ -571,7 +571,7 @@ async function determineFramework(
if (!parsedArgs.framework) {
return enquirer
.prompt([
.prompt<{ framework: Framework }>([
{
message: 'What framework should be used?',
type: 'autocomplete',
@ -579,7 +579,7 @@ async function determineFramework(
choices: frameworkChoices,
},
])
.then((a: { framework: string }) => a.framework);
.then((a) => a.framework);
}
const foundFramework = frameworkChoices
@ -607,7 +607,7 @@ async function determineStandaloneApi(
): Promise<boolean> {
if (parsedArgs.standaloneApi === undefined) {
return enquirer
.prompt([
.prompt<{ standaloneApi: 'Yes' | 'No' }>([
{
name: 'standaloneApi',
message:
@ -625,7 +625,7 @@ async function determineStandaloneApi(
initial: 'No' as any,
},
])
.then((a: { standaloneApi: 'Yes' | 'No' }) => a.standaloneApi === 'Yes');
.then((a) => a.standaloneApi === 'Yes');
}
return parsedArgs.standaloneApi;
@ -636,7 +636,7 @@ async function determineDockerfile(
): Promise<boolean> {
if (parsedArgs.docker === undefined) {
return enquirer
.prompt([
.prompt<{ docker: 'Yes' | 'No' }>([
{
name: 'docker',
message:
@ -654,7 +654,7 @@ async function determineDockerfile(
initial: 'No' as any,
},
])
.then((a: { docker: 'Yes' | 'No' }) => a.docker === 'Yes');
.then((a) => a.docker === 'Yes');
} else {
return Promise.resolve(parsedArgs.docker);
}
@ -663,7 +663,7 @@ async function determineDockerfile(
async function determineStyle(
preset: Preset,
parsedArgs: yargs.Arguments<Arguments>
): Promise<string> {
): Promise<string | null> {
if (
preset === Preset.Apps ||
preset === Preset.Core ||
@ -727,7 +727,7 @@ async function determineStyle(
if (!parsedArgs.style) {
return enquirer
.prompt([
.prompt<{ style: string }>([
{
name: 'style',
message: `Default stylesheet format `,
@ -762,7 +762,7 @@ async function determineRouting(
): Promise<boolean> {
if (!parsedArgs.routing) {
return enquirer
.prompt([
.prompt<{ routing: 'Yes' | 'No' }>([
{
name: 'routing',
message: 'Would you like to add routing?',
@ -779,7 +779,7 @@ async function determineRouting(
initial: 'Yes' as any,
},
])
.then((a: { routing: 'Yes' | 'No' }) => a.routing === 'Yes');
.then((a) => a.routing === 'Yes');
}
return parsedArgs.routing;
@ -801,7 +801,7 @@ async function determineBundler(
if (!parsedArgs.bundler) {
return enquirer
.prompt([
.prompt<{ bundler: Bundler }>([
{
name: 'bundler',
message: `Bundler to be used to build the application`,
@ -810,7 +810,7 @@ async function determineBundler(
choices: choices,
},
])
.then((a: { bundler: 'vite' | 'webpack' }) => a.bundler);
.then((a) => a.bundler);
}
const foundBundler = choices.find(
@ -838,7 +838,7 @@ async function determineNxCloud(
): Promise<boolean> {
if (parsedArgs.nxCloud === undefined) {
return enquirer
.prompt([
.prompt<{ NxCloud: 'Yes' | 'No' }>([
{
name: 'NxCloud',
message: messages.getPromptMessage('nxCloudCreation'),
@ -856,7 +856,7 @@ async function determineNxCloud(
initial: 'Yes' as any,
},
])
.then((a: { NxCloud: 'Yes' | 'No' }) => a.NxCloud === 'Yes');
.then((a) => a.NxCloud === 'Yes');
} else {
return parsedArgs.nxCloud;
}
@ -886,7 +886,7 @@ async function determineCI(
if (parsedArgs.allPrompts) {
return (
enquirer
.prompt([
.prompt<{ CI: string }>([
{
name: 'CI',
message: `CI workflow file to generate? `,

View File

@ -2,13 +2,14 @@ import * as ora from 'ora';
import { join } from 'path';
import { CreateWorkspaceOptions } from './create-workspace-options';
import { execAndWait } from './utils/child-process-utils';
import { mapErrorToBodyLines } from './utils/error-utils';
import { output } from './utils/output';
import {
getPackageManagerCommand,
getPackageManagerVersion,
PackageManager,
} from './utils/package-manager';
import { getFileName, mapErrorToBodyLines } from './utils/string-utils';
import { getFileName } from './utils/string-utils';
import { unparse } from './utils/unparse';
/**
@ -67,10 +68,14 @@ export async function createEmptyWorkspace<T extends CreateWorkspaceOptions>(
);
} catch (e) {
workspaceSetupSpinner.fail();
if (e instanceof Error) {
output.error({
title: `Nx failed to create a workspace.`,
bodyLines: mapErrorToBodyLines(e),
});
} else {
console.error(e);
}
process.exit(1);
} finally {
workspaceSetupSpinner.stop();

View File

@ -10,8 +10,8 @@ import {
} from './utils/package-manager';
import { execAndWait } from './utils/child-process-utils';
import { output } from './utils/output';
import { mapErrorToBodyLines } from './utils/string-utils';
import { nxVersion } from './utils/nx/nx-version';
import { mapErrorToBodyLines } from './utils/error-utils';
/**
* Creates a temporary directory and installs Nx in it.
@ -44,10 +44,14 @@ export async function createSandbox(packageManager: PackageManager) {
installSpinner.succeed();
} catch (e) {
installSpinner.fail();
if (e instanceof Error) {
output.error({
title: `Nx failed to install dependencies`,
bodyLines: mapErrorToBodyLines(e),
});
} else {
console.error(e);
}
process.exit(1);
} finally {
installSpinner.stop();

View File

@ -10,6 +10,7 @@ import { messages, recordStat } from './utils/nx/ab-testing';
import { initializeGitRepo } from './utils/git/git';
import { nxVersion } from './utils/nx/nx-version';
import { getThirdPartyPreset } from './utils/preset/get-third-party-preset';
import { mapErrorToBodyLines } from './utils/error-utils';
export async function createWorkspace<T extends CreateWorkspaceOptions>(
preset: string,
@ -59,23 +60,27 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
name,
ci,
packageManager,
nxCloud && nxCloudInstallRes.code === 0
nxCloud && nxCloudInstallRes?.code === 0
);
}
if (!skipGit) {
try {
await initializeGitRepo(directory, { defaultBase, commit });
} catch (e) {
if (e instanceof Error) {
output.error({
title: 'Could not initialize git repository',
bodyLines: [e.message],
bodyLines: mapErrorToBodyLines(e),
});
} else {
console.error(e);
}
}
}
showNxWarning(name);
if (nxCloud && nxCloudInstallRes.code === 0) {
if (nxCloud && nxCloudInstallRes?.code === 0) {
printNxCloudSuccessMessage(nxCloudInstallRes.stdout);
}

View File

@ -1,6 +1,7 @@
import { spawn, exec } from 'child_process';
import { writeFileSync } from 'fs';
import { join } from 'path';
import { CreateNxWorkspaceError } from './error-utils';
/**
* Use spawn only for interactive shells
@ -24,7 +25,7 @@ export function spawnAndWait(command: string, args: string[], cwd: string) {
}
export function execAndWait(command: string, cwd: string) {
return new Promise((res, rej) => {
return new Promise<{ code: number; stdout: string }>((res, rej) => {
exec(
command,
{ cwd, env: { ...process.env, NX_DAEMON: 'false' } },
@ -32,7 +33,7 @@ export function execAndWait(command: string, cwd: string) {
if (error) {
const logFile = join(cwd, 'error.log');
writeFileSync(logFile, `${stdout}\n${stderr}`);
rej({ code: error.code, logFile, logMessage: stderr });
rej(new CreateNxWorkspaceError(stderr, error.code, logFile));
} else {
res({ code: 0, stdout });
}

View File

@ -2,9 +2,10 @@ import * as ora from 'ora';
import { join } from 'path';
import { execAndWait } from '../child-process-utils';
import { mapErrorToBodyLines } from '../error-utils';
import { output } from '../output';
import { getPackageManagerCommand, PackageManager } from '../package-manager';
import { getFileName, mapErrorToBodyLines } from '../string-utils';
import { getFileName } from '../string-utils';
export async function setupCI(
name: string,
@ -32,11 +33,14 @@ export async function setupCI(
return res;
} catch (e) {
ciSpinner.fail();
if (e instanceof Error) {
output.error({
title: `Nx failed to generate CI workflow`,
bodyLines: mapErrorToBodyLines(e),
});
} else {
console.error(e);
}
process.exit(1);
} finally {

View File

@ -0,0 +1,32 @@
export class CreateNxWorkspaceError extends Error {
constructor(
public logMessage: string,
public code: number | null | undefined,
public logFile: string
) {
super(logMessage);
this.name = 'CreateNxWorkspaceError';
}
}
export function mapErrorToBodyLines(error: Error): string[] {
const errorLines = error.message?.split('\n').filter((line) => !!line);
if (errorLines.length < 3) {
const lines = [`Error: ${error.message}`];
if (process.env.NX_VERBOSE_LOGGING) {
lines.push(`Stack: ${error.stack}`);
}
return lines;
}
const lines =
error instanceof CreateNxWorkspaceError
? [`Exit code: ${error.code}`, `Log file: ${error.logFile}`]
: [];
if (process.env.NX_VERBOSE_LOGGING) {
lines.push(`Error: ${error.message}`);
lines.push(`Stack: ${error.stack}`);
}
return lines;
}

View File

@ -2,10 +2,10 @@ import { execSync, spawn, SpawnOptions } from 'child_process';
import { deduceDefaultBase } from './default-base';
import { output } from '../output';
export function checkGitVersion(): string | null {
export function checkGitVersion(): string | null | undefined {
try {
let gitVersionOutput = execSync('git --version').toString().trim();
return gitVersionOutput.match(/[0-9]+\.[0-9]+\.+[0-9]+/)[0];
return gitVersionOutput.match(/[0-9]+\.[0-9]+\.+[0-9]+/)?.[0];
} catch {
return null;
}
@ -15,7 +15,7 @@ export async function initializeGitRepo(
directory: string,
options: {
defaultBase: string;
commit: { message: string; name: string; email: string };
commit?: { message: string; name: string; email: string };
}
) {
const execute = (args: ReadonlyArray<string>, ignoreErrorStream = false) => {
@ -27,13 +27,13 @@ export async function initializeGitRepo(
cwd: directory,
env: {
...process.env,
...(options.commit.name
...(options.commit?.name
? {
GIT_AUTHOR_NAME: options.commit.name,
GIT_COMMITTER_NAME: options.commit.name,
}
: {}),
...(options.commit.email
...(options.commit?.email
? {
GIT_AUTHOR_EMAIL: options.commit.email,
GIT_COMMITTER_EMAIL: options.commit.email,

View File

@ -1,7 +1,6 @@
import { isCI } from '../ci/is-ci';
export class PromptMessages {
private messages = {
const messageOptions = {
nxCloudCreation: [
{
code: 'set-up-distributed-caching-ci',
@ -14,26 +13,33 @@ export class PromptMessages {
message: `Enable distributed caching to make your CI faster?`,
},
],
};
} as const;
private selectedMessages = {};
type MessageKey = keyof typeof messageOptions;
getPromptMessage(key: string): string {
export class PromptMessages {
private selectedMessages: { [key in MessageKey]?: number } = {};
getPromptMessage(key: MessageKey): string {
if (this.selectedMessages[key] === undefined) {
if (process.env.NX_GENERATE_DOCS_PROCESS === 'true') {
this.selectedMessages[key] = 0;
} else {
this.selectedMessages[key] = Math.floor(
Math.random() * this.messages[key].length
Math.random() * messageOptions[key].length
);
}
}
return this.messages[key][this.selectedMessages[key]].message;
return messageOptions[key][this.selectedMessages[key]!].message;
}
codeOfSelectedPromptMessage(key: string): string {
if (this.selectedMessages[key] === undefined) return null;
return this.messages[key][this.selectedMessages[key]].code;
codeOfSelectedPromptMessage(key: MessageKey): string {
const selected = this.selectedMessages[key];
if (selected === undefined) {
return messageOptions[key][0].code;
} else {
return messageOptions[key][selected].code;
}
}
}

View File

@ -3,7 +3,8 @@ import { join } from 'path';
import { execAndWait } from '../child-process-utils';
import { output } from '../output';
import { getPackageManagerCommand, PackageManager } from '../package-manager';
import { getFileName, mapErrorToBodyLines } from '../string-utils';
import { getFileName } from '../string-utils';
import { mapErrorToBodyLines } from '../error-utils';
export async function setupNxCloud(
name: string,
@ -21,10 +22,14 @@ export async function setupNxCloud(
} catch (e) {
nxCloudSpinner.fail();
if (e instanceof Error) {
output.error({
title: `Nx failed to setup NxCloud`,
bodyLines: mapErrorToBodyLines(e),
});
} else {
console.error(e);
}
process.exit(1);
} finally {

View File

@ -95,10 +95,10 @@ class CLIOutput {
applyNxPrefix(color = 'cyan', text: string): string {
let nxPrefix = '';
if (chalk[color]) {
nxPrefix = `${chalk[color]('>')} ${chalk.reset.inverse.bold[color](
' NX '
)}`;
if ((chalk as any)[color]) {
nxPrefix = `${(chalk as any)[color]('>')} ${(
chalk as any
).reset.inverse.bold[color](' NX ')}`;
} else {
nxPrefix = `${chalk.keyword(color)(
'>'
@ -119,7 +119,9 @@ class CLIOutput {
addVerticalSeparatorWithoutNewLines(color = 'gray') {
this.writeToStdOut(
`${this.X_PADDING}${chalk.dim[color](this.VERTICAL_SEPARATOR)}${EOL}`
`${this.X_PADDING}${(chalk as any).dim[color](
this.VERTICAL_SEPARATOR
)}${EOL}`
);
}
@ -217,7 +219,7 @@ class CLIOutput {
this.writeOutputTitle({
color: 'cyan',
title: color ? chalk[color](title) : title,
title: color ? (chalk as any)[color](title) : title,
});
this.writeOptionalOutputBody(bodyLines);

View File

@ -24,7 +24,7 @@ export async function getThirdPartyPreset(
bodyLines: [
`There was an error with the preset npm package you provided:`,
'',
...validateResult.errors,
...(validateResult.errors ?? []),
],
});
throw new Error('Invalid preset npm package');

View File

@ -8,19 +8,3 @@ export function getFileName(name: string) {
.toLowerCase()
.replace(/[ _]/g, '-');
}
export function mapErrorToBodyLines(error: {
logMessage: string;
code: number;
logFile: string;
}): string[] {
if (error.logMessage?.split('\n').filter((line) => !!line).length < 3) {
// print entire log message only if it's only a single message
return [`Error: ${error.logMessage}`];
}
const lines = [`Exit code: ${error.code}`, `Log file: ${error.logFile}`];
if (process.env.NX_VERBOSE_LOGGING) {
lines.push(`Error: ${error.logMessage}`);
}
return lines;
}

View File

@ -1,9 +1,9 @@
import { flatten } from 'flat';
export function unparse(options: Object): string[] {
const unparsed = [];
const unparsed: string[] = [];
for (const key of Object.keys(options)) {
const value = options[key];
const value = options[key as keyof typeof options];
unparseOption(key, value, unparsed);
}

View File

@ -14,8 +14,8 @@ export interface ValidateNpmResult {
}
export function validateNpmPackage(name: string): ValidateNpmResult {
let warnings = [];
let errors = [];
let warnings: string[] = [];
let errors: string[] = [];
if (name === null) {
errors.push('name cannot be null');

View File

@ -1,7 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node", "jest"]
"types": ["node", "jest"],
"strict": true
},
"include": [],
"files": [],