From 60276188314e7999b904c877d42692584600d1d3 Mon Sep 17 00:00:00 2001 From: vincent067 Date: Fri, 20 Mar 2026 18:10:38 +0000 Subject: [PATCH 1/3] docs: Add practical examples directory Add examples/ directory with 4 common use cases: - build-script.js: Build automation with error handling - git-helpers.js: Git operation wrappers - parallel-tasks.js: Running commands in parallel - stream-processing.js: Output filtering with transforms All examples use ES modules and are self-contained. --- examples/build-script.js | 39 ++++++++++++++ examples/git-helpers.js | 70 ++++++++++++++++++++++++++ examples/parallel-tasks.js | 80 +++++++++++++++++++++++++++++ examples/readme.md | 39 ++++++++++++++ examples/stream-processing.js | 95 +++++++++++++++++++++++++++++++++++ 5 files changed, 323 insertions(+) create mode 100644 examples/build-script.js create mode 100644 examples/git-helpers.js create mode 100644 examples/parallel-tasks.js create mode 100644 examples/readme.md create mode 100644 examples/stream-processing.js diff --git a/examples/build-script.js b/examples/build-script.js new file mode 100644 index 0000000000..33a7da2230 --- /dev/null +++ b/examples/build-script.js @@ -0,0 +1,39 @@ +import { execa } from 'execa'; + +/** + * Build automation example with error handling + * Demonstrates running build commands with proper error handling + */ + +async function runBuild() { + console.log('๐Ÿ”จ Starting build process...\n'); + + try { + // Clean previous build + console.log('๐Ÿงน Cleaning previous build...'); + await execa('rm', ['-rf', 'dist']); + console.log('โœ… Clean complete\n'); + + // Type checking + console.log('๐Ÿ” Running type check...'); + await execa('tsc', ['--noEmit'], { stdio: 'inherit' }); + console.log('โœ… Type check passed\n'); + + // Building + console.log('๐Ÿ“ฆ Building project...'); + const { stdout } = await execa('npm', ['run', 'build']); + console.log(stdout); + console.log('โœ… Build successful!\n'); + + } catch (error) { + console.error('โŒ Build failed:', error.message); + process.exit(1); + } +} + +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + runBuild(); +} + +export { runBuild }; diff --git a/examples/git-helpers.js b/examples/git-helpers.js new file mode 100644 index 0000000000..1050b4c4ce --- /dev/null +++ b/examples/git-helpers.js @@ -0,0 +1,70 @@ +import { execa } from 'execa'; + +/** + * Git helper functions using execa + * Wrappers for common git operations + */ + +/** + * Get the current git branch + */ +async function getCurrentBranch() { + const { stdout } = await execa('git', ['branch', '--show-current']); + return stdout.trim(); +} + +/** + * Get the latest commit message + */ +async function getLatestCommit() { + const { stdout } = await execa('git', ['log', '-1', '--pretty=%s']); + return stdout.trim(); +} + +/** + * Check if working directory is clean + */ +async function isWorkingDirectoryClean() { + try { + await execa('git', ['diff', '--quiet']); + await execa('git', ['diff', '--staged', '--quiet']); + return true; + } catch { + return false; + } +} + +/** + * Get repository status + */ +async function getStatus() { + const { stdout } = await execa('git', ['status', '--short']); + return stdout || 'Working directory clean'; +} + +/** + * Stage all changes and commit + */ +async function stageAndCommit(message) { + await execa('git', ['add', '.']); + await execa('git', ['commit', '-m', message]); + console.log(`โœ… Committed: ${message}`); +} + +// Demo +if (import.meta.url === `file://${process.argv[1]}`) { + console.log('๐Ÿ“ Git Repository Info\n'); + + console.log(`Branch: ${await getCurrentBranch()}`); + console.log(`Last commit: ${await getLatestCommit()}`); + console.log(`Working directory: ${await isWorkingDirectoryClean() ? 'โœ… Clean' : 'โš ๏ธ Dirty'}`); + console.log(`Status:\n${await getStatus()}`); +} + +export { + getCurrentBranch, + getLatestCommit, + isWorkingDirectoryClean, + getStatus, + stageAndCommit, +}; diff --git a/examples/parallel-tasks.js b/examples/parallel-tasks.js new file mode 100644 index 0000000000..d9f9e24ffc --- /dev/null +++ b/examples/parallel-tasks.js @@ -0,0 +1,80 @@ +import { execa } from 'execa'; + +/** + * Running commands in parallel + * Demonstrates concurrent execution with proper error handling + */ + +/** + * Run multiple linting tasks in parallel + */ +async function runParallelLints() { + console.log('๐Ÿ” Running linters in parallel...\n'); + + try { + const tasks = [ + { name: 'ESLint', cmd: ['eslint', '.'] }, + { name: 'Prettier', cmd: ['prettier', '--check', '.'] }, + { name: 'TypeScript', cmd: ['tsc', '--noEmit'] }, + ]; + + const results = await Promise.allSettled( + tasks.map(async ({ name, cmd }) => { + console.log(`๐Ÿš€ Starting ${name}...`); + await execa('npx', cmd); + return { name, status: 'passed' }; + }) + ); + + console.log('\n๐Ÿ“Š Results:'); + let hasErrors = false; + + for (const result of results) { + if (result.status === 'fulfilled') { + console.log(` โœ… ${result.value.name}`); + } else { + console.log(` โŒ ${result.reason.name || 'Task'}`); + hasErrors = true; + } + } + + if (hasErrors) { + process.exit(1); + } + + } catch (error) { + console.error('Unexpected error:', error); + process.exit(1); + } +} + +/** + * Run multiple independent commands in parallel + * with a limit on concurrency + */ +async function runWithLimit(tasks, limit = 3) { + const results = []; + const executing = []; + + for (const [index, task] of tasks.entries()) { + const promise = task().then(result => ({ index, result })); + results.push(promise); + + if (tasks.length >= limit) { + executing.push(promise); + if (executing.length >= limit) { + await Promise.race(executing); + executing.splice(executing.findIndex(p => p === promise), 1); + } + } + } + + return Promise.all(results); +} + +// Demo +if (import.meta.url === `file://${process.argv[1]}`) { + runParallelLints(); +} + +export { runParallelLints, runWithLimit }; diff --git a/examples/readme.md b/examples/readme.md new file mode 100644 index 0000000000..a996dbb9d8 --- /dev/null +++ b/examples/readme.md @@ -0,0 +1,39 @@ +# Execa Examples + +This directory contains practical examples demonstrating common use cases with `execa`. + +## Examples + +### build-script.js +Build automation with error handling. Shows how to chain build commands and handle failures gracefully. + +```bash +node examples/build-script.js +``` + +### git-helpers.js +Git operation wrappers. Demonstrates async helper functions for common git operations. + +```bash +node examples/git-helpers.js +``` + +### parallel-tasks.js +Running commands in parallel. Shows concurrent execution with `Promise.allSettled()` and progress tracking. + +```bash +node examples/parallel-tasks.js +``` + +### stream-processing.js +Output filtering with transforms. Demonstrates using Node.js Transform streams to process command output. + +```bash +node examples/stream-processing.js +``` + +## Notes + +- All examples use ES modules (`.js` extension with `import` syntax) +- Each example can be run directly or imported as a module +- Error handling is included in all examples diff --git a/examples/stream-processing.js b/examples/stream-processing.js new file mode 100644 index 0000000000..8937d58f06 --- /dev/null +++ b/examples/stream-processing.js @@ -0,0 +1,95 @@ +import { execa } from 'execa'; +import { Transform } from 'node:stream'; + +/** + * Stream processing with transforms + * Demonstrates filtering and transforming command output + */ + +/** + * Filter lines containing specific keywords + */ +function createKeywordFilter(keywords) { + return new Transform({ + transform(chunk, encoding, callback) { + const lines = chunk.toString().split('\n'); + const filtered = lines + .filter(line => keywords.some(kw => line.toLowerCase().includes(kw.toLowerCase()))) + .join('\n'); + callback(null, filtered ? filtered + '\n' : ''); + }, + }); +} + +/** + * Prefix each line with a timestamp + */ +function createTimestampPrefix() { + return new Transform({ + transform(chunk, encoding, callback) { + const lines = chunk.toString().split('\n'); + const prefixed = lines + .filter(line => line.trim()) + .map(line => `[${new Date().toISOString()}] ${line}`) + .join('\n'); + callback(null, prefixed + '\n'); + }, + }); +} + +/** + * Count lines and show progress + */ +function createProgressCounter() { + let count = 0; + return new Transform({ + transform(chunk, encoding, callback) { + const lines = chunk.toString().split('\n'); + const nonEmpty = lines.filter(line => line.trim()); + count += nonEmpty.length; + process.stdout.write(`\r๐Ÿ“Š Processed ${count} lines...`); + callback(null, chunk); + }, + flush(callback) { + console.log(`\nโœ… Total: ${count} lines`); + callback(); + }, + }); +} + +/** + * Run a command and process its output through transforms + */ +async function runWithTransforms(command, args, transforms) { + const subprocess = execa(command, args); + + let stream = subprocess.stdout; + for (const transform of transforms) { + stream = stream.pipe(transform); + } + + stream.pipe(process.stdout); + await subprocess; +} + +// Demo +if (import.meta.url === `file://${process.argv[1]}`) { + console.log('๐Ÿ” Running npm audit with filtered output:\n'); + + try { + await runWithTransforms( + 'npm', + ['audit', '--json'], + [createKeywordFilter(['severity', 'critical', 'high'])] + ); + } catch { + // npm audit exits with non-zero on vulnerabilities + } +} + +export { + createKeywordFilter, + createTimestampPrefix, + createProgressCounter, + runWithTransforms, +}; From 6b2d1d3bac4aeed602c1b88d887b3e88959b26c4 Mon Sep 17 00:00:00 2001 From: vincent067 Date: Fri, 20 Mar 2026 18:43:08 +0000 Subject: [PATCH 2/3] docs: Improve Windows documentation with detailed cross-spawn fixes Add comprehensive documentation for Windows-specific fixes: - PATHEXT support explanation with examples - How Windows execution works under the hood - Comparison table: automatic fixes vs shell mode - Detailed breakdown of node-cross-spawn fixes - Best practices for choosing between default mode and shell mode Fixes sindresorhus/execa#997 --- docs/windows.md | 61 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/docs/windows.md b/docs/windows.md index f475e8e43f..e5286c1a34 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -24,17 +24,19 @@ await execa`node ./script.js`; Although Windows does not natively support shebangs, Execa adds support for them. -## Signals +## PATHEXT support -Only few [signals](termination.md#other-signals) work on Windows with Node.js: [`SIGTERM`](termination.md#sigterm), [`SIGKILL`](termination.md#sigkill), [`SIGINT`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGINT) and [`SIGQUIT`](termination.md#sigquit). Also, sending signals from other processes is [not supported](termination.md#signal-name-and-description). Finally, the [`forceKillAfterDelay`](api.md#optionsforcekillafterdelay) option [is a noop](termination.md#forceful-termination) on Windows. +On Windows, the [`PATHEXT`](https://ss64.com/nt/path.html#pathext) environment variable defines which file extensions can be executed without specifying the extension. For example, if `.JS` is in `PATHEXT`: -## Asynchronous I/O - -The default value for the [`stdin`](api.md#optionsstdin), [`stdout`](api.md#optionsstdout) and [`stderr`](api.md#optionsstderr) options is [`'pipe'`](output.md#stdout-and-stderr). This returns the output as [`result.stdout`](api.md#resultstdout) and [`result.stderr`](api.md#resultstderr) and allows for [manual streaming](streams.md#manual-streaming). +```js +// On Windows, if PATHEXT includes .JS, both work: +await execa`node script.js`; +await execa`node script`; // Automatically finds script.js +``` -Instead of `'pipe'`, `'overlapped'` can be used instead to use [asynchronous I/O](https://learn.microsoft.com/en-us/windows/win32/fileio/synchronous-and-asynchronous-i-o) under-the-hood on Windows, instead of the default behavior which is synchronous. On other platforms, asynchronous I/O is always used, so `'overlapped'` behaves the same way as `'pipe'`. +Execa uses [`node-which`](https://github.com/npm/node-which) internally to resolve the absolute file path of the executable, ensuring proper `PATHEXT` support on Windows. -## Escaping +## Commands with spaces Windows requires files and arguments to be quoted when they contain spaces, tabs, backslashes or double quotes. Unlike Unix, this is needed even when no [shell](shell.md) is used. @@ -44,6 +46,51 @@ When not using any shell, Execa performs that quoting automatically. This ensure await execa`npm run ${'task with space'}`; ``` +## How Windows execution works + +Under the hood, Execa uses [`node-cross-spawn`](https://github.com/moxystudio/node-cross-spawn) to fix several Windows-specific issues. Here's what happens when you run a command on Windows: + +1. **Resolve the executable**: The command name is resolved to an absolute path using `node-which`, which properly handles `PATHEXT`. + +2. **Detect shebangs**: If the file is not a `.exe` or `.com` file, Execa reads the first 150 bytes to detect any shebang (e.g., `#!/usr/bin/env node`). + +3. **Execute via cmd.exe**: For files with shebangs or scripts (like `.bat`, `.cmd` files), Execa runs them through `cmd.exe /d /s /c` with proper escaping, rather than using Node.js's default behavior. + +This process is automatic and only applies to Windows. It is skipped when using [`shell: true`](shell.md), though using a shell has different trade-offs (see below). + +## Comparison: Automatic fixes vs shell mode + +When deciding whether to use a shell on Windows, consider the following: + +| Feature | Automatic fixes (default) | `shell: true` | +|---------|--------------------------|---------------| +| PATHEXT support | โœ… Yes | โœ… Yes (via `cmd.exe`) | +| Shebang support | โœ… Yes | โŒ No | +| Escaping quality | โœ… Excellent (cross-spawn) | โš ๏ธ Good (Node.js built-in) | +| Extra overhead | Minimal | One more process | + +**Recommendation**: Use the default (automatic fixes) for most cases. Use `shell: true` only when you specifically need shell features like redirection (`>`, `|`) or environment variable expansion. + +```js +// Default: Uses automatic Windows fixes +await execa`./my-script.js`; + +// Shell mode: Use only when you need shell features +await execa({shell: true})`echo %PATH% | findstr "Node"`; +``` + +## Signals + +Only few [signals](termination.md#other-signals) work on Windows with Node.js: [`SIGTERM`](termination.md#sigterm), [`SIGKILL`](termination.md#sigkill), [`SIGINT`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGINT) and [`SIGQUIT`](termination.md#sigquit). Also, sending signals from other processes is [not supported](termination.md#signal-name-and-description). Finally, the [`forceKillAfterDelay`](api.md#optionsforcekillafterdelay) option [is a noop](termination.md#forceful-termination) on Windows. + +## Asynchronous I/O + +The default value for the [`stdin`](api.md#optionsstdin), [`stdout`](api.md#optionsstdout) and [`stderr`](api.md#optionsstderr) options is [`'pipe'`](output.md#stdout-and-stderr). This returns the output as [`result.stdout`](api.md#resultstdout) and [`result.stderr`](api.md#resultstderr) and allows for [manual streaming](streams.md#manual-streaming). + +Instead of `'pipe'`, `'overlapped'` can be used instead to use [asynchronous I/O](https://learn.microsoft.com/en-us/windows/win32/fileio/synchronous-and-asynchronous-i-o) under-the-hood on Windows, instead of the default behavior which is synchronous. On other platforms, asynchronous I/O is always used, so `'overlapped'` behaves the same way as `'pipe'`. + +## Escaping with shells + When using a [shell](shell.md), the user must manually perform shell-specific quoting, on both Unix and Windows. When the [`shell`](api.md#optionsshell) option is `true`, [`cmd.exe`](https://en.wikipedia.org/wiki/Cmd.exe) is used on Windows and `sh` on Unix. Unfortunately, both shells use different quoting rules. With `cmd.exe`, this mostly involves double quoting arguments and prepending double quotes with a backslash. ```js From 66af41533b066d44eaed3a9140241e3400cf7a55 Mon Sep 17 00:00:00 2001 From: liwenjun-dev Date: Sun, 22 Mar 2026 00:50:29 +0000 Subject: [PATCH 3/3] docs(examples): add security scanning utilities example Add a new example demonstrating security scanning use cases with execa: - npm audit integration for dependency vulnerability scanning - semgrep integration for static analysis security testing - gitleaks integration for secret detection in git history - Comprehensive security report generation This example shows how to: - Handle JSON output from security tools - Process exit codes (security tools often exit non-zero when issues found) - Gracefully handle missing tools with helpful error messages - Aggregate results from multiple security scans The example follows the same patterns as other examples in the directory and includes proper error handling, ES module syntax, and can be run directly or imported as a module. --- examples/readme.md | 7 ++ examples/security-scanning.js | 214 ++++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 examples/security-scanning.js diff --git a/examples/readme.md b/examples/readme.md index a996dbb9d8..4ce2322d5d 100644 --- a/examples/readme.md +++ b/examples/readme.md @@ -25,6 +25,13 @@ Running commands in parallel. Shows concurrent execution with `Promise.allSettle node examples/parallel-tasks.js ``` +### security-scanning.js +Security scanning utilities. Demonstrates integrating security tools like npm audit, semgrep, and gitleaks. + +```bash +node examples/security-scanning.js +``` + ### stream-processing.js Output filtering with transforms. Demonstrates using Node.js Transform streams to process command output. diff --git a/examples/security-scanning.js b/examples/security-scanning.js new file mode 100644 index 0000000000..88e99ebd72 --- /dev/null +++ b/examples/security-scanning.js @@ -0,0 +1,214 @@ +import { execa } from 'execa'; + +/** + * Security scanning utilities using execa + * Demonstrates running security tools and processing their output + */ + +/** + * Run npm audit and parse the results + * Returns summary of vulnerabilities found + */ +async function runNpmAudit(options = {}) { + const args = ['audit', '--json']; + + // Add audit level filter if specified + if (options.level) { + args.push('--audit-level', options.level); + } + + try { + const { stdout } = await execa('npm', args); + const audit = JSON.parse(stdout); + + return { + success: true, + vulnerabilities: audit.metadata?.vulnerabilities || {}, + total: audit.metadata?.totalDependencies || 0, + advisories: Object.keys(audit.advisories || {}).length, + }; + } catch (error) { + // npm audit returns exit code 1 when vulnerabilities are found + // but still outputs valid JSON + if (error.stdout) { + try { + const audit = JSON.parse(error.stdout); + return { + success: false, + vulnerabilities: audit.metadata?.vulnerabilities || {}, + total: audit.metadata?.totalDependencies || 0, + advisories: Object.keys(audit.advisories || {}).length, + exitCode: error.exitCode, + }; + } catch { + // JSON parsing failed + } + } + throw error; + } +} + +/** + * Run a security linter (semgrep) on the codebase + * Requires semgrep to be installed: pip install semgrep + */ +async function runSemgrepScan(rules = 'p/security-audit') { + try { + const { stdout } = await execa('semgrep', [ + '--config', rules, + '--json', + '--quiet', + '.', + ]); + + const results = JSON.parse(stdout); + return { + success: true, + findings: results.results?.length || 0, + errors: results.errors?.length || 0, + rules: Object.keys(results.rules || {}).length, + details: results.results || [], + }; + } catch (error) { + // semgrep returns non-zero when findings exist + if (error.stdout) { + try { + const results = JSON.parse(error.stdout); + return { + success: false, + findings: results.results?.length || 0, + errors: results.errors?.length || 0, + rules: Object.keys(results.rules || {}).length, + details: results.results || [], + exitCode: error.exitCode, + }; + } catch { + // JSON parsing failed + } + } + + // semgrep not installed or other error + if (error.message?.includes('ENOENT')) { + return { + success: false, + error: 'semgrep not installed. Install with: pip install semgrep', + }; + } + throw error; + } +} + +/** + * Check for secrets in git history using gitleaks + * Requires gitleaks to be installed: https://github.com/gitleaks/gitleaks + */ +async function runSecretScan() { + try { + const { stdout } = await execa('gitleaks', [ + 'git', + '.', + '--verbose', + '--redact', + ]); + + return { + success: true, + secretsFound: false, + message: 'No secrets detected', + }; + } catch (error) { + // gitleaks exits with code 1 when secrets are found + if (error.exitCode === 1 && error.stdout) { + const lines = error.stdout.split('\n').filter(line => line.includes('Finding:')); + return { + success: false, + secretsFound: true, + findings: lines.length, + message: `Found ${lines.length} potential secret(s)`, + hint: 'Run "gitleaks git . -v" for details (secrets redacted in output)', + }; + } + + if (error.message?.includes('ENOENT')) { + return { + success: false, + error: 'gitleaks not installed. See: https://github.com/gitleaks/gitleaks', + }; + } + throw error; + } +} + +/** + * Generate a security report summary + */ +async function generateSecurityReport() { + console.log('๐Ÿ”’ Running Security Scans...\n'); + + const report = { + timestamp: new Date().toISOString(), + scans: {}, + }; + + // Run npm audit + console.log('๐Ÿ“ฆ Running npm audit...'); + try { + report.scans.npmAudit = await runNpmAudit(); + const v = report.scans.npmAudit.vulnerabilities; + if (v.critical > 0 || v.high > 0) { + console.log(` โš ๏ธ ${v.critical} critical, ${v.high} high severity vulnerabilities`); + } else { + console.log(` โœ… ${report.scans.npmAudit.total} dependencies checked`); + } + } catch (error) { + console.log(` โŒ npm audit failed: ${error.message}`); + report.scans.npmAudit = { error: error.message }; + } + + // Run secret scan + console.log('\n๐Ÿ” Scanning for secrets...'); + try { + report.scans.secrets = await runSecretScan(); + if (report.scans.secrets.secretsFound) { + console.log(` โš ๏ธ ${report.scans.secrets.message}`); + } else if (report.scans.secrets.error) { + console.log(` โ„น๏ธ ${report.scans.secrets.error}`); + } else { + console.log(` โœ… ${report.scans.secrets.message}`); + } + } catch (error) { + console.log(` โŒ Secret scan failed: ${error.message}`); + report.scans.secrets = { error: error.message }; + } + + // Run semgrep + console.log('\n๐Ÿ›ก๏ธ Running semgrep security audit...'); + try { + report.scans.semgrep = await runSemgrepScan(); + if (report.scans.semgrep.findings > 0) { + console.log(` โš ๏ธ Found ${report.scans.semgrep.findings} security issue(s)`); + } else if (report.scans.semgrep.error) { + console.log(` โ„น๏ธ ${report.scans.semgrep.error}`); + } else { + console.log(' โœ… No security issues found'); + } + } catch (error) { + console.log(` โŒ Semgrep failed: ${error.message}`); + report.scans.semgrep = { error: error.message }; + } + + console.log('\n๐Ÿ“Š Security Report Complete'); + return report; +} + +// Demo +if (import.meta.url === `file://${process.argv[1]}`) { + await generateSecurityReport(); +} + +export { + runNpmAudit, + runSemgrepScan, + runSecretScan, + generateSecurityReport, +};