├── .eslintignore ├── test ├── fixtures │ ├── console.js │ ├── timeout.js │ ├── timeout-stderr.js │ ├── timeout-and-exit.js │ └── child-process-with-unclosed-stdio.cjs ├── helper.ts └── runscript.test.ts ├── .eslintrc ├── tsconfig.json ├── .gitignore ├── .github └── workflows │ ├── release.yml │ ├── nodejs.yml │ └── nodejs-14.yml ├── LICENSE.txt ├── package.json ├── README.md ├── CHANGELOG.md └── src └── index.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures/ts/**/*.js 2 | -------------------------------------------------------------------------------- /test/fixtures/console.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.stdout.write('stdout'); 4 | process.stderr.write('stderr'); 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-egg/typescript", 4 | "eslint-config-egg/lib/rules/enforce-node-prefix" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/timeout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | console.log('timer start'); 4 | setInterval(() => { 5 | console.log('echo every 500ms'); 6 | }, 500); 7 | -------------------------------------------------------------------------------- /test/fixtures/timeout-stderr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | console.error('timer start'); 4 | setInterval(() => { 5 | console.error('echo every 600ms'); 6 | }, 600); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eggjs/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/timeout-and-exit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | console.error('timer start'); 4 | setInterval(() => { 5 | console.error('echo every 600ms'); 6 | }, 600); 7 | 8 | setTimeout(() => { 9 | console.error('exit'); 10 | process.exit(0); 11 | }, 1500); 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.seed 2 | *.log 3 | *.csv 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | 9 | pids 10 | logs 11 | results 12 | 13 | node_modules 14 | npm-debug.log 15 | coverage/ 16 | test/fixtures/ts/**/*.js 17 | .DS_Store 18 | package-lock.json 19 | .tshy 20 | dist 21 | .eslintcache 22 | -------------------------------------------------------------------------------- /test/helper.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | const __filename = fileURLToPath(import.meta.url); 5 | const __dirname = path.dirname(__filename); 6 | 7 | export function getFixtures(filename: string) { 8 | return path.join(__dirname, 'fixtures', filename); 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: Node.js 10 | uses: node-modules/github-actions/.github/workflows/node-release.yml@master 11 | secrets: 12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | Job: 11 | name: Node.js 12 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 13 | with: 14 | os: 'ubuntu-latest, macos-latest, windows-latest' 15 | version: '16, 18, 20, 22' 16 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-14.yml: -------------------------------------------------------------------------------- 1 | name: Node.js 14 CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js 16 | uses: irby/setup-node-nvm@master 17 | with: 18 | node-version: '16.x' 19 | - run: npm install 20 | - run: npm run prepublishOnly 21 | - run: node -v 22 | - run: . /home/runner/mynvm/nvm.sh && nvm install 14 && nvm use 14 && node -v && npm run test:node14 23 | -------------------------------------------------------------------------------- /test/fixtures/child-process-with-unclosed-stdio.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { runScript } = require('../../'); 3 | const argv = process.argv.slice(2); 4 | 5 | (async () => { 6 | if (argv[0] === undefined) { 7 | runScript(`node ${path.join(__dirname, './child-process-with-unclosed-stdio.cjs')} child`); 8 | await new Promise(resolve => { 9 | setTimeout(resolve, 1000); 10 | }); 11 | console.log('child finish'); 12 | process.exit(); 13 | } else if (argv[0] === 'child') { 14 | // eslint-disable-next-line no-constant-condition 15 | while (true) { 16 | console.log('grandChild running'); 17 | await new Promise(resolve => { 18 | setTimeout(resolve, 1000); 19 | }); 20 | } 21 | } 22 | })(); 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright (c) 2016 - present node-modules and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runscript", 3 | "version": "2.0.1", 4 | "description": "Run script easy!", 5 | "scripts": { 6 | "lint": "eslint --cache src test --ext .ts", 7 | "pretest": "npm run prepublishOnly", 8 | "test": "npm run lint -- --fix && egg-bin test", 9 | "test:node14": "egg-bin test", 10 | "preci": "npm run prepublishOnly", 11 | "ci": "npm run lint && egg-bin cov && attw --pack", 12 | "prepublishOnly": "tshy && tshy-after" 13 | }, 14 | "dependencies": { 15 | "is-type-of": "^2.2.0" 16 | }, 17 | "devDependencies": { 18 | "@arethetypeswrong/cli": "^0.15.3", 19 | "@eggjs/tsconfig": "1", 20 | "@types/mocha": "10", 21 | "@types/node": "22", 22 | "autod": "^3.1.2", 23 | "egg-bin": "6", 24 | "eslint": "8", 25 | "eslint-config-egg": "14", 26 | "tshy": "2", 27 | "tshy-after": "1", 28 | "typescript": "5" 29 | }, 30 | "homepage": "https://github.com/node-modules/runscript", 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/node-modules/runscript.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/node-modules/runscript/issues" 37 | }, 38 | "keywords": [ 39 | "runscript", 40 | "run-script", 41 | "npm run" 42 | ], 43 | "engines": { 44 | "node": ">=16.0.0" 45 | }, 46 | "author": "fengmk2 (https://github.com/fengmk2)", 47 | "license": "MIT", 48 | "type": "module", 49 | "tshy": { 50 | "exports": { 51 | ".": "./src/index.ts", 52 | "./package.json": "./package.json" 53 | } 54 | }, 55 | "exports": { 56 | ".": { 57 | "import": { 58 | "types": "./dist/esm/index.d.ts", 59 | "default": "./dist/esm/index.js" 60 | }, 61 | "require": { 62 | "types": "./dist/commonjs/index.d.ts", 63 | "default": "./dist/commonjs/index.js" 64 | } 65 | }, 66 | "./package.json": "./package.json" 67 | }, 68 | "files": [ 69 | "dist", 70 | "src" 71 | ], 72 | "types": "./dist/commonjs/index.d.ts", 73 | "main": "./dist/commonjs/index.js", 74 | "module": "./dist/esm/index.js" 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # runscript 2 | 3 | 📢📢📢📢📢 You should use [execa](https://github.com/sindresorhus/execa) instead. 📢📢📢📢 4 | 5 | --- 6 | 7 | [![NPM version][npm-image]][npm-url] 8 | [![Node.js CI](https://github.com/node-modules/runscript/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/runscript/actions/workflows/nodejs.yml) 9 | [![Test coverage][codecov-image]][codecov-url] 10 | [![npm download][download-image]][download-url] 11 | 12 | [npm-image]: https://img.shields.io/npm/v/runscript.svg?style=flat-square 13 | [npm-url]: https://npmjs.org/package/runscript 14 | [codecov-image]: https://codecov.io/github/node-modules/runscript/coverage.svg?branch=master 15 | [codecov-url]: https://codecov.io/github/node-modules/runscript?branch=master 16 | [download-image]: https://img.shields.io/npm/dm/runscript.svg?style=flat-square 17 | [download-url]: https://npmjs.org/package/runscript 18 | 19 | Run script easy! 20 | 21 | ## Installation 22 | 23 | ```bash 24 | npm install runscript 25 | ``` 26 | 27 | ## Quick start 28 | 29 | Commonjs 30 | 31 | ```js 32 | const { runScript } = require('runscript'); 33 | 34 | runScript('node -v', { stdio: 'pipe' }) 35 | .then(stdio => { 36 | console.log(stdio); 37 | }) 38 | .catch(err => { 39 | console.error(err); 40 | }); 41 | ``` 42 | 43 | ESM & TypeScript 44 | 45 | ```js 46 | import { runScript } from 'runscript'; 47 | 48 | runScript('node -v', { stdio: 'pipe' }) 49 | .then(stdio => { 50 | console.log(stdio); 51 | }) 52 | .catch(err => { 53 | console.error(err); 54 | }); 55 | ``` 56 | 57 | ### run with timeout 58 | 59 | Run user script for a maximum of 10 seconds. 60 | 61 | ```js 62 | const { runScript } = require('runscript'); 63 | 64 | runScript('node user-script.js', { stdio: 'pipe' }, { timeout: 10000 }) 65 | .then(stdio => { 66 | console.log(stdio); 67 | }) 68 | .catch(err => { 69 | console.error(err); 70 | }); 71 | ``` 72 | 73 | ## Upgrade from 1.x to 2.x 74 | 75 | ```js 76 | // 1.x 77 | // const runscript = require('runscript'); 78 | 79 | // 2.x 80 | const { runscript } = require('runscript'); 81 | ``` 82 | 83 | ## License 84 | 85 | [MIT](LICENSE.txt) 86 | 87 | ## Contributors 88 | 89 | [![Contributors](https://contrib.rocks/image?repo=node-modules/runscript)](https://github.com/node-modules/runscript/graphs/contributors) 90 | 91 | Made with [contributors-img](https://contrib.rocks). 92 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.0.1](https://github.com/node-modules/runscript/compare/v2.0.0...v2.0.1) (2024-12-10) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * export runscript function ([#24](https://github.com/node-modules/runscript/issues/24)) ([70395b2](https://github.com/node-modules/runscript/commit/70395b27f10d72c3e0406eadc6a6dc42abeeb464)) 9 | 10 | ## [2.0.0](https://github.com/node-modules/runscript/compare/v1.6.0...v2.0.0) (2024-12-09) 11 | 12 | 13 | ### ⚠ BREAKING CHANGES 14 | 15 | * drop Node.js < 14.0.0 support 16 | 17 | part of https://github.com/eggjs/egg/issues/3644 18 | 19 | ### Features 20 | 21 | * support cjs and esm both by tshy ([#23](https://github.com/node-modules/runscript/issues/23)) ([eb81077](https://github.com/node-modules/runscript/commit/eb81077c199389f4889ffeb5dc00b3ca344f5008)) 22 | 23 | ## [1.6.0](https://github.com/node-modules/runscript/compare/v1.5.4...v1.6.0) (2024-01-11) 24 | 25 | 26 | ### Features 27 | 28 | * drop debug deps ([#22](https://github.com/node-modules/runscript/issues/22)) ([c0231b9](https://github.com/node-modules/runscript/commit/c0231b9e4dcc6d79c7e03de452d5eb56d9364b1f)) 29 | 30 | ## [1.5.4](https://github.com/node-modules/runscript/compare/v1.5.3...v1.5.4) (2024-01-11) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * add check cmd on Windows ([#21](https://github.com/node-modules/runscript/issues/21)) ([ff495d9](https://github.com/node-modules/runscript/commit/ff495d9163a0e655f8aebf1c466021f20ad7274a)) 36 | 37 | --- 38 | 39 | 40 | 1.5.3 / 2022-05-20 41 | ================== 42 | 43 | **fixes** 44 | * [[`fe6d304`](http://github.com/node-modules/runscript/commit/fe6d304ab66229f1e58ecac9069e15d9eedc2e04)] - fix: use object.assign instead of object.create (#16) (lusyn <>) 45 | 46 | **others** 47 | * [[`0008b4b`](http://github.com/node-modules/runscript/commit/0008b4bbcd32f348dc86eb374615562c63dfe24c)] - 👌 IMPROVE: You should use execa instead (#15) (fengmk2 <>) 48 | 49 | 1.5.2 / 2022-03-08 50 | ================== 51 | 52 | **fixes** 53 | * [[`475fc7d`](http://github.com/node-modules/runscript/commit/475fc7dcf9e8558875713f4ec016ff251315bcdd)] - fix: Use to exit event instead of close (#13) (lusyn <>) 54 | 55 | **others** 56 | * [[`53f88a2`](http://github.com/node-modules/runscript/commit/53f88a28008049b0c17be8c79a4ea24d9288b0b9)] - 📖 DOC: Add contributors (#14) (fengmk2 <>) 57 | * [[`09bb8cd`](http://github.com/node-modules/runscript/commit/09bb8cd38add3ae5b1b1ab46bb090dbcca4ae3b7)] - chore: fix ci badges (fengmk2 <>) 58 | * [[`59b76ef`](http://github.com/node-modules/runscript/commit/59b76efac12095417155602fbd39f530cf3f8600)] - test: add more tsd test cases (#12) (fengmk2 <>) 59 | 60 | 1.5.1 / 2021-05-07 61 | ================== 62 | 63 | **fixes** 64 | * [[`0fd91a4`](http://github.com/node-modules/runscript/commit/0fd91a420da493d1885f7ccd66a6a77394144551)] - fix(interface): Add the missing `extraOpts` paramter in type declaration (#11) (Aaron <>) 65 | 66 | **others** 67 | * [[`6fe00d6`](http://github.com/node-modules/runscript/commit/6fe00d69fd91914f7f0a05f18c38fdc1252946fb)] - deps: upgrade dev deps (#10) (fengmk2 <>) 68 | 69 | 1.5.0 / 2020-05-22 70 | ================== 71 | 72 | **features** 73 | * [[`4d82803`](http://github.com/node-modules/runscript/commit/4d82803172f0a0ef0dd4a5ffecf6e4c44ae63484)] - feat: expose exitcode in error instance (#9) (Otto Mao <>) 74 | 75 | 1.4.0 / 2019-07-06 76 | ================== 77 | 78 | **features** 79 | * [[`a0d7ffb`](http://github.com/node-modules/runscript/commit/a0d7ffb815041baa89b46fb5d76b23f759cd56fb)] - feat: run script with timeout (#8) (fengmk2 <>) 80 | 81 | 1.3.1 / 2019-06-15 82 | ================== 83 | 84 | **fixes** 85 | * [[`8998c8f`](http://github.com/node-modules/runscript/commit/8998c8f778ce24bb36c653903719fd4ff2189a70)] - fix: add declarations (#7) (吖猩 <>) 86 | 87 | **others** 88 | * [[`f618799`](http://github.com/node-modules/runscript/commit/f618799676b43ff2ecda94f7e1677b51cacb8af5)] - test: node 10, 12 (#6) (fengmk2 <>) 89 | 90 | 1.3.0 / 2017-07-28 91 | ================== 92 | 93 | * feat: support relative path on windows (#5) 94 | 95 | 1.2.1 / 2017-02-22 96 | ================== 97 | 98 | * fix: exit code < 0 as error too (#3) 99 | 100 | 1.2.0 / 2017-02-04 101 | ================== 102 | 103 | * feat: add options stdout and stderr (#2) 104 | 105 | 1.1.0 / 2016-03-06 106 | ================== 107 | 108 | * feat: support return stdio 109 | 110 | 1.0.0 / 2016-02-05 111 | ================== 112 | 113 | * First release 114 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { debuglog } from 'node:util'; 2 | import assert from 'node:assert'; 3 | import path from 'node:path'; 4 | import { spawn, spawnSync, type SpawnOptions } from 'node:child_process'; 5 | import { type Writable } from 'node:stream'; 6 | import { isWritable } from 'is-type-of'; 7 | 8 | const debug = debuglog('runscript'); 9 | 10 | function isCmd() { 11 | if (process.platform !== 'win32') { 12 | return false; 13 | } 14 | 15 | try { 16 | const result = spawnSync('ls', { 17 | stdio: 'pipe', 18 | }); 19 | 20 | return result.error !== undefined; 21 | } catch (err) { 22 | return true; 23 | } 24 | } 25 | 26 | export interface Options extends SpawnOptions { 27 | stdout?: Writable; 28 | stderr?: Writable; 29 | } 30 | 31 | export interface ExtraOptions { 32 | timeout?: number; 33 | } 34 | 35 | export interface Stdio { 36 | stdout: Buffer | null; 37 | stderr: Buffer | null; 38 | } 39 | 40 | export interface StdError extends Error { 41 | stdio: Stdio; 42 | } 43 | 44 | export class RunScriptError extends Error { 45 | stdio: Stdio; 46 | exitcode: number | null; 47 | 48 | constructor(message: string, stdio: Stdio, exitcode: number | null, options?: ErrorOptions) { 49 | super(message, options); 50 | this.name = this.constructor.name; 51 | this.stdio = stdio; 52 | this.exitcode = exitcode; 53 | Error.captureStackTrace(this, this.constructor); 54 | } 55 | } 56 | 57 | export class RunScriptTimeoutError extends Error { 58 | stdio: Stdio; 59 | timeout: number; 60 | 61 | constructor(message: string, stdio: Stdio, timeout: number, options?: ErrorOptions) { 62 | super(message, options); 63 | this.name = this.constructor.name; 64 | this.stdio = stdio; 65 | this.timeout = timeout; 66 | Error.captureStackTrace(this, this.constructor); 67 | } 68 | } 69 | 70 | /** 71 | * Run shell script in child process 72 | * Support OSX, Linux and Windows 73 | * @param {String} script - full script string, like `git clone https://github.com/node-modules/runscript.git` 74 | * @param {Object} [options] - spawn options 75 | * @see https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options 76 | * @param {Object} [extraOptions] - extra options for running 77 | * - {Number} [extraOptions.timeout] - child process running timeout 78 | * @return {Object} stdio object, will contains stdio.stdout and stdio.stderr buffer. 79 | */ 80 | export function runScript(script: string, options: Options = {}, extraOptions: ExtraOptions = {}): Promise { 81 | return new Promise((resolve, reject) => { 82 | options.env = options.env || Object.assign({}, process.env); 83 | options.cwd = options.cwd || process.cwd(); 84 | if (typeof options.cwd === 'object') { 85 | // convert URL object to string 86 | options.cwd = String(options.cwd); 87 | } 88 | options.stdio = options.stdio || 'inherit'; 89 | if (options.stdout) { 90 | assert(isWritable(options.stdout), 'options.stdout should be writable stream'); 91 | } 92 | if (options.stderr) { 93 | assert(isWritable(options.stderr), 'options.stderr should be writable stream'); 94 | } 95 | 96 | let sh = 'sh'; 97 | let shFlag = '-c'; 98 | 99 | if (process.platform === 'win32') { 100 | sh = process.env.comspec || 'cmd'; 101 | shFlag = '/d /s /c'; 102 | options.windowsVerbatimArguments = true; 103 | if (script.indexOf('./') === 0 || script.indexOf('.\\') === 0 || 104 | script.indexOf('../') === 0 || script.indexOf('..\\') === 0) { 105 | const splits = script.split(' '); 106 | // in bash C:\Windows\system32 -> C:\\Windows\\system32 107 | splits[0] = path.join(isCmd() ? options.cwd : path.normalize(options.cwd), splits[0]); 108 | script = splits.join(' '); 109 | } 110 | } 111 | 112 | debug('%s %s %s, %j, %j', sh, shFlag, script, options, extraOptions); 113 | const proc = spawn(sh, [ shFlag, script ], options); 114 | const stdout: Buffer[] = []; 115 | const stderr: Buffer[] = []; 116 | let isEnd = false; 117 | let timeoutTimer: NodeJS.Timeout; 118 | 119 | if (proc.stdout) { 120 | proc.stdout.on('data', (buf: Buffer) => { 121 | debug('stdout %d bytes', buf.length); 122 | stdout.push(buf); 123 | }); 124 | if (options.stdout) { 125 | proc.stdout.pipe(options.stdout); 126 | } 127 | } 128 | if (proc.stderr) { 129 | proc.stderr.on('data', (buf: Buffer) => { 130 | debug('stderr %d bytes', buf.length); 131 | stderr.push(buf); 132 | }); 133 | if (options.stderr) { 134 | proc.stderr.pipe(options.stderr); 135 | } 136 | } 137 | 138 | proc.on('error', err => { 139 | debug('proc emit error: %s', err); 140 | if (isEnd) { 141 | return; 142 | } 143 | isEnd = true; 144 | clearTimeout(timeoutTimer); 145 | 146 | reject(err); 147 | }); 148 | 149 | proc.on('exit', code => { 150 | debug('proc emit exit: %s', code); 151 | if (isEnd) { 152 | return; 153 | } 154 | isEnd = true; 155 | clearTimeout(timeoutTimer); 156 | 157 | const stdio: Stdio = { 158 | stdout: null, 159 | stderr: null, 160 | }; 161 | if (stdout.length > 0) { 162 | stdio.stdout = Buffer.concat(stdout); 163 | } 164 | if (stderr.length > 0) { 165 | stdio.stderr = Buffer.concat(stderr); 166 | } 167 | if (code !== 0) { 168 | const err = new RunScriptError( 169 | `Run "${sh} ${shFlag} ${script}" error, exit code ${code}`, stdio, code); 170 | return reject(err); 171 | } 172 | return resolve(stdio); 173 | }); 174 | 175 | proc.on('close', code => { 176 | debug('proc emit close: %s', code); 177 | }); 178 | 179 | if (typeof extraOptions.timeout === 'number' && extraOptions.timeout > 0) { 180 | const timeout = extraOptions.timeout; 181 | // start timer 182 | timeoutTimer = setTimeout(() => { 183 | debug('proc run timeout: %dms', timeout); 184 | isEnd = true; 185 | debug('kill child process %s', proc.pid); 186 | proc.kill(); 187 | 188 | const stdio: Stdio = { 189 | stdout: null, 190 | stderr: null, 191 | }; 192 | if (stdout.length > 0) { 193 | stdio.stdout = Buffer.concat(stdout); 194 | } 195 | if (stderr.length > 0) { 196 | stdio.stderr = Buffer.concat(stderr); 197 | } 198 | const err = new RunScriptTimeoutError( 199 | `Run "${sh} ${shFlag} ${script}" timeout in ${extraOptions.timeout}ms`, stdio, timeout); 200 | return reject(err); 201 | }, timeout); 202 | } 203 | }); 204 | } 205 | 206 | export const runscript = runScript; 207 | -------------------------------------------------------------------------------- /test/runscript.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { strict as assert } from 'node:assert'; 3 | import { runscript, runScript, RunScriptTimeoutError } from '../src/index.js'; 4 | import { getFixtures } from './helper.js'; 5 | 6 | describe('test/runscript.test.ts', () => { 7 | it('should run `$ node -v`', () => { 8 | return runScript('node -v'); 9 | }); 10 | 11 | it('should support alias runscript function', () => { 12 | return runscript('node -v'); 13 | }); 14 | 15 | it('should run `$ npm -v`', () => { 16 | return runScript('npm -v'); 17 | }); 18 | 19 | it('should run `$ echo "hello"`', () => { 20 | return runScript('echo "hello"'); 21 | }); 22 | 23 | it('should reject on exit code < 0', () => { 24 | return runScript('node -e "process.exit(-1)"') 25 | .catch(err => { 26 | console.log(err); 27 | assert(err.name === 'RunScriptError'); 28 | // node 10 on windows -1 equal to 4294967295 29 | assert(err.exitcode === 255 || err.exitcode === 4294967295); 30 | }); 31 | }); 32 | 33 | it('should reject on exit code = 1', () => { 34 | return runScript('node -e "process.exit(1)"') 35 | .catch(err => { 36 | console.log(err); 37 | assert(err.name === 'RunScriptError'); 38 | assert(err.exitcode === 1); 39 | }); 40 | }); 41 | 42 | it('should reject on cmd not exists', () => { 43 | return runScript('node-not-exists -e "process.exit(-1)"', { 44 | shell: true, 45 | stdio: 'pipe', 46 | }) 47 | .catch(err => { 48 | console.log(err); 49 | assert(err.name === 'RunScriptError'); 50 | }); 51 | }); 52 | 53 | it('should reject on timeout (stdout)', () => { 54 | return runScript(`node ${getFixtures('timeout.js')}`, { 55 | stdio: 'pipe', 56 | }, { timeout: 1200 }) 57 | .catch((err: unknown) => { 58 | // console.log(err); 59 | assert(err instanceof RunScriptTimeoutError); 60 | assert.equal(err.name, 'RunScriptTimeoutError'); 61 | assert.equal(err.timeout, 1200); 62 | assert.match(err.stdio.stdout!.toString(), /timer start\necho every 500ms\n/); 63 | }); 64 | }); 65 | 66 | it('should reject on timeout (stderr)', () => { 67 | return runScript(`node ${getFixtures('timeout-stderr.js')}`, { 68 | stdio: 'pipe', 69 | }, { timeout: 1700 }) 70 | .catch((err: unknown) => { 71 | // console.log(err); 72 | assert(err instanceof RunScriptTimeoutError); 73 | assert.equal(err.name, 'RunScriptTimeoutError'); 74 | assert.equal(err.timeout, 1700); 75 | assert.match(err.stdio.stderr!.toString(), /timer start\necho every 600ms\n/); 76 | }); 77 | }); 78 | 79 | it('should normal exit before timeout', () => { 80 | return runScript(`node ${getFixtures('timeout-and-exit.js')}`, { 81 | stdio: 'pipe', 82 | }, { timeout: 30000 }) 83 | .then(stdio => { 84 | assert.match(stdio.stderr!.toString(), /timer start\necho every 600ms\n/); 85 | assert.match(stdio.stderr!.toString(), /\nexit\n/); 86 | }); 87 | }); 88 | 89 | it('should pipe and get stdout string', () => { 90 | return runScript('node -v', { 91 | stdio: 'pipe', 92 | }).then(stdio => { 93 | console.log(stdio.stdout!.toString()); 94 | assert(Buffer.isBuffer(stdio.stdout)); 95 | assert(/^v\d+\.\d+\.\d+$/.test(stdio.stdout.toString().trim()), JSON.stringify(stdio.stdout.toString())); 96 | assert.equal(stdio.stderr, null); 97 | }); 98 | }); 99 | 100 | it('should pipe and get stderr string', () => { 101 | return runScript('node -e "foo"', { 102 | stdio: 'pipe', 103 | }).catch(err => { 104 | assert(err.message.indexOf('node -e "foo"" error, exit code') > 0, err.message); 105 | const stdio = err.stdio; 106 | assert(Buffer.isBuffer(stdio.stderr)); 107 | const stderr = stdio.stderr.toString(); 108 | assert(stderr.indexOf('ReferenceError: foo is not defined') > -1, stderr); 109 | assert.equal(stdio.stdout, null); 110 | }); 111 | }); 112 | 113 | it('should not return stderr when stdio.stderr = inherit', () => { 114 | return runScript('node -e "foo"', { 115 | stdio: 'inherit', 116 | }).catch(err => { 117 | assert(err.message.indexOf('node -e "foo"" error, exit code') > 0, err.message); 118 | const stdio = err.stdio; 119 | assert.equal(stdio.stdout, null); 120 | assert.equal(stdio.stderr, null); 121 | }); 122 | }); 123 | 124 | it('should pipe and send to stdout and stderr stream', () => { 125 | const stdoutPath = getFixtures('stdout.log'); 126 | const stderrPath = getFixtures('stderr.log'); 127 | return runScript(`node ${getFixtures('console.js')}`, { 128 | stdio: 'pipe', 129 | stdout: fs.createWriteStream(stdoutPath), 130 | stderr: fs.createWriteStream(stderrPath), 131 | }).then(stdio => { 132 | assert(stdio.stdout!.toString() === 'stdout'); 133 | assert(stdio.stderr!.toString() === 'stderr'); 134 | assert(fs.readFileSync(stdoutPath, 'utf8') === 'stdout'); 135 | assert(fs.readFileSync(stderrPath, 'utf8') === 'stderr'); 136 | }); 137 | }); 138 | 139 | it('should throw when options.stdout is not writable stream', () => { 140 | return runScript('node -v', { 141 | stdout: fs.createReadStream(getFixtures('console.js')) as any, 142 | }).then(() => { 143 | throw new Error('should not run'); 144 | }).catch(err => { 145 | assert(err.message === 'options.stdout should be writable stream'); 146 | }); 147 | }); 148 | 149 | it('should throw when options.stderr is not writable stream', () => { 150 | return runScript('node -v', { 151 | stderr: fs.createReadStream(getFixtures('console.js')) as any, 152 | }).then(() => { 153 | throw new Error('should not run'); 154 | }).catch(err => { 155 | assert(err.message === 'options.stderr should be writable stream'); 156 | }); 157 | }); 158 | 159 | it('should run relative path ./node_modules/.bin/autod', () => { 160 | return runScript('./node_modules/.bin/autod -V', { 161 | stdio: 'pipe', 162 | }).then(stdio => { 163 | // console.log(stdio.stdout.toString()); 164 | assert(/^\d+\.\d+\.\d+$/.test(stdio.stdout!.toString().trim())); 165 | assert.equal(stdio.stderr, null); 166 | }); 167 | }); 168 | 169 | it('should run relative path ../../node_modules/.bin/autod', () => { 170 | return runScript('../../node_modules/.bin/autod -V', { 171 | stdio: 'pipe', 172 | cwd: getFixtures(''), 173 | }).then(stdio => { 174 | // console.log(stdio.stdout.toString()); 175 | assert(/^\d+\.\d+\.\d+$/.test(stdio.stdout!.toString().trim())); 176 | assert.equal(stdio.stderr, null); 177 | }); 178 | }); 179 | 180 | it('should exit when child process has not closed stdio streams', () => { 181 | return runScript(`node ${getFixtures('child-process-with-unclosed-stdio.cjs')}`, { 182 | stdio: 'pipe', 183 | }).then(stdio => { 184 | assert(/child finish/.test(stdio.stdout!.toString().trim())); 185 | }); 186 | }); 187 | 188 | if (process.platform === 'win32') { 189 | it('should run relative path .\\node_modules\\.bin\\autod', () => { 190 | return runScript('.\\node_modules\\.bin\\autod -V', { 191 | stdio: 'pipe', 192 | }).then(stdio => { 193 | // console.log(stdio.stdout.toString()); 194 | assert(/^\d+\.\d+\.\d+$/.test(stdio.stdout!.toString().trim())); 195 | assert.equal(stdio.stderr, null); 196 | }); 197 | }); 198 | 199 | it('should run relative path ..\\..\\node_modules\\.bin\\autod', () => { 200 | return runScript('..\\..\\node_modules\\.bin\\autod -V', { 201 | stdio: 'pipe', 202 | cwd: getFixtures(''), 203 | }).then(stdio => { 204 | // console.log(stdio.stdout.toString()); 205 | assert(/^\d+\.\d+\.\d+$/.test(stdio.stdout!.toString().trim())); 206 | assert.equal(stdio.stderr, null); 207 | }); 208 | }); 209 | } 210 | }); 211 | --------------------------------------------------------------------------------