From 2037023d8eed3024a61751e324e96a02a5679501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 3 Oct 2025 17:15:17 +0100 Subject: [PATCH 1/7] feat: support custom log function --- packages/build/src/error/monitor/start.ts | 4 +- packages/build/src/log/logger.ts | 46 ++++++++++++------ packages/build/src/log/messages/config.js | 2 +- packages/build/src/log/messages/core.ts | 4 +- .../build/tests/log/snapshots/tests.js.md | 32 +++++++++++- .../build/tests/log/snapshots/tests.js.snap | Bin 991 -> 1096 bytes packages/build/tests/log/tests.js | 10 ++++ 7 files changed, 76 insertions(+), 22 deletions(-) diff --git a/packages/build/src/error/monitor/start.ts b/packages/build/src/error/monitor/start.ts index 50094d3e13..d07722b224 100644 --- a/packages/build/src/error/monitor/start.ts +++ b/packages/build/src/error/monitor/start.ts @@ -4,13 +4,13 @@ import Bugsnag from '@bugsnag/js' import memoizeOne from 'memoize-one' import type { ResolvedFlags } from '../../core/normalize_flags.js' -import { BufferedLogs, log } from '../../log/logger.js' +import { Logs, log } from '../../log/logger.js' import { ROOT_PACKAGE_JSON } from '../../utils/json.js' const projectRoot = fileURLToPath(new URL('../../..', import.meta.url)) // Start a client to monitor errors -export const startErrorMonitor = function (config: { flags: ResolvedFlags; logs?: BufferedLogs; bugsnagKey?: string }) { +export const startErrorMonitor = function (config: { flags: ResolvedFlags; logs?: Logs; bugsnagKey?: string }) { const { flags: { mode }, logs, diff --git a/packages/build/src/log/logger.ts b/packages/build/src/log/logger.ts index 4e82b61497..cfc3a26dca 100644 --- a/packages/build/src/log/logger.ts +++ b/packages/build/src/log/logger.ts @@ -12,7 +12,7 @@ import { THEME } from './theme.js' export type Logs = BufferedLogs | StreamedLogs export type BufferedLogs = { stdout: string[]; stderr: string[]; outputFlusher?: OutputFlusher } -export type StreamedLogs = { outputFlusher?: OutputFlusher } +export type StreamedLogs = { outputFlusher?: OutputFlusher; logFunction?: (message: string) => void } export const logsAreBuffered = (logs: Logs | undefined): logs is BufferedLogs => { return logs !== undefined && 'stdout' in logs @@ -31,8 +31,16 @@ const EMPTY_LINE = '\u{200B}' * When the `buffer` option is true, we return logs instead of printing them * on the console. The logs are accumulated in a `logs` array variable. */ -export const getBufferLogs = (config: { buffer?: boolean }): BufferedLogs | undefined => { - const { buffer = false } = config +export const getBufferLogs = (config: { + buffer?: boolean + logs?: { logFunction?: (message: string) => void } +}): Logs | undefined => { + const { buffer = false, logs } = config + + if (logs?.logFunction) { + return { logFunction: logs.logFunction } + } + if (!buffer) { return } @@ -64,6 +72,12 @@ export const log = function ( return } + if (typeof logs?.logFunction === 'function') { + logs.logFunction(stringC) + + return + } + console.log(stringC) } @@ -75,61 +89,61 @@ const serializeIndentedItem = function (item) { return indentString(item, INDENT_SIZE + 1).trimStart() } -export const logError = function (logs: BufferedLogs | undefined, string: string, opts = {}) { +export const logError = function (logs: Logs | undefined, string: string, opts = {}) { log(logs, string, { color: THEME.errorLine, ...opts }) } -export const logWarning = function (logs: BufferedLogs | undefined, string: string, opts = {}) { +export const logWarning = function (logs: Logs | undefined, string: string, opts = {}) { log(logs, string, { color: THEME.warningLine, ...opts }) } // Print a message that is under a header/subheader, i.e. indented -export const logMessage = function (logs: BufferedLogs | undefined, string: string, opts = {}) { +export const logMessage = function (logs: Logs | undefined, string: string, opts = {}) { log(logs, string, { indent: true, ...opts }) } // Print an object -export const logObject = function (logs: BufferedLogs | undefined, object, opts) { +export const logObject = function (logs: Logs | undefined, object, opts) { logMessage(logs, serializeObject(object), opts) } // Print an array -export const logArray = function (logs: BufferedLogs | undefined, array, opts = {}) { +export const logArray = function (logs: Logs | undefined, array, opts = {}) { logMessage(logs, serializeIndentedArray(array), { color: THEME.none, ...opts }) } // Print an array of errors -export const logErrorArray = function (logs: BufferedLogs | undefined, array, opts = {}) { +export const logErrorArray = function (logs: Logs | undefined, array, opts = {}) { logMessage(logs, serializeIndentedArray(array), { color: THEME.errorLine, ...opts }) } // Print an array of warnings -export const logWarningArray = function (logs: BufferedLogs | undefined, array, opts = {}) { +export const logWarningArray = function (logs: Logs | undefined, array, opts = {}) { logMessage(logs, serializeIndentedArray(array), { color: THEME.warningLine, ...opts }) } // Print a main section header -export const logHeader = function (logs: BufferedLogs | undefined, string: string, opts = {}) { +export const logHeader = function (logs: Logs | undefined, string: string, opts = {}) { log(logs, `\n${getHeader(string)}`, { color: THEME.header, ...opts }) } // Print a main section header, when an error happened -export const logErrorHeader = function (logs: BufferedLogs | undefined, string: string, opts = {}) { +export const logErrorHeader = function (logs: Logs | undefined, string: string, opts = {}) { logHeader(logs, string, { color: THEME.errorHeader, ...opts }) } // Print a sub-section header -export const logSubHeader = function (logs: BufferedLogs | undefined, string: string, opts = {}) { +export const logSubHeader = function (logs: Logs | undefined, string: string, opts = {}) { log(logs, `\n${figures.pointer} ${string}`, { color: THEME.subHeader, ...opts }) } // Print a sub-section header, when an error happened -export const logErrorSubHeader = function (logs: BufferedLogs | undefined, string: string, opts = {}) { +export const logErrorSubHeader = function (logs: Logs | undefined, string: string, opts = {}) { logSubHeader(logs, string, { color: THEME.errorSubHeader, ...opts }) } // Print a sub-section header, when a warning happened -export const logWarningSubHeader = function (logs: BufferedLogs | undefined, string: string, opts = {}) { +export const logWarningSubHeader = function (logs: Logs | undefined, string: string, opts = {}) { logSubHeader(logs, string, { color: THEME.warningSubHeader, ...opts }) } @@ -162,7 +176,7 @@ export const reduceLogLines = function (lines) { * the user-facing build logs) */ export const getSystemLogger = function ( - logs: BufferedLogs | undefined, + logs: Logs | undefined, debug: boolean, /** A system log file descriptor, if non is provided it will be a noop logger */ systemLogFile?: number, diff --git a/packages/build/src/log/messages/config.js b/packages/build/src/log/messages/config.js index 32342369b5..a68c4458b8 100644 --- a/packages/build/src/log/messages/config.js +++ b/packages/build/src/log/messages/config.js @@ -61,7 +61,7 @@ const INTERNAL_FLAGS = [ 'eventHandlers', ] const HIDDEN_FLAGS = [...SECURE_FLAGS, ...TEST_FLAGS, ...INTERNAL_FLAGS] -const HIDDEN_DEBUG_FLAGS = [...SECURE_FLAGS, ...TEST_FLAGS, 'eventHandlers'] +const HIDDEN_DEBUG_FLAGS = [...SECURE_FLAGS, ...TEST_FLAGS, 'eventHandlers', 'logs'] export const logBuildDir = function (logs, buildDir) { logSubHeader(logs, 'Current directory') diff --git a/packages/build/src/log/messages/core.ts b/packages/build/src/log/messages/core.ts index e856a3f8c2..f3c4b08233 100644 --- a/packages/build/src/log/messages/core.ts +++ b/packages/build/src/log/messages/core.ts @@ -6,13 +6,13 @@ import { serializeLogError } from '../../error/parse/serialize_log.js' import { roundTimerToMillisecs } from '../../time/measure.js' import { ROOT_PACKAGE_JSON } from '../../utils/json.js' import { getLogHeaderFunc } from '../header_func.js' -import { log, logMessage, logWarning, logHeader, logSubHeader, logWarningArray, BufferedLogs } from '../logger.js' +import { log, logMessage, logWarning, logHeader, logSubHeader, logWarningArray, Logs } from '../logger.js' import { OutputFlusher } from '../output_flusher.js' import { THEME } from '../theme.js' import { logConfigOnError } from './config.js' -export const logBuildStart = function (logs?: BufferedLogs) { +export const logBuildStart = function (logs?: Logs) { logHeader(logs, 'Netlify Build') logSubHeader(logs, 'Version') logMessage(logs, `${ROOT_PACKAGE_JSON.name} ${ROOT_PACKAGE_JSON.version}`) diff --git a/packages/build/tests/log/snapshots/tests.js.md b/packages/build/tests/log/snapshots/tests.js.md index 2513e7c1d9..736aa4434e 100644 --- a/packages/build/tests/log/snapshots/tests.js.md +++ b/packages/build/tests/log/snapshots/tests.js.md @@ -1,4 +1,4 @@ -# Snapshot report for `tests/log/tests.js` +# Snapshot report for `packages/build/tests/log/tests.js` The actual snapshot is saved in `tests.js.snap`. @@ -204,3 +204,33 @@ Generated by [AVA](https://avajs.dev). - inputs: {}␊ origin: config␊ package: ./plugin` + +## Accepts a custom log function + +> Snapshot 1 + + `␊ + Netlify Build ␊ + ────────────────────────────────────────────────────────────────␊ + > Version @netlify/build 1.0.0␊ + > Flags debug: true␊ + repositoryRoot: packages/build/tests/log/fixtures/verbose␊ + testOpts:␊ + pluginsListUrl: test␊ + silentLingeringProcesses: true␊ + verbose: true␊ + > Current directory packages/build/tests/log/fixtures/verbose␊ + > Config file packages/build/tests/log/fixtures/verbose/netlify.toml␊ + > Resolved config build:␊ + publish: packages/build/tests/log/fixtures/verbose␊ + publishOrigin: default␊ + plugins:␊ + - inputs: {}␊ + origin: config␊ + package: ./plugin␊ + > Context production␊ + > Loading plugins - ./plugin@1.0.0 from netlify.toml␊ + ./plugin (onPreBuild event) ␊ + ────────────────────────────────────────────────────────────────Step completed.(./plugin onPreBuild completed in 1ms)Build step duration: ./plugin onPreBuild completed in 1msStep starting.Step started.Step ended.␊ + Netlify Build Complete ␊ + ────────────────────────────────────────────────────────────────(Netlify Build completed in 1ms)Build step duration: Netlify Build completed in 1ms` diff --git a/packages/build/tests/log/snapshots/tests.js.snap b/packages/build/tests/log/snapshots/tests.js.snap index d107a909d4ddf98a27d21896e3ebdbc659de1baf..845e53837959276f8a128120318597ccb6a67432 100644 GIT binary patch literal 1096 zcmV-O1h@M^RzV_kXtjZg>PR<(xRl1VfyJ~ zmI)T`?|t{h+WMM%KKpX*!RO!Ji(VrunV-V5+SGNQ=#O81ywz_MZNodXB#n*$o@$Rq z?16pU-|BC<6)%)1C<4fESQQDdtuO+xSQ?US?CHKSoIokkBT*oE?U<36Vr7armnU3V zq!`B~pGE*q^rmD=A_rorszPeomBeqYN@{5>AgNIEuG9rusf#^pQY1nOCy2ITP+5zb zA(IwUcajJeK88~_I+q3HQn~xQHjqv~z_B36Fqay$1S*oca6d(*7$!nhNPTBwAIYc* zWRcPacGv- zWL3(}&(X8lwDAekq=6{Q6^Qw-&N?LO)iC!1COyc7RJd?G_yiyUcy6t+Fh(L#xO5ma z@!_F3mOKJHkqoKB?wh%wZcQkY3ibu%rH0Fc&E?6wotvCgZr22cA_G&pTPD*ZG(^=q z93f254h8xNDEagpYWlrHO~2hy(|<}$!Cn6tIYAQuf0wM9gU!*^!m4qA zuFqzyuH>w1B+C@D3)7_q&>Pjuc;ax;Qi|V8iG=i$2S6f|?(#MtU7EmOv~lOI`G5eqz>-_B=Mq6f&KD zt)ssqLY+Cm0hz{Dg`9R)S>a3VBC40) zYxZboD(oN_TA>QN(M4cEFI=wFvf#Uh?BLkgBk~zQjJP;o zg>8F{b9x{(qt!zBZ9jRZ%ulJaAV&ZJp$u6_<$`^$$(8W5DFaPeZAHGCy6Lkin_g5% z{(8h*QWD37?oD&A=aUb*?w?QG*+9E$ zlD^3$t?42j?xZP}Oduewh|Sm~9psg+3*0$l^ovdd@#JodZu^G|+K13R-24&xoUY+I z19YcbSbBV(>k@9m@Z5F>o#tS!H(0DSSng1Yy*WDWP!FskS9Up{EFA@20Ks`R3pu|yAmuEOyd0cp>vW$t&0;~AxG~_kcBhjs%%VW(s<+y zr?s3bfM9x23+F0;SevFI%lpLg(wn@swgx7HR`$tO=*dhse&%uuQHB zVpGPQrOwR3x)Nb~o9s**Di>`pJXK_KXeeUQj12MQI$-urU8t00+MwWBK(qF)Yzhf= zR*x5H6A9DNK-cvx==smyEQlJkFb@+Z0~l*Exb{4F4 z7-4L51!(oiAg6~U=YJthzb#4AuXob)pGs3I*FPps&^5r{6{{|>6}ej5HA~P9#ZAqX znsp7b%rU!=Ep%O)t3C(`caV1?nWuP0Yy(v!9aQf`VW;9hKpHD<; z&a?gE{vGk$kXR{#D{GHriDu>6<*# N{saLxrxN}Z003?{+7JK$ diff --git a/packages/build/tests/log/tests.js b/packages/build/tests/log/tests.js index 601befc90f..7a0df73520 100644 --- a/packages/build/tests/log/tests.js +++ b/packages/build/tests/log/tests.js @@ -58,3 +58,13 @@ test('Does not truncate long redirects in logs', async (t) => { const output = await new Fixture('./fixtures/truncate_redirects').runWithBuild() t.false(output.includes('999')) }) + +test('Accepts a custom log function', async (t) => { + const logs = [] + const logFunction = (message) => { + logs.push(message) + } + await new Fixture('./fixtures/verbose').withFlags({ logs: { logFunction }, verbose: true }).runBuildProgrammatic() + + t.snapshot(normalizeOutput(logs.join(''))) +}) From fa000ff896b59474091d86f74eeae9c8879b4572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 3 Oct 2025 17:18:47 +0100 Subject: [PATCH 2/7] refactor: simplify logic --- packages/build/src/log/logger.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/build/src/log/logger.ts b/packages/build/src/log/logger.ts index cfc3a26dca..ece0201f85 100644 --- a/packages/build/src/log/logger.ts +++ b/packages/build/src/log/logger.ts @@ -37,15 +37,13 @@ export const getBufferLogs = (config: { }): Logs | undefined => { const { buffer = false, logs } = config - if (logs?.logFunction) { - return { logFunction: logs.logFunction } + if (buffer) { + return { stdout: [], stderr: [] } } - if (!buffer) { - return + if (logs?.logFunction) { + return { logFunction: logs.logFunction } } - - return { stdout: [], stderr: [] } } // Core logging utility, used by the other methods. From b9033cf62be0caeeeed196b548215d14131f324c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 3 Oct 2025 17:49:33 +0100 Subject: [PATCH 3/7] refactor: rename property --- packages/build/src/core/main.ts | 4 ++-- packages/build/src/core/types.ts | 2 ++ packages/build/src/log/logger.ts | 12 ++++++------ packages/build/src/log/messages/config.js | 3 ++- packages/build/tests/log/snapshots/tests.js.md | 2 +- packages/build/tests/log/tests.js | 10 +++++++--- packages/testing/src/fixture.ts | 7 ++++--- 7 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/build/src/core/main.ts b/packages/build/src/core/main.ts index 7c7c4e5b3a..2fcdb43b8b 100644 --- a/packages/build/src/core/main.ts +++ b/packages/build/src/core/main.ts @@ -4,7 +4,7 @@ import { trace, context } from '@opentelemetry/api' import { handleBuildError } from '../error/handle.js' import { reportError } from '../error/report.js' import { getSystemLogger } from '../log/logger.js' -import type { BufferedLogs } from '../log/logger.js' +import type { Logs } from '../log/logger.js' import { logTimer, logBuildSuccess } from '../log/messages/core.js' import { getGeneratedFunctions } from '../steps/return_values.js' import { trackBuildComplete } from '../telemetry/main.js' @@ -27,7 +27,7 @@ const tracer = trace.getTracer('core') export async function buildSite(flags: Partial = {}): Promise<{ success: boolean severityCode: number - logs: BufferedLogs | undefined + logs: Logs | undefined netlifyConfig?: any configMutations?: any }> { diff --git a/packages/build/src/core/types.ts b/packages/build/src/core/types.ts index d746c361df..39336b5066 100644 --- a/packages/build/src/core/types.ts +++ b/packages/build/src/core/types.ts @@ -44,6 +44,8 @@ export type BuildCLIFlags = { export type BuildFlags = BuildCLIFlags & { env?: Record eventHandlers?: EventHandlers + /** Custom logger function to capture build output */ + logger?: (message: string) => void } type EventHandlers = { diff --git a/packages/build/src/log/logger.ts b/packages/build/src/log/logger.ts index ece0201f85..d8b8041110 100644 --- a/packages/build/src/log/logger.ts +++ b/packages/build/src/log/logger.ts @@ -33,16 +33,16 @@ const EMPTY_LINE = '\u{200B}' */ export const getBufferLogs = (config: { buffer?: boolean - logs?: { logFunction?: (message: string) => void } + logger?: (message: string) => void }): Logs | undefined => { - const { buffer = false, logs } = config + const { buffer = false, logger } = config - if (buffer) { - return { stdout: [], stderr: [] } + if (logger) { + return { logFunction: logger } } - if (logs?.logFunction) { - return { logFunction: logs.logFunction } + if (buffer) { + return { stdout: [], stderr: [] } } } diff --git a/packages/build/src/log/messages/config.js b/packages/build/src/log/messages/config.js index a68c4458b8..9aadf3606a 100644 --- a/packages/build/src/log/messages/config.js +++ b/packages/build/src/log/messages/config.js @@ -59,9 +59,10 @@ const INTERNAL_FLAGS = [ 'enhancedSecretScan', 'edgeFunctionsBootstrapURL', 'eventHandlers', + 'logger', ] const HIDDEN_FLAGS = [...SECURE_FLAGS, ...TEST_FLAGS, ...INTERNAL_FLAGS] -const HIDDEN_DEBUG_FLAGS = [...SECURE_FLAGS, ...TEST_FLAGS, 'eventHandlers', 'logs'] +const HIDDEN_DEBUG_FLAGS = [...SECURE_FLAGS, ...TEST_FLAGS, 'eventHandlers', 'logger'] export const logBuildDir = function (logs, buildDir) { logSubHeader(logs, 'Current directory') diff --git a/packages/build/tests/log/snapshots/tests.js.md b/packages/build/tests/log/snapshots/tests.js.md index 736aa4434e..0782f03e14 100644 --- a/packages/build/tests/log/snapshots/tests.js.md +++ b/packages/build/tests/log/snapshots/tests.js.md @@ -1,4 +1,4 @@ -# Snapshot report for `packages/build/tests/log/tests.js` +# Snapshot report for `tests/log/tests.js` The actual snapshot is saved in `tests.js.snap`. diff --git a/packages/build/tests/log/tests.js b/packages/build/tests/log/tests.js index 7a0df73520..dac64bf956 100644 --- a/packages/build/tests/log/tests.js +++ b/packages/build/tests/log/tests.js @@ -61,10 +61,14 @@ test('Does not truncate long redirects in logs', async (t) => { test('Accepts a custom log function', async (t) => { const logs = [] - const logFunction = (message) => { + const logger = (message) => { logs.push(message) } - await new Fixture('./fixtures/verbose').withFlags({ logs: { logFunction }, verbose: true }).runBuildProgrammatic() + await new Fixture('./fixtures/verbose') + .withFlags({ logger, verbose: true }) + .runBuildProgrammatic() - t.snapshot(normalizeOutput(logs.join(''))) + t.true(logs.length > 0, 'logger should have been called with messages') + t.true(logs.some(log => log.includes('Netlify Build')), 'logs should contain build header') + t.true(logs.some(log => log.includes('onPreBuild')), 'logs should contain plugin event') }) diff --git a/packages/testing/src/fixture.ts b/packages/testing/src/fixture.ts index d096124684..f227f37761 100644 --- a/packages/testing/src/fixture.ts +++ b/packages/testing/src/fixture.ts @@ -205,9 +205,10 @@ export class Fixture { async runWithBuildAndIntrospect(): Promise> & { output: string }> { const buildResult = await build(this.getBuildFlags()) - const output = [buildResult.logs?.stdout.join('\n'), buildResult.logs?.stderr.join('\n')] - .filter(Boolean) - .join('\n\n') + const output = + buildResult.logs && 'stdout' in buildResult.logs + ? [buildResult.logs.stdout.join('\n'), buildResult.logs.stderr.join('\n')].filter(Boolean).join('\n\n') + : '' return { ...buildResult, From b13fa68951406409315d8cedb8bdd9bf57cd16a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 3 Oct 2025 17:59:05 +0100 Subject: [PATCH 4/7] refactor: improve output --- packages/build/src/core/main.ts | 4 +-- packages/build/src/index.ts | 1 + packages/build/src/log/logger.ts | 16 +++++++--- .../build/tests/log/snapshots/tests.js.md | 30 ------------------ .../build/tests/log/snapshots/tests.js.snap | Bin 1096 -> 991 bytes packages/build/tests/log/tests.js | 16 +++++++--- 6 files changed, 26 insertions(+), 41 deletions(-) diff --git a/packages/build/src/core/main.ts b/packages/build/src/core/main.ts index 2fcdb43b8b..fae361d1d4 100644 --- a/packages/build/src/core/main.ts +++ b/packages/build/src/core/main.ts @@ -3,7 +3,7 @@ import { trace, context } from '@opentelemetry/api' import { handleBuildError } from '../error/handle.js' import { reportError } from '../error/report.js' -import { getSystemLogger } from '../log/logger.js' +import { getLogsOutput, getSystemLogger } from '../log/logger.js' import type { Logs } from '../log/logger.js' import { logTimer, logBuildSuccess } from '../log/messages/core.js' import { getGeneratedFunctions } from '../steps/return_values.js' @@ -123,7 +123,7 @@ export async function buildSite(flags: Partial = {}): Promise<{ success, severityCode, netlifyConfig: netlifyConfigA, - logs, + logs: getLogsOutput(logs), configMutations, generatedFunctions: getGeneratedFunctions(returnValues), } diff --git a/packages/build/src/index.ts b/packages/build/src/index.ts index 888e21ed1b..318469080d 100644 --- a/packages/build/src/index.ts +++ b/packages/build/src/index.ts @@ -1,6 +1,7 @@ import { buildSite } from './core/main.js' export { NetlifyPluginConstants } from './core/constants.js' +export type { BufferedLogs as Logs } from './log/logger.js' export type { GeneratedFunction } from './steps/return_values.js' // export the legacy types export type { NetlifyPlugin } from './types/netlify_plugin.js' diff --git a/packages/build/src/log/logger.ts b/packages/build/src/log/logger.ts index d8b8041110..10840ffa43 100644 --- a/packages/build/src/log/logger.ts +++ b/packages/build/src/log/logger.ts @@ -31,10 +31,7 @@ const EMPTY_LINE = '\u{200B}' * When the `buffer` option is true, we return logs instead of printing them * on the console. The logs are accumulated in a `logs` array variable. */ -export const getBufferLogs = (config: { - buffer?: boolean - logger?: (message: string) => void -}): Logs | undefined => { +export const getBufferLogs = (config: { buffer?: boolean; logger?: (message: string) => void }): Logs | undefined => { const { buffer = false, logger } = config if (logger) { @@ -79,6 +76,17 @@ export const log = function ( console.log(stringC) } +// Returns a `logs` object to be returned in the public interface, +// always containing a `stderr` and `stdout` arrays, regardless of +// whether the `buffer` input property was used. +export const getLogsOutput = (logs: Logs | undefined): BufferedLogs | undefined => { + if (!logs || logsAreBuffered(logs)) { + return logs + } + + return { stdout: [], stderr: [] } +} + const serializeIndentedArray = function (array) { return serializeArray(array.map(serializeIndentedItem)) } diff --git a/packages/build/tests/log/snapshots/tests.js.md b/packages/build/tests/log/snapshots/tests.js.md index 0782f03e14..2513e7c1d9 100644 --- a/packages/build/tests/log/snapshots/tests.js.md +++ b/packages/build/tests/log/snapshots/tests.js.md @@ -204,33 +204,3 @@ Generated by [AVA](https://avajs.dev). - inputs: {}␊ origin: config␊ package: ./plugin` - -## Accepts a custom log function - -> Snapshot 1 - - `␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - > Version @netlify/build 1.0.0␊ - > Flags debug: true␊ - repositoryRoot: packages/build/tests/log/fixtures/verbose␊ - testOpts:␊ - pluginsListUrl: test␊ - silentLingeringProcesses: true␊ - verbose: true␊ - > Current directory packages/build/tests/log/fixtures/verbose␊ - > Config file packages/build/tests/log/fixtures/verbose/netlify.toml␊ - > Resolved config build:␊ - publish: packages/build/tests/log/fixtures/verbose␊ - publishOrigin: default␊ - plugins:␊ - - inputs: {}␊ - origin: config␊ - package: ./plugin␊ - > Context production␊ - > Loading plugins - ./plugin@1.0.0 from netlify.toml␊ - ./plugin (onPreBuild event) ␊ - ────────────────────────────────────────────────────────────────Step completed.(./plugin onPreBuild completed in 1ms)Build step duration: ./plugin onPreBuild completed in 1msStep starting.Step started.Step ended.␊ - Netlify Build Complete ␊ - ────────────────────────────────────────────────────────────────(Netlify Build completed in 1ms)Build step duration: Netlify Build completed in 1ms` diff --git a/packages/build/tests/log/snapshots/tests.js.snap b/packages/build/tests/log/snapshots/tests.js.snap index 845e53837959276f8a128120318597ccb6a67432..d107a909d4ddf98a27d21896e3ebdbc659de1baf 100644 GIT binary patch literal 991 zcmV<510eiCRzVg1Yy*WDWP!FskS9Up{EFA@20Ks`R3pu|yAmuEOyd0cp>vW$t&0;~AxG~_kcBhjs%%VW(s<+y zr?s3bfM9x23+F0;SevFI%lpLg(wn@swgx7HR`$tO=*dhse&%uuQHB zVpGPQrOwR3x)Nb~o9s**Di>`pJXK_KXeeUQj12MQI$-urU8t00+MwWBK(qF)Yzhf= zR*x5H6A9DNK-cvx==smyEQlJkFb@+Z0~l*Exb{4F4 z7-4L51!(oiAg6~U=YJthzb#4AuXob)pGs3I*FPps&^5r{6{{|>6}ej5HA~P9#ZAqX znsp7b%rU!=Ep%O)t3C(`caV1?nWuP0Yy(v!9aQf`VW;9hKpHD<; z&a?gE{vGk$kXR{#D{GHriDu>6<*# N{saLxrxN}Z003?{+7JK$ literal 1096 zcmV-O1h@M^RzV_kXtjZg>PR<(xRl1VfyJ~ zmI)T`?|t{h+WMM%KKpX*!RO!Ji(VrunV-V5+SGNQ=#O81ywz_MZNodXB#n*$o@$Rq z?16pU-|BC<6)%)1C<4fESQQDdtuO+xSQ?US?CHKSoIokkBT*oE?U<36Vr7armnU3V zq!`B~pGE*q^rmD=A_rorszPeomBeqYN@{5>AgNIEuG9rusf#^pQY1nOCy2ITP+5zb zA(IwUcajJeK88~_I+q3HQn~xQHjqv~z_B36Fqay$1S*oca6d(*7$!nhNPTBwAIYc* zWRcPacGv- zWL3(}&(X8lwDAekq=6{Q6^Qw-&N?LO)iC!1COyc7RJd?G_yiyUcy6t+Fh(L#xO5ma z@!_F3mOKJHkqoKB?wh%wZcQkY3ibu%rH0Fc&E?6wotvCgZr22cA_G&pTPD*ZG(^=q z93f254h8xNDEagpYWlrHO~2hy(|<}$!Cn6tIYAQuf0wM9gU!*^!m4qA zuFqzyuH>w1B+C@D3)7_q&>Pjuc;ax;Qi|V8iG=i$2S6f|?(#MtU7EmOv~lOI`G5eqz>-_B=Mq6f&KD zt)ssqLY+Cm0hz{Dg`9R)S>a3VBC40) zYxZboD(oN_TA>QN(M4cEFI=wFvf#Uh?BLkgBk~zQjJP;o zg>8F{b9x{(qt!zBZ9jRZ%ulJaAV&ZJp$u6_<$`^$$(8W5DFaPeZAHGCy6Lkin_g5% z{(8h*QWD37?oD&A=aUb*?w?QG*+9E$ zlD^3$t?42j?xZP}Oduewh|Sm~9psg+3*0$l^ovdd@#JodZu^G|+K13R-24&xoUY+I z19YcbSbBV(>k@9m@Z5F>o#tS!H(0DSSn { const logger = (message) => { logs.push(message) } - await new Fixture('./fixtures/verbose') - .withFlags({ logger, verbose: true }) - .runBuildProgrammatic() + const result = await new Fixture('./fixtures/verbose').withFlags({ logger, verbose: true }).runBuildProgrammatic() + t.deepEqual(result.logs.stdout, []) + t.deepEqual(result.logs.stderr, []) t.true(logs.length > 0, 'logger should have been called with messages') - t.true(logs.some(log => log.includes('Netlify Build')), 'logs should contain build header') - t.true(logs.some(log => log.includes('onPreBuild')), 'logs should contain plugin event') + t.true( + logs.some((log) => log.includes('Netlify Build')), + 'logs should contain build header', + ) + t.true( + logs.some((log) => log.includes('onPreBuild')), + 'logs should contain plugin event', + ) }) From 8e6584f56c6888ab4b0fb772a107177d9e503046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Wed, 15 Oct 2025 12:27:23 +0100 Subject: [PATCH 5/7] fix: pass `logFunction` to plugins and core steps --- packages/build/src/log/logger.ts | 16 +++-------- .../with_plugin_and_functions/manifest.yml | 2 ++ .../with_plugin_and_functions/netlify.toml | 2 ++ .../netlify/functions/hello.mjs | 5 ++++ .../with_plugin_and_functions/plugin.js | 3 ++ packages/build/tests/log/tests.js | 28 +++++++++++-------- 6 files changed, 33 insertions(+), 23 deletions(-) create mode 100644 packages/build/tests/log/fixtures/with_plugin_and_functions/manifest.yml create mode 100644 packages/build/tests/log/fixtures/with_plugin_and_functions/netlify.toml create mode 100644 packages/build/tests/log/fixtures/with_plugin_and_functions/netlify/functions/hello.mjs create mode 100644 packages/build/tests/log/fixtures/with_plugin_and_functions/plugin.js diff --git a/packages/build/src/log/logger.ts b/packages/build/src/log/logger.ts index 10840ffa43..bf1bef75af 100644 --- a/packages/build/src/log/logger.ts +++ b/packages/build/src/log/logger.ts @@ -212,15 +212,7 @@ export const getSystemLogger = function ( return (...args) => fileDescriptor.write(`${reduceLogLines(args)}\n`) } -export const addOutputFlusher = (logs: Logs, outputFlusher: OutputFlusher): Logs => { - if (logsAreBuffered(logs)) { - return { - ...logs, - outputFlusher, - } - } - - return { - outputFlusher, - } -} +export const addOutputFlusher = (logs: Logs, outputFlusher: OutputFlusher): Logs => ({ + ...logs, + outputFlusher, +}) diff --git a/packages/build/tests/log/fixtures/with_plugin_and_functions/manifest.yml b/packages/build/tests/log/fixtures/with_plugin_and_functions/manifest.yml new file mode 100644 index 0000000000..a3512f0259 --- /dev/null +++ b/packages/build/tests/log/fixtures/with_plugin_and_functions/manifest.yml @@ -0,0 +1,2 @@ +name: test +inputs: [] diff --git a/packages/build/tests/log/fixtures/with_plugin_and_functions/netlify.toml b/packages/build/tests/log/fixtures/with_plugin_and_functions/netlify.toml new file mode 100644 index 0000000000..81b0ce8bb1 --- /dev/null +++ b/packages/build/tests/log/fixtures/with_plugin_and_functions/netlify.toml @@ -0,0 +1,2 @@ +[[plugins]] +package = "./plugin" diff --git a/packages/build/tests/log/fixtures/with_plugin_and_functions/netlify/functions/hello.mjs b/packages/build/tests/log/fixtures/with_plugin_and_functions/netlify/functions/hello.mjs new file mode 100644 index 0000000000..f45d45d8d1 --- /dev/null +++ b/packages/build/tests/log/fixtures/with_plugin_and_functions/netlify/functions/hello.mjs @@ -0,0 +1,5 @@ +export default async () => new Response("Hello") + +export const config = { + path: "/hello" +} \ No newline at end of file diff --git a/packages/build/tests/log/fixtures/with_plugin_and_functions/plugin.js b/packages/build/tests/log/fixtures/with_plugin_and_functions/plugin.js new file mode 100644 index 0000000000..f7e96fc421 --- /dev/null +++ b/packages/build/tests/log/fixtures/with_plugin_and_functions/plugin.js @@ -0,0 +1,3 @@ +export const onPreBuild = function () { + console.log('test') +} diff --git a/packages/build/tests/log/tests.js b/packages/build/tests/log/tests.js index 642442b14f..ebe8f798b7 100644 --- a/packages/build/tests/log/tests.js +++ b/packages/build/tests/log/tests.js @@ -59,22 +59,28 @@ test('Does not truncate long redirects in logs', async (t) => { t.false(output.includes('999')) }) -test('Accepts a custom log function', async (t) => { +test.only('Accepts a custom log function', async (t) => { const logs = [] const logger = (message) => { logs.push(message) } - const result = await new Fixture('./fixtures/verbose').withFlags({ logger, verbose: true }).runBuildProgrammatic() + const result = await new Fixture('./fixtures/with_plugin_and_functions') + .withFlags({ logger, verbose: true }) + .runBuildProgrammatic() t.deepEqual(result.logs.stdout, []) t.deepEqual(result.logs.stderr, []) - t.true(logs.length > 0, 'logger should have been called with messages') - t.true( - logs.some((log) => log.includes('Netlify Build')), - 'logs should contain build header', - ) - t.true( - logs.some((log) => log.includes('onPreBuild')), - 'logs should contain plugin event', - ) + + t.true(logs.length > 0) + + // From main logic. + t.true(logs.some((log) => log.includes('Netlify Build'))) + t.true(logs.some((log) => log.includes('onPreBuild'))) + + // From core step. + t.true(logs.some((log) => log.includes('Packaging Functions from '))) + + // From plugin. + t.true(logs.some((log) => log.includes('Step started.'))) + t.true(logs.some((log) => log.includes('Step ended.'))) }) From 63a6a262775b8f3801cd77a1cd4ccb04e1389720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Wed, 15 Oct 2025 12:30:27 +0100 Subject: [PATCH 6/7] fix: simplify types --- packages/build/src/index.ts | 2 +- packages/build/src/log/logger.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/build/src/index.ts b/packages/build/src/index.ts index 318469080d..4e7b97b525 100644 --- a/packages/build/src/index.ts +++ b/packages/build/src/index.ts @@ -1,7 +1,7 @@ import { buildSite } from './core/main.js' export { NetlifyPluginConstants } from './core/constants.js' -export type { BufferedLogs as Logs } from './log/logger.js' +export type { LogOutput as Logs } from './log/logger.js' export type { GeneratedFunction } from './steps/return_values.js' // export the legacy types export type { NetlifyPlugin } from './types/netlify_plugin.js' diff --git a/packages/build/src/log/logger.ts b/packages/build/src/log/logger.ts index bf1bef75af..b6199e1eb7 100644 --- a/packages/build/src/log/logger.ts +++ b/packages/build/src/log/logger.ts @@ -76,15 +76,17 @@ export const log = function ( console.log(stringC) } +export type LogOutput = Pick + // Returns a `logs` object to be returned in the public interface, // always containing a `stderr` and `stdout` arrays, regardless of // whether the `buffer` input property was used. -export const getLogsOutput = (logs: Logs | undefined): BufferedLogs | undefined => { - if (!logs || logsAreBuffered(logs)) { - return logs +export const getLogsOutput = (logs: Logs | undefined): LogOutput => { + if (!logs || !logsAreBuffered(logs)) { + return { stdout: [], stderr: [] } } - return { stdout: [], stderr: [] } + return { stdout: logs.stdout, stderr: logs.stderr } } const serializeIndentedArray = function (array) { From 1665e1ccddf8eefc85c7ca8d6bbbba6d8b002587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Wed, 15 Oct 2025 12:37:16 +0100 Subject: [PATCH 7/7] chore: ooops --- packages/build/tests/log/tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/build/tests/log/tests.js b/packages/build/tests/log/tests.js index ebe8f798b7..1de8a698d9 100644 --- a/packages/build/tests/log/tests.js +++ b/packages/build/tests/log/tests.js @@ -59,7 +59,7 @@ test('Does not truncate long redirects in logs', async (t) => { t.false(output.includes('999')) }) -test.only('Accepts a custom log function', async (t) => { +test('Accepts a custom log function', async (t) => { const logs = [] const logger = (message) => { logs.push(message)