Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/commands/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
NETLIFY_CYAN,
USER_AGENT,
logError,
isBrokenPipe,
} from '../utils/command-helpers.js'
import execa from '../utils/execa.js'
import { EXIT_CODES } from '../utils/exit-codes.js'
Expand Down Expand Up @@ -76,9 +77,22 @@ export const CI_FORCED_COMMANDS = {

const SYSTEM_INFO_TIMEOUT = 5_000

const exitIfBrokenPipe = (err: NodeJS.ErrnoException) => {
if (isBrokenPipe(err)) {
process.exit(0)
}
throw err
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

process.stdout.on('error', exitIfBrokenPipe)
process.stderr.on('error', exitIfBrokenPipe)

let isHandlingUncaughtException = false

process.on('uncaughtException', async (err: AddressInUseError | Error) => {
if (isBrokenPipe(err)) {
process.exit(0)
}
if (isHandlingUncaughtException) {
process.exit(1)
}
Expand Down
27 changes: 23 additions & 4 deletions src/utils/command-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,31 @@ export const getToken = async (tokenFromOptions?: string): Promise<TokenTuple> =
// 'functions:invoke' need to return the data from the function as is
const isDefaultJson = () => argv[0] === 'functions:invoke' || (argv[0] === 'api' && !argv.includes('--list'))

export const isBrokenPipe = (err: unknown): boolean => {
if (!err || typeof err !== 'object') {
return false
}
const code = 'code' in err ? err.code : undefined
return code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED'
}

const writeOrExit = (stream: NodeJS.WriteStream, chunk: string) => {
try {
stream.write(chunk)
} catch (err) {
if (isBrokenPipe(err)) {
process.exit(0)
}
throw err
}
}

/**
* logs a json message
*/
export const logJson = (message: unknown = '') => {
if (argv.includes('--json') || isDefaultJson()) {
process.stdout.write(JSON.stringify(message, null, 2))
writeOrExit(process.stdout, JSON.stringify(message, null, 2))
}
}

Expand All @@ -168,7 +187,7 @@ export const log = (message = '', ...args: string[]) => {
return
}
message = typeof message === 'string' ? message : inspect(message)
process.stdout.write(`${format(message, ...args)}\n`)
writeOrExit(process.stdout, `${format(message, ...args)}\n`)
}

export const logPadded = (message = '', ...args: string[]) => {
Expand Down Expand Up @@ -204,9 +223,9 @@ export const logError = (message: unknown): void => {

const bang = chalk.red(BANG)
if (process.env.DEBUG) {
process.stderr.write(` ${bang} Warning: ${err.stack?.split('\n').join(`\n ${bang} `)}\n`)
writeOrExit(process.stderr, ` ${bang} Warning: ${err.stack?.split('\n').join(`\n ${bang} `)}\n`)
} else {
process.stderr.write(` ${bang} ${chalk.red(`${err.name}:`)} ${err.message}\n`)
writeOrExit(process.stderr, ` ${bang} ${chalk.red(`${err.name}:`)} ${err.message}\n`)
}
}

Expand Down
51 changes: 49 additions & 2 deletions tests/unit/utils/command-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,53 @@
import { describe, expect, test } from 'vitest'
import { describe, expect, test, vi } from 'vitest'

import { isBrokenPipe, log, normalizeConfig } from '../../../src/utils/command-helpers.js'

describe('normalizeConfig', () => {
test('should remove publish and publishOrigin property if publishOrigin is "default"', () => {
const config = { build: { publish: 'a', publishOrigin: 'default' } }

// @ts-expect-error TS(2345) FIXME: Argument of type '{ build: { publish: string; publ... Remove this comment to see the full error message
expect(normalizeConfig(config)).toEqual({ build: {} })
})

test('should return same config object if publishOrigin is not "default"', () => {
const config = { build: { publish: 'a', publishOrigin: 'b' } }

// @ts-expect-error TS(2345) FIXME: Argument of type '{ build: { publish: string; publ... Remove this comment to see the full error message
expect(normalizeConfig(config)).toBe(config)
})
})

describe('isBrokenPipe', () => {
test('matches EPIPE and destroyed stream codes', () => {
expect(isBrokenPipe({ code: 'EPIPE' })).toBe(true)
expect(isBrokenPipe({ code: 'ERR_STREAM_DESTROYED' })).toBe(true)
expect(isBrokenPipe({ code: 'EIO' })).toBe(false)
expect(isBrokenPipe(null)).toBe(false)
})
})

describe('log', () => {
test('exits 0 when stdout write throws EPIPE', () => {
const write = vi.spyOn(process.stdout, 'write').mockImplementation(() => {
const err = new Error('broken pipe') as NodeJS.ErrnoException
err.code = 'EPIPE'
throw err
})
const exit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('exited')
})

expect(() => {
log('hello')
}).toThrow('exited')
expect(exit).toHaveBeenCalledWith(0)

write.mockRestore()
exit.mockRestore()
})
})

import { normalizeConfig } from '../../../src/utils/command-helpers.js'

describe('normalizeConfig', () => {
test('should remove publish and publishOrigin property if publishOrigin is "default"', () => {
Expand Down