Skip to content

[WIP] Update to Node 22#810

Closed
ecraig12345 wants to merge 3 commits intomainfrom
node22
Closed

[WIP] Update to Node 22#810
ecraig12345 wants to merge 3 commits intomainfrom
node22

Conversation

@ecraig12345
Copy link
Member

Overview

Test Notes

): Promise<string | undefined> {
return new Promise((resolve, reject) => {
const child = cp.exec(cmd, opts, (error: ExecError | null, stdout?: string, stderr?: string) => {
const child = cp.exec(cmd, opts, (error, stdout, stderr) => {

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium

This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 1 day ago

In general, the fix is to stop passing a fully constructed command string into cp.exec/cp.execSync when that string depends on environment or filesystem values, and instead call a no-shell API (cp.spawn / cp.execFile / cp.spawnSync) with the executable and arguments passed separately as an array.

For this codebase, the best targeted fix is:

  • Change the generic exec helper in packages/just-scripts/src/utils/exec.ts to accept a cmd and args array, and implement it using cp.spawn rather than cp.exec. This avoids the shell entirely.
  • Update its TypeScript type signature accordingly, and keep the promise-based API and stdout/stderr piping behavior unchanged.
  • Update the call sites in tslintTask.ts and tscTask.ts that currently build a single cmd string via encodeArgs(...).join(' ') to instead:
    • Build an argument array [process.execPath, tslintCmd, ...args] or [process.execPath, ...args].
    • Call the new exec as exec(process.execPath, args, ...) without using encodeArgs or joining into a string.
    • Keep logging behavior; they can still log encodeArgs([...]).join(' ') for display only.
  • Leave encodeArgs implemented as-is for logging or any other callers that may still want shell-style strings; just don’t use it to build the command that is actually executed in these tasks.

Concretely:

  • In packages/just-scripts/src/utils/exec.ts, modify exec to:

    export function exec(
      cmd: string,
      args: ReadonlyArray<string> = [],
      opts: cp.SpawnOptions & { stdout?: NodeJS.WritableStream; stderr?: NodeJS.WritableStream } = {},
    ): Promise<string | undefined> {
      // implement using cp.spawn, buffer stdout/stderr when not piped, etc.
    }

    and replace the current cp.exec-based implementation with a cp.spawn-based one that:

    • Pipes child.stdout/child.stderr into the provided streams when present.
    • Otherwise, collects them into buffers/strings so the promise can resolve with the stdout string or reject with an ExecError containing stdout and stderr.
    • Honors opts for cwd, env, etc. (forward them to cp.spawn).
  • In tslintTask.ts:

    • Replace const cmd = encodeArgs([process.execPath, tslintCmd, ...args]).join(' '); with separate cmdArgs = [tslintCmd, ...args];.
    • Keep logger.info logging a shell-like representation for humans: logger.info(encodeArgs([process.execPath, tslintCmd, ...args]).join(' '));.
    • Call return exec(process.execPath, [tslintCmd, ...args], { stdout: process.stdout, stderr: process.stderr });.
  • In tscTask.ts:

    • For tscTask, keep args = argsFromOptions(tscCmd, options); as-is but:
      • Log with encodeArgs([process.execPath, ...args]).join(' ');.
      • Call return exec(process.execPath, args);.
    • tscWatchTask already uses spawn(process.execPath, cmd, ...) with an argument array, which is safe, so no change is required there.

No new helper methods or imports are required beyond reusing the existing cp.spawn import and the already-exported spawn helper; we only adjust types and logic within the shown snippets.

Suggested changeset 3
packages/just-scripts/src/utils/exec.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/just-scripts/src/utils/exec.ts b/packages/just-scripts/src/utils/exec.ts
--- a/packages/just-scripts/src/utils/exec.ts
+++ b/packages/just-scripts/src/utils/exec.ts
@@ -12,34 +12,67 @@
  * Execute a command.
  *
  * @param cmd Command to execute
- * @param opts Normal exec options plus stdout/stderr for piping output. Can pass `process` for this param.
- * @returns Promise which will settle when the command completes. If output was not piped, it will be
- * returned as the promise's value. If the promise was rejected, the error will be of type `ExecError`.
+ * @param args Args for the command
+ * @param opts Normal spawn options plus stdout/stderr for piping output. Can pass `process` for this param.
+ * @returns Promise which will settle when the command completes. If the promise was rejected, the error will be
+ * of type `ExecError`. If output was not piped, the promise will resolve with the collected stdout.
  */
 export function exec(
   cmd: string,
-  opts: cp.ExecOptions & { stdout?: NodeJS.WritableStream; stderr?: NodeJS.WritableStream } = {},
+  args: ReadonlyArray<string> = [],
+  opts: cp.SpawnOptions & { stdout?: NodeJS.WritableStream; stderr?: NodeJS.WritableStream } = {},
 ): Promise<string | undefined> {
   return new Promise((resolve, reject) => {
-    const child = cp.exec(cmd, opts, (error, stdout, stderr) => {
-      stdout = !stdout || typeof stdout === 'string' ? stdout : stdout.toString();
-      stderr = !stderr || typeof stderr === 'string' ? stderr : stderr.toString();
-      if (error) {
-        error.stdout = stdout;
-        error.stderr = stderr;
-        reject(error);
+    const { stdout: outStream, stderr: errStream, ...spawnOpts } = opts;
+    const child = cp.spawn(cmd, args, spawnOpts);
+
+    let stdoutBuffer = '';
+    let stderrBuffer = '';
+
+    if (child.stdout) {
+      if (outStream) {
+        child.stdout.pipe(outStream);
       } else {
-        resolve(stdout);
+        child.stdout.on('data', chunk => {
+          stdoutBuffer += chunk.toString();
+        });
       }
-    });
-
-    if (opts.stdout) {
-      child.stdout?.pipe(opts.stdout);
     }
 
-    if (opts.stderr) {
-      child.stderr?.pipe(opts.stderr);
+    if (child.stderr) {
+      if (errStream) {
+        child.stderr.pipe(errStream);
+      } else {
+        child.stderr.on('data', chunk => {
+          stderrBuffer += chunk.toString();
+        });
+      }
     }
+
+    child.on('error', err => {
+      const execError = err as ExecError;
+      execError.stdout = stdoutBuffer || undefined;
+      execError.stderr = stderrBuffer || undefined;
+      reject(execError);
+    });
+
+    child.on('close', (code: number | null, signal: NodeJS.Signals | null) => {
+      if (code !== 0) {
+        const error = new Error('Command failed: ' + [cmd, ...args].join(' ')) as ExecError;
+        (error as any).code = code;
+        error.stdout = stdoutBuffer || undefined;
+        error.stderr = stderrBuffer || undefined;
+        reject(error);
+      } else if (signal) {
+        const error = new Error(`Command terminated by signal ${signal}: ` + [cmd, ...args].join(' ')) as ExecError;
+        (error as any).signal = signal;
+        error.stdout = stdoutBuffer || undefined;
+        error.stderr = stderrBuffer || undefined;
+        reject(error);
+      } else {
+        resolve(outStream ? undefined : stdoutBuffer || undefined);
+      }
+    });
   });
 }
 
EOF
packages/just-scripts/src/tasks/tscTask.ts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/just-scripts/src/tasks/tscTask.ts b/packages/just-scripts/src/tasks/tscTask.ts
--- a/packages/just-scripts/src/tasks/tscTask.ts
+++ b/packages/just-scripts/src/tasks/tscTask.ts
@@ -25,9 +25,9 @@
       logger.info(`Running ${tscCmd} with ${options.project || options.build}`);
 
       const args = argsFromOptions(tscCmd, options);
-      const cmd = encodeArgs([process.execPath, ...args]).join(' ');
-      logger.info(`Executing: ${cmd}`);
-      return exec(cmd);
+      const displayCmd = encodeArgs([process.execPath, ...args]).join(' ');
+      logger.info(`Executing: ${displayCmd}`);
+      return exec(process.execPath, args);
     }
     return Promise.resolve();
   };
EOF
@@ -25,9 +25,9 @@
logger.info(`Running ${tscCmd} with ${options.project || options.build}`);

const args = argsFromOptions(tscCmd, options);
const cmd = encodeArgs([process.execPath, ...args]).join(' ');
logger.info(`Executing: ${cmd}`);
return exec(cmd);
const displayCmd = encodeArgs([process.execPath, ...args]).join(' ');
logger.info(`Executing: ${displayCmd}`);
return exec(process.execPath, args);
}
return Promise.resolve();
};
packages/just-scripts/src/tasks/tslintTask.ts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/just-scripts/src/tasks/tslintTask.ts b/packages/just-scripts/src/tasks/tslintTask.ts
--- a/packages/just-scripts/src/tasks/tslintTask.ts
+++ b/packages/just-scripts/src/tasks/tslintTask.ts
@@ -31,9 +31,9 @@
         args.push('--fix');
       }
 
-      const cmd = encodeArgs([process.execPath, tslintCmd, ...args]).join(' ');
-      logger.info(cmd);
-      return exec(cmd, { stdout: process.stdout, stderr: process.stderr });
+      const displayCmd = encodeArgs([process.execPath, tslintCmd, ...args]).join(' ');
+      logger.info(displayCmd);
+      return exec(process.execPath, [tslintCmd, ...args], { stdout: process.stdout, stderr: process.stderr });
     } else {
       return Promise.resolve();
     }
EOF
@@ -31,9 +31,9 @@
args.push('--fix');
}

const cmd = encodeArgs([process.execPath, tslintCmd, ...args]).join(' ');
logger.info(cmd);
return exec(cmd, { stdout: process.stdout, stderr: process.stderr });
const displayCmd = encodeArgs([process.execPath, tslintCmd, ...args]).join(' ');
logger.info(displayCmd);
return exec(process.execPath, [tslintCmd, ...args], { stdout: process.stdout, stderr: process.stderr });
} else {
return Promise.resolve();
}
Copilot is powered by AI and may make mistakes. Always verify output.
@ecraig12345 ecraig12345 deleted the node22 branch March 25, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant