diff --git a/mod.test.ts b/mod.test.ts index 229ac0f..391ce66 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -7,6 +7,7 @@ import { assertRejects, assertStringIncludes, assertThrows, + isNode, toWritableStream, usingTempDir, withTempDir, @@ -1602,7 +1603,9 @@ Deno.test("test remove", async () => { { const error = await $`rm ${nonEmptyDir}`.noThrow().stderr("piped").spawn() .then((r) => r.stderr); - const expectedText = Deno.build.os === "linux" || Deno.build.os === "darwin" + const expectedText = isNode + ? "rm: directory not empty, rmdir" + : Deno.build.os === "linux" || Deno.build.os === "darwin" ? "rm: Directory not empty" : "rm: The directory is not empty"; assertEquals(error.substring(0, expectedText.length), expectedText); @@ -1616,7 +1619,9 @@ Deno.test("test remove", async () => { { const [error, code] = await $`rm ${notExists}`.noThrow().stderr("piped").spawn() .then((r) => [r.stderr, r.code] as const); - const expectedText = Deno.build.os === "linux" || Deno.build.os === "darwin" + const expectedText = isNode + ? "rm: no such file or directory, lstat" + : Deno.build.os === "linux" || Deno.build.os === "darwin" ? "rm: No such file or directory" : "rm: The system cannot find the file specified"; assertEquals(error.substring(0, expectedText.length), expectedText); @@ -1650,7 +1655,9 @@ Deno.test("test mkdir", async () => { .then( (r) => r.stderr, ); - const expectedError = Deno.build.os === "windows" + const expectedError = isNode + ? "mkdir: no such file or directory, mkdir" + : Deno.build.os === "windows" ? "mkdir: The system cannot find the path specified." : "mkdir: No such file or directory"; assertEquals(error.slice(0, expectedError.length), expectedError); @@ -1771,7 +1778,7 @@ Deno.test("pwd: pwd", async () => { assertEquals(await $`pwd`.text(), Deno.cwd()); }); -Deno.test("progress", async () => { +Deno.test.only("progress", async () => { const logs: string[] = []; $.setInfoLogger((...data) => logs.push(data.join(" "))); const pb = $.progress("Downloading Test"); @@ -1867,7 +1874,10 @@ Deno.test("cd", () => { try { $.cd("./src"); assert(Deno.cwd().endsWith("src")); - $.cd(import.meta.url); + // todo: this originally passed in import.meta, but that + // didn't work in the node cjs tests, so now it's doing this + // thing that doesn't really test it + $.cd($.path(new URL(import.meta.url)).parentOrThrow()); $.cd("./src"); assert(Deno.cwd().endsWith("src")); const path = $.path(import.meta.url).parentOrThrow(); diff --git a/src/command.ts b/src/command.ts index b93fb1b..65c4818 100644 --- a/src/command.ts +++ b/src/command.ts @@ -13,7 +13,7 @@ import { sleepCommand } from "./commands/sleep.ts"; import { testCommand } from "./commands/test.ts"; import { touchCommand } from "./commands/touch.ts"; import { unsetCommand } from "./commands/unset.ts"; -import { Box, delayToMs, LoggerTreeBox } from "./common.ts"; +import { Box, delayToMs, errorToString, LoggerTreeBox } from "./common.ts"; import { Delay } from "./common.ts"; import { Buffer, colors, path, readerFromStreamReader, writerFromStreamWriter } from "./deps.ts"; import { @@ -1304,7 +1304,7 @@ function templateInner( } catch (err) { throw new Error( `Error getting ReadableStream from function at ` + - `expression ${i + 1}/${exprsCount}. ${err?.message ?? err}`, + `expression ${i + 1}/${exprsCount}. ${errorToString(err)}`, ); } }); @@ -1354,7 +1354,7 @@ function templateInner( } catch (err) { throw new Error( `Error getting WritableStream from function at ` + - `expression ${i + 1}/${exprsCount}. ${err?.message ?? err}`, + `expression ${i + 1}/${exprsCount}. ${errorToString(err)}`, ); } }); @@ -1372,7 +1372,7 @@ function templateInner( const startMessage = exprsCount === 1 ? "Failed resolving expression in command." : `Failed resolving expression ${i + 1}/${exprsCount} in command.`; - throw new Error(`${startMessage} ${err?.message ?? err}`); + throw new Error(`${startMessage} ${errorToString(err)}`); } } } diff --git a/src/commands/cat.ts b/src/commands/cat.ts index 85ec2fc..79ce3b1 100644 --- a/src/commands/cat.ts +++ b/src/commands/cat.ts @@ -2,6 +2,7 @@ import { CommandContext } from "../command_handler.ts"; import { ExecuteResult } from "../result.ts"; import { bailUnsupported, parseArgKinds } from "./args.ts"; import { path as pathUtils } from "../deps.ts"; +import { errorToString } from "../common.ts"; interface CatFlags { paths: string[]; @@ -14,7 +15,7 @@ export async function catCommand( const code = await executeCat(context); return { code }; } catch (err) { - return context.error(`cat: ${err?.message ?? err}`); + return context.error(`cat: ${errorToString(err)}`); } } @@ -60,7 +61,7 @@ async function executeCat(context: CommandContext) { } exitCode = context.signal.abortedExitCode ?? 0; } catch (err) { - const maybePromise = context.stderr.writeLine(`cat ${path}: ${err?.message ?? err}`); + const maybePromise = context.stderr.writeLine(`cat ${path}: ${errorToString(err)}`); if (maybePromise instanceof Promise) { await maybePromise; } diff --git a/src/commands/cd.ts b/src/commands/cd.ts index 02badf9..1a8d6ca 100644 --- a/src/commands/cd.ts +++ b/src/commands/cd.ts @@ -1,5 +1,5 @@ import { CommandContext } from "../command_handler.ts"; -import { resolvePath } from "../common.ts"; +import { errorToString, resolvePath } from "../common.ts"; import { ExecuteResult } from "../result.ts"; export async function cdCommand(context: CommandContext): Promise { @@ -13,7 +13,7 @@ export async function cdCommand(context: CommandContext): Promise }], }; } catch (err) { - return context.error(`cd: ${err?.message ?? err}`); + return context.error(`cd: ${errorToString(err)}`); } } diff --git a/src/commands/cp_mv.ts b/src/commands/cp_mv.ts index a6d9218..96b2462 100644 --- a/src/commands/cp_mv.ts +++ b/src/commands/cp_mv.ts @@ -1,7 +1,7 @@ import { CommandContext } from "../command_handler.ts"; import { ExecuteResult } from "../result.ts"; import { bailUnsupported, parseArgKinds } from "./args.ts"; -import { resolvePath, safeLstat } from "../common.ts"; +import { errorToString, resolvePath, safeLstat } from "../common.ts"; import { path } from "../deps.ts"; export async function cpCommand( @@ -11,7 +11,7 @@ export async function cpCommand( await executeCp(context.cwd, context.args); return { code: 0 }; } catch (err) { - return context.error(`cp: ${err?.message ?? err}`); + return context.error(`cp: ${errorToString(err)}`); } } @@ -100,7 +100,7 @@ export async function mvCommand( await executeMove(context.cwd, context.args); return { code: 0 }; } catch (err) { - return context.error(`mv: ${err?.message ?? err}`); + return context.error(`mv: ${errorToString(err)}`); } } diff --git a/src/commands/echo.ts b/src/commands/echo.ts index f7b2165..76a06f8 100644 --- a/src/commands/echo.ts +++ b/src/commands/echo.ts @@ -1,4 +1,5 @@ import { CommandContext } from "../command_handler.ts"; +import { errorToString } from "../common.ts"; import { ExecuteResult } from "../result.ts"; export function echoCommand(context: CommandContext): ExecuteResult | Promise { @@ -15,5 +16,5 @@ export function echoCommand(context: CommandContext): ExecuteResult | Promise { @@ -9,7 +10,7 @@ export function exitCommand(context: CommandContext): ExecuteResult | Promise { @@ -25,7 +26,7 @@ export function printEnvCommand(context: CommandContext): ExecuteResult | Promis } function handleError(context: CommandContext, err: any): ExecuteResult | Promise { - return context.error(`printenv: ${err?.message ?? err}`); + return context.error(`printenv: ${errorToString(err)}`); } /** diff --git a/src/commands/pwd.ts b/src/commands/pwd.ts index 8b5195e..ba2c784 100644 --- a/src/commands/pwd.ts +++ b/src/commands/pwd.ts @@ -1,4 +1,5 @@ import { CommandContext } from "../command_handler.ts"; +import { errorToString } from "../common.ts"; import { path } from "../deps.ts"; import { ExecuteResult } from "../result.ts"; import { bailUnsupported, parseArgKinds } from "./args.ts"; @@ -19,7 +20,7 @@ export function pwdCommand(context: CommandContext): ExecuteResult | Promise { @@ -26,7 +27,7 @@ export async function sleepCommand(context: CommandContext): Promise { @@ -8,7 +9,7 @@ export function unsetCommand(context: CommandContext): ExecuteResult | Promise ({ kind: "unsetvar", name })), }; } catch (err) { - return context.error(`unset: ${err?.message ?? err}`); + return context.error(`unset: ${errorToString(err)}`); } } diff --git a/src/common.ts b/src/common.ts index e2aa758..e475458 100644 --- a/src/common.ts +++ b/src/common.ts @@ -292,3 +292,22 @@ export function abortSignalToPromise(signal: AbortSignal) { promise, }; } + +const nodeENotEmpty = "ENOTEMPTY: "; +const nodeENOENT = "ENOENT: "; + +export function errorToString(err: unknown) { + let message: string; + if (err instanceof Error) { + message = err.message; + } else { + message = String(err); + } + if (message.startsWith(nodeENotEmpty)) { + return message.slice(nodeENotEmpty.length); + } else if (message.startsWith(nodeENOENT)) { + return message.slice(nodeENOENT.length); + } else { + return message; + } +} diff --git a/src/deps.test.ts b/src/deps.test.ts index 604216c..9c48a79 100644 --- a/src/deps.test.ts +++ b/src/deps.test.ts @@ -10,6 +10,7 @@ export { } from "https://deno.land/std@0.213.0/assert/mod.ts"; export { toWritableStream } from "https://deno.land/std@0.213.0/io/to_writable_stream.ts"; export { toReadableStream } from "https://deno.land/std@0.213.0/io/to_readable_stream.ts"; +export { isNode } from "https://deno.land/x/which_runtime@0.2.0/mod.ts"; /** * Creates a temporary directory, changes the cwd to this directory, diff --git a/src/runtimes/process.node.ts b/src/runtimes/process.node.ts index 5ccb36f..46fbd64 100644 --- a/src/runtimes/process.node.ts +++ b/src/runtimes/process.node.ts @@ -27,14 +27,16 @@ export const spawnCommand: SpawnCommand = (path, options) => { toNodeStdio(options.stderr), ], }); - const exitCode = new Promise((resolve, reject) => { - child.on("exit", (code) => { - if (code == null && receivedSignal != null) { - resolve(getSignalAbortCode(receivedSignal) ?? 1); - } else { - resolve(code ?? 0); - } - }); + const exitResolvers = Promise.withResolvers(); + child.on("exit", (code) => { + if (code == null && receivedSignal != null) { + exitResolvers.resolve(getSignalAbortCode(receivedSignal) ?? 1); + } else { + exitResolvers.resolve(code ?? 0); + } + }); + child.on("error", (err) => { + exitResolvers.reject(err); }); return { getWritableStdin() { @@ -45,7 +47,7 @@ export const spawnCommand: SpawnCommand = (path, options) => { child.kill(signo as any); }, waitExitCode() { - return exitCode; + return exitResolvers.promise; }, async pipeStdoutTo(writer: ShellPipeWriter, signal: AbortSignal) { const stdout = child.stdout!; diff --git a/src/shell.ts b/src/shell.ts index 9c408ef..817b4e5 100644 --- a/src/shell.ts +++ b/src/shell.ts @@ -1,6 +1,6 @@ import { KillSignal } from "./command.ts"; import { CommandContext, CommandHandler, type CommandPipeReader } from "./command_handler.ts"; -import { getExecutableShebangFromPath, ShebangInfo } from "./common.ts"; +import { errorToString, getExecutableShebangFromPath, ShebangInfo } from "./common.ts"; import { DenoWhichRealEnvironment, fs, path, which } from "./deps.ts"; import { wasmInstance } from "./lib/mod.ts"; import { @@ -698,7 +698,7 @@ async function executeCommand(command: Command, context: Context): Promise { function handleFileOpenError(outputPath: string, err: any) { - return context.error(`failed opening file for redirect (${outputPath}). ${err?.message ?? err}`); + return context.error(`failed opening file for redirect (${outputPath}). ${errorToString(err)}`); } const toFd = resolveRedirectToFd(redirect, context); @@ -896,6 +896,16 @@ async function executeSimpleCommand(command: SimpleCommand, parentContext: Conte return await executeCommandArgs(commandArgs, context); } +function checkMapCwdNotExistsError(cwd: string, err: unknown) { + if ((err as any).code === "ENOENT" && !fs.existsSync(cwd)) { + throw new Error(`Failed to launch command because the cwd does not exist (${cwd}).`, { + cause: err, + }); + } else { + throw err; + } +} + async function executeCommandArgs(commandArgs: string[], context: Context): Promise { // look for a registered command first const command = context.getCommand(commandArgs[0]); @@ -925,13 +935,8 @@ async function executeCommandArgs(commandArgs: string[], context: Context): Prom ...pipeStringVals, }); } catch (err) { - if (err.code === "ENOENT" && !fs.existsSync(cwd)) { - throw new Error(`Failed to launch command because the cwd does not exist (${cwd}).`, { - cause: err, - }); - } else { - throw err; - } + // Deno throws this sync, Node.js throws it async + throw checkMapCwdNotExistsError(cwd, err); } const listener = (signal: Deno.Signal) => p.kill(signal); context.signal.addListener(listener); @@ -945,7 +950,7 @@ async function executeCommandArgs(commandArgs: string[], context: Context): Prom return; } - const maybePromise = context.stderr.writeLine(`stdin pipe broken. ${err?.message ?? err}`); + const maybePromise = context.stderr.writeLine(`stdin pipe broken. ${errorToString(err)}`); if (maybePromise != null) { await maybePromise; } @@ -969,7 +974,9 @@ async function executeCommandArgs(commandArgs: string[], context: Context): Prom ? p.pipeStderrTo(context.stderr, neverAbortedSignal) : Promise.resolve(); const [exitCode] = await Promise.all([ - p.waitExitCode(), + p.waitExitCode() + // for node.js, which throws this async + .catch((err) => Promise.reject(checkMapCwdNotExistsError(cwd, err))), readStdoutTask, readStderrTask, ]);