From ff81fb1f57c32b178634c7d7463e80098ac8dde7 Mon Sep 17 00:00:00 2001 From: mag123c Date: Thu, 7 Aug 2025 10:27:06 +0900 Subject: [PATCH] feat(common): add force-console option to nestfactory.create() --- .../hello-world/e2e/force-console.spec.ts | 163 ++++++++++++++++++ ...t-application-context-options.interface.ts | 7 + packages/core/nest-factory.ts | 8 +- 3 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 integration/hello-world/e2e/force-console.spec.ts diff --git a/integration/hello-world/e2e/force-console.spec.ts b/integration/hello-world/e2e/force-console.spec.ts new file mode 100644 index 000000000..cb6aa733b --- /dev/null +++ b/integration/hello-world/e2e/force-console.spec.ts @@ -0,0 +1,163 @@ +import { ConsoleLogger, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import * as request from 'supertest'; +import { AppModule } from '../src/app.module'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; + +describe('ForceConsole Option', () => { + let app: INestApplication; + + describe('When forceConsole is true', () => { + let consoleLogSpy: sinon.SinonSpy; + let consoleErrorSpy: sinon.SinonSpy; + let processStdoutSpy: sinon.SinonSpy; + let processStderrSpy: sinon.SinonSpy; + + beforeEach(async () => { + // Spy on console and process methods + consoleLogSpy = sinon.spy(console, 'log'); + consoleErrorSpy = sinon.spy(console, 'error'); + processStdoutSpy = sinon.spy(process.stdout, 'write'); + processStderrSpy = sinon.spy(process.stderr, 'write'); + + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleRef.createNestApplication({ + forceConsole: true, + logger: ['log', 'error'], + }); + + await app.init(); + }); + + afterEach(async () => { + consoleLogSpy.restore(); + consoleErrorSpy.restore(); + processStdoutSpy.restore(); + processStderrSpy.restore(); + await app.close(); + }); + + it('should use console.log instead of process.stdout.write', async () => { + const logger = new ConsoleLogger('TestContext', { forceConsole: true }); + logger.log('Test log message'); + + // Should use console.log when forceConsole is true + expect(consoleLogSpy.called).to.be.true; + // Verify console.log was called with the message + const consoleLogCalls = consoleLogSpy + .getCalls() + .filter(call => + call.args.some(arg => String(arg).includes('Test log message')), + ); + expect(consoleLogCalls.length).to.be.greaterThan(0); + }); + + it('should use console.error instead of process.stderr.write', async () => { + const logger = new ConsoleLogger('TestContext', { forceConsole: true }); + logger.error('Test error message'); + + // Should use console.error when forceConsole is true + expect(consoleErrorSpy.called).to.be.true; + // Verify console.error was called with the message + const consoleErrorCalls = consoleErrorSpy + .getCalls() + .filter(call => + call.args.some(arg => String(arg).includes('Test error message')), + ); + expect(consoleErrorCalls.length).to.be.greaterThan(0); + }); + + it('should handle GET request with forceConsole option enabled', () => { + return request(app.getHttpServer()).get('/hello').expect(200); + }); + }); + + describe('When forceConsole is false (default)', () => { + let consoleLogSpy: sinon.SinonSpy; + let consoleErrorSpy: sinon.SinonSpy; + let processStdoutSpy: sinon.SinonSpy; + let processStderrSpy: sinon.SinonSpy; + + beforeEach(async () => { + // Spy on console and process methods + consoleLogSpy = sinon.spy(console, 'log'); + consoleErrorSpy = sinon.spy(console, 'error'); + processStdoutSpy = sinon.spy(process.stdout, 'write'); + processStderrSpy = sinon.spy(process.stderr, 'write'); + + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleRef.createNestApplication({ + logger: ['log', 'error'], + // forceConsole is not set, defaults to false + }); + + await app.init(); + }); + + afterEach(async () => { + consoleLogSpy.restore(); + consoleErrorSpy.restore(); + processStdoutSpy.restore(); + processStderrSpy.restore(); + await app.close(); + }); + + it('should not directly call console.log when forceConsole is false', async () => { + const logger = new ConsoleLogger('TestContext'); + + // Reset spy to ensure clean state + consoleLogSpy.resetHistory(); + + logger.log('Test log message'); + + // When forceConsole is false, should not call console.log + expect(consoleLogSpy.called).to.be.false; + }); + + it('should not directly call console.error when forceConsole is false', async () => { + const logger = new ConsoleLogger('TestContext'); + + // Reset spy to ensure clean state + consoleErrorSpy.resetHistory(); + + logger.error('Test error message'); + + // When forceConsole is false, should not call console.error + expect(consoleErrorSpy.called).to.be.false; + }); + }); + + describe('When forceConsole is set via NestFactory.create', () => { + it('should apply forceConsole to the default logger', async () => { + const consoleLogSpy = sinon.spy(console, 'log'); + const processStdoutSpy = sinon.spy(process.stdout, 'write'); + + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + const testApp = moduleRef.createNestApplication({ + forceConsole: true, + }); + + await testApp.init(); + + // The logger created by NestFactory should respect forceConsole option + const logger = new ConsoleLogger('AppContext', { forceConsole: true }); + logger.log('Application started'); + + expect(consoleLogSpy.called).to.be.true; + + consoleLogSpy.restore(); + processStdoutSpy.restore(); + await testApp.close(); + }); + }); +}); diff --git a/packages/common/interfaces/nest-application-context-options.interface.ts b/packages/common/interfaces/nest-application-context-options.interface.ts index 63a9b793a..98aad7391 100644 --- a/packages/common/interfaces/nest-application-context-options.interface.ts +++ b/packages/common/interfaces/nest-application-context-options.interface.ts @@ -67,4 +67,11 @@ export class NestApplicationContextOptions { */ instanceDecorator: (instance: unknown) => unknown; }; + + /** + * If enabled, will force the use of console.log/console.error instead of process.stdout/stderr.write + * in the default ConsoleLogger. This is useful for test environments like Jest that can buffer console calls. + * @default false + */ + forceConsole?: boolean; } diff --git a/packages/core/nest-factory.ts b/packages/core/nest-factory.ts index 3cc157d9e..ab62ea0f8 100644 --- a/packages/core/nest-factory.ts +++ b/packages/core/nest-factory.ts @@ -10,6 +10,7 @@ import { import { NestMicroserviceOptions } from '@nestjs/common/interfaces/microservices/nest-microservice-options.interface'; import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface'; import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface'; +import { ConsoleLogger } from '@nestjs/common/services/console-logger.service'; import { Logger } from '@nestjs/common/services/logger.service'; import { loadPackage } from '@nestjs/common/utils/load-package.util'; import { isFunction, isNil } from '@nestjs/common/utils/shared.utils'; @@ -299,9 +300,14 @@ export class NestFactoryStatic { if (!options) { return; } - const { logger, bufferLogs, autoFlushLogs } = options; + const { logger, bufferLogs, autoFlushLogs, forceConsole } = options; if ((logger as boolean) !== true && !isNil(logger)) { Logger.overrideLogger(logger); + } else if (forceConsole) { + // If no custom logger is provided but forceConsole is true, + // create a ConsoleLogger with forceConsole option + const consoleLogger = new ConsoleLogger({ forceConsole: true }); + Logger.overrideLogger(consoleLogger); } if (bufferLogs) { Logger.attachBuffer();