Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mops watch #254

Merged
merged 19 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/main/registry/getDefaultPackages.mo
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ module {
case ("0.20.2") [("base", "0.11.1")];
case ("0.21.0") [("base", "0.11.1")];
case ("0.22.0") [("base", "0.11.2")];
case ("0.23.0") [("base", "0.11.2")];
case (_) {
switch (registry.getHighestVersion("base")) {
case (?ver) [("base", ver)];
Expand Down
15 changes: 15 additions & 0 deletions cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {toolchain} from './commands/toolchain/index.js';
import {Tool} from './types.js';
import * as self from './commands/self.js';
import {resolvePackages} from './resolve-packages.js';
import {watch} from './commands/watch/watch.js';

declare global {
// eslint-disable-next-line no-var
Expand Down Expand Up @@ -392,4 +393,18 @@ selfCommand

program.addCommand(selfCommand);

// watch
program
.command('watch')
.description('Watch *.mo files and check for syntax errors, warnings, run tests, generate declarations and deploy canisters')
.option('-e, --error', 'Check Motoko canisters or *.mo files for syntax errors')
.option('-w, --warning', 'Check Motoko canisters or *.mo files for warnings')
.option('-t, --test', 'Run tests')
.option('-g, --generate', 'Generate declarations for Motoko canisters')
.option('-d, --deploy', 'Deploy Motoko canisters')
.action(async (options) => {
checkConfigFile(true);
await watch(options);
});

program.parse();
4 changes: 4 additions & 0 deletions cli/commands/test/mmf1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export class MMF1 {
this.output = [];
}

getErrorMessages() {
return this.output.filter(out => out.type === 'fail').map(out => out.message);
}

parseLine(line : string) {
if (line.startsWith('mops:1:start ')) {
this._testStart(line.split('mops:1:start ')[1] || '');
Expand Down
26 changes: 22 additions & 4 deletions cli/commands/test/reporters/silent-reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@ import {Reporter} from './reporter.js';
import {TestMode} from '../../../types.js';

export class SilentReporter implements Reporter {
total = 0;
passed = 0;
failed = 0;
skipped = 0;
passedFiles = 0;
failedFiles = 0;
passedNamesFlat : string[] = [];
flushOnError = true;
errorOutput = '';
onProgress = () => {};
Comment on lines +15 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor to Avoid Redundant Default Values

The properties flushOnError and onProgress are assigned default values both in their declarations and in the constructor parameters. This redundancy can be eliminated to improve code clarity.

Suggested Refactor:

Option 1—Remove default values from property declarations:

- flushOnError = true;
- errorOutput = '';
- onProgress = () => {};

constructor(flushOnError = true, onProgress = () => {}) {
	this.flushOnError = flushOnError;
	this.onProgress = onProgress;
+	this.errorOutput = '';
}

Option 2—Keep defaults in property declarations and adjust constructor parameters:

constructor(flushOnError?: boolean, onProgress?: () => void) {
	if (flushOnError !== undefined) {
		this.flushOnError = flushOnError;
	}
	if (onProgress !== undefined) {
		this.onProgress = onProgress;
	}
}

Also applies to: 19-22


addFiles(_files : string[]) {}
constructor(flushOnError = true, onProgress = () => {}) {
this.flushOnError = flushOnError;
this.onProgress = onProgress;
}

addFiles(files : string[]) {
this.total = files.length;
}

addRun(file : string, mmf : MMF1, state : Promise<void>, _mode : TestMode) {
state.then(() => {
Expand All @@ -30,10 +41,17 @@ export class SilentReporter implements Reporter {
this.failedFiles += Number(mmf.failed !== 0);

if (mmf.failed) {
console.log(chalk.red('✖'), absToRel(file));
mmf.flush('fail');
console.log('-'.repeat(50));
let output = `${chalk.red('✖'), absToRel(file)}\n${mmf.getErrorMessages().join('\n')}\n${'-'.repeat(50)}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix Incorrect Use of Comma Operator in Template Literal

At line 44, the use of the comma operator within the template literal results in only the last expression being included. This means chalk.red('✖') is not output as intended.

Suggested Fix:

Replace the comma with a + operator or properly format the template literal:

-let output = `${chalk.red('✖'), absToRel(file)}\n${mmf.getErrorMessages().join('\n')}\n${'-'.repeat(50)}`;
+let output = `${chalk.red('✖')} ${absToRel(file)}\n${mmf.getErrorMessages().join('\n')}\n${'-'.repeat(50)}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let output = `${chalk.red('✖'), absToRel(file)}\n${mmf.getErrorMessages().join('\n')}\n${'-'.repeat(50)}`;
let output = `${chalk.red('✖')} ${absToRel(file)}\n${mmf.getErrorMessages().join('\n')}\n${'-'.repeat(50)}`;


if (this.flushOnError) {
console.log(output);
}
else {
this.errorOutput = `${this.errorOutput}\n${output}`.trim();
}
Comment on lines +46 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor Error Output Accumulation for Efficiency

Accumulating error messages using string concatenation may become inefficient with a large number of errors. Consider using an array to collect error messages and join them when needed.

Apply this refactor to improve performance and maintainability:

-        else {
-          this.errorOutput = `${this.errorOutput}\n${output}`.trim();
-        }
+        else {
+          if (!this.errorOutputArray) {
+            this.errorOutputArray = [];
+          }
+          this.errorOutputArray.push(output);
+        }

Then, when you need to access the accumulated error output:

getErrorOutput() {
  return this.errorOutputArray.join('\n');
}

}

this.onProgress();
});
}

Expand Down
18 changes: 17 additions & 1 deletion cli/commands/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ async function runAll(reporterName : ReporterName | undefined, filter = '', mode
return done;
}

export async function testWithReporter(reporterName : ReporterName | Reporter | undefined, filter = '', defaultMode : TestMode = 'interpreter', replicaType : ReplicaName, watch = false) : Promise<boolean> {
export async function testWithReporter(reporterName : ReporterName | Reporter | undefined, filter = '', defaultMode : TestMode = 'interpreter', replicaType : ReplicaName, watch = false, signal ?: AbortSignal) : Promise<boolean> {
let rootDir = getRootDir();
let files : string[] = [];
let libFiles = globSync('**/test?(s)/lib.mo', globConfig);
Expand Down Expand Up @@ -274,6 +274,10 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |

await startReplicaOnce(replica, replicaType);

if (signal?.aborted) {
return;
}
Comment on lines +320 to +322
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure child processes are terminated when signal is aborted

While checking signal?.aborted and returning early prevents further code execution, any spawned child processes (e.g., buildProc, proc) will continue to run in the background. This could lead to unwanted resource consumption or conflicts.

To address this, consider adding an event listener to the signal that terminates any running child processes when the signal is aborted. Here's how you might modify the code:

 // Example for handling buildProc in Wasi mode
 let buildProc = spawn(mocPath, [`-o=${wasmFile}`, '-wasi-system-api', ...mocArgs]);

+ if (signal) {
+   signal.addEventListener('abort', () => {
+     buildProc.kill();
+   });
+ }

 pipeMMF(buildProc, mmf).then(async () => {
   // existing code...

Repeat similar logic for other child processes like proc in the interpreter and replica modes. This ensures that all subprocesses are properly terminated when an abort signal is received.

Also applies to: 289-291, 309-311, 330-332


let canisterName = path.parse(file).name;
let idlFactory = ({IDL} : any) => {
return IDL.Service({'runTests': IDL.Func([], [], [])});
Expand All @@ -282,6 +286,10 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |

let {stream} = await replica.deploy(canisterName, wasmFile, idlFactory);

if (signal?.aborted) {
return;
}

pipeStdoutToMMF(stream, mmf);

let actor = await replica.getActor(canisterName) as _SERVICE;
Expand All @@ -298,6 +306,10 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
});
}

if (signal?.aborted) {
return;
}

globalThis.mopsReplicaTestRunning = true;
await actor.runTests();
globalThis.mopsReplicaTestRunning = false;
Expand All @@ -315,6 +327,10 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
}
});

if (signal?.aborted) {
return;
}

reporter.addRun(file, mmf, promise, mode);

await promise;
Expand Down
155 changes: 155 additions & 0 deletions cli/commands/watch/deployer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import chalk from 'chalk';
import os from 'node:os';
import {promisify} from 'node:util';
import {exec, execSync} from 'node:child_process';

import {ErrorChecker} from './error-checker.js';
import {Generator} from './generator.js';
import {parallel} from '../../parallel.js';
import {getRootDir} from '../../mops.js';

export class Deployer {
verbose = false;
canisters : Record<string, string> = {};
status : 'pending' | 'running' | 'syntax-error' | 'dfx-error' | 'error' | 'success' = 'pending';
errorChecker : ErrorChecker;
generator : Generator;
success = 0;
errors : string[] = [];
aborted = false;
controllers = new Map<string, AbortController>();
currentRun : Promise<any> | undefined;

constructor({verbose, canisters, errorChecker, generator} : {verbose : boolean, canisters : Record<string, string>, errorChecker : ErrorChecker, generator : Generator}) {
this.verbose = verbose;
this.canisters = canisters;
this.errorChecker = errorChecker;
this.generator = generator;
}

reset() {
this.status = 'pending';
this.success = 0;
this.errors = [];
}

async abortCurrent() {
this.aborted = true;
for (let controller of this.controllers.values()) {
controller.abort();
}
this.controllers.clear();
await this.currentRun;
this.reset();
this.aborted = false;
}

async run(onProgress : () => void) {
await this.abortCurrent();

if (this.errorChecker.status === 'error') {
this.status = 'syntax-error';
onProgress();
return;
}

if (Object.keys(this.canisters).length === 0) {
this.status = 'success';
onProgress();
return;
}

let rootDir = getRootDir();

try {
execSync('dfx ping', {cwd: rootDir});
}
catch (error) {
this.status = 'dfx-error';
onProgress();
return;
}
Comment on lines +47 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using an asynchronous approach for the DFX check.

The initial checks and verifications in the run method are thorough. However, the DFX check uses execSync, which could potentially block the event loop. Consider using an asynchronous approach, such as promisify(execFile), to maintain consistency with the rest of the method and avoid potential performance issues.

Example:

try {
  await promisify(execFile)('dfx', ['ping'], { cwd: rootDir });
} catch (error) {
  this.status = 'dfx-error';
  onProgress();
  return;
}

Comment on lines +64 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using an asynchronous approach for the DFX check.

The current implementation uses execSync for the DFX check, which could potentially block the event loop. Consider using an asynchronous approach to maintain consistency with the rest of the method and avoid potential performance issues.

Example:

try {
  await promisify(execFile)('dfx', ['ping'], { cwd: rootDir });
} catch (error) {
  this.status = 'dfx-error';
  onProgress();
  return;
}


this.status = 'running';
onProgress();

// create canisters (sequentially to avoid DFX errors)
let resolve : (() => void) | undefined;
this.currentRun = new Promise<void>((res) => resolve = res);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid assignment within expressions for improved clarity.

Assigning a variable within an expression can make the code less readable and harder to maintain. It's recommended to perform assignments outside of expressions.

Apply this diff to make the assignment explicit:

- this.currentRun = new Promise<void>((res) => resolve = res);
+ this.currentRun = new Promise<void>((res) => {
+   resolve = res;
+ });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.currentRun = new Promise<void>((res) => resolve = res);
this.currentRun = new Promise<void>((res) => {
resolve = res;
});
🧰 Tools
🪛 Biome

[error] 78-78: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

Comment on lines +76 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid assignment within expressions for improved clarity.

The assignment within the Promise constructor can make the code less readable. Consider separating the assignment for better clarity.

Apply this diff to make the assignment explicit:

- this.currentRun = new Promise<void>((res) => resolve = res);
+ this.currentRun = new Promise<void>((res) => {
+   resolve = res;
+ });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// create canisters (sequentially to avoid DFX errors)
let resolve : (() => void) | undefined;
this.currentRun = new Promise<void>((res) => resolve = res);
// create canisters (sequentially to avoid DFX errors)
let resolve : (() => void) | undefined;
this.currentRun = new Promise<void>((res) => {
resolve = res;
});
🧰 Tools
🪛 Biome

[error] 78-78: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

for (let canister of Object.keys(this.canisters)) {
let controller = new AbortController();
let {signal} = controller;
this.controllers.set(canister, controller);

await promisify(exec)(`dfx canister create ${canister}`, {cwd: rootDir, signal}).catch((error) => {
if (error.code === 'ABORT_ERR') {
return {stderr: ''};
}
throw error;
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Sanitize shell command inputs to prevent command injection vulnerabilities.

Interpolating variables directly into shell commands can expose the application to command injection attacks if the variables contain malicious input. Ensure that the canister variable is properly validated or use safer methods to execute commands.

Consider using execFile to pass command arguments as an array, which avoids shell interpretation:

For the dfx canister create command:

- await promisify(exec)(`dfx canister create ${canister}`, {cwd: rootDir, signal}).catch((error) => {
+ await promisify(execFile)('dfx', ['canister', 'create', canister], {cwd: rootDir, signal}).catch((error) => {

For the dfx build command:

- await promisify(exec)(`dfx build ${canister}`, {cwd: rootDir, signal}).catch((error) => {
+ await promisify(execFile)('dfx', ['build', canister], {cwd: rootDir, signal}).catch((error) => {

For the dfx canister install command:

- await promisify(exec)(`dfx canister install --mode=auto ${canister}`, {cwd: rootDir, signal}).catch((error) => {
+ await promisify(execFile)('dfx', ['canister', 'install', '--mode=auto', canister], {cwd: rootDir, signal}).catch((error) => {

Also applies to: 107-113, 116-121


this.controllers.delete(canister);
}

resolve?.();

if (this.aborted) {
return;
}

this.currentRun = parallel(os.cpus().length, [...Object.keys(this.canisters)], async (canister) => {
let controller = new AbortController();
let {signal} = controller;
this.controllers.set(canister, controller);

// build
if (this.generator.status !== 'success') {
await promisify(exec)(`dfx build ${canister}`, {cwd: rootDir, signal}).catch((error) => {
if (error.code === 'ABORT_ERR') {
return {stderr: ''};
}
throw error;
});
}

// install
await promisify(exec)(`dfx canister install --mode=auto ${canister}`, {cwd: rootDir, signal}).catch((error) => {
if (error.code === 'ABORT_ERR') {
return {stderr: ''};
}
throw error;
});

this.success += 1;
this.controllers.delete(canister);
onProgress();
});

await this.currentRun;

if (!this.aborted) {
this.status = 'success';
}
onProgress();
}

getOutput() : string {
let get = (v : number) => v.toString();
let count = (this.status === 'running' ? get : chalk.bold[this.errors.length > 0 ? 'redBright' : 'green'])(this.errors.length || this.success);

if (this.status === 'pending') {
return `Deploy: ${chalk.gray('(pending)')}`;
}
if (this.status === 'running') {
return `Deploy: ${count}/${Object.keys(this.canisters).length} ${chalk.gray('(running)')}`;
}
if (this.status === 'syntax-error') {
return `Deploy: ${chalk.gray('(errors)')}`;
}
if (this.status === 'dfx-error') {
return `Deploy: ${chalk.gray('(dfx not running)')}`;
}

return `Deploy: ${count}`;
}
}
87 changes: 87 additions & 0 deletions cli/commands/watch/error-checker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {exec} from 'node:child_process';
import {promisify} from 'node:util';
import os from 'node:os';
import chalk from 'chalk';

import {getMocPath} from '../../helpers/get-moc-path.js';
import {getRootDir} from '../../mops.js';
import {sources} from '../sources.js';
import {parallel} from '../../parallel.js';
import {globMoFiles} from './globMoFiles.js';

export class ErrorChecker {
verbose = false;
canisters : Record<string, string> = {};
status : 'pending' | 'running' | 'error' | 'success' = 'pending';
errors : string[] = [];

constructor({verbose, canisters} : {verbose : boolean, canisters : Record<string, string>}) {
this.verbose = verbose;
this.canisters = canisters;
}

reset() {
this.status = 'pending';
this.errors = [];
}

async run(onProgress : () => void) {
this.reset();

this.status = 'running';
onProgress();

let rootDir = getRootDir();
let mocPath = getMocPath();
let deps = await sources({cwd: rootDir});

let paths = [...Object.values(this.canisters)];
if (!paths.length) {
paths = globMoFiles(rootDir);
}

await parallel(os.cpus().length, paths, async (file) => {
try {
await promisify(exec)(`${mocPath} --check ${deps.join(' ')} ${file}`, {cwd: rootDir});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential command injection risk when using exec with unescaped variables.

Using exec() with a command string that includes variables (mocPath, deps, file) can lead to command injection vulnerabilities if any of these variables contain unexpected or malicious input. To enhance security and prevent shell interpretation of the arguments, consider using execFile() and passing the command and its arguments as an array.

Apply this diff to implement the change:

- import {exec} from 'node:child_process';
+ import {execFile} from 'node:child_process';

...

- await promisify(exec)(`${mocPath} --check ${deps.join(' ')} ${file}`, {cwd: rootDir});
+ await promisify(execFile)(mocPath, ['--check', ...deps, file], {cwd: rootDir});

Committable suggestion was skipped due to low confidence.

}
catch (error : any) {
error.message.split('\n').forEach((line : string) => {
if (line.match(/: \w+ error \[/)) {
// better formatting
let str = line
.replace(/: (\w+ error) \[/, (_, m1) => `: ${chalk.red(m1)} [`)
.replace(/unbound type (\w+)/, `unbound type ${chalk.bold('$1')}`)
.replace(/unbound variable (\w+)/, `unbound variable ${chalk.bold('$1')}`)
Comment on lines +53 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect usage of captured groups in String.replace methods.

In the replace calls, the captured groups ($1) are not being substituted correctly. Using template literals with ${chalk.bold('$1')} inserts the literal string '$1' instead of the captured value. To fix this, use a replacer function as the second argument of replace to access the captured groups properly.

Apply this diff to fix the issue:

- .replace(/unbound type (\w+)/, `unbound type ${chalk.bold('$1')}`)
- .replace(/unbound variable (\w+)/, `unbound variable ${chalk.bold('$1')}`)
+ .replace(/unbound type (\w+)/, (_, m1) => `unbound type ${chalk.bold(m1)}`)
+ .replace(/unbound variable (\w+)/, (_, m1) => `unbound variable ${chalk.bold(m1)}`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.replace(/unbound type (\w+)/, `unbound type ${chalk.bold('$1')}`)
.replace(/unbound variable (\w+)/, `unbound variable ${chalk.bold('$1')}`)
.replace(/unbound type (\w+)/, (_, m1) => `unbound type ${chalk.bold(m1)}`)
.replace(/unbound variable (\w+)/, (_, m1) => `unbound variable ${chalk.bold(m1)}`)

.trim();
this.errors.push(str);
}
else if (line.startsWith(' ') && this.errors.length) {
this.errors[this.errors.length - 1] += '\n ' + line;
}
else {
// console.log('UNKNOWN ERROR', line);
}
Comment on lines +62 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Handle unknown error lines or remove commented code for cleanliness.

The else block contains commented-out code, which can clutter the codebase. It's advisable to either handle the unknown error lines appropriately or remove the commented code to maintain code clarity.

Consider updating the code as follows:

- // console.log('UNKNOWN ERROR', line);
+ // Optionally handle or log unknown error lines here

Or remove the commented code entirely if it's not needed.

Committable suggestion was skipped due to low confidence.

});
onProgress();
}
});

this.status = this.errors.length ? 'error' : 'success';
onProgress();
}

getOutput() : string {
if (this.status === 'pending') {
return `Errors: ${chalk.gray('(pending)')}`;
}
if (this.status === 'running') {
return `Errors: ${chalk.gray('(running)')}`;
}
let output = '';
output += `Errors: ${chalk.bold[this.errors.length ? 'redBright' : 'green'](this.errors.length)}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance readability of dynamic chalk styling in the output.

The current dynamic access of chalk styles can be made more readable. Assigning the style to a variable or using a more explicit approach improves clarity.

Consider refactoring the code:

- output += `Errors: ${chalk.bold[this.errors.length ? 'redBright' : 'green'](this.errors.length)}`;
+ const color = this.errors.length ? chalk.bold.redBright : chalk.bold.green;
+ output += `Errors: ${color(this.errors.length)}`;

This change enhances readability by clearly showing which color function is being used based on the condition.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
output += `Errors: ${chalk.bold[this.errors.length ? 'redBright' : 'green'](this.errors.length)}`;
const color = this.errors.length ? chalk.bold.redBright : chalk.bold.green;
output += `Errors: ${color(this.errors.length)}`;

if (this.verbose && this.errors.length) {
output += `\n ${this.errors.join('\n ')}`;
}
return output;
}
}
Loading