From 0695d567d8a7d7eda5c70cff22aa8730f56d0d56 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 8 May 2026 11:31:32 -0400 Subject: [PATCH 1/2] refactor(@angular-devkit/architect): use custom indent logger Replaces `createConsoleLogger` with a custom `logging.IndentLogger` implementation to remove dependency on `@angular-devkit/core/node` helper. --- .../angular_devkit/architect/bin/architect.ts | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/angular_devkit/architect/bin/architect.ts b/packages/angular_devkit/architect/bin/architect.ts index 05888c276e55..c835fed8394c 100644 --- a/packages/angular_devkit/architect/bin/architect.ts +++ b/packages/angular_devkit/architect/bin/architect.ts @@ -8,7 +8,7 @@ */ import { JsonValue, json, logging, schema, strings, tags, workspaces } from '@angular-devkit/core'; -import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node'; +import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { existsSync } from 'node:fs'; import * as path from 'node:path'; import { parseArgs, styleText } from 'node:util'; @@ -217,12 +217,35 @@ async function main(args: string[]): Promise { const { positionals, cliOptions, builderOptions } = parseOptions(args); /** Create the DevKit Logger used through the CLI. */ - const logger = createConsoleLogger(!!cliOptions['verbose'], process.stdout, process.stderr, { + const logger = new logging.IndentLogger('architect'); + const colorLevels: Record string> = { info: (s) => s, debug: (s) => s, - warn: (s) => styleText(['yellow', 'bold'], s), - error: (s) => styleText(['red', 'bold'], s), - fatal: (s) => styleText(['red', 'bold'], s), + warn: (s) => styleText(['yellow', 'bold'], s, { stream: process.stderr }), + error: (s) => styleText(['red', 'bold'], s, { stream: process.stderr }), + fatal: (s) => styleText(['red', 'bold'], s, { stream: process.stderr }), + }; + + logger.subscribe((entry) => { + if (entry.level === 'debug' && !cliOptions['verbose']) { + return; + } + + const color = colorLevels[entry.level]; + const message = color ? color(entry.message) : entry.message; + + switch (entry.level) { + case 'warn': + case 'fatal': + case 'error': + // eslint-disable-next-line no-console + console.error(message); + break; + default: + // eslint-disable-next-line no-console + console.log(message); + break; + } }); // Check the target. From 4334bd3ce583d3a1816f13d8e802625f788a5bf0 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 8 May 2026 11:31:59 -0400 Subject: [PATCH 2/2] refactor(@angular-devkit/schematics-cli): use custom indent logger and improve tests Replaces `createConsoleLogger` with a custom `logging.IndentLogger` implementation that respects custom output streams. Also refactors tests to use standard `PassThrough` streams and `stripVTControlCharacters` instead of custom mocks. --- .../schematics_cli/bin/schematics.ts | 45 ++++++++++++---- .../schematics_cli/test/schematics_spec.ts | 51 +++++++++---------- 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/packages/angular_devkit/schematics_cli/bin/schematics.ts b/packages/angular_devkit/schematics_cli/bin/schematics.ts index 08d72f9d01d5..8420e520dd39 100644 --- a/packages/angular_devkit/schematics_cli/bin/schematics.ts +++ b/packages/angular_devkit/schematics_cli/bin/schematics.ts @@ -8,7 +8,6 @@ */ import { JsonValue, logging, schema } from '@angular-devkit/core'; -import { ProcessOutput, createConsoleLogger } from '@angular-devkit/core/node'; import { UnsuccessfulWorkflowExecution, strings } from '@angular-devkit/schematics'; import { NodeWorkflow } from '@angular-devkit/schematics/tools'; import { existsSync } from 'node:fs'; @@ -50,8 +49,8 @@ function removeLeadingSlash(value: string): string { export interface MainOptions { args: string[]; - stdout?: ProcessOutput; - stderr?: ProcessOutput; + stdout?: NodeJS.WritableStream; + stderr?: NodeJS.WritableStream; } function _listSchematics(workflow: NodeWorkflow, collectionName: string, logger: logging.Logger) { @@ -217,6 +216,37 @@ function getPackageManagerName() { return 'npm'; } +function createLogger( + verbose: boolean, + stdout: NodeJS.WritableStream, + stderr: NodeJS.WritableStream, +): logging.Logger { + const logger = new logging.IndentLogger('schematics'); + const colorLevels: Record string> = { + info: (s) => s, + debug: (s) => s, + warn: (s, stream) => styleText(['bold', 'yellow'], s, { stream }), + error: (s, stream) => styleText(['bold', 'red'], s, { stream }), + fatal: (s, stream) => styleText(['bold', 'red'], s, { stream }), + }; + + logger.subscribe((entry) => { + if (entry.level === 'debug' && !verbose) { + return; + } + + const output = + entry.level === 'warn' || entry.level === 'fatal' || entry.level === 'error' + ? stderr + : stdout; + const color = colorLevels[entry.level]; + const message = color ? color(entry.message, output) : entry.message; + output.write(message + '\n'); + }); + + return logger; +} + export async function main({ args, stdout = process.stdout, @@ -224,14 +254,7 @@ export async function main({ }: MainOptions): Promise<0 | 1> { const { cliOptions, schematicOptions, _ } = parseOptions(args); - /** Create the DevKit Logger used through the CLI. */ - const logger = createConsoleLogger(!!cliOptions.verbose, stdout, stderr, { - info: (s) => s, - debug: (s) => s, - warn: (s) => styleText(['bold', 'yellow'], s), - error: (s) => styleText(['bold', 'red'], s), - fatal: (s) => styleText(['bold', 'red'], s), - }); + const logger = createLogger(!!cliOptions.verbose, stdout, stderr); if (cliOptions.help) { logger.info(getUsage()); diff --git a/packages/angular_devkit/schematics_cli/test/schematics_spec.ts b/packages/angular_devkit/schematics_cli/test/schematics_spec.ts index 5dcf2fc962f0..e6d54b6ddb34 100644 --- a/packages/angular_devkit/schematics_cli/test/schematics_spec.ts +++ b/packages/angular_devkit/schematics_cli/test/schematics_spec.ts @@ -6,32 +6,24 @@ * found in the LICENSE file at https://angular.dev/license */ +import { PassThrough } from 'node:stream'; +import { stripVTControlCharacters } from 'node:util'; import { main } from '../bin/schematics'; -// We only care about the write method in these mocks of NodeJS.WriteStream. -class MockWriteStream { - lines: string[] = []; - write(str: string) { - // Strip color control characters. - this.lines.push(str.replace(/[^\x20-\x7F]\[\d+m/g, '')); - - return true; - } -} - describe('schematics-cli binary', () => { - let stdout: MockWriteStream, stderr: MockWriteStream; + let stdout: PassThrough, stderr: PassThrough; beforeEach(() => { - stdout = new MockWriteStream(); - stderr = new MockWriteStream(); + stdout = new PassThrough(); + stderr = new PassThrough(); }); it('list-schematics works', async () => { const args = ['--list-schematics']; const res = await main({ args, stdout, stderr }); - expect(stdout.lines).toMatch(/blank/); - expect(stdout.lines).toMatch(/schematic/); + const output = stripVTControlCharacters(stdout.read()?.toString() || ''); + expect(output).toMatch(/blank/); + expect(output).toMatch(/schematic/); expect(res).toEqual(0); }); @@ -45,30 +37,33 @@ describe('schematics-cli binary', () => { it('dry-run works', async () => { const args = ['blank', 'foo', '--dry-run']; const res = await main({ args, stdout, stderr }); - expect(stdout.lines).toMatch(/CREATE foo\/README.md/); - expect(stdout.lines).toMatch(/CREATE foo\/.gitignore/); - expect(stdout.lines).toMatch(/CREATE foo\/src\/foo\/index.ts/); - expect(stdout.lines).toMatch(/CREATE foo\/src\/foo\/index_spec.ts/); - expect(stdout.lines).toMatch(/Dry run enabled./); + const output = stripVTControlCharacters(stdout.read()?.toString() || ''); + expect(output).toMatch(/CREATE foo\/README.md/); + expect(output).toMatch(/CREATE foo\/.gitignore/); + expect(output).toMatch(/CREATE foo\/src\/foo\/index.ts/); + expect(output).toMatch(/CREATE foo\/src\/foo\/index_spec.ts/); + expect(output).toMatch(/Dry run enabled./); expect(res).toEqual(0); }); it('dry-run is default when debug mode', async () => { const args = ['blank', 'foo', '--debug']; const res = await main({ args, stdout, stderr }); - expect(stdout.lines).toMatch(/Debug mode enabled./); - expect(stdout.lines).toMatch(/CREATE foo\/README.md/); - expect(stdout.lines).toMatch(/CREATE foo\/.gitignore/); - expect(stdout.lines).toMatch(/CREATE foo\/src\/foo\/index.ts/); - expect(stdout.lines).toMatch(/CREATE foo\/src\/foo\/index_spec.ts/); - expect(stdout.lines).toMatch(/Dry run enabled by default in debug mode./); + const output = stripVTControlCharacters(stdout.read()?.toString() || ''); + expect(output).toMatch(/Debug mode enabled./); + expect(output).toMatch(/CREATE foo\/README.md/); + expect(output).toMatch(/CREATE foo\/.gitignore/); + expect(output).toMatch(/CREATE foo\/src\/foo\/index.ts/); + expect(output).toMatch(/CREATE foo\/src\/foo\/index_spec.ts/); + expect(output).toMatch(/Dry run enabled by default in debug mode./); expect(res).toEqual(0); }); it('error when no name is provided', async () => { const args = ['blank']; const res = await main({ args, stdout, stderr }); - expect(stderr.lines).toMatch(/Error: name option is required/); + const output = stripVTControlCharacters(stderr.read()?.toString() || ''); + expect(output).toMatch(/Error: name option is required/); expect(res).toEqual(1); }); });