diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 736805e498..bccf33fd25 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { "packages/build-info": "7.13.2", - "packages/build": "29.41.6", + "packages/build": "29.42.1", "packages/edge-bundler": "12.0.1", "packages/cache-utils": "5.1.5", "packages/config": "20.12.5", diff --git a/package-lock.json b/package-lock.json index 9e0c0b093a..3259249a55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6658,9 +6658,9 @@ "link": true }, "node_modules/@netlify/plugins-list": { - "version": "6.79.0", - "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.79.0.tgz", - "integrity": "sha512-ejgn9al6mzCCDTCkmoBfz4PY+aAGsbMeBXGI1YMWycHl8RVmEXJDgR0RwQEFxIIE9HEMfUtg2rFmxgvg2x6i5Q==", + "version": "6.80.0", + "resolved": "https://registry.npmjs.org/@netlify/plugins-list/-/plugins-list-6.80.0.tgz", + "integrity": "sha512-bCKLI51UZ70ziIWsf2nvgPd4XuG6m8AMCoHiYtl/BSsiaSBfmryZnTTqdRXerH09tBRpbPPwzaEgUJwyU9o8Qw==", "license": "MIT", "engines": { "node": "^14.14.0 || >=16.0.0" @@ -26594,7 +26594,7 @@ }, "packages/build": { "name": "@netlify/build", - "version": "29.41.6", + "version": "29.42.1", "license": "MIT", "dependencies": { "@bugsnag/js": "^7.0.0", @@ -26606,7 +26606,7 @@ "@netlify/functions-utils": "^5.2.57", "@netlify/git-utils": "^5.1.1", "@netlify/opentelemetry-utils": "^1.2.1", - "@netlify/plugins-list": "^6.79.0", + "@netlify/plugins-list": "^6.80.0", "@netlify/run-utils": "^5.1.1", "@netlify/zip-it-and-ship-it": "9.33.0", "@sindresorhus/slugify": "^2.0.0", diff --git a/packages/build/CHANGELOG.md b/packages/build/CHANGELOG.md index 2141d0a421..8283173ce1 100644 --- a/packages/build/CHANGELOG.md +++ b/packages/build/CHANGELOG.md @@ -105,6 +105,20 @@ * dependencies * @netlify/config bumped from ^20.8.0 to ^20.8.1 +## [29.42.1](https://github.com/netlify/build/compare/build-v29.42.0...build-v29.42.1) (2024-05-22) + + +### Bug Fixes + +* flush output from "Deploy site" step ([#5652](https://github.com/netlify/build/issues/5652)) ([076b4b0](https://github.com/netlify/build/commit/076b4b07c4595d943b0375edffe97eac22910cfd)) + +## [29.42.0](https://github.com/netlify/build/compare/build-v29.41.6...build-v29.42.0) (2024-05-22) + + +### Features + +* reduce build log verbosity ([#5643](https://github.com/netlify/build/issues/5643)) ([58def4f](https://github.com/netlify/build/commit/58def4f72eec4c1eb20cddceab0d8fd3c4420e45)) + ## [29.41.6](https://github.com/netlify/build/compare/build-v29.41.5...build-v29.41.6) (2024-05-22) diff --git a/packages/build/package.json b/packages/build/package.json index 653b8a2a4f..7835b4b17a 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -1,6 +1,6 @@ { "name": "@netlify/build", - "version": "29.41.6", + "version": "29.42.1", "description": "Netlify build module", "type": "module", "exports": "./lib/index.js", @@ -76,7 +76,7 @@ "@netlify/functions-utils": "^5.2.57", "@netlify/git-utils": "^5.1.1", "@netlify/opentelemetry-utils": "^1.2.1", - "@netlify/plugins-list": "^6.79.0", + "@netlify/plugins-list": "^6.80.0", "@netlify/run-utils": "^5.1.1", "@netlify/zip-it-and-ship-it": "9.33.0", "@sindresorhus/slugify": "^2.0.0", diff --git a/packages/build/src/core/feature_flags.ts b/packages/build/src/core/feature_flags.ts index 4c3022327d..a59bb864da 100644 --- a/packages/build/src/core/feature_flags.ts +++ b/packages/build/src/core/feature_flags.ts @@ -20,5 +20,6 @@ export const DEFAULT_FEATURE_FLAGS: FeatureFlags = { buildbot_zisi_system_log: false, edge_functions_cache_cli: false, edge_functions_system_logger: false, + netlify_build_reduced_output: false, netlify_build_updated_plugin_compatibility: false, } diff --git a/packages/build/src/core/types.ts b/packages/build/src/core/types.ts index 535be6325d..d746c361df 100644 --- a/packages/build/src/core/types.ts +++ b/packages/build/src/core/types.ts @@ -52,6 +52,7 @@ type EventHandlers = { | { handler: NetlifyPlugin[K] description: string + quiet?: boolean } } diff --git a/packages/build/src/install/missing.js b/packages/build/src/install/missing.js index b189fc843f..8df8d5eb29 100644 --- a/packages/build/src/install/missing.js +++ b/packages/build/src/install/missing.js @@ -17,7 +17,7 @@ import { addExactDependencies } from './main.js' // their `package.json`. export const installMissingPlugins = async function ({ missingPlugins, autoPluginsDir, mode, logs }) { const packages = missingPlugins.map(getPackage) - logInstallMissingPlugins(logs, packages) + logInstallMissingPlugins(logs, missingPlugins, packages) if (packages.length === 0) { return diff --git a/packages/build/src/log/logger.ts b/packages/build/src/log/logger.ts index cae5b84d12..1aa33cad9f 100644 --- a/packages/build/src/log/logger.ts +++ b/packages/build/src/log/logger.ts @@ -4,10 +4,17 @@ import figures from 'figures' import indentString from 'indent-string' import { getHeader } from './header.js' +import { OutputFlusher } from './output_flusher.js' import { serializeArray, serializeObject } from './serialize.js' import { THEME } from './theme.js' -export type BufferedLogs = { stdout: string[]; stderr: string[] } +export type Logs = BufferedLogs | StreamedLogs +export type BufferedLogs = { stdout: string[]; stderr: string[]; outputFlusher?: OutputFlusher } +export type StreamedLogs = { outputFlusher?: OutputFlusher } + +export const logsAreBuffered = (logs: Logs | undefined): logs is BufferedLogs => { + return logs !== undefined && 'stdout' in logs +} const INDENT_SIZE = 2 @@ -35,7 +42,7 @@ export const getBufferLogs = (config: { buffer?: boolean }): BufferedLogs | unde // This should be used instead of `console.log()` as it allows us to instrument // how any build logs is being printed. export const log = function ( - logs: BufferedLogs | undefined, + logs: Logs | undefined, string: string, config: { indent?: boolean; color?: (string: string) => string } = {}, ) { @@ -44,10 +51,12 @@ export const log = function ( const stringB = String(stringA).replace(EMPTY_LINES_REGEXP, EMPTY_LINE) const stringC = color === undefined ? stringB : color(stringB) - if (logs !== undefined) { - // `logs` is a stateful variable + logs?.outputFlusher?.flush() + if (logsAreBuffered(logs)) { + // `logs` is a stateful variable logs.stdout.push(stringC) + return } @@ -178,3 +187,16 @@ export const getSystemLogger = function ( return (...args) => fileDescriptor.write(`${reduceLogLines(args)}\n`) } + +export const addOutputGate = (logs: Logs, outputFlusher: OutputFlusher): Logs => { + if (logsAreBuffered(logs)) { + return { + ...logs, + outputFlusher, + } + } + + return { + outputFlusher, + } +} diff --git a/packages/build/src/log/messages/core.ts b/packages/build/src/log/messages/core.ts index 7ad0505433..e856a3f8c2 100644 --- a/packages/build/src/log/messages/core.ts +++ b/packages/build/src/log/messages/core.ts @@ -7,6 +7,7 @@ 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 { OutputFlusher } from '../output_flusher.js' import { THEME } from '../theme.js' import { logConfigOnError } from './config.js' @@ -29,13 +30,17 @@ export const logBuildError = function ({ error, netlifyConfig, logs, debug }) { export const logBuildSuccess = function (logs) { logHeader(logs, 'Netlify Build Complete') - logMessage(logs, '') } -export const logTimer = function (logs, durationNs, timerName, systemLog) { +export const logTimer = function (logs, durationNs, timerName, systemLog, outputFlusher?: OutputFlusher) { const durationMs = roundTimerToMillisecs(durationNs) const duration = prettyMs(durationMs) - log(logs, THEME.dimWords(`(${timerName} completed in ${duration})`)) + + if (!outputFlusher || outputFlusher.flushed) { + log(logs, '') + log(logs, THEME.dimWords(`(${timerName} completed in ${duration})`)) + } + systemLog(`Build step duration: ${timerName} completed in ${durationMs}ms`) } diff --git a/packages/build/src/log/messages/install.js b/packages/build/src/log/messages/install.js index ae04593cc8..873c2a332d 100644 --- a/packages/build/src/log/messages/install.js +++ b/packages/build/src/log/messages/install.js @@ -1,20 +1,13 @@ import { isRuntime } from '../../utils/runtime.js' import { log, logArray, logSubHeader } from '../logger.js' -export const logInstallMissingPlugins = function (logs, packages) { - const runtimes = packages.filter((pkg) => isRuntime(pkg)) - const plugins = packages.filter((pkg) => !isRuntime(pkg)) +export const logInstallMissingPlugins = function (logs, missingPlugins, packages) { + const plugins = missingPlugins.filter((pkg) => !isRuntime(pkg)) if (plugins.length !== 0) { logSubHeader(logs, 'Installing plugins') logArray(logs, packages) } - - if (runtimes.length !== 0) { - const [nextRuntime] = runtimes - - logSubHeader(logs, `Using Next.js Runtime - v${nextRuntime.pluginPackageJson.version}`) - } } export const logInstallIntegrations = function (logs, integrations) { diff --git a/packages/build/src/log/messages/steps.js b/packages/build/src/log/messages/steps.js index 1c9d9ea7be..698aed1515 100644 --- a/packages/build/src/log/messages/steps.js +++ b/packages/build/src/log/messages/steps.js @@ -16,7 +16,3 @@ const getDescription = function ({ coreStepDescription, netlifyConfig, packageNa export const logBuildCommandStart = function (logs, buildCommand) { log(logs, THEME.highlightWords(`$ ${buildCommand}`)) } - -export const logStepSuccess = function (logs) { - logMessage(logs, '') -} diff --git a/packages/build/src/log/output_flusher.ts b/packages/build/src/log/output_flusher.ts new file mode 100644 index 0000000000..c5fbdda656 --- /dev/null +++ b/packages/build/src/log/output_flusher.ts @@ -0,0 +1,48 @@ +import { Transform } from 'stream' + +const flusherSymbol = Symbol.for('@netlify/output-gate') + +/** + * Utility class for conditionally rendering certain output only if additional + * data flows through. The constructor takes a "buffer" function that renders + * the optional data. When flushed, that function is called. + */ +export class OutputFlusher { + private buffer: () => void + + flushed: boolean + + constructor(bufferFn: () => void) { + this.flushed = false + this.buffer = bufferFn + } + + flush() { + if (!this.flushed) { + this.buffer() + this.flushed = true + } + } +} + +/** + * A `Transform` stream that takes an `OutputFlusher` instance and flushes it + * whenever data flows through, before piping the data to its destination. + */ +export class OutputFlusherTransform extends Transform { + [flusherSymbol]: OutputFlusher + + constructor(flusher: OutputFlusher) { + super() + + this[flusherSymbol] = flusher + } + + _transform(chunk: any, _: string, callback: () => void) { + this[flusherSymbol].flush() + + this.push(chunk) + + callback() + } +} diff --git a/packages/build/src/log/stream.js b/packages/build/src/log/stream.js index 5db088fd36..cbc97f9b09 100644 --- a/packages/build/src/log/stream.js +++ b/packages/build/src/log/stream.js @@ -1,15 +1,18 @@ import { stdout, stderr } from 'process' import { promisify } from 'util' +import { logsAreBuffered } from './logger.js' +import { OutputFlusherTransform } from './output_flusher.js' + // TODO: replace with `timers/promises` after dropping Node < 15.0.0 const pSetTimeout = promisify(setTimeout) // We try to use `stdio: inherit` because it keeps `stdout/stderr` as `TTY`, // which solves many problems. However we can only do it in build.command. // Plugins have several events, so need to be switch on and off instead. -// In buffer mode (`logs` not `undefined`), `pipe` is necessary. +// In buffer mode, `pipe` is necessary. export const getBuildCommandStdio = function (logs) { - if (logs !== undefined) { + if (logsAreBuffered(logs)) { return 'pipe' } @@ -18,7 +21,7 @@ export const getBuildCommandStdio = function (logs) { // Add build command output export const handleBuildCommandOutput = function ({ stdout: commandStdout, stderr: commandStderr }, logs) { - if (logs === undefined) { + if (!logsAreBuffered(logs)) { return } @@ -35,12 +38,12 @@ const pushBuildCommandOutput = function (output, logsArray) { } // Start plugin step output -export const pipePluginOutput = function (childProcess, logs) { - if (logs === undefined) { - return streamOutput(childProcess) +export const pipePluginOutput = function (childProcess, logs, outputFlusher) { + if (!logsAreBuffered(logs)) { + return streamOutput(childProcess, outputFlusher) } - return pushOutputToLogs(childProcess, logs) + return pushOutputToLogs(childProcess, logs, outputFlusher) } // Stop streaming/buffering plugin step output @@ -48,7 +51,7 @@ export const unpipePluginOutput = async function (childProcess, logs, listeners) // Let `childProcess` `stdout` and `stderr` flush before stopping redirecting await pSetTimeout(0) - if (logs === undefined) { + if (!logsAreBuffered(logs)) { return unstreamOutput(childProcess) } @@ -56,7 +59,14 @@ export const unpipePluginOutput = async function (childProcess, logs, listeners) } // Usually, we stream stdout/stderr because it is more efficient -const streamOutput = function (childProcess) { +const streamOutput = function (childProcess, outputFlusher) { + if (outputFlusher) { + childProcess.stdout.pipe(new OutputFlusherTransform(outputFlusher)).pipe(stdout) + childProcess.stderr.pipe(new OutputFlusherTransform(outputFlusher)).pipe(stderr) + + return + } + childProcess.stdout.pipe(stdout) childProcess.stderr.pipe(stderr) } @@ -67,15 +77,21 @@ const unstreamOutput = function (childProcess) { } // In tests, we push to the `logs` array instead -const pushOutputToLogs = function (childProcess, logs) { - const stdoutListener = logsListener.bind(null, logs.stdout) - const stderrListener = logsListener.bind(null, logs.stderr) +const pushOutputToLogs = function (childProcess, logs, outputFlusher) { + const stdoutListener = logsListener.bind(null, logs.stdout, outputFlusher) + const stderrListener = logsListener.bind(null, logs.stderr, outputFlusher) + childProcess.stdout.on('data', stdoutListener) childProcess.stderr.on('data', stderrListener) + return { stdoutListener, stderrListener } } -const logsListener = function (logs, chunk) { +const logsListener = function (logs, outputFlusher, chunk) { + if (outputFlusher) { + outputFlusher.flush() + } + logs.push(chunk.toString().trimEnd()) } diff --git a/packages/build/src/plugins_core/deploy/index.js b/packages/build/src/plugins_core/deploy/index.js index cd48749c19..dd02159fd7 100644 --- a/packages/build/src/plugins_core/deploy/index.js +++ b/packages/build/src/plugins_core/deploy/index.js @@ -28,6 +28,9 @@ const coreStep = async function ({ }) { const client = createBuildbotClient(buildbotServerSocket) try { + // buildbot will emit logs. Flush the output to preserve the right order. + logs?.outputFlusher?.flush() + await connectBuildbotClient(client) await saveUpdatedConfig({ configMutations, diff --git a/packages/build/src/plugins_core/pre_cleanup/index.ts b/packages/build/src/plugins_core/pre_cleanup/index.ts index fad0cf290b..e189b63128 100644 --- a/packages/build/src/plugins_core/pre_cleanup/index.ts +++ b/packages/build/src/plugins_core/pre_cleanup/index.ts @@ -24,4 +24,5 @@ export const preCleanup: CoreStep = { coreStepName: 'Pre cleanup', coreStepDescription: () => 'Cleaning up leftover files from previous builds', condition: blobsPresent, + quiet: true, } diff --git a/packages/build/src/plugins_core/pre_dev_cleanup/index.ts b/packages/build/src/plugins_core/pre_dev_cleanup/index.ts index 4c91ede997..c4a41a9c1c 100644 --- a/packages/build/src/plugins_core/pre_dev_cleanup/index.ts +++ b/packages/build/src/plugins_core/pre_dev_cleanup/index.ts @@ -67,4 +67,5 @@ export const preDevCleanup: CoreStep = { coreStepName: 'Pre Dev cleanup', coreStepDescription: () => 'Cleaning up leftover files from previous builds', condition, + quiet: true, } diff --git a/packages/build/src/steps/core_step.ts b/packages/build/src/steps/core_step.ts index 173ec98970..cfaab75cb2 100644 --- a/packages/build/src/steps/core_step.ts +++ b/packages/build/src/steps/core_step.ts @@ -1,5 +1,6 @@ import { setEnvChanges } from '../env/changes.js' import { addErrorInfo, isBuildError } from '../error/info.js' +import { addOutputGate } from '../log/logger.js' import { updateNetlifyConfig, listConfigSideFiles } from './update_config.js' @@ -37,7 +38,10 @@ export const fireCoreStep = async function ({ explicitSecretKeys, edgeFunctionsBootstrapURL, deployId, + outputFlusher, }) { + const logsA = addOutputGate(logs, outputFlusher) + try { const configSideFiles = await listConfigSideFiles([headersPath, redirectsPath]) const childEnvA = setEnvChanges(envChanges, { ...childEnv }) @@ -55,7 +59,7 @@ export const fireCoreStep = async function ({ packagePath, buildbotServerSocket, events, - logs, + logs: logsA, quiet, context, branch, @@ -88,7 +92,7 @@ export const fireCoreStep = async function ({ newConfigMutations, configSideFiles, errorParams, - logs, + logs: logsA, systemLog, debug, }) diff --git a/packages/build/src/steps/get.ts b/packages/build/src/steps/get.ts index 01ce46694d..449c265af1 100644 --- a/packages/build/src/steps/get.ts +++ b/packages/build/src/steps/get.ts @@ -67,6 +67,7 @@ const getEventSteps = function (eventHandlers?: any[]) { coreStepId: `options_${event}`, coreStepName: `options.${event}`, coreStepDescription: () => description, + quiet: eventHandler.quiet, } }) } diff --git a/packages/build/src/steps/plugin.js b/packages/build/src/steps/plugin.js index 6f68103174..50e4eabc8b 100644 --- a/packages/build/src/steps/plugin.js +++ b/packages/build/src/steps/plugin.js @@ -1,6 +1,7 @@ import { context, propagation } from '@opentelemetry/api' import { addErrorInfo } from '../error/info.js' +import { addOutputGate } from '../log/logger.js' import { logStepCompleted } from '../log/messages/ipc.js' import { pipePluginOutput, unpipePluginOutput } from '../log/stream.js' import { callChild } from '../plugins/ipc.js' @@ -31,16 +32,19 @@ export const firePluginStep = async function ({ steps, error, logs, + outputFlusher, systemLog, featureFlags, debug, verbose, }) { - const listeners = pipePluginOutput(childProcess, logs) + const listeners = pipePluginOutput(childProcess, logs, outputFlusher) const otelCarrier = {} propagation.inject(context.active(), otelCarrier) + const logsA = addOutputGate(logs, outputFlusher) + try { const configSideFiles = await listConfigSideFiles([headersPath, redirectsPath]) const { @@ -77,7 +81,7 @@ export const firePluginStep = async function ({ newConfigMutations, configSideFiles, errorParams, - logs, + logs: logsA, systemLog, debug, source: packageName, diff --git a/packages/build/src/steps/return.js b/packages/build/src/steps/return.js index dcc0f0b467..1c9707a324 100644 --- a/packages/build/src/steps/return.js +++ b/packages/build/src/steps/return.js @@ -1,5 +1,4 @@ import { logTimer } from '../log/messages/core.js' -import { logStepSuccess } from '../log/messages/steps.js' import { handleStepError } from './error.js' @@ -22,6 +21,7 @@ export const getStepReturn = function ({ headersPath, redirectsPath, logs, + outputFlusher, debug, timers, durationNs, @@ -48,9 +48,7 @@ export const getStepReturn = function ({ } if (!quiet) { - logStepSuccess(logs) - - logTimer(logs, durationNs, timerName, systemLog) + logTimer(logs, durationNs, timerName, systemLog, outputFlusher) } return { newEnvChanges, netlifyConfig, configMutations, headersPath, redirectsPath, newStatus, timers, metrics } diff --git a/packages/build/src/steps/run_step.ts b/packages/build/src/steps/run_step.ts index 450152a36a..5a7540f00a 100644 --- a/packages/build/src/steps/run_step.ts +++ b/packages/build/src/steps/run_step.ts @@ -3,6 +3,7 @@ import { trace } from '@opentelemetry/api' import { addMutableConstants } from '../core/constants.js' import { logStepStart } from '../log/messages/steps.js' +import { OutputFlusher } from '../log/output_flusher.js' import { runsAlsoOnBuildFailure, runsOnlyOnBuildFailure } from '../plugins/events.js' import { normalizeTagName } from '../report/statsd.js' import { measureDuration } from '../time/main.js' @@ -114,8 +115,19 @@ export const runStep = async function ({ return {} } - if (!quiet && !coreStepQuiet) { - logStepStart({ logs, event, packageName, coreStepDescription, error, netlifyConfig }) + const logPluginStart = + !quiet && !coreStepQuiet + ? () => logStepStart({ logs, event, packageName, coreStepDescription, error, netlifyConfig }) + : () => { + // no-op + } + + let outputFlusher: OutputFlusher | undefined + + if (featureFlags.netlify_build_reduced_output) { + outputFlusher = new OutputFlusher(logPluginStart) + } else { + logPluginStart() } const fireStep = getFireStep(packageName, coreStepId, event) @@ -136,6 +148,7 @@ export const runStep = async function ({ packageName, pluginPackageJson, loadedFrom, + outputFlusher, origin, coreStep, coreStepId, @@ -193,6 +206,7 @@ export const runStep = async function ({ headersPath: headersPathA, redirectsPath: redirectsPathA, logs, + outputFlusher, debug, timers: timersA, durationNs, @@ -295,6 +309,7 @@ const tFireStep = function ({ packageName, pluginPackageJson, loadedFrom, + outputFlusher, origin, coreStep, coreStepId, @@ -346,6 +361,7 @@ const tFireStep = function ({ buildbotServerSocket, events, logs, + outputFlusher, quiet, nodePath, childEnv, @@ -376,6 +392,7 @@ const tFireStep = function ({ packagePath, pluginPackageJson, loadedFrom, + outputFlusher, origin, envChanges, errorParams, diff --git a/packages/build/tests/core/tests.js b/packages/build/tests/core/tests.js index 6a6f55d164..c61de04624 100644 --- a/packages/build/tests/core/tests.js +++ b/packages/build/tests/core/tests.js @@ -79,10 +79,7 @@ test('Event handlers are called', async (t) => { test('Event handlers with description are called', async (t) => { let flag = false - const { - success, - logs: { stdout }, - } = await new Fixture('./fixtures/empty') + const { success } = await new Fixture('./fixtures/empty') .withFlags({ eventHandlers: { onPostBuild: { @@ -99,7 +96,6 @@ test('Event handlers with description are called', async (t) => { t.true(success) t.true(flag) - t.true(stdout.join('\n').includes('Test onPostBuild')) }) test('Event handlers do not displace plugin methods', async (t) => { diff --git a/packages/build/tests/error/snapshots/tests.js.snap b/packages/build/tests/error/snapshots/tests.js.snap index 44b038fdf5..fcc4bc616d 100644 Binary files a/packages/build/tests/error/snapshots/tests.js.snap and b/packages/build/tests/error/snapshots/tests.js.snap differ diff --git a/packages/build/tests/functions/tests.js b/packages/build/tests/functions/tests.js index 2fde6268f2..673156931d 100644 --- a/packages/build/tests/functions/tests.js +++ b/packages/build/tests/functions/tests.js @@ -1,5 +1,4 @@ import { readdir, rm, stat, writeFile } from 'fs/promises' -import { sep } from 'path' import { fileURLToPath } from 'url' import { Fixture, normalizeOutput, removeDir, getTempName } from '@netlify/testing' @@ -95,7 +94,7 @@ test('Functions: internal functions are cleared on the dev timeline', async (t) code: 'ENOENT', }) - const output = await fixture.runDev(() => {}) + await fixture.runDev(() => {}) // After running Netlify Build, the leftover files should have been removed // but the generated files should have been preserved. @@ -105,9 +104,6 @@ test('Functions: internal functions are cleared on the dev timeline', async (t) await t.throwsAsync(() => stat(`${fixture.repositoryRoot}/.netlify/edge-functions/leftover.mjs`), { code: 'ENOENT' }) await stat(`${fixture.repositoryRoot}/.netlify/functions-internal/from-plugin.mjs`) await stat(`${fixture.repositoryRoot}/.netlify/edge-functions/from-plugin.mjs`) - - t.true(output.includes('Cleaning up leftover files from previous builds')) - t.true(output.includes(`Cleaned up .netlify${sep}functions-internal, .netlify${sep}edge-functions`)) }) test('Functions: cleanup is only triggered when there are internal functions', async (t) => { diff --git a/packages/build/tests/log/snapshots/tests.js.snap b/packages/build/tests/log/snapshots/tests.js.snap index d107a909d4..fb0c886c73 100644 Binary files a/packages/build/tests/log/snapshots/tests.js.snap and b/packages/build/tests/log/snapshots/tests.js.snap differ diff --git a/packages/build/tests/plugins/fixtures/mixed_events/manifest.yml b/packages/build/tests/plugins/fixtures/mixed_events/manifest.yml new file mode 100644 index 0000000000..a3512f0259 --- /dev/null +++ b/packages/build/tests/plugins/fixtures/mixed_events/manifest.yml @@ -0,0 +1,2 @@ +name: test +inputs: [] diff --git a/packages/build/tests/plugins/fixtures/mixed_events/netlify.toml b/packages/build/tests/plugins/fixtures/mixed_events/netlify.toml new file mode 100644 index 0000000000..81b0ce8bb1 --- /dev/null +++ b/packages/build/tests/plugins/fixtures/mixed_events/netlify.toml @@ -0,0 +1,2 @@ +[[plugins]] +package = "./plugin" diff --git a/packages/build/tests/plugins/fixtures/mixed_events/plugin.js b/packages/build/tests/plugins/fixtures/mixed_events/plugin.js new file mode 100644 index 0000000000..17da379a6d --- /dev/null +++ b/packages/build/tests/plugins/fixtures/mixed_events/plugin.js @@ -0,0 +1,21 @@ +const noop = () => { + // no-op +} + +export const onPreBuild = async function () { + console.log('Hello from onPreBuild') +} + +export const onBuild = async function () { + await new Promise(resolve => setTimeout(resolve, 1_000)) + + noop() +} + +export const onPostBuild = async function () { + console.log('Hello from onPostBuild') +} + +export const onEnd = async function () { + noop() +} diff --git a/packages/build/tests/plugins/snapshots/tests.js.md b/packages/build/tests/plugins/snapshots/tests.js.md index aa3fcc5a4f..c235927f2f 100644 --- a/packages/build/tests/plugins/snapshots/tests.js.md +++ b/packages/build/tests/plugins/snapshots/tests.js.md @@ -2702,3 +2702,48 @@ Generated by [AVA](https://avajs.dev). ␊ (Netlify Build completed in 1ms)␊ Build step duration: Netlify Build completed in 1ms` + +## Plugin events that do not emit to stderr/stdout are hidden from the logs + +> Snapshot 1 + + `␊ + Netlify Build ␊ + ────────────────────────────────────────────────────────────────␊ + ␊ + > Version␊ + @netlify/build 1.0.0␊ + ␊ + > Flags␊ + debug: false␊ + ␊ + > Current directory␊ + packages/build/tests/plugins/fixtures/mixed_events␊ + ␊ + > Config file␊ + packages/build/tests/plugins/fixtures/mixed_events/netlify.toml␊ + ␊ + > Context␊ + production␊ + ␊ + > Loading plugins␊ + - ./plugin@1.0.0 from netlify.toml␊ + ␊ + ./plugin (onPreBuild event) ␊ + ────────────────────────────────────────────────────────────────␊ + ␊ + Hello from onPreBuild␊ + ␊ + (./plugin onPreBuild completed in 1ms)␊ + ␊ + ./plugin (onPostBuild event) ␊ + ────────────────────────────────────────────────────────────────␊ + ␊ + Hello from onPostBuild␊ + ␊ + (./plugin onPostBuild completed in 1ms)␊ + ␊ + Netlify Build Complete ␊ + ────────────────────────────────────────────────────────────────␊ + ␊ + (Netlify Build completed in 1ms)` diff --git a/packages/build/tests/plugins/snapshots/tests.js.snap b/packages/build/tests/plugins/snapshots/tests.js.snap index e9296c07c6..d91c72e00a 100644 Binary files a/packages/build/tests/plugins/snapshots/tests.js.snap and b/packages/build/tests/plugins/snapshots/tests.js.snap differ diff --git a/packages/build/tests/plugins/tests.js b/packages/build/tests/plugins/tests.js index f1940b2351..d59d1916c1 100644 --- a/packages/build/tests/plugins/tests.js +++ b/packages/build/tests/plugins/tests.js @@ -340,3 +340,10 @@ test('Plugins which export a factory function receive the inputs and a metadata const output = await new Fixture('./fixtures/dynamic_plugin').runWithBuild() t.snapshot(normalizeOutput(output)) }) + +test('Plugin events that do not emit to stderr/stdout are hidden from the logs', async (t) => { + const output = await new Fixture('./fixtures/mixed_events') + .withFlags({ debug: false, featureFlags: { netlify_build_reduced_output: true } }) + .runWithBuild() + t.snapshot(normalizeOutput(output)) +}) diff --git a/packages/build/tests/pre_cleanup/tests.js b/packages/build/tests/pre_cleanup/tests.js index 7af6e8c8ee..6ce58292b9 100644 --- a/packages/build/tests/pre_cleanup/tests.js +++ b/packages/build/tests/pre_cleanup/tests.js @@ -11,17 +11,13 @@ test('Build removes blobs directory before starting', async (t) => { await t.notThrowsAsync(access(blobsDir)) - const { - success, - logs: { stdout }, - } = await fixture + const { success } = await fixture .withFlags({ cwd: fixture.repositoryRoot, }) .runBuildProgrammatic() t.true(success) - t.true(stdout.join('\n').includes('Cleaning up leftover files from previous builds')) await t.throwsAsync(access(blobsDir)) }) @@ -51,10 +47,7 @@ test('monorepo > Build removes blobs directory before starting', async (t) => { const blobsDir = join(fixture.repositoryRoot, 'apps/app-1/.netlify/blobs/deploy') await t.notThrowsAsync(access(blobsDir)) - const { - success, - logs: { stdout }, - } = await fixture + const { success } = await fixture .withFlags({ cwd: fixture.repositoryRoot, packagePath: 'apps/app-1', @@ -62,7 +55,6 @@ test('monorepo > Build removes blobs directory before starting', async (t) => { .runBuildProgrammatic() t.true(success) - t.true(stdout.join('\n').includes('Cleaning up leftover files from previous builds')) await t.throwsAsync(access(blobsDir)) }) diff --git a/packages/zip-it-and-ship-it/tests/v2api.test.ts b/packages/zip-it-and-ship-it/tests/v2api.test.ts index 66910e41b0..859d4be115 100644 --- a/packages/zip-it-and-ship-it/tests/v2api.test.ts +++ b/packages/zip-it-and-ship-it/tests/v2api.test.ts @@ -550,4 +550,27 @@ describe.runIf(semver.gte(nodeVersion, '18.13.0'))('V2 functions API', () => { expect(func.displayName).toBe('SSR Function') expect(func.generator).toBe('next-runtime@1.2.3') }) + + testMany( + 'Bootstrap is imported before user code so it can apply side-effects before the user code runs', + ['bundler_default', 'bundler_esbuild', 'bundler_esbuild_zisi', 'bundler_default_nft', 'bundler_nft'], + async (options) => { + const { files } = await zipFixture('v2-api', { + fixtureDir: FIXTURES_ESM_DIR, + opts: options, + }) + + const unzippedFunctions = await unzipFiles(files) + + const contents = await readFile(join(unzippedFunctions[0].unzipPath, files[0].entryFilename), { + encoding: 'utf-8', + }) + const positionOfBootstrapImport = contents.indexOf('___netlify-bootstrap.mjs') + const positionOfUserCodeImport = contents.indexOf('function.mjs') + + expect(positionOfBootstrapImport).toBeGreaterThan(0) + expect(positionOfUserCodeImport).toBeGreaterThan(0) + expect(positionOfBootstrapImport).toBeLessThan(positionOfUserCodeImport) + }, + ) })