diff --git a/ext/node/polyfills/internal/child_process.ts b/ext/node/polyfills/internal/child_process.ts index 9c9e084787..d3d53b652d 100644 --- a/ext/node/polyfills/internal/child_process.ts +++ b/ext/node/polyfills/internal/child_process.ts @@ -59,6 +59,7 @@ import { Socket } from "node:net"; import { kDetached, kExtraStdio, + kInputOption, kIpc, kNeedsNpmProcessState, } from "ext:deno_process/40_process.js"; @@ -985,6 +986,27 @@ function parseSpawnSyncOutputStreams( } } +function normalizeInput(input: unknown) { + if (input == null) { + return null; + } + if (typeof input === "string") { + return Buffer.from(input); + } + if (input instanceof Uint8Array) { + return input; + } + if (input instanceof DataView) { + return Buffer.from(input.buffer, input.byteOffset, input.byteLength); + } + throw new ERR_INVALID_ARG_TYPE("input", [ + "string", + "Buffer", + "TypedArray", + "DataView", + ], input); +} + export function spawnSync( command: string, args: string[], @@ -992,6 +1014,7 @@ export function spawnSync( ): SpawnSyncResult { const { env = Deno.env.toObject(), + input, stdio = ["pipe", "pipe", "pipe"], shell = false, cwd, @@ -1008,6 +1031,7 @@ export function spawnSync( _channel, // TODO(kt3k): handle this correctly ] = normalizeStdioOption(stdio); [command, args] = buildCommand(command, args ?? [], shell); + const input_ = normalizeInput(input); const result: SpawnSyncResult = {}; try { @@ -1021,6 +1045,7 @@ export function spawnSync( uid, gid, windowsRawArguments: windowsVerbatimArguments, + [kInputOption]: input_, }).outputSync(); const status = output.signal ? null : output.code; diff --git a/ext/process/40_process.js b/ext/process/40_process.js index fde97fac64..a4afe336bd 100644 --- a/ext/process/40_process.js +++ b/ext/process/40_process.js @@ -41,6 +41,9 @@ import { writableStreamForRid, } from "ext:deno_web/06_streams.js"; +// The key for private `input` option for `Deno.Command` +const kInputOption = Symbol("kInputOption"); + function opKill(pid, signo, apiName) { op_kill(pid, signo, apiName); } @@ -404,6 +407,7 @@ function spawnSync(command, { stdout = "piped", stderr = "piped", windowsRawArguments = false, + [kInputOption]: input, } = { __proto__: null }) { if (stdin === "piped") { throw new TypeError( @@ -425,6 +429,7 @@ function spawnSync(command, { extraStdio: [], detached: false, needsNpmProcessState: false, + input, }); return { success: result.status.success, @@ -484,4 +489,4 @@ class Command { } } -export { ChildProcess, Command, kill, Process, run }; +export { ChildProcess, Command, kill, kInputOption, Process, run }; diff --git a/ext/process/lib.rs b/ext/process/lib.rs index f568d283ae..5a75325235 100644 --- a/ext/process/lib.rs +++ b/ext/process/lib.rs @@ -20,6 +20,7 @@ use deno_core::op2; use deno_core::serde_json; use deno_core::AsyncMutFuture; use deno_core::AsyncRefCell; +use deno_core::JsBuffer; use deno_core::OpState; use deno_core::RcRef; use deno_core::Resource; @@ -193,6 +194,8 @@ pub struct SpawnArgs { #[serde(flatten)] stdio: ChildStdio, + input: Option, + extra_stdio: Vec, detached: bool, needs_npm_process_state: bool, @@ -437,6 +440,8 @@ fn create_command( if args.stdio.stdin.is_ipc() { args.ipc = Some(0); + } else if args.input.is_some() { + command.stdin(std::process::Stdio::piped()); } else { command.stdin(args.stdio.stdin.as_stdio(state)?); } @@ -936,13 +941,31 @@ fn op_spawn_sync( ) -> Result { let stdout = matches!(args.stdio.stdout, StdioOrRid::Stdio(Stdio::Piped)); let stderr = matches!(args.stdio.stderr, StdioOrRid::Stdio(Stdio::Piped)); + let input = args.input.clone(); let (mut command, _, _, _) = create_command(state, args, "Deno.Command().outputSync()")?; - let output = command.output().map_err(|e| ProcessError::SpawnFailed { + + let mut child = command.spawn().map_err(|e| ProcessError::SpawnFailed { command: command.get_program().to_string_lossy().to_string(), error: Box::new(e.into()), })?; - + if let Some(input) = input { + let mut stdin = child.stdin.take().ok_or_else(|| { + ProcessError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "stdin is not available", + )) + })?; + stdin.write_all(&input)?; + stdin.flush()?; + } + let output = + child + .wait_with_output() + .map_err(|e| ProcessError::SpawnFailed { + command: command.get_program().to_string_lossy().to_string(), + error: Box::new(e.into()), + })?; Ok(SpawnOutput { status: output.status.try_into()?, stdout: if stdout { diff --git a/tests/unit_node/child_process_test.ts b/tests/unit_node/child_process_test.ts index 1732b9d2bf..617aa0d96d 100644 --- a/tests/unit_node/child_process_test.ts +++ b/tests/unit_node/child_process_test.ts @@ -1128,3 +1128,46 @@ Deno.test(async function noWarningsFlag() { await timeout.promise; }); + +Deno.test({ + name: "[node/child_process] spawnSync supports input option", + fn() { + const text = " console.log('hello')"; + const expected = `console.log("hello");\n`; + { + const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], { + input: text, + }); + assertEquals(stdout.toString(), expected); + } + { + const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], { + input: Buffer.from(text), + }); + assertEquals(stdout.toString(), expected); + } + { + const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], { + input: new TextEncoder().encode(text), + }); + assertEquals(stdout.toString(), expected); + } + { + const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], { + input: new DataView(Buffer.from(text).buffer), + }); + assertEquals(stdout.toString(), expected); + } + + assertThrows( + () => { + spawnSync(Deno.execPath(), ["fmt", "-"], { + // deno-lint-ignore no-explicit-any + input: {} as any, + }); + }, + Error, + 'The "input" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Object', + ); + }, +});