Skip to content

Commit 2a4e213

Browse files
authoredMay 28, 2024··
feat: pass output flusher to callChild (#5677)
* feat: pass output flusher to plugins * refactor: move file to TypeScript
1 parent be380af commit 2a4e213

File tree

5 files changed

+146
-105
lines changed

5 files changed

+146
-105
lines changed
 

‎packages/build/src/log/output_flusher.ts

+18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { stdout, stderr } from 'process'
12
import { Transform } from 'stream'
23

4+
import type { StandardStreams } from './stream.js'
5+
36
const flusherSymbol = Symbol.for('@netlify/output-gate')
47

58
/**
@@ -46,3 +49,18 @@ export class OutputFlusherTransform extends Transform {
4649
callback()
4750
}
4851
}
52+
53+
export const getStandardStreams = (outputFlusher?: OutputFlusher): StandardStreams => {
54+
if (!outputFlusher) {
55+
return {
56+
stdout,
57+
stderr,
58+
}
59+
}
60+
61+
return {
62+
outputFlusher,
63+
stdout: new OutputFlusherTransform(outputFlusher).pipe(stdout),
64+
stderr: new OutputFlusherTransform(outputFlusher).pipe(stderr),
65+
}
66+
}

‎packages/build/src/log/stream.js

-101
This file was deleted.

‎packages/build/src/log/stream.ts

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { promisify } from 'util'
2+
3+
import type { ChildProcess } from '../plugins/spawn.js'
4+
5+
import { BufferedLogs, logsAreBuffered, Logs } from './logger.js'
6+
import type { OutputFlusher } from './output_flusher.js'
7+
8+
export type StandardStreams = {
9+
stderr: NodeJS.WriteStream
10+
stdout: NodeJS.WriteStream
11+
outputFlusher?: OutputFlusher
12+
}
13+
14+
type LogsListener = (logs: string[], outputFlusher: OutputFlusher | undefined, chunk: Buffer) => void
15+
type LogsListeners = { stderrListener: LogsListener; stdoutListener: LogsListener }
16+
17+
// TODO: replace with `timers/promises` after dropping Node < 15.0.0
18+
const pSetTimeout = promisify(setTimeout)
19+
20+
// We try to use `stdio: inherit` because it keeps `stdout/stderr` as `TTY`,
21+
// which solves many problems. However we can only do it in build.command.
22+
// Plugins have several events, so need to be switch on and off instead.
23+
// In buffer mode, `pipe` is necessary.
24+
export const getBuildCommandStdio = function (logs: Logs) {
25+
if (logsAreBuffered(logs)) {
26+
return 'pipe'
27+
}
28+
29+
return 'inherit'
30+
}
31+
32+
// Add build command output
33+
export const handleBuildCommandOutput = function (
34+
{ stdout: commandStdout, stderr: commandStderr }: { stdout: string; stderr: string },
35+
logs: Logs,
36+
) {
37+
if (!logsAreBuffered(logs)) {
38+
return
39+
}
40+
41+
pushBuildCommandOutput(commandStdout, logs.stdout)
42+
pushBuildCommandOutput(commandStderr, logs.stderr)
43+
}
44+
45+
const pushBuildCommandOutput = function (output: string, logsArray: string[]) {
46+
if (output === '') {
47+
return
48+
}
49+
50+
logsArray.push(output)
51+
}
52+
53+
// Start plugin step output
54+
export const pipePluginOutput = function (childProcess: ChildProcess, logs: Logs, standardStreams: StandardStreams) {
55+
if (!logsAreBuffered(logs)) {
56+
return streamOutput(childProcess, standardStreams)
57+
}
58+
59+
return pushOutputToLogs(childProcess, logs, standardStreams.outputFlusher)
60+
}
61+
62+
// Stop streaming/buffering plugin step output
63+
export const unpipePluginOutput = async function (
64+
childProcess: ChildProcess,
65+
logs: Logs,
66+
listeners: LogsListeners,
67+
standardStreams: StandardStreams,
68+
) {
69+
// Let `childProcess` `stdout` and `stderr` flush before stopping redirecting
70+
await pSetTimeout(0)
71+
72+
if (!logsAreBuffered(logs)) {
73+
return unstreamOutput(childProcess, standardStreams)
74+
}
75+
76+
unpushOutputToLogs(childProcess, listeners.stdoutListener, listeners.stderrListener)
77+
}
78+
79+
// Usually, we stream stdout/stderr because it is more efficient
80+
const streamOutput = function (childProcess: ChildProcess, standardStreams: StandardStreams) {
81+
childProcess.stdout?.pipe(standardStreams.stdout)
82+
childProcess.stderr?.pipe(standardStreams.stderr)
83+
}
84+
85+
const unstreamOutput = function (childProcess: ChildProcess, standardStreams: StandardStreams) {
86+
childProcess.stdout?.unpipe(standardStreams.stdout)
87+
childProcess.stderr?.unpipe(standardStreams.stderr)
88+
}
89+
90+
// In tests, we push to the `logs` array instead
91+
const pushOutputToLogs = function (
92+
childProcess: ChildProcess,
93+
logs: BufferedLogs,
94+
outputFlusher?: OutputFlusher,
95+
): LogsListeners {
96+
const stdoutListener = logsListener.bind(null, logs.stdout, outputFlusher)
97+
const stderrListener = logsListener.bind(null, logs.stderr, outputFlusher)
98+
99+
childProcess.stdout?.on('data', stdoutListener)
100+
childProcess.stderr?.on('data', stderrListener)
101+
102+
return { stdoutListener, stderrListener }
103+
}
104+
105+
const logsListener: LogsListener = function (logs, outputFlusher, chunk) {
106+
if (outputFlusher) {
107+
outputFlusher.flush()
108+
}
109+
110+
logs.push(chunk.toString().trimEnd())
111+
}
112+
113+
const unpushOutputToLogs = function (
114+
childProcess: ChildProcess,
115+
stdoutListener: LogsListener,
116+
stderrListener: LogsListener,
117+
) {
118+
childProcess.stdout?.removeListener('data', stdoutListener)
119+
childProcess.stderr?.removeListener('data', stderrListener)
120+
}

‎packages/build/src/plugins/spawn.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { callChild, getEventFromChild } from './ipc.js'
2222
import { PluginsOptions } from './node_version.js'
2323
import { getSpawnInfo } from './options.js'
2424

25+
export type ChildProcess = ExecaChildProcess<string>
26+
2527
const CHILD_MAIN_FILE = fileURLToPath(new URL('child/main.js', import.meta.url))
2628

2729
const require = createRequire(import.meta.url)

‎packages/build/src/steps/plugin.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { context, propagation } from '@opentelemetry/api'
33
import { addErrorInfo } from '../error/info.js'
44
import { addOutputFlusher } from '../log/logger.js'
55
import { logStepCompleted } from '../log/messages/ipc.js'
6+
import { getStandardStreams } from '../log/output_flusher.js'
67
import { pipePluginOutput, unpipePluginOutput } from '../log/stream.js'
78
import { callChild } from '../plugins/ipc.js'
89
import { getSuccessStatus } from '../status/success.js'
@@ -38,12 +39,13 @@ export const firePluginStep = async function ({
3839
debug,
3940
verbose,
4041
}) {
41-
const listeners = pipePluginOutput(childProcess, logs, outputFlusher)
42+
const standardStreams = getStandardStreams(outputFlusher)
43+
const listeners = pipePluginOutput(childProcess, logs, standardStreams)
4244

4345
const otelCarrier = {}
4446
propagation.inject(context.active(), otelCarrier)
4547

46-
const logsA = addOutputFlusher(logs, outputFlusher)
48+
const logsA = outputFlusher ? addOutputFlusher(logs, outputFlusher) : logs
4749

4850
try {
4951
const configSideFiles = await listConfigSideFiles([headersPath, redirectsPath])
@@ -63,7 +65,7 @@ export const firePluginStep = async function ({
6365
constants,
6466
otelCarrier,
6567
},
66-
logs,
68+
logs: logsA,
6769
verbose,
6870
})
6971
const {
@@ -104,7 +106,7 @@ export const firePluginStep = async function ({
104106
})
105107
return { newError }
106108
} finally {
107-
await unpipePluginOutput(childProcess, logs, listeners)
109+
await unpipePluginOutput(childProcess, logs, listeners, standardStreams)
108110
logStepCompleted(logs, verbose)
109111
}
110112
}

0 commit comments

Comments
 (0)
Please sign in to comment.