diff --git a/packages/build/src/core/main.ts b/packages/build/src/core/main.ts index 7c7c4e5b3a..fae361d1d4 100644 --- a/packages/build/src/core/main.ts +++ b/packages/build/src/core/main.ts @@ -3,8 +3,8 @@ 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 { 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' 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 }> { @@ -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/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/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/index.ts b/packages/build/src/index.ts index 888e21ed1b..4e7b97b525 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 { 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 4e82b61497..b6199e1eb7 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,13 +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 - if (!buffer) { - return +export const getBufferLogs = (config: { buffer?: boolean; logger?: (message: string) => void }): Logs | undefined => { + const { buffer = false, logger } = config + + if (logger) { + return { logFunction: logger } } - return { stdout: [], stderr: [] } + if (buffer) { + return { stdout: [], stderr: [] } + } } // Core logging utility, used by the other methods. @@ -64,9 +67,28 @@ export const log = function ( return } + if (typeof logs?.logFunction === 'function') { + logs.logFunction(stringC) + + return + } + 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): LogOutput => { + if (!logs || !logsAreBuffered(logs)) { + return { stdout: [], stderr: [] } + } + + return { stdout: logs.stdout, stderr: logs.stderr } +} + const serializeIndentedArray = function (array) { return serializeArray(array.map(serializeIndentedItem)) } @@ -75,61 +97,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 +184,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, @@ -192,15 +214,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/src/log/messages/config.js b/packages/build/src/log/messages/config.js index 32342369b5..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'] +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/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/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 601befc90f..1de8a698d9 100644 --- a/packages/build/tests/log/tests.js +++ b/packages/build/tests/log/tests.js @@ -58,3 +58,29 @@ 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 logger = (message) => { + logs.push(message) + } + 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) + + // 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.'))) +}) 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,