Skip to content

Commit ff81fb1

Browse files
committed
feat(common): add force-console option to nestfactory.create()
1 parent 0577b54 commit ff81fb1

File tree

3 files changed

+177
-1
lines changed

3 files changed

+177
-1
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { ConsoleLogger, INestApplication } from '@nestjs/common';
2+
import { Test } from '@nestjs/testing';
3+
import * as request from 'supertest';
4+
import { AppModule } from '../src/app.module';
5+
import * as sinon from 'sinon';
6+
import { expect } from 'chai';
7+
8+
describe('ForceConsole Option', () => {
9+
let app: INestApplication;
10+
11+
describe('When forceConsole is true', () => {
12+
let consoleLogSpy: sinon.SinonSpy;
13+
let consoleErrorSpy: sinon.SinonSpy;
14+
let processStdoutSpy: sinon.SinonSpy;
15+
let processStderrSpy: sinon.SinonSpy;
16+
17+
beforeEach(async () => {
18+
// Spy on console and process methods
19+
consoleLogSpy = sinon.spy(console, 'log');
20+
consoleErrorSpy = sinon.spy(console, 'error');
21+
processStdoutSpy = sinon.spy(process.stdout, 'write');
22+
processStderrSpy = sinon.spy(process.stderr, 'write');
23+
24+
const moduleRef = await Test.createTestingModule({
25+
imports: [AppModule],
26+
}).compile();
27+
28+
app = moduleRef.createNestApplication({
29+
forceConsole: true,
30+
logger: ['log', 'error'],
31+
});
32+
33+
await app.init();
34+
});
35+
36+
afterEach(async () => {
37+
consoleLogSpy.restore();
38+
consoleErrorSpy.restore();
39+
processStdoutSpy.restore();
40+
processStderrSpy.restore();
41+
await app.close();
42+
});
43+
44+
it('should use console.log instead of process.stdout.write', async () => {
45+
const logger = new ConsoleLogger('TestContext', { forceConsole: true });
46+
logger.log('Test log message');
47+
48+
// Should use console.log when forceConsole is true
49+
expect(consoleLogSpy.called).to.be.true;
50+
// Verify console.log was called with the message
51+
const consoleLogCalls = consoleLogSpy
52+
.getCalls()
53+
.filter(call =>
54+
call.args.some(arg => String(arg).includes('Test log message')),
55+
);
56+
expect(consoleLogCalls.length).to.be.greaterThan(0);
57+
});
58+
59+
it('should use console.error instead of process.stderr.write', async () => {
60+
const logger = new ConsoleLogger('TestContext', { forceConsole: true });
61+
logger.error('Test error message');
62+
63+
// Should use console.error when forceConsole is true
64+
expect(consoleErrorSpy.called).to.be.true;
65+
// Verify console.error was called with the message
66+
const consoleErrorCalls = consoleErrorSpy
67+
.getCalls()
68+
.filter(call =>
69+
call.args.some(arg => String(arg).includes('Test error message')),
70+
);
71+
expect(consoleErrorCalls.length).to.be.greaterThan(0);
72+
});
73+
74+
it('should handle GET request with forceConsole option enabled', () => {
75+
return request(app.getHttpServer()).get('/hello').expect(200);
76+
});
77+
});
78+
79+
describe('When forceConsole is false (default)', () => {
80+
let consoleLogSpy: sinon.SinonSpy;
81+
let consoleErrorSpy: sinon.SinonSpy;
82+
let processStdoutSpy: sinon.SinonSpy;
83+
let processStderrSpy: sinon.SinonSpy;
84+
85+
beforeEach(async () => {
86+
// Spy on console and process methods
87+
consoleLogSpy = sinon.spy(console, 'log');
88+
consoleErrorSpy = sinon.spy(console, 'error');
89+
processStdoutSpy = sinon.spy(process.stdout, 'write');
90+
processStderrSpy = sinon.spy(process.stderr, 'write');
91+
92+
const moduleRef = await Test.createTestingModule({
93+
imports: [AppModule],
94+
}).compile();
95+
96+
app = moduleRef.createNestApplication({
97+
logger: ['log', 'error'],
98+
// forceConsole is not set, defaults to false
99+
});
100+
101+
await app.init();
102+
});
103+
104+
afterEach(async () => {
105+
consoleLogSpy.restore();
106+
consoleErrorSpy.restore();
107+
processStdoutSpy.restore();
108+
processStderrSpy.restore();
109+
await app.close();
110+
});
111+
112+
it('should not directly call console.log when forceConsole is false', async () => {
113+
const logger = new ConsoleLogger('TestContext');
114+
115+
// Reset spy to ensure clean state
116+
consoleLogSpy.resetHistory();
117+
118+
logger.log('Test log message');
119+
120+
// When forceConsole is false, should not call console.log
121+
expect(consoleLogSpy.called).to.be.false;
122+
});
123+
124+
it('should not directly call console.error when forceConsole is false', async () => {
125+
const logger = new ConsoleLogger('TestContext');
126+
127+
// Reset spy to ensure clean state
128+
consoleErrorSpy.resetHistory();
129+
130+
logger.error('Test error message');
131+
132+
// When forceConsole is false, should not call console.error
133+
expect(consoleErrorSpy.called).to.be.false;
134+
});
135+
});
136+
137+
describe('When forceConsole is set via NestFactory.create', () => {
138+
it('should apply forceConsole to the default logger', async () => {
139+
const consoleLogSpy = sinon.spy(console, 'log');
140+
const processStdoutSpy = sinon.spy(process.stdout, 'write');
141+
142+
const moduleRef = await Test.createTestingModule({
143+
imports: [AppModule],
144+
}).compile();
145+
146+
const testApp = moduleRef.createNestApplication({
147+
forceConsole: true,
148+
});
149+
150+
await testApp.init();
151+
152+
// The logger created by NestFactory should respect forceConsole option
153+
const logger = new ConsoleLogger('AppContext', { forceConsole: true });
154+
logger.log('Application started');
155+
156+
expect(consoleLogSpy.called).to.be.true;
157+
158+
consoleLogSpy.restore();
159+
processStdoutSpy.restore();
160+
await testApp.close();
161+
});
162+
});
163+
});

