@arcgis/node-toolkit
Advanced tools
| import { ExecSyncOptionsWithStringEncoding, SpawnSyncOptionsWithStringEncoding, SpawnOptions } from 'node:child_process'; | ||
| type SyncProcessOptions = Omit<SpawnSyncOptionsWithStringEncoding, "stdio">; | ||
| type AsyncProcessOptions = Omit<SpawnOptions, "stdio"> & { | ||
| input?: NonNullable<SpawnSyncOptionsWithStringEncoding["input"]>; | ||
| }; | ||
| /** | ||
| * Synchronously execute a shell command and return the output. | ||
| * | ||
| * Prefer {@link runCommandSync} for command-only execution, or {@link collectOutputSync} | ||
| * when you need to capture stdout. {@link sh} goes through a shell, which | ||
| * requires careful escaping and can mis-handle dynamic input with spaces or | ||
| * shell metacharacters. | ||
| * | ||
| * Use {@link sh} only when shell features are intentionally required, such as | ||
| * pipes, redirects, `&&`, or command substitution. | ||
| * | ||
| * Example (shell features required): | ||
| * `sh("pnpm list | grep @arcgis/core")` | ||
| */ | ||
| export declare function sh(command: string, options?: Partial<ExecSyncOptionsWithStringEncoding>): string; | ||
| /** | ||
| * Asynchronously execute a shell command and return the output. | ||
| * | ||
| * Prefer {@link runCommand} for command-only execution, or {@link collectOutput} | ||
| * when you need to capture stdout. {@link asyncSh} executes through a shell, | ||
| * so callers must handle shell escaping and quoting correctly. | ||
| * | ||
| * Use {@link asyncSh} only when shell syntax is intentionally needed. | ||
| * | ||
| * Example (shell features required): | ||
| * `await asyncSh("pnpm list | grep @arcgis/core")` | ||
| */ | ||
| export declare function asyncSh(command: string, options?: Partial<ExecSyncOptionsWithStringEncoding>): Promise<string>; | ||
| /** | ||
| * Synchronously execute a command without shell interpolation and return the output. | ||
| * | ||
| * This is usually safer than {@link sh} because arguments are passed as | ||
| * discrete tokens, which avoids most shell escaping issues and command | ||
| * injection pitfalls. | ||
| * | ||
| * Example: | ||
| * `sp("git", ["show", `${baseRef}:pnpm-workspace.yaml`])` | ||
| * | ||
| * Avoid {@link sp} only when you explicitly need shell features. | ||
| * @deprecated Use {@link collectOutputSync} or {@link runCommandSync} instead. | ||
| */ | ||
| export declare function sp(command: string, args: string[], options?: Partial<SpawnSyncOptionsWithStringEncoding>): string; | ||
| /** | ||
| * Synchronously execute a command and stream output to the current process. | ||
| * Note 1: | ||
| * `pnpm` command will be automatically invoked with `shell: process.platform === "win32"` | ||
| * if the `shell` option is not explicitly provided. | ||
| * Note 2: | ||
| * If the `input` option is provided, it will be piped to the child process's stdin. | ||
| */ | ||
| export declare function runCommandSync(command: string, args: string[], options?: Partial<SyncProcessOptions>): void; | ||
| /** | ||
| * Asynchronously execute a command and stream output to the current process. | ||
| * Note 1: | ||
| * `pnpm` command will be automatically invoked with `shell: process.platform === "win32"` | ||
| * if the `shell` option is not explicitly provided. | ||
| * Note 2: | ||
| * If the `input` option is provided, it will be piped to the child process's stdin. | ||
| */ | ||
| export declare function runCommand(command: string, args: string[], options?: Partial<AsyncProcessOptions>): Promise<void>; | ||
| /** | ||
| * Synchronously execute a command and collect stdout while streaming stderr. | ||
| * Note that `pnpm` command will be automatically invoked with `shell: process.platform === "win32"`. | ||
| */ | ||
| export declare function collectOutputSync(command: string, args: string[], options?: Partial<SyncProcessOptions>): string; | ||
| /** | ||
| * Asynchronously execute a command and collect stdout while streaming stderr. | ||
| * Note that `pnpm` command will be automatically invoked with `shell: process.platform === "win32"`. | ||
| */ | ||
| export declare function collectOutput(command: string, args: string[], options?: Partial<AsyncProcessOptions>): Promise<string>; | ||
| export {}; |
+154
| import { exec, spawn, spawnSync, execSync } from "node:child_process"; | ||
| import { styleText } from "node:util"; | ||
| function sh(command, options = {}) { | ||
| try { | ||
| const normalizedOptions = { encoding: "utf8", ...options }; | ||
| return execSync(command.trim(), normalizedOptions).trim(); | ||
| } catch (error) { | ||
| makeExecErrorReadable(error); | ||
| throw error; | ||
| } | ||
| } | ||
| async function asyncSh(command, options = {}) { | ||
| const normalizedOptions = { encoding: "utf8", ...options }; | ||
| return await new Promise((resolve, reject) => { | ||
| exec(command.trim(), normalizedOptions, (error, stdout, stderr) => { | ||
| if (error) { | ||
| makeExecErrorReadable(error); | ||
| reject(error); | ||
| return; | ||
| } | ||
| resolve(stdout.trim() || stderr.trim()); | ||
| }); | ||
| }); | ||
| } | ||
| function makeExecErrorReadable(error) { | ||
| if (error instanceof Error && error.stack && "output" in error && Array.isArray(error.output) && "status" in error) { | ||
| const stackIndex = error.stack.indexOf("\n at "); | ||
| if (stackIndex !== -1) { | ||
| const output = error.output.filter(Boolean).join("\n").trim(); | ||
| const newHeader = `${styleText("red", error.message)} (exit code: ${String(error.status)}) | ||
| ${output}`; | ||
| const oldStackFrames = error.stack.substring(stackIndex); | ||
| error.stack = `Error: ${newHeader}${oldStackFrames}`; | ||
| } | ||
| Object.defineProperties(error, { | ||
| output: { enumerable: false }, | ||
| stdout: { enumerable: false }, | ||
| stderr: { enumerable: false }, | ||
| signal: { enumerable: false }, | ||
| status: { enumerable: false }, | ||
| pid: { enumerable: false }, | ||
| stdio: { enumerable: false } | ||
| }); | ||
| } | ||
| } | ||
| function sp(command, args, options = {}) { | ||
| if (process.platform === "win32" && command === "pnpm" && // Allow explicit shell config including shell:false for environment-specific handling. | ||
| !("shell" in options)) { | ||
| throw Error( | ||
| 'If invoking pnpm on Windows, provide { shell: true } or { shell: process.platform === "win32" } and handle shell escaping.' | ||
| ); | ||
| } | ||
| const normalizedOptions = { encoding: "utf8", ...options }; | ||
| const result = spawnSync(command, args, normalizedOptions); | ||
| if (result.error) { | ||
| throw result.error; | ||
| } | ||
| const sep = result.stdout && result.stderr ? "\n" : ""; | ||
| const output = `${result.stdout ?? ""}${sep}${result.stderr ?? ""}`.trim(); | ||
| const exitCode = result.status ?? 0; | ||
| if (exitCode !== 0) { | ||
| throw makeSpawnError(command, args, exitCode, result.signal, output); | ||
| } | ||
| return output; | ||
| } | ||
| function runCommandSync(command, args, options = {}) { | ||
| const fixedOptions = fixPnpmUsage(command, options); | ||
| const { input } = fixedOptions; | ||
| const stdio = input === void 0 ? "inherit" : ["pipe", "inherit", "inherit"]; | ||
| const result = spawnSync(command, args, { ...fixedOptions, stdio }); | ||
| assertSyncResultSuccess(command, args, result); | ||
| } | ||
| async function runCommand(command, args, options = {}) { | ||
| const fixedOptions = fixPnpmUsage(command, options); | ||
| const { input } = fixedOptions; | ||
| const stdio = input === void 0 ? "inherit" : ["pipe", "inherit", "inherit"]; | ||
| const child = spawn(command, args, { ...fixedOptions, stdio }); | ||
| if (input !== void 0) { | ||
| child.stdin?.end(input); | ||
| } | ||
| await new Promise((resolve, reject) => { | ||
| child.on("error", (error) => reject(error)); | ||
| child.on("close", (code, signal) => { | ||
| if (code !== 0) { | ||
| reject(makeSpawnError(command, args, code, signal, "")); | ||
| return; | ||
| } | ||
| resolve(); | ||
| }); | ||
| }); | ||
| } | ||
| function collectOutputSync(command, args, options = {}) { | ||
| const fixedOptions = fixPnpmUsage(command, options); | ||
| const result = spawnSync(command, args, { | ||
| encoding: "utf8", | ||
| ...fixedOptions, | ||
| stdio: ["ignore", "pipe", "inherit"] | ||
| }); | ||
| assertSyncResultSuccess(command, args, result); | ||
| return (result.stdout ?? "").trim(); | ||
| } | ||
| async function collectOutput(command, args, options = {}) { | ||
| const fixedOptions = fixPnpmUsage(command, options); | ||
| const child = spawn(command, args, { | ||
| ...fixedOptions, | ||
| stdio: ["ignore", "pipe", "inherit"] | ||
| }); | ||
| const stdoutChunks = []; | ||
| child.stdout?.on( | ||
| "data", | ||
| (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk)) | ||
| ); | ||
| await new Promise((resolve, reject) => { | ||
| child.on("error", (error) => reject(error)); | ||
| child.on("close", (code, signal) => { | ||
| if (code !== 0) { | ||
| reject(makeSpawnError(command, args, code, signal, "")); | ||
| return; | ||
| } | ||
| resolve(); | ||
| }); | ||
| }); | ||
| return stdoutChunks.join("").trim(); | ||
| } | ||
| function fixPnpmUsage(command, options) { | ||
| if (process.platform === "win32" && command === "pnpm") { | ||
| const fixedOptions = { shell: process.platform === "win32", ...options }; | ||
| return fixedOptions; | ||
| } | ||
| return options; | ||
| } | ||
| function assertSyncResultSuccess(command, args, result) { | ||
| if (result.error) { | ||
| throw result.error; | ||
| } | ||
| if (result.status !== 0) { | ||
| throw makeSpawnError(command, args, result.status, result.signal, String(result.stderr ?? "").trim()); | ||
| } | ||
| } | ||
| function makeSpawnError(command, args, code, signal, output) { | ||
| const commandText = `${command} ${args.join(" ")}`.trim(); | ||
| const statusText = code !== null ? `exit code ${String(code)}` : `signal ${signal ?? "unknown"}`; | ||
| return new Error(`Command failed with ${statusText}: ${commandText} | ||
| ${output}`.trim()); | ||
| } | ||
| export { | ||
| asyncSh, | ||
| collectOutput, | ||
| collectOutputSync, | ||
| runCommand, | ||
| runCommandSync, | ||
| sh, | ||
| sp | ||
| }; |
+0
-46
@@ -1,2 +0,1 @@ | ||
| import { ExecSyncOptionsWithStringEncoding, SpawnOptions, SpawnSyncOptionsWithStringEncoding } from 'node:child_process'; | ||
| /** | ||
@@ -9,47 +8,2 @@ * Asynchronously check if a file or directory exists. | ||
| /** | ||
| * Synchronously execute a shell command and return the output. | ||
| * | ||
| * Prefer {@link sp} for most use cases. {@link sh} goes through a shell, | ||
| * which requires careful escaping and can mis-handle dynamic input with spaces | ||
| * or shell metacharacters. | ||
| * | ||
| * Use {@link sh} only when shell features are intentionally required, such as | ||
| * pipes, redirects, `&&`, or command substitution. | ||
| */ | ||
| export declare function sh(command: string, options?: Partial<ExecSyncOptionsWithStringEncoding>): string; | ||
| /** | ||
| * Asynchronously execute a shell command and return the output. | ||
| * | ||
| * Prefer {@link asyncSp} for most use cases. {@link asyncSh} executes through | ||
| * a shell, so callers must handle shell escaping and quoting correctly. | ||
| * | ||
| * Use {@link asyncSh} only when shell syntax is intentionally needed. | ||
| */ | ||
| export declare function asyncSh(command: string, options?: Partial<ExecSyncOptionsWithStringEncoding>): Promise<string>; | ||
| /** | ||
| * Synchronously execute a command without shell interpolation and return the output. | ||
| * | ||
| * This is usually safer than {@link sh} because arguments are passed as | ||
| * discrete tokens, which avoids most shell escaping issues and command | ||
| * injection pitfalls. | ||
| * | ||
| * Example: | ||
| * `sp("git", ["show", `${baseRef}:pnpm-workspace.yaml`])` | ||
| * | ||
| * Avoid {@link sp} only when you explicitly need shell features. | ||
| */ | ||
| export declare function sp(command: string, args: string[], options?: Partial<SpawnSyncOptionsWithStringEncoding>): string; | ||
| /** | ||
| * Asynchronously execute a command without shell interpolation and return the output. | ||
| * | ||
| * This is usually safer than {@link asyncSh} because arguments are not parsed | ||
| * by a shell. | ||
| * | ||
| * Example: | ||
| * `await asyncSp("pnpm", ["--filter=@arcgis/map-components", "list", "@arcgis/core", "--json"])` | ||
| * | ||
| * Prefer {@link asyncSp} by default for external command execution. | ||
| */ | ||
| export declare function asyncSp(command: string, args: string[], options?: Partial<SpawnOptions>): Promise<string>; | ||
| /** | ||
| * Create a file with the specified content if it does not already exist. | ||
@@ -56,0 +10,0 @@ */ |
+1
-106
@@ -5,4 +5,2 @@ import { access, existsSync } from "node:fs"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { execSync, exec, spawnSync, spawn } from "node:child_process"; | ||
| import { styleText } from "node:util"; | ||
| const existsAsync = async (file) => ( | ||
@@ -12,101 +10,2 @@ //#endregion existsAsync | ||
| ); | ||
| function sh(command, options = {}) { | ||
| try { | ||
| const normalizedOptions = { encoding: "utf8", ...options }; | ||
| return execSync(command.trim(), normalizedOptions).trim(); | ||
| } catch (error) { | ||
| makeExecErrorReadable(error); | ||
| throw error; | ||
| } | ||
| } | ||
| async function asyncSh(command, options = {}) { | ||
| const normalizedOptions = { encoding: "utf8", ...options }; | ||
| return await new Promise((resolve2, reject) => { | ||
| exec(command.trim(), normalizedOptions, (error, stdout, stderr) => { | ||
| if (error) { | ||
| makeExecErrorReadable(error); | ||
| reject(error); | ||
| return; | ||
| } | ||
| resolve2(stdout.trim() || stderr.trim()); | ||
| }); | ||
| }); | ||
| } | ||
| function sp(command, args, options = {}) { | ||
| validatePnpmUsage(command, options); | ||
| const normalizedOptions = { encoding: "utf8", ...options }; | ||
| const result = spawnSync(command, args, normalizedOptions); | ||
| if (result.error) { | ||
| throw result.error; | ||
| } | ||
| const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim(); | ||
| const exitCode = result.status ?? 0; | ||
| if (exitCode !== 0) { | ||
| throw new Error( | ||
| `Command failed with exit code ${String(exitCode)}: ${command} ${args.join(" ")} | ||
| ${output}`.trim() | ||
| ); | ||
| } | ||
| return output; | ||
| } | ||
| async function asyncSp(command, args, options = {}) { | ||
| validatePnpmUsage(command, options); | ||
| const child = spawn(command, args, options); | ||
| const stdoutChunks = []; | ||
| const stderrChunks = []; | ||
| child.stdout?.on( | ||
| "data", | ||
| (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk)) | ||
| ); | ||
| child.stderr?.on( | ||
| "data", | ||
| (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk)) | ||
| ); | ||
| return await new Promise((resolve2, reject) => { | ||
| child.on("error", (error) => reject(error)); | ||
| child.on("close", (code) => { | ||
| const output = `${stdoutChunks.join("")}${stderrChunks.join("")}`.trim(); | ||
| const exitCode = code ?? 0; | ||
| if (exitCode !== 0) { | ||
| reject( | ||
| new Error( | ||
| `Command failed with exit code ${String(exitCode)}: ${command} ${args.join(" ")} | ||
| ${output}`.trim() | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| resolve2(output); | ||
| }); | ||
| }); | ||
| } | ||
| function validatePnpmUsage(command, options) { | ||
| if (command === "pnpm" && // Don't error for explicit shell:false to permit system-dependent handling | ||
| !("shell" in options)) { | ||
| throw Error( | ||
| 'If invoking pnpm using sp/asyncSp, must provide {shell:true} or {shell:process.platform==="win32"} option and manually handle shell escaping' | ||
| ); | ||
| } | ||
| } | ||
| function makeExecErrorReadable(error) { | ||
| if (error instanceof Error && error.stack && "output" in error && Array.isArray(error.output) && "status" in error) { | ||
| const stackIndex = error.stack.indexOf("\n at "); | ||
| if (stackIndex !== -1) { | ||
| const output = error.output.filter(Boolean).join("\n").trim(); | ||
| const newHeader = `${styleText("red", error.message)} (exit code: ${String(error.status)}) | ||
| ${output}`; | ||
| const oldStackFrames = error.stack.substring(stackIndex); | ||
| error.stack = `Error: ${newHeader}${oldStackFrames}`; | ||
| } | ||
| Object.defineProperties(error, { | ||
| output: { enumerable: false }, | ||
| stdout: { enumerable: false }, | ||
| stderr: { enumerable: false }, | ||
| signal: { enumerable: false }, | ||
| status: { enumerable: false }, | ||
| pid: { enumerable: false }, | ||
| stdio: { enumerable: false } | ||
| }); | ||
| } | ||
| } | ||
| async function createFileIfNotExists(filePath, content) { | ||
@@ -149,9 +48,5 @@ await mkdir(dirname(filePath), { recursive: true }); | ||
| asyncFindPath, | ||
| asyncSh, | ||
| asyncSp, | ||
| createFileIfNotExists, | ||
| existsAsync, | ||
| findPath, | ||
| sh, | ||
| sp | ||
| findPath | ||
| }; |
+2
-1
| { | ||
| "name": "@arcgis/node-toolkit", | ||
| "version": "5.2.0-next.52", | ||
| "version": "5.2.0-next.53", | ||
| "description": "Collection of common internal build-time patterns and utilities for ArcGIS Maps SDK for JavaScript components.", | ||
@@ -9,2 +9,3 @@ "homepage": "https://developers.arcgis.com/javascript/latest/", | ||
| "./file": "./dist/file.js", | ||
| "./shell": "./dist/shell.js", | ||
| "./glob": "./dist/glob.js", | ||
@@ -11,0 +12,0 @@ "./path": "./dist/path.js", |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
35844
11.04%17
13.33%862
10.09%