packages/common/interfaces/nest-application-context-options.interface.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,11 @@ export class NestApplicationContextOptions {
6767
*/
6868
instanceDecorator: (instance: unknown) => unknown;
6969
};
70+
71+
/**
72+
* If enabled, will force the use of console.log/console.error instead of process.stdout/stderr.write
73+
* in the default ConsoleLogger. This is useful for test environments like Jest that can buffer console calls.
74+
* @default false
75+
*/
76+
forceConsole?: boolean;
7077
}

packages/core/nest-factory.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { NestMicroserviceOptions } from '@nestjs/common/interfaces/microservices/nest-microservice-options.interface';
1111
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
1212
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface';
13+
import { ConsoleLogger } from '@nestjs/common/services/console-logger.service';
1314
import { Logger } from '@nestjs/common/services/logger.service';
1415
import { loadPackage } from '@nestjs/common/utils/load-package.util';
1516
import { isFunction, isNil } from '@nestjs/common/utils/shared.utils';
@@ -299,9 +300,14 @@ export class NestFactoryStatic {
299300
if (!options) {
300301
return;
301302
}
302-
const { logger, bufferLogs, autoFlushLogs } = options;
303+
const { logger, bufferLogs, autoFlushLogs, forceConsole } = options;
303304
if ((logger as boolean) !== true && !isNil(logger)) {
304305
Logger.overrideLogger(logger);
306+
} else if (forceConsole) {
307+
// If no custom logger is provided but forceConsole is true,
308+
// create a ConsoleLogger with forceConsole option
309+
const consoleLogger = new ConsoleLogger({ forceConsole: true });
310+
Logger.overrideLogger(consoleLogger);
305311
}
306312
if (bufferLogs) {
307313
Logger.attachBuffer();

0 commit comments

Comments
 (0)