Add web front-end and fix API issues
This commit is contained in:
1 parent
45ca4de0ad
commit
a7524412f0
2571 files changed
+86998
-5
No files matched your search
+1
@@ -0,0 +1 @@
|
||||
../concurrently/dist/bin/index.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../concurrently/dist/bin/index.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../tree-kill/cli.js
|
||||
+262
@@ -217,6 +217,32 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
|
||||
"integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/basic-auth": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||
@@ -308,6 +334,59 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.5.tgz",
|
||||
"integrity": "sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "5.6.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.9.0",
|
||||
"supports-color": "10.2.2",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/index.js",
|
||||
"concurrently": "dist/bin/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||
@@ -498,6 +577,13 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -579,6 +665,16 @@
|
||||
"@esbuild/win32-x64": "0.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -685,6 +781,29 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
|
||||
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -1239,6 +1358,16 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
@@ -1323,6 +1452,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz",
|
||||
"integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||
@@ -1413,6 +1555,53 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
|
||||
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -1422,6 +1611,23 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.23.12",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz",
|
||||
@@ -1547,6 +1753,24 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"string-width": "^7.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@@ -1561,6 +1785,44 @@
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
||||
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^9.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"string-width": "^7.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^22.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||||
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
export type Options = {
|
||||
/**
|
||||
Match only the first ANSI escape.
|
||||
|
||||
@default false
|
||||
*/
|
||||
readonly onlyFirst: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
Regular expression for matching ANSI escape codes.
|
||||
|
||||
@example
|
||||
```
|
||||
import ansiRegex from 'ansi-regex';
|
||||
|
||||
ansiRegex().test('\u001B[4mcake\u001B[0m');
|
||||
//=> true
|
||||
|
||||
ansiRegex().test('cake');
|
||||
//=> false
|
||||
|
||||
'\u001B[4mcake\u001B[0m'.match(ansiRegex());
|
||||
//=> ['\u001B[4m', '\u001B[0m']
|
||||
|
||||
'\u001B[4mcake\u001B[0m'.match(ansiRegex({onlyFirst: true}));
|
||||
//=> ['\u001B[4m']
|
||||
|
||||
'\u001B]8;;https://github.com\u0007click\u001B]8;;\u0007'.match(ansiRegex());
|
||||
//=> ['\u001B]8;;https://github.com\u0007', '\u001B]8;;\u0007']
|
||||
```
|
||||
*/
|
||||
export default function ansiRegex(options?: Options): RegExp;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
export default function ansiRegex({onlyFirst = false} = {}) {
|
||||
// Valid string terminator sequences are BEL, ESC\, and 0x9c
|
||||
const ST = '(?:\\u0007|\\u001B\\u005C|\\u009C)';
|
||||
|
||||
// OSC sequences only: ESC ] ... ST
|
||||
// The payload stops at the first terminator character rather than scanning ahead for one, so an unterminated `ESC ]` cannot rescan the rest of the input. Terminals likewise abort a control string on an unexpected ESC.
|
||||
const osc = `(?:\\u001B\\][^\\u0007\\u001B\\u009C]*${ST})`;
|
||||
|
||||
// CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :) then final byte
|
||||
const csi = '[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]';
|
||||
|
||||
const pattern = `${osc}|${csi}`;
|
||||
|
||||
return new RegExp(pattern, onlyFirst ? undefined : 'g');
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "ansi-regex",
|
||||
"version": "6.3.0",
|
||||
"description": "Regular expression for matching ANSI escape codes",
|
||||
"license": "MIT",
|
||||
"repository": "chalk/ansi-regex",
|
||||
"funding": "https://github.com/chalk/ansi-regex?sponsor=1",
|
||||
"author": {
|
||||
"name": "Sindre Sorhus",
|
||||
"email": "sindresorhus@gmail.com",
|
||||
"url": "https://sindresorhus.com"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": "./index.js",
|
||||
"types": "./index.d.ts",
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "xo && ava && tsd",
|
||||
"view-supported": "node fixtures/view-codes.js"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts"
|
||||
],
|
||||
"keywords": [
|
||||
"ansi",
|
||||
"styles",
|
||||
"color",
|
||||
"colour",
|
||||
"colors",
|
||||
"terminal",
|
||||
"console",
|
||||
"cli",
|
||||
"string",
|
||||
"tty",
|
||||
"escape",
|
||||
"formatting",
|
||||
"rgb",
|
||||
"256",
|
||||
"shell",
|
||||
"xterm",
|
||||
"command-line",
|
||||
"text",
|
||||
"regex",
|
||||
"regexp",
|
||||
"re",
|
||||
"match",
|
||||
"test",
|
||||
"find",
|
||||
"pattern"
|
||||
],
|
||||
"devDependencies": {
|
||||
"ansi-escapes": "^5.0.0",
|
||||
"ava": "^3.15.0",
|
||||
"tsd": "^0.21.0",
|
||||
"xo": "^0.54.2"
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
# ansi-regex
|
||||
|
||||
> Regular expression for matching [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install ansi-regex
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import ansiRegex from 'ansi-regex';
|
||||
|
||||
ansiRegex().test('\u001B[4mcake\u001B[0m');
|
||||
//=> true
|
||||
|
||||
ansiRegex().test('cake');
|
||||
//=> false
|
||||
|
||||
'\u001B[4mcake\u001B[0m'.match(ansiRegex());
|
||||
//=> ['\u001B[4m', '\u001B[0m']
|
||||
|
||||
'\u001B[4mcake\u001B[0m'.match(ansiRegex({onlyFirst: true}));
|
||||
//=> ['\u001B[4m']
|
||||
|
||||
'\u001B]8;;https://github.com\u0007click\u001B]8;;\u0007'.match(ansiRegex());
|
||||
//=> ['\u001B]8;;https://github.com\u0007', '\u001B]8;;\u0007']
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### ansiRegex(options?)
|
||||
|
||||
Returns a regex for matching ANSI escape codes.
|
||||
|
||||
#### options
|
||||
|
||||
Type: `object`
|
||||
|
||||
##### onlyFirst
|
||||
|
||||
Type: `boolean`\
|
||||
Default: `false` *(Matches any ANSI escape codes in a string)*
|
||||
|
||||
Match only the first ANSI escape.
|
||||
|
||||
## Important
|
||||
|
||||
If you run the regex against untrusted user input in a server context, you should [give it a timeout](https://github.com/sindresorhus/super-regex).
|
||||
|
||||
**I do not consider [ReDoS](https://blog.yossarian.net/2022/12/28/ReDoS-vulnerabilities-and-misaligned-incentives) a valid vulnerability for this package.**
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why do you test for codes not in the ECMA 48 standard?
|
||||
|
||||
Some of the codes we run as a test are codes that we acquired finding various lists of non-standard or manufacturer specific codes. We test for both standard and non-standard codes, as most of them follow the same or similar format and can be safely matched in strings without the risk of removing actual string content. There are a few non-standard control codes that do not follow the traditional format (i.e. they end in numbers) thus forcing us to exclude them from the test because we cannot reliably match them.
|
||||
|
||||
On the historical side, those ECMA standards were established in the early 90's whereas the VT100, for example, was designed in the mid/late 70's. At that point in time, control codes were still pretty ungoverned and engineers used them for a multitude of things, namely to activate hardware ports that may have been proprietary. Somewhere else you see a similar 'anarchy' of codes is in the x86 architecture for processors; there are a ton of "interrupts" that can mean different things on certain brands of processors, most of which have been phased out.
|
||||
|
||||
## Maintainers
|
||||
|
||||
- [Sindre Sorhus](https://github.com/sindresorhus)
|
||||
- [Josh Junon](https://github.com/qix-)
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
export type CSPair = { // eslint-disable-line @typescript-eslint/naming-convention
|
||||
/**
|
||||
The ANSI terminal control sequence for starting this style.
|
||||
*/
|
||||
readonly open: string;
|
||||
|
||||
/**
|
||||
The ANSI terminal control sequence for ending this style.
|
||||
*/
|
||||
readonly close: string;
|
||||
};
|
||||
|
||||
export type ColorBase = {
|
||||
/**
|
||||
The ANSI terminal control sequence for ending this color.
|
||||
*/
|
||||
readonly close: string;
|
||||
|
||||
ansi(code: number): string;
|
||||
|
||||
ansi256(code: number): string;
|
||||
|
||||
ansi16m(red: number, green: number, blue: number): string;
|
||||
};
|
||||
|
||||
export type Modifier = {
|
||||
/**
|
||||
Resets the current color chain.
|
||||
*/
|
||||
readonly reset: CSPair;
|
||||
|
||||
/**
|
||||
Make text bold.
|
||||
*/
|
||||
readonly bold: CSPair;
|
||||
|
||||
/**
|
||||
Emitting only a small amount of light.
|
||||
*/
|
||||
readonly dim: CSPair;
|
||||
|
||||
/**
|
||||
Make text italic. (Not widely supported)
|
||||
*/
|
||||
readonly italic: CSPair;
|
||||
|
||||
/**
|
||||
Make text underline. (Not widely supported)
|
||||
*/
|
||||
readonly underline: CSPair;
|
||||
|
||||
/**
|
||||
Make text overline.
|
||||
|
||||
Supported on VTE-based terminals, the GNOME terminal, mintty, and Git Bash.
|
||||
*/
|
||||
readonly overline: CSPair;
|
||||
|
||||
/**
|
||||
Inverse background and foreground colors.
|
||||
*/
|
||||
readonly inverse: CSPair;
|
||||
|
||||
/**
|
||||
Prints the text, but makes it invisible.
|
||||
*/
|
||||
readonly hidden: CSPair;
|
||||
|
||||
/**
|
||||
Puts a horizontal line through the center of the text. (Not widely supported)
|
||||
*/
|
||||
readonly strikethrough: CSPair;
|
||||
};
|
||||
|
||||
export type ForegroundColor = {
|
||||
readonly black: CSPair;
|
||||
readonly red: CSPair;
|
||||
readonly green: CSPair;
|
||||
readonly yellow: CSPair;
|
||||
readonly blue: CSPair;
|
||||
readonly cyan: CSPair;
|
||||
readonly magenta: CSPair;
|
||||
readonly white: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly gray: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly grey: CSPair;
|
||||
|
||||
readonly blackBright: CSPair;
|
||||
readonly redBright: CSPair;
|
||||
readonly greenBright: CSPair;
|
||||
readonly yellowBright: CSPair;
|
||||
readonly blueBright: CSPair;
|
||||
readonly cyanBright: CSPair;
|
||||
readonly magentaBright: CSPair;
|
||||
readonly whiteBright: CSPair;
|
||||
};
|
||||
|
||||
export type BackgroundColor = {
|
||||
readonly bgBlack: CSPair;
|
||||
readonly bgRed: CSPair;
|
||||
readonly bgGreen: CSPair;
|
||||
readonly bgYellow: CSPair;
|
||||
readonly bgBlue: CSPair;
|
||||
readonly bgCyan: CSPair;
|
||||
readonly bgMagenta: CSPair;
|
||||
readonly bgWhite: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGray: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGrey: CSPair;
|
||||
|
||||
readonly bgBlackBright: CSPair;
|
||||
readonly bgRedBright: CSPair;
|
||||
readonly bgGreenBright: CSPair;
|
||||
readonly bgYellowBright: CSPair;
|
||||
readonly bgBlueBright: CSPair;
|
||||
readonly bgCyanBright: CSPair;
|
||||
readonly bgMagentaBright: CSPair;
|
||||
readonly bgWhiteBright: CSPair;
|
||||
};
|
||||
|
||||
export type ConvertColor = {
|
||||
/**
|
||||
Convert from the RGB color space to the ANSI 256 color space.
|
||||
|
||||
@param red - (`0...255`)
|
||||
@param green - (`0...255`)
|
||||
@param blue - (`0...255`)
|
||||
*/
|
||||
rgbToAnsi256(red: number, green: number, blue: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the RGB color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToRgb(hex: string): [red: number, green: number, blue: number];
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the ANSI 256 color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToAnsi256(hex: string): number;
|
||||
|
||||
/**
|
||||
Convert from the ANSI 256 color space to the ANSI 16 color space.
|
||||
|
||||
@param code - A number representing the ANSI 256 color.
|
||||
*/
|
||||
ansi256ToAnsi(code: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB color space to the ANSI 16 color space.
|
||||
|
||||
@param red - (`0...255`)
|
||||
@param green - (`0...255`)
|
||||
@param blue - (`0...255`)
|
||||
*/
|
||||
rgbToAnsi(red: number, green: number, blue: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the ANSI 16 color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToAnsi(hex: string): number;
|
||||
};
|
||||
|
||||
/**
|
||||
Basic modifier names.
|
||||
*/
|
||||
export type ModifierName = keyof Modifier;
|
||||
|
||||
/**
|
||||
Basic foreground color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type ForegroundColorName = keyof ForegroundColor;
|
||||
|
||||
/**
|
||||
Basic background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type BackgroundColorName = keyof BackgroundColor;
|
||||
|
||||
/**
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type ColorName = ForegroundColorName | BackgroundColorName;
|
||||
|
||||
/**
|
||||
Basic modifier names.
|
||||
*/
|
||||
export const modifierNames: readonly ModifierName[];
|
||||
|
||||
/**
|
||||
Basic foreground color names.
|
||||
*/
|
||||
export const foregroundColorNames: readonly ForegroundColorName[];
|
||||
|
||||
/**
|
||||
Basic background color names.
|
||||
*/
|
||||
export const backgroundColorNames: readonly BackgroundColorName[];
|
||||
|
||||
/*
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
*/
|
||||
export const colorNames: readonly ColorName[];
|
||||
|
||||
declare const ansiStyles: {
|
||||
readonly modifier: Modifier;
|
||||
readonly color: ColorBase & ForegroundColor;
|
||||
readonly bgColor: ColorBase & BackgroundColor;
|
||||
readonly codes: ReadonlyMap<number, number>;
|
||||
} & ForegroundColor & BackgroundColor & Modifier & ConvertColor;
|
||||
|
||||
export default ansiStyles;
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
const ANSI_BACKGROUND_OFFSET = 10;
|
||||
|
||||
const wrapAnsi16 = (offset = 0) => code => `\u001B[${code + offset}m`;
|
||||
|
||||
const wrapAnsi256 = (offset = 0) => code => `\u001B[${38 + offset};5;${code}m`;
|
||||
|
||||
const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`;
|
||||
|
||||
const styles = {
|
||||
modifier: {
|
||||
reset: [0, 0],
|
||||
// 21 isn't widely supported and 22 does the same thing
|
||||
bold: [1, 22],
|
||||
dim: [2, 22],
|
||||
italic: [3, 23],
|
||||
underline: [4, 24],
|
||||
overline: [53, 55],
|
||||
inverse: [7, 27],
|
||||
hidden: [8, 28],
|
||||
strikethrough: [9, 29],
|
||||
},
|
||||
color: {
|
||||
black: [30, 39],
|
||||
red: [31, 39],
|
||||
green: [32, 39],
|
||||
yellow: [33, 39],
|
||||
blue: [34, 39],
|
||||
magenta: [35, 39],
|
||||
cyan: [36, 39],
|
||||
white: [37, 39],
|
||||
|
||||
// Bright color
|
||||
blackBright: [90, 39],
|
||||
gray: [90, 39], // Alias of `blackBright`
|
||||
grey: [90, 39], // Alias of `blackBright`
|
||||
redBright: [91, 39],
|
||||
greenBright: [92, 39],
|
||||
yellowBright: [93, 39],
|
||||
blueBright: [94, 39],
|
||||
magentaBright: [95, 39],
|
||||
cyanBright: [96, 39],
|
||||
whiteBright: [97, 39],
|
||||
},
|
||||
bgColor: {
|
||||
bgBlack: [40, 49],
|
||||
bgRed: [41, 49],
|
||||
bgGreen: [42, 49],
|
||||
bgYellow: [43, 49],
|
||||
bgBlue: [44, 49],
|
||||
bgMagenta: [45, 49],
|
||||
bgCyan: [46, 49],
|
||||
bgWhite: [47, 49],
|
||||
|
||||
// Bright color
|
||||
bgBlackBright: [100, 49],
|
||||
bgGray: [100, 49], // Alias of `bgBlackBright`
|
||||
bgGrey: [100, 49], // Alias of `bgBlackBright`
|
||||
bgRedBright: [101, 49],
|
||||
bgGreenBright: [102, 49],
|
||||
bgYellowBright: [103, 49],
|
||||
bgBlueBright: [104, 49],
|
||||
bgMagentaBright: [105, 49],
|
||||
bgCyanBright: [106, 49],
|
||||
bgWhiteBright: [107, 49],
|
||||
},
|
||||
};
|
||||
|
||||
export const modifierNames = Object.keys(styles.modifier);
|
||||
export const foregroundColorNames = Object.keys(styles.color);
|
||||
export const backgroundColorNames = Object.keys(styles.bgColor);
|
||||
export const colorNames = [...foregroundColorNames, ...backgroundColorNames];
|
||||
|
||||
function assembleStyles() {
|
||||
const codes = new Map();
|
||||
|
||||
for (const [groupName, group] of Object.entries(styles)) {
|
||||
for (const [styleName, style] of Object.entries(group)) {
|
||||
styles[styleName] = {
|
||||
open: `\u001B[${style[0]}m`,
|
||||
close: `\u001B[${style[1]}m`,
|
||||
};
|
||||
|
||||
group[styleName] = styles[styleName];
|
||||
|
||||
codes.set(style[0], style[1]);
|
||||
}
|
||||
|
||||
Object.defineProperty(styles, groupName, {
|
||||
value: group,
|
||||
enumerable: false,
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(styles, 'codes', {
|
||||
value: codes,
|
||||
enumerable: false,
|
||||
});
|
||||
|
||||
styles.color.close = '\u001B[39m';
|
||||
styles.bgColor.close = '\u001B[49m';
|
||||
|
||||
styles.color.ansi = wrapAnsi16();
|
||||
styles.color.ansi256 = wrapAnsi256();
|
||||
styles.color.ansi16m = wrapAnsi16m();
|
||||
styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
|
||||
styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
|
||||
styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
|
||||
|
||||
// From https://github.com/Qix-/color-convert/blob/3f0e0d4e92e235796ccb17f6e85c72094a651f49/conversions.js
|
||||
Object.defineProperties(styles, {
|
||||
rgbToAnsi256: {
|
||||
value(red, green, blue) {
|
||||
// We use the extended greyscale palette here, with the exception of
|
||||
// black and white. normal palette only has 4 greyscale shades.
|
||||
if (red === green && green === blue) {
|
||||
if (red < 8) {
|
||||
return 16;
|
||||
}
|
||||
|
||||
if (red > 248) {
|
||||
return 231;
|
||||
}
|
||||
|
||||
return Math.round(((red - 8) / 247) * 24) + 232;
|
||||
}
|
||||
|
||||
return 16
|
||||
+ (36 * Math.round(red / 255 * 5))
|
||||
+ (6 * Math.round(green / 255 * 5))
|
||||
+ Math.round(blue / 255 * 5);
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
hexToRgb: {
|
||||
value(hex) {
|
||||
const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
|
||||
if (!matches) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
let [colorString] = matches;
|
||||
|
||||
if (colorString.length === 3) {
|
||||
colorString = [...colorString].map(character => character + character).join('');
|
||||
}
|
||||
|
||||
const integer = Number.parseInt(colorString, 16);
|
||||
|
||||
return [
|
||||
/* eslint-disable no-bitwise */
|
||||
(integer >> 16) & 0xFF,
|
||||
(integer >> 8) & 0xFF,
|
||||
integer & 0xFF,
|
||||
/* eslint-enable no-bitwise */
|
||||
];
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
hexToAnsi256: {
|
||||
value: hex => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
|
||||
enumerable: false,
|
||||
},
|
||||
ansi256ToAnsi: {
|
||||
value(code) {
|
||||
if (code < 8) {
|
||||
return 30 + code;
|
||||
}
|
||||
|
||||
if (code < 16) {
|
||||
return 90 + (code - 8);
|
||||
}
|
||||
|
||||
let red;
|
||||
let green;
|
||||
let blue;
|
||||
|
||||
if (code >= 232) {
|
||||
red = (((code - 232) * 10) + 8) / 255;
|
||||
green = red;
|
||||
blue = red;
|
||||
} else {
|
||||
code -= 16;
|
||||
|
||||
const remainder = code % 36;
|
||||
|
||||
red = Math.floor(code / 36) / 5;
|
||||
green = Math.floor(remainder / 6) / 5;
|
||||
blue = (remainder % 6) / 5;
|
||||
}
|
||||
|
||||
const value = Math.max(red, green, blue) * 2;
|
||||
|
||||
if (value === 0) {
|
||||
return 30;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red));
|
||||
|
||||
if (value === 2) {
|
||||
result += 60;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
rgbToAnsi: {
|
||||
value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
|
||||
enumerable: false,
|
||||
},
|
||||
hexToAnsi: {
|
||||
value: hex => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
|
||||
enumerable: false,
|
||||
},
|
||||
});
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
const ansiStyles = assembleStyles();
|
||||
|
||||
export default ansiStyles;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "ansi-styles",
|
||||
"version": "6.2.3",
|
||||
"description": "ANSI escape codes for styling strings in the terminal",
|
||||
"license": "MIT",
|
||||
"repository": "chalk/ansi-styles",
|
||||
"funding": "https://github.com/chalk/ansi-styles?sponsor=1",
|
||||
"author": {
|
||||
"name": "Sindre Sorhus",
|
||||
"email": "sindresorhus@gmail.com",
|
||||
"url": "https://sindresorhus.com"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": "./index.js",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "xo && ava && tsd",
|
||||
"screenshot": "svg-term --command='node screenshot' --out=screenshot.svg --padding=3 --width=55 --height=3 --at=1000 --no-cursor"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts"
|
||||
],
|
||||
"keywords": [
|
||||
"ansi",
|
||||
"styles",
|
||||
"color",
|
||||
"colour",
|
||||
"colors",
|
||||
"terminal",
|
||||
"console",
|
||||
"cli",
|
||||
"string",
|
||||
"tty",
|
||||
"escape",
|
||||
"formatting",
|
||||
"rgb",
|
||||
"256",
|
||||
"shell",
|
||||
"xterm",
|
||||
"log",
|
||||
"logging",
|
||||
"command-line",
|
||||
"text"
|
||||
],
|
||||
"devDependencies": {
|
||||
"ava": "^6.1.3",
|
||||
"svg-term-cli": "^2.1.1",
|
||||
"tsd": "^0.31.1",
|
||||
"xo": "^0.58.0"
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
# ansi-styles
|
||||
|
||||
> [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors_and_Styles) for styling strings in the terminal
|
||||
|
||||
You probably want the higher-level [chalk](https://github.com/chalk/chalk) module for styling your strings.
|
||||
|
||||

|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install ansi-styles
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import styles from 'ansi-styles';
|
||||
|
||||
console.log(`${styles.green.open}Hello world!${styles.green.close}`);
|
||||
|
||||
|
||||
// Color conversion between 256/truecolor
|
||||
// NOTE: When converting from truecolor to 256 colors, the original color
|
||||
// may be degraded to fit the new color palette. This means terminals
|
||||
// that do not support 16 million colors will best-match the
|
||||
// original color.
|
||||
console.log(`${styles.color.ansi(styles.rgbToAnsi(199, 20, 250))}Hello World${styles.color.close}`)
|
||||
console.log(`${styles.color.ansi256(styles.rgbToAnsi256(199, 20, 250))}Hello World${styles.color.close}`)
|
||||
console.log(`${styles.color.ansi16m(...styles.hexToRgb('#abcdef'))}Hello World${styles.color.close}`)
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `open` and `close`
|
||||
|
||||
Each style has an `open` and `close` property.
|
||||
|
||||
### `modifierNames`, `foregroundColorNames`, `backgroundColorNames`, and `colorNames`
|
||||
|
||||
All supported style strings are exposed as an array of strings for convenience. `colorNames` is the combination of `foregroundColorNames` and `backgroundColorNames`.
|
||||
|
||||
This can be useful if you need to validate input:
|
||||
|
||||
```js
|
||||
import {modifierNames, foregroundColorNames} from 'ansi-styles';
|
||||
|
||||
console.log(modifierNames.includes('bold'));
|
||||
//=> true
|
||||
|
||||
console.log(foregroundColorNames.includes('pink'));
|
||||
//=> false
|
||||
```
|
||||
|
||||
## Styles
|
||||
|
||||
### Modifiers
|
||||
|
||||
- `reset`
|
||||
- `bold`
|
||||
- `dim`
|
||||
- `italic` *(Not widely supported)*
|
||||
- `underline`
|
||||
- `overline` *Supported on VTE-based terminals, the GNOME terminal, mintty, and Git Bash.*
|
||||
- `inverse`
|
||||
- `hidden`
|
||||
- `strikethrough` *(Not widely supported)*
|
||||
|
||||
### Colors
|
||||
|
||||
- `black`
|
||||
- `red`
|
||||
- `green`
|
||||
- `yellow`
|
||||
- `blue`
|
||||
- `magenta`
|
||||
- `cyan`
|
||||
- `white`
|
||||
- `blackBright` (alias: `gray`, `grey`)
|
||||
- `redBright`
|
||||
- `greenBright`
|
||||
- `yellowBright`
|
||||
- `blueBright`
|
||||
- `magentaBright`
|
||||
- `cyanBright`
|
||||
- `whiteBright`
|
||||
|
||||
### Background colors
|
||||
|
||||
- `bgBlack`
|
||||
- `bgRed`
|
||||
- `bgGreen`
|
||||
- `bgYellow`
|
||||
- `bgBlue`
|
||||
- `bgMagenta`
|
||||
- `bgCyan`
|
||||
- `bgWhite`
|
||||
- `bgBlackBright` (alias: `bgGray`, `bgGrey`)
|
||||
- `bgRedBright`
|
||||
- `bgGreenBright`
|
||||
- `bgYellowBright`
|
||||
- `bgBlueBright`
|
||||
- `bgMagentaBright`
|
||||
- `bgCyanBright`
|
||||
- `bgWhiteBright`
|
||||
|
||||
## Advanced usage
|
||||
|
||||
By default, you get a map of styles, but the styles are also available as groups. They are non-enumerable so they don't show up unless you access them explicitly. This makes it easier to expose only a subset in a higher-level module.
|
||||
|
||||
- `styles.modifier`
|
||||
- `styles.color`
|
||||
- `styles.bgColor`
|
||||
|
||||
###### Example
|
||||
|
||||
```js
|
||||
import styles from 'ansi-styles';
|
||||
|
||||
console.log(styles.color.green.open);
|
||||
```
|
||||
|
||||
Raw escape codes (i.e. without the CSI escape prefix `\u001B[` and render mode postfix `m`) are available under `styles.codes`, which returns a `Map` with the open codes as keys and close codes as values.
|
||||
|
||||
###### Example
|
||||
|
||||
```js
|
||||
import styles from 'ansi-styles';
|
||||
|
||||
console.log(styles.codes.get(36));
|
||||
//=> 39
|
||||
```
|
||||
|
||||
## 16 / 256 / 16 million (TrueColor) support
|
||||
|
||||
`ansi-styles` allows converting between various color formats and ANSI escapes, with support for 16, 256 and [16 million colors](https://gist.github.com/XVilka/8346728).
|
||||
|
||||
The following color spaces are supported:
|
||||
|
||||
- `rgb`
|
||||
- `hex`
|
||||
- `ansi256`
|
||||
- `ansi`
|
||||
|
||||
To use these, call the associated conversion function with the intended output, for example:
|
||||
|
||||
```js
|
||||
import styles from 'ansi-styles';
|
||||
|
||||
styles.color.ansi(styles.rgbToAnsi(100, 200, 15)); // RGB to 16 color ansi foreground code
|
||||
styles.bgColor.ansi(styles.hexToAnsi('#C0FFEE')); // HEX to 16 color ansi foreground code
|
||||
|
||||
styles.color.ansi256(styles.rgbToAnsi256(100, 200, 15)); // RGB to 256 color ansi foreground code
|
||||
styles.bgColor.ansi256(styles.hexToAnsi256('#C0FFEE')); // HEX to 256 color ansi foreground code
|
||||
|
||||
styles.color.ansi16m(100, 200, 15); // RGB to 16 million color foreground code
|
||||
styles.bgColor.ansi16m(...styles.hexToRgb('#C0FFEE')); // Hex (RGB) to 16 million color foreground code
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [ansi-escapes](https://github.com/sindresorhus/ansi-escapes) - ANSI escape codes for manipulating the terminal
|
||||
|
||||
## Maintainers
|
||||
|
||||
- [Sindre Sorhus](https://github.com/sindresorhus)
|
||||
- [Josh Junon](https://github.com/qix-)
|
||||
|
||||
## For enterprise
|
||||
|
||||
Available as part of the Tidelift Subscription.
|
||||
|
||||
The maintainers of `ansi-styles` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-ansi-styles?utm_source=npm-ansi-styles&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "chalk",
|
||||
"version": "5.6.2",
|
||||
"description": "Terminal string styling done right",
|
||||
"license": "MIT",
|
||||
"repository": "chalk/chalk",
|
||||
"funding": "https://github.com/chalk/chalk?sponsor=1",
|
||||
"type": "module",
|
||||
"main": "./source/index.js",
|
||||
"exports": "./source/index.js",
|
||||
"imports": {
|
||||
"#ansi-styles": "./source/vendor/ansi-styles/index.js",
|
||||
"#supports-color": {
|
||||
"node": "./source/vendor/supports-color/index.js",
|
||||
"default": "./source/vendor/supports-color/browser.js"
|
||||
}
|
||||
},
|
||||
"types": "./source/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "xo && c8 ava && tsd",
|
||||
"bench": "matcha benchmark.js"
|
||||
},
|
||||
"files": [
|
||||
"source",
|
||||
"!source/index.test-d.ts"
|
||||
],
|
||||
"keywords": [
|
||||
"color",
|
||||
"colour",
|
||||
"colors",
|
||||
"terminal",
|
||||
"console",
|
||||
"cli",
|
||||
"string",
|
||||
"ansi",
|
||||
"style",
|
||||
"styles",
|
||||
"tty",
|
||||
"formatting",
|
||||
"rgb",
|
||||
"256",
|
||||
"shell",
|
||||
"xterm",
|
||||
"log",
|
||||
"logging",
|
||||
"command-line",
|
||||
"text"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.10",
|
||||
"ava": "^3.15.0",
|
||||
"c8": "^7.10.0",
|
||||
"color-convert": "^2.0.1",
|
||||
"execa": "^6.0.0",
|
||||
"log-update": "^5.0.0",
|
||||
"matcha": "^0.7.0",
|
||||
"tsd": "^0.19.0",
|
||||
"xo": "^0.57.0",
|
||||
"yoctodelay": "^2.0.0"
|
||||
},
|
||||
"xo": {
|
||||
"rules": {
|
||||
"unicorn/prefer-string-slice": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "off",
|
||||
"@typescript-eslint/consistent-type-exports": "off",
|
||||
"@typescript-eslint/consistent-type-definitions": "off",
|
||||
"unicorn/expiring-todo-comments": "off"
|
||||
}
|
||||
},
|
||||
"c8": {
|
||||
"reporter": [
|
||||
"text",
|
||||
"lcov"
|
||||
],
|
||||
"exclude": [
|
||||
"source/vendor"
|
||||
]
|
||||
}
|
||||
}
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<br>
|
||||
<img width="320" src="media/logo.svg" alt="Chalk">
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
> Terminal string styling done right
|
||||
|
||||
[](https://codecov.io/gh/chalk/chalk)
|
||||
[](https://www.npmjs.com/package/chalk?activeTab=dependents)
|
||||
[](https://www.npmjs.com/package/chalk)
|
||||
|
||||

|
||||
|
||||
## Info
|
||||
|
||||
- [Why not switch to a smaller coloring package?](https://github.com/chalk/chalk?tab=readme-ov-file#why-not-switch-to-a-smaller-coloring-package)
|
||||
- See [yoctocolors](https://github.com/sindresorhus/yoctocolors) for a smaller alternative
|
||||
|
||||
## Highlights
|
||||
|
||||
- Expressive API
|
||||
- Highly performant
|
||||
- No dependencies
|
||||
- Ability to nest styles
|
||||
- [256/Truecolor color support](#256-and-truecolor-color-support)
|
||||
- Auto-detects color support
|
||||
- Doesn't extend `String.prototype`
|
||||
- Clean and focused
|
||||
- Actively maintained
|
||||
- [Used by ~115,000 packages](https://www.npmjs.com/browse/depended/chalk) as of July 4, 2024
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install chalk
|
||||
```
|
||||
|
||||
**IMPORTANT:** Chalk 5 is ESM. If you want to use Chalk with TypeScript or a build tool, you will probably want to use Chalk 4 for now. [Read more.](https://github.com/chalk/chalk/releases/tag/v5.0.0)
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import chalk from 'chalk';
|
||||
|
||||
console.log(chalk.blue('Hello world!'));
|
||||
```
|
||||
|
||||
Chalk comes with an easy to use composable API where you just chain and nest the styles you want.
|
||||
|
||||
```js
|
||||
import chalk from 'chalk';
|
||||
|
||||
const log = console.log;
|
||||
|
||||
// Combine styled and normal strings
|
||||
log(chalk.blue('Hello') + ' World' + chalk.red('!'));
|
||||
|
||||
// Compose multiple styles using the chainable API
|
||||
log(chalk.blue.bgRed.bold('Hello world!'));
|
||||
|
||||
// Pass in multiple arguments
|
||||
log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));
|
||||
|
||||
// Nest styles
|
||||
log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'));
|
||||
|
||||
// Nest styles of the same type even (color, underline, background)
|
||||
log(chalk.green(
|
||||
'I am a green line ' +
|
||||
chalk.blue.underline.bold('with a blue substring') +
|
||||
' that becomes green again!'
|
||||
));
|
||||
|
||||
// ES2015 template literal
|
||||
log(`
|
||||
CPU: ${chalk.red('90%')}
|
||||
RAM: ${chalk.green('40%')}
|
||||
DISK: ${chalk.yellow('70%')}
|
||||
`);
|
||||
|
||||
// Use RGB colors in terminal emulators that support it.
|
||||
log(chalk.rgb(123, 45, 67).underline('Underlined reddish color'));
|
||||
log(chalk.hex('#DEADED').bold('Bold gray!'));
|
||||
```
|
||||
|
||||
Easily define your own themes:
|
||||
|
||||
```js
|
||||
import chalk from 'chalk';
|
||||
|
||||
const error = chalk.bold.red;
|
||||
const warning = chalk.hex('#FFA500'); // Orange color
|
||||
|
||||
console.log(error('Error!'));
|
||||
console.log(warning('Warning!'));
|
||||
```
|
||||
|
||||
Take advantage of console.log [string substitution](https://nodejs.org/docs/latest/api/console.html#console_console_log_data_args):
|
||||
|
||||
```js
|
||||
import chalk from 'chalk';
|
||||
|
||||
const name = 'Sindre';
|
||||
console.log(chalk.green('Hello %s'), name);
|
||||
//=> 'Hello Sindre'
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### chalk.`<style>[.<style>...](string, [string...])`
|
||||
|
||||
Example: `chalk.red.bold.underline('Hello', 'world');`
|
||||
|
||||
Chain [styles](#styles) and call the last one as a method with a string argument. Order doesn't matter, and later styles take precedent in case of a conflict. This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`.
|
||||
|
||||
Multiple arguments will be separated by space.
|
||||
|
||||
### chalk.level
|
||||
|
||||
Specifies the level of color support.
|
||||
|
||||
Color support is automatically detected, but you can override it by setting the `level` property. You should however only do this in your own code as it applies globally to all Chalk consumers.
|
||||
|
||||
If you need to change this in a reusable module, create a new instance:
|
||||
|
||||
```js
|
||||
import {Chalk} from 'chalk';
|
||||
|
||||
const customChalk = new Chalk({level: 0});
|
||||
```
|
||||
|
||||
| Level | Description |
|
||||
| :---: | :--- |
|
||||
| `0` | All colors disabled |
|
||||
| `1` | Basic color support (16 colors) |
|
||||
| `2` | 256 color support |
|
||||
| `3` | Truecolor support (16 million colors) |
|
||||
|
||||
### supportsColor
|
||||
|
||||
Detect whether the terminal [supports color](https://github.com/chalk/supports-color). Used internally and handled for you, but exposed for convenience.
|
||||
|
||||
Can be overridden by the user with the flags `--color` and `--no-color`. For situations where using `--color` is not possible, use the environment variable `FORCE_COLOR=1` (level 1), `FORCE_COLOR=2` (level 2), or `FORCE_COLOR=3` (level 3) to forcefully enable color, or `FORCE_COLOR=0` to forcefully disable. The use of `FORCE_COLOR` overrides all other color support checks.
|
||||
|
||||
Explicit 256/Truecolor mode can be enabled using the `--color=256` and `--color=16m` flags, respectively.
|
||||
|
||||
### chalkStderr and supportsColorStderr
|
||||
|
||||
`chalkStderr` contains a separate instance configured with color support detected for `stderr` stream instead of `stdout`. Override rules from `supportsColor` apply to this too. `supportsColorStderr` is exposed for convenience.
|
||||
|
||||
### modifierNames, foregroundColorNames, backgroundColorNames, and colorNames
|
||||
|
||||
All supported style strings are exposed as an array of strings for convenience. `colorNames` is the combination of `foregroundColorNames` and `backgroundColorNames`.
|
||||
|
||||
This can be useful if you wrap Chalk and need to validate input:
|
||||
|
||||
```js
|
||||
import {modifierNames, foregroundColorNames} from 'chalk';
|
||||
|
||||
console.log(modifierNames.includes('bold'));
|
||||
//=> true
|
||||
|
||||
console.log(foregroundColorNames.includes('pink'));
|
||||
//=> false
|
||||
```
|
||||
|
||||
## Styles
|
||||
|
||||
### Modifiers
|
||||
|
||||
- `reset` - Reset the current style.
|
||||
- `bold` - Make the text bold.
|
||||
- `dim` - Make the text have lower opacity.
|
||||
- `italic` - Make the text italic. *(Not widely supported)*
|
||||
- `underline` - Put a horizontal line below the text. *(Not widely supported)*
|
||||
- `overline` - Put a horizontal line above the text. *(Not widely supported)*
|
||||
- `inverse`- Invert background and foreground colors.
|
||||
- `hidden` - Print the text but make it invisible.
|
||||
- `strikethrough` - Puts a horizontal line through the center of the text. *(Not widely supported)*
|
||||
- `visible`- Print the text only when Chalk has a color level above zero. Can be useful for things that are purely cosmetic.
|
||||
|
||||
### Colors
|
||||
|
||||
- `black`
|
||||
- `red`
|
||||
- `green`
|
||||
- `yellow`
|
||||
- `blue`
|
||||
- `magenta`
|
||||
- `cyan`
|
||||
- `white`
|
||||
- `blackBright` (alias: `gray`, `grey`)
|
||||
- `redBright`
|
||||
- `greenBright`
|
||||
- `yellowBright`
|
||||
- `blueBright`
|
||||
- `magentaBright`
|
||||
- `cyanBright`
|
||||
- `whiteBright`
|
||||
|
||||
### Background colors
|
||||
|
||||
- `bgBlack`
|
||||
- `bgRed`
|
||||
- `bgGreen`
|
||||
- `bgYellow`
|
||||
- `bgBlue`
|
||||
- `bgMagenta`
|
||||
- `bgCyan`
|
||||
- `bgWhite`
|
||||
- `bgBlackBright` (alias: `bgGray`, `bgGrey`)
|
||||
- `bgRedBright`
|
||||
- `bgGreenBright`
|
||||
- `bgYellowBright`
|
||||
- `bgBlueBright`
|
||||
- `bgMagentaBright`
|
||||
- `bgCyanBright`
|
||||
- `bgWhiteBright`
|
||||
|
||||
## 256 and Truecolor color support
|
||||
|
||||
Chalk supports 256 colors and [Truecolor](https://github.com/termstandard/colors) (16 million colors) on supported terminal apps.
|
||||
|
||||
Colors are downsampled from 16 million RGB values to an ANSI color format that is supported by the terminal emulator (or by specifying `{level: n}` as a Chalk option). For example, Chalk configured to run at level 1 (basic color support) will downsample an RGB value of #FF0000 (red) to 31 (ANSI escape for red).
|
||||
|
||||
Examples:
|
||||
|
||||
- `chalk.hex('#DEADED').underline('Hello, world!')`
|
||||
- `chalk.rgb(15, 100, 204).inverse('Hello!')`
|
||||
|
||||
Background versions of these models are prefixed with `bg` and the first level of the module capitalized (e.g. `hex` for foreground colors and `bgHex` for background colors).
|
||||
|
||||
- `chalk.bgHex('#DEADED').underline('Hello, world!')`
|
||||
- `chalk.bgRgb(15, 100, 204).inverse('Hello!')`
|
||||
|
||||
The following color models can be used:
|
||||
|
||||
- [`rgb`](https://en.wikipedia.org/wiki/RGB_color_model) - Example: `chalk.rgb(255, 136, 0).bold('Orange!')`
|
||||
- [`hex`](https://en.wikipedia.org/wiki/Web_colors#Hex_triplet) - Example: `chalk.hex('#FF8800').bold('Orange!')`
|
||||
- [`ansi256`](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) - Example: `chalk.bgAnsi256(194)('Honeydew, more or less')`
|
||||
|
||||
## Browser support
|
||||
|
||||
Since Chrome 69, ANSI escape codes are natively supported in the developer console.
|
||||
|
||||
## Windows
|
||||
|
||||
If you're on Windows, do yourself a favor and use [Windows Terminal](https://github.com/microsoft/terminal) instead of `cmd.exe`.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why not switch to a smaller coloring package?
|
||||
|
||||
Chalk may be larger, but there is a reason for that. It offers a more user-friendly API, well-documented types, supports millions of colors, and covers edge cases that smaller alternatives miss. Chalk is mature, reliable, and built to last.
|
||||
|
||||
But beyond the technical aspects, there's something more critical: trust and long-term maintenance. I have been active in open source for over a decade, and I'm committed to keeping Chalk maintained. Smaller packages might seem appealing now, but there's no guarantee they will be around for the long term, or that they won't become malicious over time.
|
||||
|
||||
Chalk is also likely already in your dependency tree (since 100K+ packages depend on it), so switching won’t save space—in fact, it might increase it. npm deduplicates dependencies, so multiple Chalk instances turn into one, but adding another package alongside it will increase your overall size.
|
||||
|
||||
If the goal is to clean up the ecosystem, switching away from Chalk won’t even make a dent. The real problem lies with packages that have very deep dependency trees (for example, those including a lot of polyfills). Chalk has no dependencies. It's better to focus on impactful changes rather than minor optimizations.
|
||||
|
||||
If absolute package size is important to you, I also maintain [yoctocolors](https://github.com/sindresorhus/yoctocolors), one of the smallest color packages out there.
|
||||
|
||||
*\- [Sindre](https://github.com/sindresorhus)*
|
||||
|
||||
### But the smaller coloring package has benchmarks showing it is faster
|
||||
|
||||
[Micro-benchmarks are flawed](https://sindresorhus.com/blog/micro-benchmark-fallacy) because they measure performance in unrealistic, isolated scenarios, often giving a distorted view of real-world performance. Don't believe marketing fluff. All the coloring packages are more than fast enough.
|
||||
|
||||
## Related
|
||||
|
||||
- [chalk-template](https://github.com/chalk/chalk-template) - [Tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) support for this module
|
||||
- [chalk-cli](https://github.com/chalk/chalk-cli) - CLI for this module
|
||||
- [ansi-styles](https://github.com/chalk/ansi-styles) - ANSI escape codes for styling strings in the terminal
|
||||
- [supports-color](https://github.com/chalk/supports-color) - Detect whether a terminal supports color
|
||||
- [strip-ansi](https://github.com/chalk/strip-ansi) - Strip ANSI escape codes
|
||||
- [strip-ansi-stream](https://github.com/chalk/strip-ansi-stream) - Strip ANSI escape codes from a stream
|
||||
- [has-ansi](https://github.com/chalk/has-ansi) - Check if a string has ANSI escape codes
|
||||
- [ansi-regex](https://github.com/chalk/ansi-regex) - Regular expression for matching ANSI escape codes
|
||||
- [wrap-ansi](https://github.com/chalk/wrap-ansi) - Wordwrap a string with ANSI escape codes
|
||||
- [slice-ansi](https://github.com/chalk/slice-ansi) - Slice a string with ANSI escape codes
|
||||
- [color-convert](https://github.com/qix-/color-convert) - Converts colors between different models
|
||||
- [chalk-animation](https://github.com/bokub/chalk-animation) - Animate strings in the terminal
|
||||
- [gradient-string](https://github.com/bokub/gradient-string) - Apply color gradients to strings
|
||||
- [chalk-pipe](https://github.com/LitoMore/chalk-pipe) - Create chalk style schemes with simpler style strings
|
||||
- [terminal-link](https://github.com/sindresorhus/terminal-link) - Create clickable links in the terminal
|
||||
|
||||
*(Not accepting additional entries)*
|
||||
|
||||
## Maintainers
|
||||
|
||||
- [Sindre Sorhus](https://github.com/sindresorhus)
|
||||
- [Josh Junon](https://github.com/qix-)
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
// TODO: Make it this when TS suports that.
|
||||
// import {ModifierName, ForegroundColor, BackgroundColor, ColorName} from '#ansi-styles';
|
||||
// import {ColorInfo, ColorSupportLevel} from '#supports-color';
|
||||
import {
|
||||
ModifierName,
|
||||
ForegroundColorName,
|
||||
BackgroundColorName,
|
||||
ColorName,
|
||||
} from './vendor/ansi-styles/index.js';
|
||||
import {ColorInfo, ColorSupportLevel} from './vendor/supports-color/index.js';
|
||||
|
||||
export interface Options {
|
||||
/**
|
||||
Specify the color support for Chalk.
|
||||
|
||||
By default, color support is automatically detected based on the environment.
|
||||
|
||||
Levels:
|
||||
- `0` - All colors disabled.
|
||||
- `1` - Basic 16 colors support.
|
||||
- `2` - ANSI 256 colors support.
|
||||
- `3` - Truecolor 16 million colors support.
|
||||
*/
|
||||
readonly level?: ColorSupportLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
Return a new Chalk instance.
|
||||
*/
|
||||
export const Chalk: new (options?: Options) => ChalkInstance; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
|
||||
export interface ChalkInstance {
|
||||
(...text: unknown[]): string;
|
||||
|
||||
/**
|
||||
The color support for Chalk.
|
||||
|
||||
By default, color support is automatically detected based on the environment.
|
||||
|
||||
Levels:
|
||||
- `0` - All colors disabled.
|
||||
- `1` - Basic 16 colors support.
|
||||
- `2` - ANSI 256 colors support.
|
||||
- `3` - Truecolor 16 million colors support.
|
||||
*/
|
||||
level: ColorSupportLevel;
|
||||
|
||||
/**
|
||||
Use RGB values to set text color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.rgb(222, 173, 237);
|
||||
```
|
||||
*/
|
||||
rgb: (red: number, green: number, blue: number) => this;
|
||||
|
||||
/**
|
||||
Use HEX value to set text color.
|
||||
|
||||
@param color - Hexadecimal value representing the desired color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.hex('#DEADED');
|
||||
```
|
||||
*/
|
||||
hex: (color: string) => this;
|
||||
|
||||
/**
|
||||
Use an [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.ansi256(201);
|
||||
```
|
||||
*/
|
||||
ansi256: (index: number) => this;
|
||||
|
||||
/**
|
||||
Use RGB values to set background color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.bgRgb(222, 173, 237);
|
||||
```
|
||||
*/
|
||||
bgRgb: (red: number, green: number, blue: number) => this;
|
||||
|
||||
/**
|
||||
Use HEX value to set background color.
|
||||
|
||||
@param color - Hexadecimal value representing the desired color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.bgHex('#DEADED');
|
||||
```
|
||||
*/
|
||||
bgHex: (color: string) => this;
|
||||
|
||||
/**
|
||||
Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set background color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.bgAnsi256(201);
|
||||
```
|
||||
*/
|
||||
bgAnsi256: (index: number) => this;
|
||||
|
||||
/**
|
||||
Modifier: Reset the current style.
|
||||
*/
|
||||
readonly reset: this;
|
||||
|
||||
/**
|
||||
Modifier: Make the text bold.
|
||||
*/
|
||||
readonly bold: this;
|
||||
|
||||
/**
|
||||
Modifier: Make the text have lower opacity.
|
||||
*/
|
||||
readonly dim: this;
|
||||
|
||||
/**
|
||||
Modifier: Make the text italic. *(Not widely supported)*
|
||||
*/
|
||||
readonly italic: this;
|
||||
|
||||
/**
|
||||
Modifier: Put a horizontal line below the text. *(Not widely supported)*
|
||||
*/
|
||||
readonly underline: this;
|
||||
|
||||
/**
|
||||
Modifier: Put a horizontal line above the text. *(Not widely supported)*
|
||||
*/
|
||||
readonly overline: this;
|
||||
|
||||
/**
|
||||
Modifier: Invert background and foreground colors.
|
||||
*/
|
||||
readonly inverse: this;
|
||||
|
||||
/**
|
||||
Modifier: Print the text but make it invisible.
|
||||
*/
|
||||
readonly hidden: this;
|
||||
|
||||
/**
|
||||
Modifier: Puts a horizontal line through the center of the text. *(Not widely supported)*
|
||||
*/
|
||||
readonly strikethrough: this;
|
||||
|
||||
/**
|
||||
Modifier: Print the text only when Chalk has a color level above zero.
|
||||
|
||||
Can be useful for things that are purely cosmetic.
|
||||
*/
|
||||
readonly visible: this;
|
||||
|
||||
readonly black: this;
|
||||
readonly red: this;
|
||||
readonly green: this;
|
||||
readonly yellow: this;
|
||||
readonly blue: this;
|
||||
readonly magenta: this;
|
||||
readonly cyan: this;
|
||||
readonly white: this;
|
||||
|
||||
/*
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly gray: this;
|
||||
|
||||
/*
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly grey: this;
|
||||
|
||||
readonly blackBright: this;
|
||||
readonly redBright: this;
|
||||
readonly greenBright: this;
|
||||
readonly yellowBright: this;
|
||||
readonly blueBright: this;
|
||||
readonly magentaBright: this;
|
||||
readonly cyanBright: this;
|
||||
readonly whiteBright: this;
|
||||
|
||||
readonly bgBlack: this;
|
||||
readonly bgRed: this;
|
||||
readonly bgGreen: this;
|
||||
readonly bgYellow: this;
|
||||
readonly bgBlue: this;
|
||||
readonly bgMagenta: this;
|
||||
readonly bgCyan: this;
|
||||
readonly bgWhite: this;
|
||||
|
||||
/*
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGray: this;
|
||||
|
||||
/*
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGrey: this;
|
||||
|
||||
readonly bgBlackBright: this;
|
||||
readonly bgRedBright: this;
|
||||
readonly bgGreenBright: this;
|
||||
readonly bgYellowBright: this;
|
||||
readonly bgBlueBright: this;
|
||||
readonly bgMagentaBright: this;
|
||||
readonly bgCyanBright: this;
|
||||
readonly bgWhiteBright: this;
|
||||
}
|
||||
|
||||
/**
|
||||
Main Chalk object that allows to chain styles together.
|
||||
|
||||
Call the last one as a method with a string argument.
|
||||
|
||||
Order doesn't matter, and later styles take precedent in case of a conflict.
|
||||
|
||||
This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`.
|
||||
*/
|
||||
declare const chalk: ChalkInstance;
|
||||
|
||||
export const supportsColor: ColorInfo;
|
||||
|
||||
export const chalkStderr: typeof chalk;
|
||||
export const supportsColorStderr: typeof supportsColor;
|
||||
|
||||
export {
|
||||
ModifierName, ForegroundColorName, BackgroundColorName, ColorName,
|
||||
modifierNames, foregroundColorNames, backgroundColorNames, colorNames,
|
||||
// } from '#ansi-styles';
|
||||
} from './vendor/ansi-styles/index.js';
|
||||
|
||||
export {
|
||||
ColorInfo,
|
||||
ColorSupport,
|
||||
ColorSupportLevel,
|
||||
// } from '#supports-color';
|
||||
} from './vendor/supports-color/index.js';
|
||||
|
||||
// TODO: Remove these aliases in the next major version
|
||||
/**
|
||||
@deprecated Use `ModifierName` instead.
|
||||
|
||||
Basic modifier names.
|
||||
*/
|
||||
export type Modifiers = ModifierName;
|
||||
|
||||
/**
|
||||
@deprecated Use `ForegroundColorName` instead.
|
||||
|
||||
Basic foreground color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type ForegroundColor = ForegroundColorName;
|
||||
|
||||
/**
|
||||
@deprecated Use `BackgroundColorName` instead.
|
||||
|
||||
Basic background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type BackgroundColor = BackgroundColorName;
|
||||
|
||||
/**
|
||||
@deprecated Use `ColorName` instead.
|
||||
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type Color = ColorName;
|
||||
|
||||
/**
|
||||
@deprecated Use `modifierNames` instead.
|
||||
|
||||
Basic modifier names.
|
||||
*/
|
||||
export const modifiers: readonly Modifiers[];
|
||||
|
||||
/**
|
||||
@deprecated Use `foregroundColorNames` instead.
|
||||
|
||||
Basic foreground color names.
|
||||
*/
|
||||
export const foregroundColors: readonly ForegroundColor[];
|
||||
|
||||
/**
|
||||
@deprecated Use `backgroundColorNames` instead.
|
||||
|
||||
Basic background color names.
|
||||
*/
|
||||
export const backgroundColors: readonly BackgroundColor[];
|
||||
|
||||
/**
|
||||
@deprecated Use `colorNames` instead.
|
||||
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
*/
|
||||
export const colors: readonly Color[];
|
||||
|
||||
export default chalk;
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
import ansiStyles from '#ansi-styles';
|
||||
import supportsColor from '#supports-color';
|
||||
import { // eslint-disable-line import/order
|
||||
stringReplaceAll,
|
||||
stringEncaseCRLFWithFirstIndex,
|
||||
} from './utilities.js';
|
||||
|
||||
const {stdout: stdoutColor, stderr: stderrColor} = supportsColor;
|
||||
|
||||
const GENERATOR = Symbol('GENERATOR');
|
||||
const STYLER = Symbol('STYLER');
|
||||
const IS_EMPTY = Symbol('IS_EMPTY');
|
||||
|
||||
// `supportsColor.level` → `ansiStyles.color[name]` mapping
|
||||
const levelMapping = [
|
||||
'ansi',
|
||||
'ansi',
|
||||
'ansi256',
|
||||
'ansi16m',
|
||||
];
|
||||
|
||||
const styles = Object.create(null);
|
||||
|
||||
const applyOptions = (object, options = {}) => {
|
||||
if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
|
||||
throw new Error('The `level` option should be an integer from 0 to 3');
|
||||
}
|
||||
|
||||
// Detect level if not set manually
|
||||
const colorLevel = stdoutColor ? stdoutColor.level : 0;
|
||||
object.level = options.level === undefined ? colorLevel : options.level;
|
||||
};
|
||||
|
||||
export class Chalk {
|
||||
constructor(options) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return chalkFactory(options);
|
||||
}
|
||||
}
|
||||
|
||||
const chalkFactory = options => {
|
||||
const chalk = (...strings) => strings.join(' ');
|
||||
applyOptions(chalk, options);
|
||||
|
||||
Object.setPrototypeOf(chalk, createChalk.prototype);
|
||||
|
||||
return chalk;
|
||||
};
|
||||
|
||||
function createChalk(options) {
|
||||
return chalkFactory(options);
|
||||
}
|
||||
|
||||
Object.setPrototypeOf(createChalk.prototype, Function.prototype);
|
||||
|
||||
for (const [styleName, style] of Object.entries(ansiStyles)) {
|
||||
styles[styleName] = {
|
||||
get() {
|
||||
const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
|
||||
Object.defineProperty(this, styleName, {value: builder});
|
||||
return builder;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
styles.visible = {
|
||||
get() {
|
||||
const builder = createBuilder(this, this[STYLER], true);
|
||||
Object.defineProperty(this, 'visible', {value: builder});
|
||||
return builder;
|
||||
},
|
||||
};
|
||||
|
||||
const getModelAnsi = (model, level, type, ...arguments_) => {
|
||||
if (model === 'rgb') {
|
||||
if (level === 'ansi16m') {
|
||||
return ansiStyles[type].ansi16m(...arguments_);
|
||||
}
|
||||
|
||||
if (level === 'ansi256') {
|
||||
return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_));
|
||||
}
|
||||
|
||||
return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_));
|
||||
}
|
||||
|
||||
if (model === 'hex') {
|
||||
return getModelAnsi('rgb', level, type, ...ansiStyles.hexToRgb(...arguments_));
|
||||
}
|
||||
|
||||
return ansiStyles[type][model](...arguments_);
|
||||
};
|
||||
|
||||
const usedModels = ['rgb', 'hex', 'ansi256'];
|
||||
|
||||
for (const model of usedModels) {
|
||||
styles[model] = {
|
||||
get() {
|
||||
const {level} = this;
|
||||
return function (...arguments_) {
|
||||
const styler = createStyler(getModelAnsi(model, levelMapping[level], 'color', ...arguments_), ansiStyles.color.close, this[STYLER]);
|
||||
return createBuilder(this, styler, this[IS_EMPTY]);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1);
|
||||
styles[bgModel] = {
|
||||
get() {
|
||||
const {level} = this;
|
||||
return function (...arguments_) {
|
||||
const styler = createStyler(getModelAnsi(model, levelMapping[level], 'bgColor', ...arguments_), ansiStyles.bgColor.close, this[STYLER]);
|
||||
return createBuilder(this, styler, this[IS_EMPTY]);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const proto = Object.defineProperties(() => {}, {
|
||||
...styles,
|
||||
level: {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return this[GENERATOR].level;
|
||||
},
|
||||
set(level) {
|
||||
this[GENERATOR].level = level;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createStyler = (open, close, parent) => {
|
||||
let openAll;
|
||||
let closeAll;
|
||||
if (parent === undefined) {
|
||||
openAll = open;
|
||||
closeAll = close;
|
||||
} else {
|
||||
openAll = parent.openAll + open;
|
||||
closeAll = close + parent.closeAll;
|
||||
}
|
||||
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
openAll,
|
||||
closeAll,
|
||||
parent,
|
||||
};
|
||||
};
|
||||
|
||||
const createBuilder = (self, _styler, _isEmpty) => {
|
||||
// Single argument is hot path, implicit coercion is faster than anything
|
||||
// eslint-disable-next-line no-implicit-coercion
|
||||
const builder = (...arguments_) => applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' '));
|
||||
|
||||
// We alter the prototype because we must return a function, but there is
|
||||
// no way to create a function with a different prototype
|
||||
Object.setPrototypeOf(builder, proto);
|
||||
|
||||
builder[GENERATOR] = self;
|
||||
builder[STYLER] = _styler;
|
||||
builder[IS_EMPTY] = _isEmpty;
|
||||
|
||||
return builder;
|
||||
};
|
||||
|
||||
const applyStyle = (self, string) => {
|
||||
if (self.level <= 0 || !string) {
|
||||
return self[IS_EMPTY] ? '' : string;
|
||||
}
|
||||
|
||||
let styler = self[STYLER];
|
||||
|
||||
if (styler === undefined) {
|
||||
return string;
|
||||
}
|
||||
|
||||
const {openAll, closeAll} = styler;
|
||||
if (string.includes('\u001B')) {
|
||||
while (styler !== undefined) {
|
||||
// Replace any instances already present with a re-opening code
|
||||
// otherwise only the part of the string until said closing code
|
||||
// will be colored, and the rest will simply be 'plain'.
|
||||
string = stringReplaceAll(string, styler.close, styler.open);
|
||||
|
||||
styler = styler.parent;
|
||||
}
|
||||
}
|
||||
|
||||
// We can move both next actions out of loop, because remaining actions in loop won't have
|
||||
// any/visible effect on parts we add here. Close the styling before a linebreak and reopen
|
||||
// after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92
|
||||
const lfIndex = string.indexOf('\n');
|
||||
if (lfIndex !== -1) {
|
||||
string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
|
||||
}
|
||||
|
||||
return openAll + string + closeAll;
|
||||
};
|
||||
|
||||
Object.defineProperties(createChalk.prototype, styles);
|
||||
|
||||
const chalk = createChalk();
|
||||
export const chalkStderr = createChalk({level: stderrColor ? stderrColor.level : 0});
|
||||
|
||||
export {
|
||||
modifierNames,
|
||||
foregroundColorNames,
|
||||
backgroundColorNames,
|
||||
colorNames,
|
||||
|
||||
// TODO: Remove these aliases in the next major version
|
||||
modifierNames as modifiers,
|
||||
foregroundColorNames as foregroundColors,
|
||||
backgroundColorNames as backgroundColors,
|
||||
colorNames as colors,
|
||||
} from './vendor/ansi-styles/index.js';
|
||||
|
||||
export {
|
||||
stdoutColor as supportsColor,
|
||||
stderrColor as supportsColorStderr,
|
||||
};
|
||||
|
||||
export default chalk;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// TODO: When targeting Node.js 16, use `String.prototype.replaceAll`.
|
||||
export function stringReplaceAll(string, substring, replacer) {
|
||||
let index = string.indexOf(substring);
|
||||
if (index === -1) {
|
||||
return string;
|
||||
}
|
||||
|
||||
const substringLength = substring.length;
|
||||
let endIndex = 0;
|
||||
let returnValue = '';
|
||||
do {
|
||||
returnValue += string.slice(endIndex, index) + substring + replacer;
|
||||
endIndex = index + substringLength;
|
||||
index = string.indexOf(substring, endIndex);
|
||||
} while (index !== -1);
|
||||
|
||||
returnValue += string.slice(endIndex);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
export function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
|
||||
let endIndex = 0;
|
||||
let returnValue = '';
|
||||
do {
|
||||
const gotCR = string[index - 1] === '\r';
|
||||
returnValue += string.slice(endIndex, (gotCR ? index - 1 : index)) + prefix + (gotCR ? '\r\n' : '\n') + postfix;
|
||||
endIndex = index + 1;
|
||||
index = string.indexOf('\n', endIndex);
|
||||
} while (index !== -1);
|
||||
|
||||
returnValue += string.slice(endIndex);
|
||||
return returnValue;
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
export interface CSPair { // eslint-disable-line @typescript-eslint/naming-convention
|
||||
/**
|
||||
The ANSI terminal control sequence for starting this style.
|
||||
*/
|
||||
readonly open: string;
|
||||
|
||||
/**
|
||||
The ANSI terminal control sequence for ending this style.
|
||||
*/
|
||||
readonly close: string;
|
||||
}
|
||||
|
||||
export interface ColorBase {
|
||||
/**
|
||||
The ANSI terminal control sequence for ending this color.
|
||||
*/
|
||||
readonly close: string;
|
||||
|
||||
ansi(code: number): string;
|
||||
|
||||
ansi256(code: number): string;
|
||||
|
||||
ansi16m(red: number, green: number, blue: number): string;
|
||||
}
|
||||
|
||||
export interface Modifier {
|
||||
/**
|
||||
Resets the current color chain.
|
||||
*/
|
||||
readonly reset: CSPair;
|
||||
|
||||
/**
|
||||
Make text bold.
|
||||
*/
|
||||
readonly bold: CSPair;
|
||||
|
||||
/**
|
||||
Emitting only a small amount of light.
|
||||
*/
|
||||
readonly dim: CSPair;
|
||||
|
||||
/**
|
||||
Make text italic. (Not widely supported)
|
||||
*/
|
||||
readonly italic: CSPair;
|
||||
|
||||
/**
|
||||
Make text underline. (Not widely supported)
|
||||
*/
|
||||
readonly underline: CSPair;
|
||||
|
||||
/**
|
||||
Make text overline.
|
||||
|
||||
Supported on VTE-based terminals, the GNOME terminal, mintty, and Git Bash.
|
||||
*/
|
||||
readonly overline: CSPair;
|
||||
|
||||
/**
|
||||
Inverse background and foreground colors.
|
||||
*/
|
||||
readonly inverse: CSPair;
|
||||
|
||||
/**
|
||||
Prints the text, but makes it invisible.
|
||||
*/
|
||||
readonly hidden: CSPair;
|
||||
|
||||
/**
|
||||
Puts a horizontal line through the center of the text. (Not widely supported)
|
||||
*/
|
||||
readonly strikethrough: CSPair;
|
||||
}
|
||||
|
||||
export interface ForegroundColor {
|
||||
readonly black: CSPair;
|
||||
readonly red: CSPair;
|
||||
readonly green: CSPair;
|
||||
readonly yellow: CSPair;
|
||||
readonly blue: CSPair;
|
||||
readonly cyan: CSPair;
|
||||
readonly magenta: CSPair;
|
||||
readonly white: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly gray: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly grey: CSPair;
|
||||
|
||||
readonly blackBright: CSPair;
|
||||
readonly redBright: CSPair;
|
||||
readonly greenBright: CSPair;
|
||||
readonly yellowBright: CSPair;
|
||||
readonly blueBright: CSPair;
|
||||
readonly cyanBright: CSPair;
|
||||
readonly magentaBright: CSPair;
|
||||
readonly whiteBright: CSPair;
|
||||
}
|
||||
|
||||
export interface BackgroundColor {
|
||||
readonly bgBlack: CSPair;
|
||||
readonly bgRed: CSPair;
|
||||
readonly bgGreen: CSPair;
|
||||
readonly bgYellow: CSPair;
|
||||
readonly bgBlue: CSPair;
|
||||
readonly bgCyan: CSPair;
|
||||
readonly bgMagenta: CSPair;
|
||||
readonly bgWhite: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGray: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGrey: CSPair;
|
||||
|
||||
readonly bgBlackBright: CSPair;
|
||||
readonly bgRedBright: CSPair;
|
||||
readonly bgGreenBright: CSPair;
|
||||
readonly bgYellowBright: CSPair;
|
||||
readonly bgBlueBright: CSPair;
|
||||
readonly bgCyanBright: CSPair;
|
||||
readonly bgMagentaBright: CSPair;
|
||||
readonly bgWhiteBright: CSPair;
|
||||
}
|
||||
|
||||
export interface ConvertColor {
|
||||
/**
|
||||
Convert from the RGB color space to the ANSI 256 color space.
|
||||
|
||||
@param red - (`0...255`)
|
||||
@param green - (`0...255`)
|
||||
@param blue - (`0...255`)
|
||||
*/
|
||||
rgbToAnsi256(red: number, green: number, blue: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the RGB color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToRgb(hex: string): [red: number, green: number, blue: number];
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the ANSI 256 color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToAnsi256(hex: string): number;
|
||||
|
||||
/**
|
||||
Convert from the ANSI 256 color space to the ANSI 16 color space.
|
||||
|
||||
@param code - A number representing the ANSI 256 color.
|
||||
*/
|
||||
ansi256ToAnsi(code: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB color space to the ANSI 16 color space.
|
||||
|
||||
@param red - (`0...255`)
|
||||
@param green - (`0...255`)
|
||||
@param blue - (`0...255`)
|
||||
*/
|
||||
rgbToAnsi(red: number, green: number, blue: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the ANSI 16 color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToAnsi(hex: string): number;
|
||||
}
|
||||
|
||||
/**
|
||||
Basic modifier names.
|
||||
*/
|
||||
export type ModifierName = keyof Modifier;
|
||||
|
||||
/**
|
||||
Basic foreground color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type ForegroundColorName = keyof ForegroundColor;
|
||||
|
||||
/**
|
||||
Basic background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type BackgroundColorName = keyof BackgroundColor;
|
||||
|
||||
/**
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type ColorName = ForegroundColorName | BackgroundColorName;
|
||||
|
||||
/**
|
||||
Basic modifier names.
|
||||
*/
|
||||
export const modifierNames: readonly ModifierName[];
|
||||
|
||||
/**
|
||||
Basic foreground color names.
|
||||
*/
|
||||
export const foregroundColorNames: readonly ForegroundColorName[];
|
||||
|
||||
/**
|
||||
Basic background color names.
|
||||
*/
|
||||
export const backgroundColorNames: readonly BackgroundColorName[];
|
||||
|
||||
/*
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
*/
|
||||
export const colorNames: readonly ColorName[];
|
||||
|
||||
declare const ansiStyles: {
|
||||
readonly modifier: Modifier;
|
||||
readonly color: ColorBase & ForegroundColor;
|
||||
readonly bgColor: ColorBase & BackgroundColor;
|
||||
readonly codes: ReadonlyMap<number, number>;
|
||||
} & ForegroundColor & BackgroundColor & Modifier & ConvertColor;
|
||||
|
||||
export default ansiStyles;
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
const ANSI_BACKGROUND_OFFSET = 10;
|
||||
|
||||
const wrapAnsi16 = (offset = 0) => code => `\u001B[${code + offset}m`;
|
||||
|
||||
const wrapAnsi256 = (offset = 0) => code => `\u001B[${38 + offset};5;${code}m`;
|
||||
|
||||
const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`;
|
||||
|
||||
const styles = {
|
||||
modifier: {
|
||||
reset: [0, 0],
|
||||
// 21 isn't widely supported and 22 does the same thing
|
||||
bold: [1, 22],
|
||||
dim: [2, 22],
|
||||
italic: [3, 23],
|
||||
underline: [4, 24],
|
||||
overline: [53, 55],
|
||||
inverse: [7, 27],
|
||||
hidden: [8, 28],
|
||||
strikethrough: [9, 29],
|
||||
},
|
||||
color: {
|
||||
black: [30, 39],
|
||||
red: [31, 39],
|
||||
green: [32, 39],
|
||||
yellow: [33, 39],
|
||||
blue: [34, 39],
|
||||
magenta: [35, 39],
|
||||
cyan: [36, 39],
|
||||
white: [37, 39],
|
||||
|
||||
// Bright color
|
||||
blackBright: [90, 39],
|
||||
gray: [90, 39], // Alias of `blackBright`
|
||||
grey: [90, 39], // Alias of `blackBright`
|
||||
redBright: [91, 39],
|
||||
greenBright: [92, 39],
|
||||
yellowBright: [93, 39],
|
||||
blueBright: [94, 39],
|
||||
magentaBright: [95, 39],
|
||||
cyanBright: [96, 39],
|
||||
whiteBright: [97, 39],
|
||||
},
|
||||
bgColor: {
|
||||
bgBlack: [40, 49],
|
||||
bgRed: [41, 49],
|
||||
bgGreen: [42, 49],
|
||||
bgYellow: [43, 49],
|
||||
bgBlue: [44, 49],
|
||||
bgMagenta: [45, 49],
|
||||
bgCyan: [46, 49],
|
||||
bgWhite: [47, 49],
|
||||
|
||||
// Bright color
|
||||
bgBlackBright: [100, 49],
|
||||
bgGray: [100, 49], // Alias of `bgBlackBright`
|
||||
bgGrey: [100, 49], // Alias of `bgBlackBright`
|
||||
bgRedBright: [101, 49],
|
||||
bgGreenBright: [102, 49],
|
||||
bgYellowBright: [103, 49],
|
||||
bgBlueBright: [104, 49],
|
||||
bgMagentaBright: [105, 49],
|
||||
bgCyanBright: [106, 49],
|
||||
bgWhiteBright: [107, 49],
|
||||
},
|
||||
};
|
||||
|
||||
export const modifierNames = Object.keys(styles.modifier);
|
||||
export const foregroundColorNames = Object.keys(styles.color);
|
||||
export const backgroundColorNames = Object.keys(styles.bgColor);
|
||||
export const colorNames = [...foregroundColorNames, ...backgroundColorNames];
|
||||
|
||||
function assembleStyles() {
|
||||
const codes = new Map();
|
||||
|
||||
for (const [groupName, group] of Object.entries(styles)) {
|
||||
for (const [styleName, style] of Object.entries(group)) {
|
||||
styles[styleName] = {
|
||||
open: `\u001B[${style[0]}m`,
|
||||
close: `\u001B[${style[1]}m`,
|
||||
};
|
||||
|
||||
group[styleName] = styles[styleName];
|
||||
|
||||
codes.set(style[0], style[1]);
|
||||
}
|
||||
|
||||
Object.defineProperty(styles, groupName, {
|
||||
value: group,
|
||||
enumerable: false,
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(styles, 'codes', {
|
||||
value: codes,
|
||||
enumerable: false,
|
||||
});
|
||||
|
||||
styles.color.close = '\u001B[39m';
|
||||
styles.bgColor.close = '\u001B[49m';
|
||||
|
||||
styles.color.ansi = wrapAnsi16();
|
||||
styles.color.ansi256 = wrapAnsi256();
|
||||
styles.color.ansi16m = wrapAnsi16m();
|
||||
styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
|
||||
styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
|
||||
styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
|
||||
|
||||
// From https://github.com/Qix-/color-convert/blob/3f0e0d4e92e235796ccb17f6e85c72094a651f49/conversions.js
|
||||
Object.defineProperties(styles, {
|
||||
rgbToAnsi256: {
|
||||
value(red, green, blue) {
|
||||
// We use the extended greyscale palette here, with the exception of
|
||||
// black and white. normal palette only has 4 greyscale shades.
|
||||
if (red === green && green === blue) {
|
||||
if (red < 8) {
|
||||
return 16;
|
||||
}
|
||||
|
||||
if (red > 248) {
|
||||
return 231;
|
||||
}
|
||||
|
||||
return Math.round(((red - 8) / 247) * 24) + 232;
|
||||
}
|
||||
|
||||
return 16
|
||||
+ (36 * Math.round(red / 255 * 5))
|
||||
+ (6 * Math.round(green / 255 * 5))
|
||||
+ Math.round(blue / 255 * 5);
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
hexToRgb: {
|
||||
value(hex) {
|
||||
const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
|
||||
if (!matches) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
let [colorString] = matches;
|
||||
|
||||
if (colorString.length === 3) {
|
||||
colorString = [...colorString].map(character => character + character).join('');
|
||||
}
|
||||
|
||||
const integer = Number.parseInt(colorString, 16);
|
||||
|
||||
return [
|
||||
/* eslint-disable no-bitwise */
|
||||
(integer >> 16) & 0xFF,
|
||||
(integer >> 8) & 0xFF,
|
||||
integer & 0xFF,
|
||||
/* eslint-enable no-bitwise */
|
||||
];
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
hexToAnsi256: {
|
||||
value: hex => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
|
||||
enumerable: false,
|
||||
},
|
||||
ansi256ToAnsi: {
|
||||
value(code) {
|
||||
if (code < 8) {
|
||||
return 30 + code;
|
||||
}
|
||||
|
||||
if (code < 16) {
|
||||
return 90 + (code - 8);
|
||||
}
|
||||
|
||||
let red;
|
||||
let green;
|
||||
let blue;
|
||||
|
||||
if (code >= 232) {
|
||||
red = (((code - 232) * 10) + 8) / 255;
|
||||
green = red;
|
||||
blue = red;
|
||||
} else {
|
||||
code -= 16;
|
||||
|
||||
const remainder = code % 36;
|
||||
|
||||
red = Math.floor(code / 36) / 5;
|
||||
green = Math.floor(remainder / 6) / 5;
|
||||
blue = (remainder % 6) / 5;
|
||||
}
|
||||
|
||||
const value = Math.max(red, green, blue) * 2;
|
||||
|
||||
if (value === 0) {
|
||||
return 30;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red));
|
||||
|
||||
if (value === 2) {
|
||||
result += 60;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
rgbToAnsi: {
|
||||
value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
|
||||
enumerable: false,
|
||||
},
|
||||
hexToAnsi: {
|
||||
value: hex => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
|
||||
enumerable: false,
|
||||
},
|
||||
});
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
const ansiStyles = assembleStyles();
|
||||
|
||||
export default ansiStyles;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {default} from './index.js';
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
const level = (() => {
|
||||
if (!('navigator' in globalThis)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (globalThis.navigator.userAgentData) {
|
||||
const brand = navigator.userAgentData.brands.find(({brand}) => brand === 'Chromium');
|
||||
if (brand && brand.version > 93) {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (/\b(Chrome|Chromium)\//.test(globalThis.navigator.userAgent)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})();
|
||||
|
||||
const colorSupport = level !== 0 && {
|
||||
level,
|
||||
hasBasic: true,
|
||||
has256: level >= 2,
|
||||
has16m: level >= 3,
|
||||
};
|
||||
|
||||
const supportsColor = {
|
||||
stdout: colorSupport,
|
||||
stderr: colorSupport,
|
||||
};
|
||||
|
||||
export default supportsColor;
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import type {WriteStream} from 'node:tty';
|
||||
|
||||
export type Options = {
|
||||
/**
|
||||
Whether `process.argv` should be sniffed for `--color` and `--no-color` flags.
|
||||
|
||||
@default true
|
||||
*/
|
||||
readonly sniffFlags?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
Levels:
|
||||
- `0` - All colors disabled.
|
||||
- `1` - Basic 16 colors support.
|
||||
- `2` - ANSI 256 colors support.
|
||||
- `3` - Truecolor 16 million colors support.
|
||||
*/
|
||||
export type ColorSupportLevel = 0 | 1 | 2 | 3;
|
||||
|
||||
/**
|
||||
Detect whether the terminal supports color.
|
||||
*/
|
||||
export type ColorSupport = {
|
||||
/**
|
||||
The color level.
|
||||
*/
|
||||
level: ColorSupportLevel;
|
||||
|
||||
/**
|
||||
Whether basic 16 colors are supported.
|
||||
*/
|
||||
hasBasic: boolean;
|
||||
|
||||
/**
|
||||
Whether ANSI 256 colors are supported.
|
||||
*/
|
||||
has256: boolean;
|
||||
|
||||
/**
|
||||
Whether Truecolor 16 million colors are supported.
|
||||
*/
|
||||
has16m: boolean;
|
||||
};
|
||||
|
||||
export type ColorInfo = ColorSupport | false;
|
||||
|
||||
export function createSupportsColor(stream?: WriteStream, options?: Options): ColorInfo;
|
||||
|
||||
declare const supportsColor: {
|
||||
stdout: ColorInfo;
|
||||
stderr: ColorInfo;
|
||||
};
|
||||
|
||||
export default supportsColor;
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
import process from 'node:process';
|
||||
import os from 'node:os';
|
||||
import tty from 'node:tty';
|
||||
|
||||
// From: https://github.com/sindresorhus/has-flag/blob/main/index.js
|
||||
/// function hasFlag(flag, argv = globalThis.Deno?.args ?? process.argv) {
|
||||
function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process.argv) {
|
||||
const prefix = flag.startsWith('-') ? '' : (flag.length === 1 ? '-' : '--');
|
||||
const position = argv.indexOf(prefix + flag);
|
||||
const terminatorPosition = argv.indexOf('--');
|
||||
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
|
||||
}
|
||||
|
||||
const {env} = process;
|
||||
|
||||
let flagForceColor;
|
||||
if (
|
||||
hasFlag('no-color')
|
||||
|| hasFlag('no-colors')
|
||||
|| hasFlag('color=false')
|
||||
|| hasFlag('color=never')
|
||||
) {
|
||||
flagForceColor = 0;
|
||||
} else if (
|
||||
hasFlag('color')
|
||||
|| hasFlag('colors')
|
||||
|| hasFlag('color=true')
|
||||
|| hasFlag('color=always')
|
||||
) {
|
||||
flagForceColor = 1;
|
||||
}
|
||||
|
||||
function envForceColor() {
|
||||
if ('FORCE_COLOR' in env) {
|
||||
if (env.FORCE_COLOR === 'true') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (env.FORCE_COLOR === 'false') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
|
||||
}
|
||||
}
|
||||
|
||||
function translateLevel(level) {
|
||||
if (level === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
level,
|
||||
hasBasic: true,
|
||||
has256: level >= 2,
|
||||
has16m: level >= 3,
|
||||
};
|
||||
}
|
||||
|
||||
function _supportsColor(haveStream, {streamIsTTY, sniffFlags = true} = {}) {
|
||||
const noFlagForceColor = envForceColor();
|
||||
if (noFlagForceColor !== undefined) {
|
||||
flagForceColor = noFlagForceColor;
|
||||
}
|
||||
|
||||
const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
|
||||
|
||||
if (forceColor === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sniffFlags) {
|
||||
if (hasFlag('color=16m')
|
||||
|| hasFlag('color=full')
|
||||
|| hasFlag('color=truecolor')) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (hasFlag('color=256')) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Azure DevOps pipelines.
|
||||
// Has to be above the `!streamIsTTY` check.
|
||||
if ('TF_BUILD' in env && 'AGENT_NAME' in env) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (haveStream && !streamIsTTY && forceColor === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const min = forceColor || 0;
|
||||
|
||||
if (env.TERM === 'dumb') {
|
||||
return min;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Windows 10 build 10586 is the first Windows release that supports 256 colors.
|
||||
// Windows 10 build 14931 is the first release that supports 16m/TrueColor.
|
||||
const osRelease = os.release().split('.');
|
||||
if (
|
||||
Number(osRelease[0]) >= 10
|
||||
&& Number(osRelease[2]) >= 10_586
|
||||
) {
|
||||
return Number(osRelease[2]) >= 14_931 ? 3 : 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ('CI' in env) {
|
||||
if (['GITHUB_ACTIONS', 'GITEA_ACTIONS', 'CIRCLECI'].some(key => key in env)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (['TRAVIS', 'APPVEYOR', 'GITLAB_CI', 'BUILDKITE', 'DRONE'].some(sign => sign in env) || env.CI_NAME === 'codeship') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
if ('TEAMCITY_VERSION' in env) {
|
||||
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
|
||||
}
|
||||
|
||||
if (env.COLORTERM === 'truecolor') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (env.TERM === 'xterm-kitty') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (env.TERM === 'xterm-ghostty') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (env.TERM === 'wezterm') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if ('TERM_PROGRAM' in env) {
|
||||
const version = Number.parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10);
|
||||
|
||||
switch (env.TERM_PROGRAM) {
|
||||
case 'iTerm.app': {
|
||||
return version >= 3 ? 3 : 2;
|
||||
}
|
||||
|
||||
case 'Apple_Terminal': {
|
||||
return 2;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
}
|
||||
|
||||
if (/-256(color)?$/i.test(env.TERM)) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ('COLORTERM' in env) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
export function createSupportsColor(stream, options = {}) {
|
||||
const level = _supportsColor(stream, {
|
||||
streamIsTTY: stream && stream.isTTY,
|
||||
...options,
|
||||
});
|
||||
|
||||
return translateLevel(level);
|
||||
}
|
||||
|
||||
const supportsColor = {
|
||||
stdout: createSupportsColor({isTTY: tty.isatty(1)}),
|
||||
stderr: createSupportsColor({isTTY: tty.isatty(2)}),
|
||||
};
|
||||
|
||||
export default supportsColor;
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [9.0.1](https://github.com/yargs/cliui/compare/v9.0.0...v9.0.1) (2025-03-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make require("cliui") work as expected for CJS ([04ccc25](https://github.com/yargs/cliui/commit/04ccc250e30a059292c03fa1ef0a8661f8d93dfe))
|
||||
|
||||
## [9.0.0](https://github.com/yargs/cliui/compare/v8.0.1...v9.0.0) (2025-03-16)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* cliui is now ESM only ([#165](https://github.com/yargs/cliui/issues/165))
|
||||
|
||||
### Features
|
||||
|
||||
* cliui is now ESM only ([#165](https://github.com/yargs/cliui/issues/165)) ([5a521de](https://github.com/yargs/cliui/commit/5a521de7ea88f262236394c8d96775bcf50ff0a4))
|
||||
|
||||
## [8.0.1](https://github.com/yargs/cliui/compare/v8.0.0...v8.0.1) (2022-10-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** move rollup-plugin-ts to dev deps ([#124](https://github.com/yargs/cliui/issues/124)) ([7c8bd6b](https://github.com/yargs/cliui/commit/7c8bd6ba024d61e4eeae310c7959ab8ab6829081))
|
||||
|
||||
## [8.0.0](https://github.com/yargs/cliui/compare/v7.0.4...v8.0.0) (2022-09-30)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **deps:** drop Node 10 to release CVE-2021-3807 patch (#122)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** drop Node 10 to release CVE-2021-3807 patch ([#122](https://github.com/yargs/cliui/issues/122)) ([f156571](https://github.com/yargs/cliui/commit/f156571ce4f2ebf313335e3a53ad905589da5a30))
|
||||
|
||||
### [7.0.4](https://www.github.com/yargs/cliui/compare/v7.0.3...v7.0.4) (2020-11-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deno:** import UIOptions from definitions ([#97](https://www.github.com/yargs/cliui/issues/97)) ([f04f343](https://www.github.com/yargs/cliui/commit/f04f3439bc78114c7e90f82ff56f5acf16268ea8))
|
||||
|
||||
### [7.0.3](https://www.github.com/yargs/cliui/compare/v7.0.2...v7.0.3) (2020-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **exports:** node 13.0 and 13.1 require the dotted object form _with_ a string fallback ([#93](https://www.github.com/yargs/cliui/issues/93)) ([eca16fc](https://www.github.com/yargs/cliui/commit/eca16fc05d26255df3280906c36d7f0e5b05c6e9))
|
||||
|
||||
### [7.0.2](https://www.github.com/yargs/cliui/compare/v7.0.1...v7.0.2) (2020-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **exports:** node 13.0-13.6 require a string fallback ([#91](https://www.github.com/yargs/cliui/issues/91)) ([b529d7e](https://www.github.com/yargs/cliui/commit/b529d7e432901af1af7848b23ed6cf634497d961))
|
||||
|
||||
### [7.0.1](https://www.github.com/yargs/cliui/compare/v7.0.0...v7.0.1) (2020-08-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **build:** main should be build/index.cjs ([dc29a3c](https://www.github.com/yargs/cliui/commit/dc29a3cc617a410aa850e06337b5954b04f2cb4d))
|
||||
|
||||
## [7.0.0](https://www.github.com/yargs/cliui/compare/v6.0.0...v7.0.0) (2020-08-16)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* tsc/ESM/Deno support (#82)
|
||||
* modernize deps and build (#80)
|
||||
|
||||
### Build System
|
||||
|
||||
* modernize deps and build ([#80](https://www.github.com/yargs/cliui/issues/80)) ([339d08d](https://www.github.com/yargs/cliui/commit/339d08dc71b15a3928aeab09042af94db2f43743))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* tsc/ESM/Deno support ([#82](https://www.github.com/yargs/cliui/issues/82)) ([4b777a5](https://www.github.com/yargs/cliui/commit/4b777a5fe01c5d8958c6708695d6aab7dbe5706c))
|
||||
|
||||
## [6.0.0](https://www.github.com/yargs/cliui/compare/v5.0.0...v6.0.0) (2019-11-10)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* update deps, drop Node 6
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* update deps, drop Node 6 ([62056df](https://www.github.com/yargs/cliui/commit/62056df))
|
||||
|
||||
## [5.0.0](https://github.com/yargs/cliui/compare/v4.1.0...v5.0.0) (2019-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Update wrap-ansi to fix compatibility with latest versions of chalk. ([#60](https://github.com/yargs/cliui/issues/60)) ([7bf79ae](https://github.com/yargs/cliui/commit/7bf79ae))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Drop support for node < 6.
|
||||
|
||||
|
||||
|
||||
<a name="4.1.0"></a>
|
||||
## [4.1.0](https://github.com/yargs/cliui/compare/v4.0.0...v4.1.0) (2018-04-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add resetOutput method ([#57](https://github.com/yargs/cliui/issues/57)) ([7246902](https://github.com/yargs/cliui/commit/7246902))
|
||||
|
||||
|
||||
|
||||
<a name="4.0.0"></a>
|
||||
## [4.0.0](https://github.com/yargs/cliui/compare/v3.2.0...v4.0.0) (2017-12-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* downgrades strip-ansi to version 3.0.1 ([#54](https://github.com/yargs/cliui/issues/54)) ([5764c46](https://github.com/yargs/cliui/commit/5764c46))
|
||||
* set env variable FORCE_COLOR. ([#56](https://github.com/yargs/cliui/issues/56)) ([7350e36](https://github.com/yargs/cliui/commit/7350e36))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* drop support for node < 4 ([#53](https://github.com/yargs/cliui/issues/53)) ([b105376](https://github.com/yargs/cliui/commit/b105376))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add fallback for window width ([#45](https://github.com/yargs/cliui/issues/45)) ([d064922](https://github.com/yargs/cliui/commit/d064922))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* officially drop support for Node < 4
|
||||
|
||||
|
||||
|
||||
<a name="3.2.0"></a>
|
||||
## [3.2.0](https://github.com/yargs/cliui/compare/v3.1.2...v3.2.0) (2016-04-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* reduces tarball size ([acc6c33](https://github.com/yargs/cliui/commit/acc6c33))
|
||||
|
||||
### Features
|
||||
|
||||
* adds standard-version for release management ([ff84e32](https://github.com/yargs/cliui/commit/ff84e32))
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
Copyright (c) 2015, Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software
|
||||
for any purpose with or without fee is hereby granted, provided
|
||||
that the above copyright notice and this permission notice
|
||||
appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE
|
||||
LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
|
||||
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
# cliui
|
||||
|
||||

|
||||
[](https://www.npmjs.com/package/cliui)
|
||||
[](https://conventionalcommits.org)
|
||||

|
||||
|
||||
easily create complex multi-column command-line-interfaces.
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
npm i cliui@latest chalk@latest
|
||||
```
|
||||
|
||||
```js
|
||||
const ui = require('cliui')()
|
||||
const {Chalk} = require('chalk');
|
||||
const chalk = new Chalk();
|
||||
|
||||
ui.div('Usage: $0 [command] [options]')
|
||||
|
||||
ui.div({
|
||||
text: 'Options:',
|
||||
padding: [2, 0, 1, 0]
|
||||
})
|
||||
|
||||
ui.div(
|
||||
{
|
||||
text: "-f, --file",
|
||||
width: 20,
|
||||
padding: [0, 4, 0, 4]
|
||||
},
|
||||
{
|
||||
text: "the file to load." +
|
||||
chalk.green("(if this description is long it wraps).")
|
||||
,
|
||||
width: 20
|
||||
},
|
||||
{
|
||||
text: chalk.red("[required]"),
|
||||
align: 'right'
|
||||
}
|
||||
)
|
||||
|
||||
console.log(ui.toString())
|
||||
```
|
||||
|
||||
## Deno/ESM Support
|
||||
|
||||
As of `v7` `cliui` supports [Deno](https://github.com/denoland/deno) and
|
||||
[ESM](https://nodejs.org/api/esm.html#esm_ecmascript_modules):
|
||||
|
||||
```typescript
|
||||
import cliui from "cliui";
|
||||
import chalk from "chalk";
|
||||
// Deno: import cliui from "https://deno.land/x/cliui/deno.ts";
|
||||
|
||||
const ui = cliui({})
|
||||
|
||||
ui.div('Usage: $0 [command] [options]')
|
||||
|
||||
ui.div({
|
||||
text: 'Options:',
|
||||
padding: [2, 0, 1, 0]
|
||||
})
|
||||
|
||||
ui.div(
|
||||
{
|
||||
text: "-f, --file",
|
||||
width: 20,
|
||||
padding: [0, 4, 0, 4]
|
||||
},
|
||||
{
|
||||
text: "the file to load." +
|
||||
chalk.green("(if this description is long it wraps).")
|
||||
,
|
||||
width: 20
|
||||
},
|
||||
{
|
||||
text: chalk.red("[required]"),
|
||||
align: 'right'
|
||||
}
|
||||
)
|
||||
|
||||
console.log(ui.toString())
|
||||
```
|
||||
|
||||
<img width="500" src="screenshot.png">
|
||||
|
||||
## Layout DSL
|
||||
|
||||
cliui exposes a simple layout DSL:
|
||||
|
||||
If you create a single `ui.div`, passing a string rather than an
|
||||
object:
|
||||
|
||||
* `\n`: characters will be interpreted as new rows.
|
||||
* `\t`: characters will be interpreted as new columns.
|
||||
* `\s`: characters will be interpreted as padding.
|
||||
|
||||
**as an example...**
|
||||
|
||||
```js
|
||||
var ui = require('./')({
|
||||
width: 60
|
||||
})
|
||||
|
||||
ui.div(
|
||||
'Usage: node ./bin/foo.js\n' +
|
||||
' <regex>\t provide a regex\n' +
|
||||
' <glob>\t provide a glob\t [required]'
|
||||
)
|
||||
|
||||
console.log(ui.toString())
|
||||
```
|
||||
|
||||
**will output:**
|
||||
|
||||
```shell
|
||||
Usage: node ./bin/foo.js
|
||||
<regex> provide a regex
|
||||
<glob> provide a glob [required]
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
```js
|
||||
cliui = require('cliui')
|
||||
```
|
||||
|
||||
### cliui({width: integer})
|
||||
|
||||
Specify the maximum width of the UI being generated.
|
||||
If no width is provided, cliui will try to get the current window's width and use it, and if that doesn't work, width will be set to `80`.
|
||||
|
||||
### cliui({wrap: boolean})
|
||||
|
||||
Enable or disable the wrapping of text in a column.
|
||||
|
||||
### cliui.div(column, column, column)
|
||||
|
||||
Create a row with any number of columns, a column
|
||||
can either be a string, or an object with the following
|
||||
options:
|
||||
|
||||
* **text:** some text to place in the column.
|
||||
* **width:** the width of a column.
|
||||
* **align:** alignment, `right` or `center`.
|
||||
* **padding:** `[top, right, bottom, left]`.
|
||||
* **border:** should a border be placed around the div?
|
||||
|
||||
### cliui.span(column, column, column)
|
||||
|
||||
Similar to `div`, except the next row will be appended without
|
||||
a new line being created.
|
||||
|
||||
### cliui.resetOutput()
|
||||
|
||||
Resets the UI elements of the current cliui instance, maintaining the values
|
||||
set for `width` and `wrap`.
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
'use strict';
|
||||
const align = {
|
||||
right: alignRight,
|
||||
center: alignCenter
|
||||
};
|
||||
const top = 0;
|
||||
const right = 1;
|
||||
const bottom = 2;
|
||||
const left = 3;
|
||||
export class UI {
|
||||
constructor(opts) {
|
||||
var _a;
|
||||
this.width = opts.width;
|
||||
this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true;
|
||||
this.rows = [];
|
||||
}
|
||||
span(...args) {
|
||||
const cols = this.div(...args);
|
||||
cols.span = true;
|
||||
}
|
||||
resetOutput() {
|
||||
this.rows = [];
|
||||
}
|
||||
div(...args) {
|
||||
if (args.length === 0) {
|
||||
this.div('');
|
||||
}
|
||||
if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') {
|
||||
return this.applyLayoutDSL(args[0]);
|
||||
}
|
||||
const cols = args.map(arg => {
|
||||
if (typeof arg === 'string') {
|
||||
return this.colFromString(arg);
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
this.rows.push(cols);
|
||||
return cols;
|
||||
}
|
||||
shouldApplyLayoutDSL(...args) {
|
||||
return args.length === 1 && typeof args[0] === 'string' &&
|
||||
/[\t\n]/.test(args[0]);
|
||||
}
|
||||
applyLayoutDSL(str) {
|
||||
const rows = str.split('\n').map(row => row.split('\t'));
|
||||
let leftColumnWidth = 0;
|
||||
// simple heuristic for layout, make sure the
|
||||
// second column lines up along the left-hand.
|
||||
// don't allow the first column to take up more
|
||||
// than 50% of the screen.
|
||||
rows.forEach(columns => {
|
||||
if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) {
|
||||
leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0]));
|
||||
}
|
||||
});
|
||||
// generate a table:
|
||||
// replacing ' ' with padding calculations.
|
||||
// using the algorithmically generated width.
|
||||
rows.forEach(columns => {
|
||||
this.div(...columns.map((r, i) => {
|
||||
return {
|
||||
text: r.trim(),
|
||||
padding: this.measurePadding(r),
|
||||
width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
|
||||
};
|
||||
}));
|
||||
});
|
||||
return this.rows[this.rows.length - 1];
|
||||
}
|
||||
colFromString(text) {
|
||||
return {
|
||||
text,
|
||||
padding: this.measurePadding(text)
|
||||
};
|
||||
}
|
||||
measurePadding(str) {
|
||||
// measure padding without ansi escape codes
|
||||
const noAnsi = mixin.stripAnsi(str);
|
||||
return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length];
|
||||
}
|
||||
toString() {
|
||||
const lines = [];
|
||||
this.rows.forEach(row => {
|
||||
this.rowToString(row, lines);
|
||||
});
|
||||
// don't display any lines with the
|
||||
// hidden flag set.
|
||||
return lines
|
||||
.filter(line => !line.hidden)
|
||||
.map(line => line.text)
|
||||
.join('\n');
|
||||
}
|
||||
rowToString(row, lines) {
|
||||
this.rasterize(row).forEach((rrow, r) => {
|
||||
let str = '';
|
||||
rrow.forEach((col, c) => {
|
||||
const { width } = row[c]; // the width with padding.
|
||||
const wrapWidth = this.negatePadding(row[c]); // the width without padding.
|
||||
let ts = col; // temporary string used during alignment/padding.
|
||||
if (wrapWidth > mixin.stringWidth(col)) {
|
||||
ts += ' '.repeat(wrapWidth - mixin.stringWidth(col));
|
||||
}
|
||||
// align the string within its column.
|
||||
if (row[c].align && row[c].align !== 'left' && this.wrap) {
|
||||
const fn = align[row[c].align];
|
||||
ts = fn(ts, wrapWidth);
|
||||
if (mixin.stringWidth(ts) < wrapWidth) {
|
||||
ts += ' '.repeat((width || 0) - mixin.stringWidth(ts) - 1);
|
||||
}
|
||||
}
|
||||
// apply border and padding to string.
|
||||
const padding = row[c].padding || [0, 0, 0, 0];
|
||||
if (padding[left]) {
|
||||
str += ' '.repeat(padding[left]);
|
||||
}
|
||||
str += addBorder(row[c], ts, '| ');
|
||||
str += ts;
|
||||
str += addBorder(row[c], ts, ' |');
|
||||
if (padding[right]) {
|
||||
str += ' '.repeat(padding[right]);
|
||||
}
|
||||
// if prior row is span, try to render the
|
||||
// current row on the prior line.
|
||||
if (r === 0 && lines.length > 0) {
|
||||
str = this.renderInline(str, lines[lines.length - 1]);
|
||||
}
|
||||
});
|
||||
// remove trailing whitespace.
|
||||
lines.push({
|
||||
text: str.replace(/ +$/, ''),
|
||||
span: row.span
|
||||
});
|
||||
});
|
||||
return lines;
|
||||
}
|
||||
// if the full 'source' can render in
|
||||
// the target line, do so.
|
||||
renderInline(source, previousLine) {
|
||||
const match = source.match(/^ */);
|
||||
const leadingWhitespace = match ? match[0].length : 0;
|
||||
const target = previousLine.text;
|
||||
const targetTextWidth = mixin.stringWidth(target.trimRight());
|
||||
if (!previousLine.span) {
|
||||
return source;
|
||||
}
|
||||
// if we're not applying wrapping logic,
|
||||
// just always append to the span.
|
||||
if (!this.wrap) {
|
||||
previousLine.hidden = true;
|
||||
return target + source;
|
||||
}
|
||||
if (leadingWhitespace < targetTextWidth) {
|
||||
return source;
|
||||
}
|
||||
previousLine.hidden = true;
|
||||
return target.trimRight() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimLeft();
|
||||
}
|
||||
rasterize(row) {
|
||||
const rrows = [];
|
||||
const widths = this.columnWidths(row);
|
||||
let wrapped;
|
||||
// word wrap all columns, and create
|
||||
// a data-structure that is easy to rasterize.
|
||||
row.forEach((col, c) => {
|
||||
// leave room for left and right padding.
|
||||
col.width = widths[c];
|
||||
if (this.wrap) {
|
||||
wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n');
|
||||
}
|
||||
else {
|
||||
wrapped = col.text.split('\n');
|
||||
}
|
||||
if (col.border) {
|
||||
wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.');
|
||||
wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'");
|
||||
}
|
||||
// add top and bottom padding.
|
||||
if (col.padding) {
|
||||
wrapped.unshift(...new Array(col.padding[top] || 0).fill(''));
|
||||
wrapped.push(...new Array(col.padding[bottom] || 0).fill(''));
|
||||
}
|
||||
wrapped.forEach((str, r) => {
|
||||
if (!rrows[r]) {
|
||||
rrows.push([]);
|
||||
}
|
||||
const rrow = rrows[r];
|
||||
for (let i = 0; i < c; i++) {
|
||||
if (rrow[i] === undefined) {
|
||||
rrow.push('');
|
||||
}
|
||||
}
|
||||
rrow.push(str);
|
||||
});
|
||||
});
|
||||
return rrows;
|
||||
}
|
||||
negatePadding(col) {
|
||||
let wrapWidth = col.width || 0;
|
||||
if (col.padding) {
|
||||
wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0);
|
||||
}
|
||||
if (col.border) {
|
||||
wrapWidth -= 4;
|
||||
}
|
||||
return wrapWidth;
|
||||
}
|
||||
columnWidths(row) {
|
||||
if (!this.wrap) {
|
||||
return row.map(col => {
|
||||
return col.width || mixin.stringWidth(col.text);
|
||||
});
|
||||
}
|
||||
let unset = row.length;
|
||||
let remainingWidth = this.width;
|
||||
// column widths can be set in config.
|
||||
const widths = row.map(col => {
|
||||
if (col.width) {
|
||||
unset--;
|
||||
remainingWidth -= col.width;
|
||||
return col.width;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
// any unset widths should be calculated.
|
||||
const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
|
||||
return widths.map((w, i) => {
|
||||
if (w === undefined) {
|
||||
return Math.max(unsetWidth, _minWidth(row[i]));
|
||||
}
|
||||
return w;
|
||||
});
|
||||
}
|
||||
}
|
||||
function addBorder(col, ts, style) {
|
||||
if (col.border) {
|
||||
if (/[.']-+[.']/.test(ts)) {
|
||||
return '';
|
||||
}
|
||||
if (ts.trim().length !== 0) {
|
||||
return style;
|
||||
}
|
||||
return ' ';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
// calculates the minimum width of
|
||||
// a column, based on padding preferences.
|
||||
function _minWidth(col) {
|
||||
const padding = col.padding || [];
|
||||
const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0);
|
||||
if (col.border) {
|
||||
return minWidth + 4;
|
||||
}
|
||||
return minWidth;
|
||||
}
|
||||
function getWindowWidth() {
|
||||
/* c8 ignore next 5: depends on terminal */
|
||||
if (typeof process === 'object' && process.stdout && process.stdout.columns) {
|
||||
return process.stdout.columns;
|
||||
}
|
||||
return 80;
|
||||
}
|
||||
function alignRight(str, width) {
|
||||
str = str.trim();
|
||||
const strWidth = mixin.stringWidth(str);
|
||||
if (strWidth < width) {
|
||||
return ' '.repeat(width - strWidth) + str;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
function alignCenter(str, width) {
|
||||
str = str.trim();
|
||||
const strWidth = mixin.stringWidth(str);
|
||||
/* c8 ignore next 3 */
|
||||
if (strWidth >= width) {
|
||||
return str;
|
||||
}
|
||||
return ' '.repeat((width - strWidth) >> 1) + str;
|
||||
}
|
||||
let mixin;
|
||||
export function cliui(opts, _mixin) {
|
||||
mixin = _mixin;
|
||||
return new UI({
|
||||
width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(),
|
||||
wrap: opts === null || opts === void 0 ? void 0 : opts.wrap
|
||||
});
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"fileNames":["../node_modules/typescript/lib/lib.es5.d.ts","../node_modules/typescript/lib/lib.es2015.d.ts","../node_modules/typescript/lib/lib.es2016.d.ts","../node_modules/typescript/lib/lib.es2017.d.ts","../node_modules/typescript/lib/lib.es2018.d.ts","../node_modules/typescript/lib/lib.es2019.d.ts","../node_modules/typescript/lib/lib.es2020.d.ts","../node_modules/typescript/lib/lib.es2021.d.ts","../node_modules/typescript/lib/lib.es2022.d.ts","../node_modules/typescript/lib/lib.es2023.d.ts","../node_modules/typescript/lib/lib.es2015.core.d.ts","../node_modules/typescript/lib/lib.es2015.collection.d.ts","../node_modules/typescript/lib/lib.es2015.generator.d.ts","../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../node_modules/typescript/lib/lib.es2015.promise.d.ts","../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../node_modules/typescript/lib/lib.es2016.intl.d.ts","../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../node_modules/typescript/lib/lib.es2017.date.d.ts","../node_modules/typescript/lib/lib.es2017.object.d.ts","../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../node_modules/typescript/lib/lib.es2017.string.d.ts","../node_modules/typescript/lib/lib.es2017.intl.d.ts","../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../node_modules/typescript/lib/lib.es2018.intl.d.ts","../node_modules/typescript/lib/lib.es2018.promise.d.ts","../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../node_modules/typescript/lib/lib.es2019.array.d.ts","../node_modules/typescript/lib/lib.es2019.object.d.ts","../node_modules/typescript/lib/lib.es2019.string.d.ts","../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../node_modules/typescript/lib/lib.es2019.intl.d.ts","../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../node_modules/typescript/lib/lib.es2020.date.d.ts","../node_modules/typescript/lib/lib.es2020.promise.d.ts","../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../node_modules/typescript/lib/lib.es2020.string.d.ts","../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../node_modules/typescript/lib/lib.es2020.intl.d.ts","../node_modules/typescript/lib/lib.es2020.number.d.ts","../node_modules/typescript/lib/lib.es2021.promise.d.ts","../node_modules/typescript/lib/lib.es2021.string.d.ts","../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../node_modules/typescript/lib/lib.es2021.intl.d.ts","../node_modules/typescript/lib/lib.es2022.array.d.ts","../node_modules/typescript/lib/lib.es2022.error.d.ts","../node_modules/typescript/lib/lib.es2022.intl.d.ts","../node_modules/typescript/lib/lib.es2022.object.d.ts","../node_modules/typescript/lib/lib.es2022.string.d.ts","../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../node_modules/typescript/lib/lib.es2023.array.d.ts","../node_modules/typescript/lib/lib.es2023.collection.d.ts","../node_modules/typescript/lib/lib.es2023.intl.d.ts","../node_modules/typescript/lib/lib.decorators.d.ts","../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../lib/index.ts","../node_modules/@types/istanbul-lib-coverage/index.d.ts","../node_modules/@types/json-schema/index.d.ts","../node_modules/@types/json5/index.d.ts","../node_modules/@types/minimist/index.d.ts","../node_modules/@types/node/compatibility/disposable.d.ts","../node_modules/@types/node/compatibility/indexable.d.ts","../node_modules/@types/node/compatibility/iterators.d.ts","../node_modules/@types/node/compatibility/index.d.ts","../node_modules/@types/node/globals.typedarray.d.ts","../node_modules/@types/node/buffer.buffer.d.ts","../node_modules/undici-types/header.d.ts","../node_modules/undici-types/readable.d.ts","../node_modules/undici-types/file.d.ts","../node_modules/undici-types/fetch.d.ts","../node_modules/undici-types/formdata.d.ts","../node_modules/undici-types/connector.d.ts","../node_modules/undici-types/client.d.ts","../node_modules/undici-types/errors.d.ts","../node_modules/undici-types/dispatcher.d.ts","../node_modules/undici-types/global-dispatcher.d.ts","../node_modules/undici-types/global-origin.d.ts","../node_modules/undici-types/pool-stats.d.ts","../node_modules/undici-types/pool.d.ts","../node_modules/undici-types/handlers.d.ts","../node_modules/undici-types/balanced-pool.d.ts","../node_modules/undici-types/agent.d.ts","../node_modules/undici-types/mock-interceptor.d.ts","../node_modules/undici-types/mock-agent.d.ts","../node_modules/undici-types/mock-client.d.ts","../node_modules/undici-types/mock-pool.d.ts","../node_modules/undici-types/mock-errors.d.ts","../node_modules/undici-types/proxy-agent.d.ts","../node_modules/undici-Line truncated
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Bootstrap cliui with CommonJS dependencies:
|
||||
import { cliui } from './build/lib/index.js'
|
||||
import stringWidth from 'string-width'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import wrapAnsi from 'wrap-ansi'
|
||||
|
||||
export default function ui (opts) {
|
||||
return cliui(opts, {
|
||||
stringWidth,
|
||||
stripAnsi,
|
||||
wrap: wrapAnsi
|
||||
})
|
||||
}
|
||||
|
||||
export {ui as 'module.exports'};
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "cliui",
|
||||
"version": "9.0.1",
|
||||
"description": "easily create complex multi-column command-line-interfaces",
|
||||
"main": "build/index.mjs",
|
||||
"exports": {
|
||||
".": "./index.mjs"
|
||||
},
|
||||
"type": "module",
|
||||
"module": "./index.mjs",
|
||||
"scripts": {
|
||||
"check": "standardx '**/*.ts' && standardx '**/*.js'",
|
||||
"fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js'",
|
||||
"pretest": "rimraf build && tsc -p tsconfig.test.json",
|
||||
"test": "c8 mocha ./test/*.mjs",
|
||||
"postest": "check",
|
||||
"coverage": "c8 report --check-coverage",
|
||||
"precompile": "rimraf build",
|
||||
"compile": "tsc",
|
||||
"prepare": "npm run compile"
|
||||
},
|
||||
"repository": "yargs/cliui",
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"**/example/**"
|
||||
],
|
||||
"globals": [
|
||||
"it"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"cli",
|
||||
"command-line",
|
||||
"layout",
|
||||
"design",
|
||||
"console",
|
||||
"wrap",
|
||||
"table"
|
||||
],
|
||||
"author": "Ben Coe <ben@npmjs.com>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"c8": "^10.1.3",
|
||||
"chai": "^5.2.0",
|
||||
"chalk": "^5.4.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"eslint": "^7.6.0",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"gts": "^6.0.2",
|
||||
"mocha": "^11.1.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"standardx": "^7.0.0",
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"files": [
|
||||
"build",
|
||||
"index.mjs",
|
||||
"!*.d.ts"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Kimmo Brunfeldt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
# concurrently
|
||||
|
||||
[](https://github.com/open-cli-tools/concurrently/releases)
|
||||
[](https://github.com/open-cli-tools/concurrently/blob/main/LICENSE)
|
||||
[](https://www.npmjs.com/package/concurrently)
|
||||
[](https://github.com/open-cli-tools/concurrently/actions/workflows/ci.yml)
|
||||
[](https://coveralls.io/github/open-cli-tools/concurrently?branch=main)
|
||||
|
||||
Run multiple commands concurrently.
|
||||
Like `npm run watch-js & npm run watch-less` but better.
|
||||
|
||||

|
||||
|
||||
**Table of Contents**
|
||||
|
||||
- [concurrently](#concurrently)
|
||||
- [Why](#why)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [API](#api)
|
||||
- [`concurrently(commands[, options])`](#concurrentlycommands-options)
|
||||
- [`Command`](#command)
|
||||
- [`CloseEvent`](#closeevent)
|
||||
- [FAQ](#faq)
|
||||
|
||||
## Why
|
||||
|
||||
I like [task automation with npm](https://web.archive.org/web/20220531064025/https://github.com/substack/blog/blob/master/npm_run.markdown)
|
||||
but the usual way to run multiple commands concurrently is
|
||||
`npm run watch-js & npm run watch-css`. That's fine but it's hard to keep
|
||||
on track of different outputs. Also if one process fails, others still keep running
|
||||
and you won't even notice the difference.
|
||||
|
||||
Another option would be to just run all commands in separate terminals. I got
|
||||
tired of opening terminals and made **concurrently**.
|
||||
|
||||
**Features:**
|
||||
|
||||
- Cross platform (including Windows)
|
||||
- Output is easy to follow with prefixes
|
||||
- With `--kill-others` switch, all commands are killed if one dies
|
||||
|
||||
## Installation
|
||||
|
||||
**concurrently** can be installed in the global scope (if you'd like to have it available and use it on the whole system) or locally for a specific package (for example if you'd like to use it in the `scripts` section of your package):
|
||||
|
||||
| | npm | Yarn | pnpm | Bun |
|
||||
| ----------- | ----------------------- | ------------------------------ | -------------------------- | ------------------------- |
|
||||
| **Global** | `npm i -g concurrently` | `yarn global add concurrently` | `pnpm add -g concurrently` | `bun add -g concurrently` |
|
||||
| **Local**\* | `npm i -D concurrently` | `yarn add -D concurrently` | `pnpm add -D concurrently` | `bun add -d concurrently` |
|
||||
|
||||
<sub>\* It's recommended to add **concurrently** to `devDependencies` as it's usually used for developing purposes. Please adjust the command if this doesn't apply in your case.</sub>
|
||||
|
||||
## Usage
|
||||
|
||||
> **Note**
|
||||
> The `concurrently` command is also available under the shorthand alias `conc`.
|
||||
|
||||
The tool is written in Node.js, but you can use it to run **any** commands.
|
||||
|
||||
Remember to surround separate commands with quotes:
|
||||
|
||||
```bash
|
||||
concurrently 'command1 arg' 'command2 arg'
|
||||
```
|
||||
|
||||
Otherwise **concurrently** would try to run 4 separate commands:
|
||||
`command1`, `arg`, `command2`, `arg`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Windows only supports double quotes:
|
||||
>
|
||||
> ```bash
|
||||
> concurrently "command1 arg" "command2 arg"
|
||||
> ```
|
||||
>
|
||||
> Remember to escape the double quotes in your package.json when using Windows:
|
||||
>
|
||||
> ```json
|
||||
> "start": "concurrently \"command1 arg\" \"command2 arg\""
|
||||
> ```
|
||||
|
||||
You can always check concurrently's flag list by running `concurrently --help`.
|
||||
For the version, run `concurrently --version`.
|
||||
|
||||
Check out documentation and other usage examples in the [`docs` directory](./docs/README.md).
|
||||
|
||||
## API
|
||||
|
||||
**concurrently** can be used programmatically by using the API documented below:
|
||||
|
||||
### `concurrently(commands[, options])`
|
||||
|
||||
- `commands`: an array of either strings (containing the commands to run) or objects
|
||||
with the shape `{ command, name, prefixColor, env, cwd, ipc }`.
|
||||
|
||||
- `options` (optional): an object containing any of the below:
|
||||
- `cwd`: the working directory to be used by all commands. Can be overridden per command.
|
||||
Default: `process.cwd()`.
|
||||
- `shell`: shell executable used to run command strings. When unset, uses `npm_config_script_shell` if present (for example when run via `npm run`), otherwise `cmd.exe` on Windows or `/bin/sh` elsewhere. See [shell resolution](./docs/shell-resolution.md).
|
||||
- `defaultInputTarget`: the default input target when reading from `inputStream`.
|
||||
Default: `0`.
|
||||
- `handleInput`: when `true`, reads input from `process.stdin`.
|
||||
- `inputStream`: a [`Readable` stream](https://nodejs.org/docs/latest/api/stream.html#readable-streams)
|
||||
to read the input from. Should only be used in the rare instance you would like to stream anything other than `process.stdin`. Overrides `handleInput`.
|
||||
- `pauseInputStreamOnFinish`: by default, pauses the input stream (`process.stdin` when `handleInput` is enabled, or `inputStream` if provided) when all of the processes have finished. If you need to read from the input stream after `concurrently` has finished, set this to `false`. ([#252](https://github.com/kimmobrunfeldt/concurrently/issues/252)).
|
||||
- `killOthersOn`: once the first command exits with one of these statuses, kill other commands.
|
||||
Can be an array containing the strings `success` (status code zero) and/or `failure` (non-zero exit status).
|
||||
- `maxProcesses`: how many processes should run at once.
|
||||
- `outputStream`: a [`Writable` stream](https://nodejs.org/docs/latest/api/stream.html#writable-streams)
|
||||
to write logs to. Default: `process.stdout`.
|
||||
- `prefix`: the prefix type to use when logging processes output.
|
||||
Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`).
|
||||
Default: the name of the process, or its index if no name is set.
|
||||
Templates can wrap any portion of the prefix with `{color}` and `{/color}` to restrict coloring to that region (eg `[{color}{name}{/color}]` colors only the name, leaving the brackets uncolored). If either marker is omitted the missing side is implicit, so a template with no markers is colored in full.
|
||||
- `prefixColors`: a list of colors or a string as supported by [Chalk](https://www.npmjs.com/package/chalk) and additional style `auto` for an automatically picked color.
|
||||
Supports all Chalk color functions: `#RRGGBB`, `bg#RRGGBB`, `hex()`, `bgHex()`, `rgb()`, `bgRgb()`, `ansi256()`, `bgAnsi256()`.
|
||||
Functions and modifiers can be chained (e.g., `rgb(255,136,0).bold`, `black.bgHex(#00FF00).dim`).
|
||||
If concurrently would run more commands than there are colors, the last color is repeated, unless if the last color value is `auto` which means following colors are automatically picked to vary.
|
||||
Prefix colors specified per-command take precedence over this list.
|
||||
- `prefixLength`: how many characters to show when prefixing with `command`. Default: `10`
|
||||
- `raw`: whether raw mode should be used, meaning strictly process output will
|
||||
be logged, without any prefixes, coloring or extra stuff. Can be overridden per command.
|
||||
- `successCondition`: the condition to consider the run was successful.
|
||||
If `first`, only the first process to exit will make up the success of the run; if `last`, the last process that exits will determine whether the run succeeds.
|
||||
Anything else means all processes should exit successfully.
|
||||
- `restartTries`: how many attempts to restart a process that dies will be made. Default: `0`.
|
||||
- `restartDelay`: how many milliseconds to wait between process restarts. Default: `0`.
|
||||
- `timestampFormat`: a [Unicode format](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
|
||||
to use when prefixing with `time`. Default: `yyyy-MM-dd HH:mm:ss.SSS`
|
||||
- `additionalArguments`: list of additional arguments passed that will get replaced in each command. If not defined, no argument replacing will happen.
|
||||
|
||||
> **Returns:** an object in the shape `{ result, commands }`.
|
||||
>
|
||||
> - `result`: a `Promise` that resolves if the run was successful (according to `successCondition` option),
|
||||
> or rejects, containing an array of [`CloseEvent`](#CloseEvent), in the order that the commands terminated.
|
||||
> - `commands`: an array of all spawned [`Command`s](#Command).
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const concurrently = require('concurrently');
|
||||
const { result } = concurrently(
|
||||
[
|
||||
'npm:watch-*',
|
||||
{ command: 'nodemon', name: 'server' },
|
||||
{ command: 'deploy', name: 'deploy', env: { PUBLIC_KEY: '...' } },
|
||||
{
|
||||
command: 'watch',
|
||||
name: 'watch',
|
||||
cwd: path.resolve(__dirname, 'scripts/watchers'),
|
||||
},
|
||||
],
|
||||
{
|
||||
prefix: 'name',
|
||||
killOthersOn: ['failure', 'success'],
|
||||
restartTries: 3,
|
||||
cwd: path.resolve(__dirname, 'scripts'),
|
||||
},
|
||||
);
|
||||
result.then(success, failure);
|
||||
```
|
||||
|
||||
### `Command`
|
||||
|
||||
An object that contains all information about a spawned command, and ways to interact with it.<br>
|
||||
It has the following properties:
|
||||
|
||||
- `index`: the index of the command among all commands spawned.
|
||||
- `command`: the command line of the command.
|
||||
- `name`: the name of the command; defaults to an empty string.
|
||||
- `cwd`: the current working directory of the command.
|
||||
- `env`: an object with all the environment variables that the command will be spawned with.
|
||||
- `killed`: whether the command has been killed.
|
||||
- `state`: the command's state. Can be one of
|
||||
- `stopped`: if the command was never started
|
||||
- `started`: if the command is currently running
|
||||
- `errored`: if the command failed spawning
|
||||
- `exited`: if the command is not running anymore, e.g. it received a close event
|
||||
- `pid`: the command's process ID.
|
||||
- `stdin`: a Writable stream to the command's `stdin`.
|
||||
- `stdout`: an RxJS observable to the command's `stdout`.
|
||||
- `stderr`: an RxJS observable to the command's `stderr`.
|
||||
- `error`: an RxJS observable to the command's error events (e.g. when it fails to spawn).
|
||||
- `timer`: an RxJS observable to the command's timing events (e.g. starting, stopping).
|
||||
- `stateChange`: an RxJS observable for changes to the command's `state` property.
|
||||
- `messages`: an object with the following properties:
|
||||
- `incoming`: an RxJS observable for the IPC messages received from the underlying process.
|
||||
- `outgoing`: an RxJS observable for the IPC messages sent to the underlying process.
|
||||
|
||||
Both observables emit [`MessageEvent`](#messageevent)s.<br>
|
||||
Note that if the command wasn't spawned with IPC support, these won't emit any values.
|
||||
|
||||
- `close`: an RxJS observable to the command's close events.
|
||||
See [`CloseEvent`](#CloseEvent) for more information.
|
||||
- `start()`: starts the command and sets up all of the above streams
|
||||
- `send(message[, handle, options])`: sends a message to the underlying process via IPC channels,
|
||||
returning a promise that resolves once the message has been sent.
|
||||
See [Node.js docs](https://nodejs.org/docs/latest/api/child_process.html#subprocesssendmessage-sendhandle-options-callback).
|
||||
- `kill([signal])`: kills the command, optionally specifying a signal (e.g. `SIGTERM`, `SIGKILL`, etc).
|
||||
|
||||
### `MessageEvent`
|
||||
|
||||
An object that represents a message that was received from/sent to the underlying command process.<br>
|
||||
It has the following properties:
|
||||
|
||||
- `message`: the message itself.
|
||||
- `handle`: a [`net.Socket`](https://nodejs.org/docs/latest/api/net.html#class-netsocket),
|
||||
[`net.Server`](https://nodejs.org/docs/latest/api/net.html#class-netserver) or
|
||||
[`dgram.Socket`](https://nodejs.org/docs/latest/api/dgram.html#class-dgramsocket),
|
||||
if one was sent, or `undefined`.
|
||||
|
||||
### `CloseEvent`
|
||||
|
||||
An object with information about a command's closing event.<br>
|
||||
It contains the following properties:
|
||||
|
||||
- `command`: a stripped down version of [`Command`](#command), including only `name`, `command`, `env` and `cwd` properties.
|
||||
- `index`: the index of the command among all commands spawned.
|
||||
- `killed`: whether the command exited because it was killed.
|
||||
- `exitCode`: the exit code of the command's process, or the signal which it was killed with.
|
||||
- `timings`: an object in the shape `{ startDate, endDate, durationSeconds }`.
|
||||
|
||||
## FAQ
|
||||
|
||||
- Process exited with code `null`?
|
||||
|
||||
From [Node child_process documentation](https://nodejs.org/docs/latest/api/child_process.html#event-exit), `exit` event:
|
||||
|
||||
> This event is emitted after the child process ends. If the process
|
||||
> terminated normally, code is the final exit code of the process,
|
||||
> otherwise null. If the process terminated due to receipt of a signal,
|
||||
> signal is the string name of the signal, otherwise null.
|
||||
|
||||
So `null` means the process didn't terminate normally. This will make **concurrently**
|
||||
to return non-zero exit code too.
|
||||
|
||||
- Does this work with the npm-replacements [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), or [Bun](https://bun.sh/)?
|
||||
|
||||
Yes! In all examples above, you may replace "`npm`" with "`yarn`", "`pnpm`", or "`bun`".
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { spawnSync as baseSpawnSync } from 'node:child_process';
|
||||
export type SpawnSync = typeof baseSpawnSync;
|
||||
/**
|
||||
* On Windows, concurrently pipes child-process output through a freshly-allocated console whose
|
||||
* codepage may not be UTF-8, garbling non-ASCII output from commands that rely on it (see
|
||||
* open-cli-tools/concurrently#302). This sets the console's codepage to UTF-8 once, which spawned
|
||||
* children then inherit, and returns a function that restores the original codepage.
|
||||
*
|
||||
* No-ops (and never throws) if not on Windows, if the codepage is already UTF-8, or if reading/setting
|
||||
* it fails for any reason -- this is a best-effort convenience, never something that should block
|
||||
* concurrently from running.
|
||||
*/
|
||||
export declare function ensureUtf8Codepage(spawnSync?: SpawnSync, process?: Pick<NodeJS.Process, 'platform'>): () => void;
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { spawnSync as baseSpawnSync } from 'node:child_process';
|
||||
import nodeProcess from 'node:process';
|
||||
const UTF8_CODEPAGE = 65001;
|
||||
/**
|
||||
* On Windows, concurrently pipes child-process output through a freshly-allocated console whose
|
||||
* codepage may not be UTF-8, garbling non-ASCII output from commands that rely on it (see
|
||||
* open-cli-tools/concurrently#302). This sets the console's codepage to UTF-8 once, which spawned
|
||||
* children then inherit, and returns a function that restores the original codepage.
|
||||
*
|
||||
* No-ops (and never throws) if not on Windows, if the codepage is already UTF-8, or if reading/setting
|
||||
* it fails for any reason -- this is a best-effort convenience, never something that should block
|
||||
* concurrently from running.
|
||||
*/
|
||||
export function ensureUtf8Codepage(spawnSync = baseSpawnSync, process = nodeProcess) {
|
||||
const noop = () => { };
|
||||
if (process.platform !== 'win32') {
|
||||
return noop;
|
||||
}
|
||||
try {
|
||||
const original = readCodepage(spawnSync);
|
||||
if (original == null || original === UTF8_CODEPAGE) {
|
||||
return noop;
|
||||
}
|
||||
if (!setCodepage(spawnSync, UTF8_CODEPAGE)) {
|
||||
return noop;
|
||||
}
|
||||
let restored = false;
|
||||
return () => {
|
||||
if (restored) {
|
||||
return;
|
||||
}
|
||||
restored = true;
|
||||
try {
|
||||
setCodepage(spawnSync, original);
|
||||
}
|
||||
catch {
|
||||
// Best-effort restore; nothing more we can do.
|
||||
}
|
||||
};
|
||||
}
|
||||
catch {
|
||||
return noop;
|
||||
}
|
||||
}
|
||||
function readCodepage(spawnSync) {
|
||||
const result = spawnSync('cmd.exe', ['/s', '/c', 'chcp'], { encoding: 'utf8' });
|
||||
if (result.error || result.status !== 0 || !result.stdout) {
|
||||
return null;
|
||||
}
|
||||
// Locale-dependent text (e.g. "Active code page: 65001." or "Aktive Codepage: 65001."),
|
||||
// so match on the number rather than the surrounding words.
|
||||
const match = /(\d+)/.exec(result.stdout);
|
||||
return match ? Number(match[1]) : null;
|
||||
}
|
||||
function setCodepage(spawnSync, codepage) {
|
||||
// `chcp` changes the codepage of the console this process is attached to regardless of where its
|
||||
// own stdio points, so output can be safely discarded here rather than inherited -- otherwise its
|
||||
// confirmation text (e.g. "Active code page: 65001.") would leak into concurrently's own output.
|
||||
const result = spawnSync('cmd.exe', ['/s', '/c', `chcp ${codepage}`], { stdio: 'ignore' });
|
||||
return !result.error && result.status === 0;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env node
|
||||
import process from 'node:process';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import * as defaults from '../lib/defaults.js';
|
||||
import { concurrently } from '../lib/index.js';
|
||||
import { castArray, splitOutsideParens } from '../lib/utils.js';
|
||||
import { ensureUtf8Codepage } from './console-codepage.js';
|
||||
import { normalizeCliCommand } from './normalize-cli-command.js';
|
||||
import { readPackageJson } from './read-package-json.js';
|
||||
const version = String(readPackageJson().version);
|
||||
const epilogue = `For documentation and more examples, visit:\nhttps://github.com/open-cli-tools/concurrently/tree/v${version}/docs`;
|
||||
// Clean-up arguments (yargs expects only the arguments after the program name)
|
||||
const program = yargs(hideBin(process.argv))
|
||||
.parserConfiguration({
|
||||
// Avoids options that can be specified multiple times from requiring a `--` to pass commands
|
||||
'greedy-arrays': false,
|
||||
// Makes sure that --passthrough-arguments works correctly
|
||||
'populate--': true,
|
||||
})
|
||||
.usage('$0 [options] <command ...>')
|
||||
.help('h')
|
||||
.alias('h', 'help')
|
||||
.version(version)
|
||||
.alias('version', 'v')
|
||||
.alias('version', 'V')
|
||||
// TODO: Add some tests for this.
|
||||
.env('CONCURRENTLY')
|
||||
.options({
|
||||
// General
|
||||
'max-processes': {
|
||||
alias: 'm',
|
||||
describe: 'How many processes should run at once.\n' +
|
||||
'New processes only spawn after all restart tries of a process.\n' +
|
||||
'Exact number or a percent of CPUs available (for example "50%")',
|
||||
type: 'string',
|
||||
},
|
||||
names: {
|
||||
alias: 'n',
|
||||
describe: 'List of custom names to be used in prefix template.\n' +
|
||||
'Example names: "main,browser,server"',
|
||||
type: 'string',
|
||||
},
|
||||
success: {
|
||||
alias: 's',
|
||||
describe: 'Which command(s) must exit with code 0 in order for concurrently exit with ' +
|
||||
'code 0 too. Options are:\n' +
|
||||
'- "first" for the first command to exit;\n' +
|
||||
'- "last" for the last command to exit;\n' +
|
||||
'- "all" for all commands;\n' +
|
||||
// Note: not a typo. Multiple commands can have the same name.
|
||||
'- "command-{name}"/"command-{index}" for the commands with that name or index;\n' +
|
||||
'- "!command-{name}"/"!command-{index}" for all commands but the ones with that ' +
|
||||
'name or index.\n',
|
||||
default: defaults.success,
|
||||
},
|
||||
raw: {
|
||||
alias: 'r',
|
||||
describe: 'Output only raw output of processes, disables prettifying ' +
|
||||
'and concurrently coloring.',
|
||||
type: 'boolean',
|
||||
},
|
||||
// This one is provided for free. Chalk reads this itself and removes colors.
|
||||
// https://www.npmjs.com/package/chalk#supportscolor
|
||||
'no-color': {
|
||||
describe: 'Disables colors from logging',
|
||||
type: 'boolean',
|
||||
},
|
||||
hide: {
|
||||
describe: 'Comma-separated list of processes to hide the output.\n' +
|
||||
'The processes can be identified by their name or index.',
|
||||
default: defaults.hide,
|
||||
type: 'string',
|
||||
},
|
||||
group: {
|
||||
alias: 'g',
|
||||
describe: 'Order the output as if the commands were run sequentially.',
|
||||
type: 'boolean',
|
||||
},
|
||||
timings: {
|
||||
describe: 'Show timing information for all processes.',
|
||||
type: 'boolean',
|
||||
default: defaults.timings,
|
||||
},
|
||||
shell: {
|
||||
describe: 'Shell to run commands with. Defaults to cmd.exe on Windows and /bin/sh elsewhere.',
|
||||
type: 'string',
|
||||
},
|
||||
'passthrough-arguments': {
|
||||
alias: 'P',
|
||||
describe: 'Passthrough additional arguments to commands (accessible via placeholders) ' +
|
||||
'instead of treating them as commands.',
|
||||
type: 'boolean',
|
||||
default: defaults.passthroughArguments,
|
||||
},
|
||||
teardown: {
|
||||
describe: 'Clean up command(s) to execute before exiting concurrently. Might be specified multiple times.\n' +
|
||||
"These aren't prefixed and they don't affect concurrently's exit code.",
|
||||
type: 'string',
|
||||
array: true,
|
||||
},
|
||||
// Kill others
|
||||
'kill-others': {
|
||||
alias: 'k',
|
||||
describe: 'Kill other processes once the first exits.',
|
||||
type: 'boolean',
|
||||
},
|
||||
'kill-others-on-fail': {
|
||||
describe: 'Kill other processes if one exits with non zero status code.',
|
||||
type: 'boolean',
|
||||
},
|
||||
'kill-signal': {
|
||||
alias: 'ks',
|
||||
describe: 'Signal to send to other processes if one exits or dies. (SIGTERM/SIGKILL, defaults to SIGTERM)',
|
||||
type: 'string',
|
||||
default: defaults.killSignal,
|
||||
},
|
||||
'kill-timeout': {
|
||||
describe: 'How many milliseconds to wait before forcing process terminating.',
|
||||
type: 'number',
|
||||
},
|
||||
// Prefix
|
||||
prefix: {
|
||||
alias: 'p',
|
||||
describe: 'Prefix used in logging for each process.\n' +
|
||||
'Possible values: index, pid, time, command, name, none, or a template. ' +
|
||||
'Example template: "{time}-{pid}"',
|
||||
defaultDescription: 'index or name (when --names is set)',
|
||||
type: 'string',
|
||||
},
|
||||
'prefix-colors': {
|
||||
alias: 'c',
|
||||
describe: 'Comma-separated list of Chalk colors to use on prefixes. ' +
|
||||
'If there are more commands than colors, the last color will be repeated.\n' +
|
||||
'- Available modifiers: reset, bold, dim, italic, underline, inverse, hidden, strikethrough\n' +
|
||||
'- Available colors: black, red, green, yellow, blue, magenta, cyan, white, gray, \n' +
|
||||
'any hex values for colors (e.g. #23de43) or auto for an automatically picked color\n' +
|
||||
'- Available background colors: bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite\n' +
|
||||
'See https://www.npmjs.com/package/chalk for more information.',
|
||||
default: defaults.prefixColors,
|
||||
type: 'string',
|
||||
},
|
||||
'prefix-length': {
|
||||
alias: 'l',
|
||||
describe: 'Limit how many characters of the command is displayed in prefix. ' +
|
||||
'The option can be used to shorten the prefix when it is set to "command"',
|
||||
default: defaults.prefixLength,
|
||||
type: 'number',
|
||||
},
|
||||
'pad-prefix': {
|
||||
describe: 'Pads short prefixes with spaces so that the length of all prefixes match',
|
||||
type: 'boolean',
|
||||
},
|
||||
'timestamp-format': {
|
||||
alias: 't',
|
||||
describe: 'Specify the timestamp in Unicode format:\n' +
|
||||
'https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table',
|
||||
default: defaults.timestampFormat,
|
||||
type: 'string',
|
||||
},
|
||||
// Restarting
|
||||
'restart-tries': {
|
||||
describe: 'How many times a process that died should restart.\n' +
|
||||
'Negative numbers will make the process restart forever.',
|
||||
default: defaults.restartTries,
|
||||
type: 'number',
|
||||
},
|
||||
'restart-after': {
|
||||
describe: 'Delay before restarting the process, in milliseconds, or "exponential".',
|
||||
default: defaults.restartDelay,
|
||||
type: 'string',
|
||||
},
|
||||
// Input
|
||||
'handle-input': {
|
||||
alias: 'i',
|
||||
describe: 'Whether input should be forwarded to the child processes. ' +
|
||||
'See examples for more information.',
|
||||
type: 'boolean',
|
||||
},
|
||||
'default-input-target': {
|
||||
default: defaults.defaultInputTarget,
|
||||
describe: 'Identifier for child process to which input on stdin ' +
|
||||
'should be sent if not specified at start of input.\n' +
|
||||
'Can be either the index or the name of the process.',
|
||||
},
|
||||
})
|
||||
.group(['m', 'n', 's', 'r', 'no-color', 'hide', 'g', 'timings', 'shell', 'P', 'teardown'], 'General')
|
||||
.group(['p', 'c', 'l', 't', 'pad-prefix'], 'Prefix styling')
|
||||
.group(['i', 'default-input-target'], 'Input handling')
|
||||
.group(['k', 'kill-others-on-fail', 'kill-signal', 'kill-timeout'], 'Killing other processes')
|
||||
.group(['restart-tries', 'restart-after'], 'Restarting')
|
||||
.epilogue(epilogue);
|
||||
const args = program.parseSync();
|
||||
const names = (args.names || '').split(',');
|
||||
const additionalArguments = castArray(args['--'] ?? []).map(String);
|
||||
const commands = args.passthroughArguments ? args._ : args._.concat(additionalArguments);
|
||||
if (!commands.length) {
|
||||
program.showHelp();
|
||||
process.exit();
|
||||
}
|
||||
// Piped output on Windows uses the console's active codepage, which may not be UTF-8 -- see
|
||||
// open-cli-tools/concurrently#302. Raw mode inherits the real console directly instead of piping
|
||||
// through concurrently, so it isn't affected and doesn't need this.
|
||||
const restoreCodepage = args.raw ? () => { } : ensureUtf8Codepage();
|
||||
const exitProcess = (code) => {
|
||||
restoreCodepage();
|
||||
process.exit(code);
|
||||
};
|
||||
try {
|
||||
concurrently(commands.map((command, index) => ({
|
||||
command: normalizeCliCommand(String(command)),
|
||||
name: names[index],
|
||||
})), {
|
||||
handleInput: args.handleInput,
|
||||
defaultInputTarget: args.defaultInputTarget,
|
||||
killOthersOn: args.killOthers
|
||||
? ['success', 'failure']
|
||||
: args.killOthersOnFail
|
||||
? ['failure']
|
||||
: [],
|
||||
killSignal: args.killSignal,
|
||||
killTimeout: args.killTimeout,
|
||||
maxProcesses: args.maxProcesses,
|
||||
raw: args.raw,
|
||||
hide: args.hide.split(','),
|
||||
group: args.group,
|
||||
prefix: args.prefix,
|
||||
prefixColors: splitOutsideParens(args.prefixColors, ','),
|
||||
prefixLength: args.prefixLength,
|
||||
padPrefix: args.padPrefix,
|
||||
restartDelay: args.restartAfter === 'exponential' ? 'exponential' : Number(args.restartAfter),
|
||||
restartTries: args.restartTries,
|
||||
successCondition: args.success,
|
||||
timestampFormat: args.timestampFormat,
|
||||
timings: args.timings,
|
||||
shell: args.shell,
|
||||
teardown: args.teardown,
|
||||
additionalArguments: args.passthroughArguments ? additionalArguments : undefined,
|
||||
}).result.then(() => exitProcess(0), () => exitProcess(1));
|
||||
}
|
||||
catch (error) {
|
||||
restoreCodepage();
|
||||
throw error;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function normalizeCliCommand(command: string): string;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
export function normalizeCliCommand(command) {
|
||||
if (command.length < 2) {
|
||||
return command;
|
||||
}
|
||||
const quote = command[0];
|
||||
const last = command.at(-1);
|
||||
if ((quote !== '"' && quote !== "'") || last !== quote) {
|
||||
return command;
|
||||
}
|
||||
const inner = command.slice(1, -1);
|
||||
if (inner.includes(quote)) {
|
||||
return command;
|
||||
}
|
||||
return inner;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Read the package.json file of `concurrently`
|
||||
*/
|
||||
export declare function readPackageJson(): Record<string, unknown>;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
/**
|
||||
* Read the package.json file of `concurrently`
|
||||
*/
|
||||
export function readPackageJson() {
|
||||
let resolver;
|
||||
try {
|
||||
resolver = require.resolve;
|
||||
}
|
||||
catch {
|
||||
resolver = createRequire(import.meta.url).resolve;
|
||||
}
|
||||
const path = resolver('concurrently/package.json');
|
||||
const content = readFileSync(path, 'utf8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Asserts that some condition is true, and if not, prints a warning about it being deprecated.
|
||||
* The message is printed only once.
|
||||
*/
|
||||
export declare function assertDeprecated(check: boolean, name: string, message: string): void;
|
||||
/**
|
||||
* Asserts that some condition is true, and if not, prints a warning about the runtime not being well supported.
|
||||
* The message is printed only once.
|
||||
*/
|
||||
export declare function assertNotRuntime(check: boolean, name: string, message: string): void;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
const deprecations = new Set();
|
||||
/**
|
||||
* Asserts that some condition is true, and if not, prints a warning about it being deprecated.
|
||||
* The message is printed only once.
|
||||
*/
|
||||
export function assertDeprecated(check, name, message) {
|
||||
if (!check && !deprecations.has(name)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[concurrently] ${name} is deprecated. ${message}`);
|
||||
deprecations.add(name);
|
||||
}
|
||||
}
|
||||
const runtimes = new Set();
|
||||
/**
|
||||
* Asserts that some condition is true, and if not, prints a warning about the runtime not being well supported.
|
||||
* The message is printed only once.
|
||||
*/
|
||||
export function assertNotRuntime(check, name, message) {
|
||||
if (!check && !runtimes.has(name)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[concurrently] Running via ${name} is not well supported. ${message}`);
|
||||
runtimes.add(name);
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { CommandInfo } from '../command.js';
|
||||
import { CommandParser } from './command-parser.js';
|
||||
/**
|
||||
* Replace placeholders with additional arguments.
|
||||
*/
|
||||
export declare class ExpandArguments implements CommandParser {
|
||||
private readonly additionalArguments;
|
||||
constructor(additionalArguments: string[]);
|
||||
parse(commandInfo: CommandInfo): {
|
||||
command: string;
|
||||
name: string;
|
||||
env?: Record<string, unknown>;
|
||||
cwd?: string;
|
||||
prefixColor?: string;
|
||||
ipc?: number;
|
||||
raw?: boolean;
|
||||
};
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { quote } from 'shell-quote';
|
||||
/**
|
||||
* Replace placeholders with additional arguments.
|
||||
*/
|
||||
export class ExpandArguments {
|
||||
additionalArguments;
|
||||
constructor(additionalArguments) {
|
||||
this.additionalArguments = additionalArguments;
|
||||
}
|
||||
parse(commandInfo) {
|
||||
const command = commandInfo.command.replace(/\\?\{([@*]|[1-9]\d*)\}/g, (match, placeholderTarget) => {
|
||||
// Don't replace the placeholder if it is escaped by a backslash.
|
||||
if (match.startsWith('\\')) {
|
||||
return match.slice(1);
|
||||
}
|
||||
if (this.additionalArguments.length > 0) {
|
||||
// Replace numeric placeholder if value exists in additional arguments.
|
||||
if (+placeholderTarget <= this.additionalArguments.length) {
|
||||
return quote([this.additionalArguments[+placeholderTarget - 1]]);
|
||||
}
|
||||
// Replace all arguments placeholder.
|
||||
if (placeholderTarget === '@') {
|
||||
return quote(this.additionalArguments);
|
||||
}
|
||||
// Replace combined arguments placeholder.
|
||||
if (placeholderTarget === '*') {
|
||||
return quote([this.additionalArguments.join(' ')]);
|
||||
}
|
||||
}
|
||||
// Replace placeholder with empty string
|
||||
// if value doesn't exist in additional arguments.
|
||||
return '';
|
||||
});
|
||||
return { ...commandInfo, command };
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { CommandInfo } from '../command.js';
|
||||
import { CommandParser } from './command-parser.js';
|
||||
/**
|
||||
* Expands shortcuts according to the following table:
|
||||
*
|
||||
* | Syntax | Expands to |
|
||||
* | --------------- | --------------------- |
|
||||
* | `npm:<script>` | `npm run <script>` |
|
||||
* | `pnpm:<script>` | `pnpm run <script>` |
|
||||
* | `yarn:<script>` | `yarn run <script>` |
|
||||
* | `bun:<script>` | `bun run <script>` |
|
||||
* | `node:<script>` | `node --run <script>` |
|
||||
* | `deno:<script>` | `deno task <script>` |
|
||||
*/
|
||||
export declare class ExpandShortcut implements CommandParser {
|
||||
parse(commandInfo: CommandInfo): CommandInfo;
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Expands shortcuts according to the following table:
|
||||
*
|
||||
* | Syntax | Expands to |
|
||||
* | --------------- | --------------------- |
|
||||
* | `npm:<script>` | `npm run <script>` |
|
||||
* | `pnpm:<script>` | `pnpm run <script>` |
|
||||
* | `yarn:<script>` | `yarn run <script>` |
|
||||
* | `bun:<script>` | `bun run <script>` |
|
||||
* | `node:<script>` | `node --run <script>` |
|
||||
* | `deno:<script>` | `deno task <script>` |
|
||||
*/
|
||||
export class ExpandShortcut {
|
||||
parse(commandInfo) {
|
||||
const [, prefix, script, args] = /^(npm|yarn|pnpm|bun|node|deno):(\S+)(.*)/.exec(commandInfo.command) || [];
|
||||
if (!script) {
|
||||
return commandInfo;
|
||||
}
|
||||
let command;
|
||||
if (prefix === 'node') {
|
||||
command = 'node --run';
|
||||
}
|
||||
else if (prefix === 'deno') {
|
||||
command = 'deno task';
|
||||
}
|
||||
else {
|
||||
command = `${prefix} run`;
|
||||
}
|
||||
return {
|
||||
...commandInfo,
|
||||
name: commandInfo.name || script,
|
||||
command: `${command} ${script}${args}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { CommandInfo } from '../command.js';
|
||||
import { CommandParser } from './command-parser.js';
|
||||
/**
|
||||
* Finds wildcards in 'npm/yarn/pnpm/bun run', 'node --run' and 'deno task'
|
||||
* commands and replaces them with all matching scripts in the NodeJS and Deno
|
||||
* configuration files of the current directory.
|
||||
*/
|
||||
export declare class ExpandWildcard implements CommandParser {
|
||||
private readonly readDeno;
|
||||
private readonly readPackage;
|
||||
static readDeno(): any;
|
||||
static readPackage(): any;
|
||||
private packageScripts?;
|
||||
private denoTasks?;
|
||||
constructor(readDeno?: typeof ExpandWildcard.readDeno, readPackage?: typeof ExpandWildcard.readPackage);
|
||||
private relevantScripts;
|
||||
parse(commandInfo: CommandInfo): CommandInfo | CommandInfo[];
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import fs from 'node:fs';
|
||||
import JSONC from '../jsonc.js';
|
||||
import { escapeRegExp } from '../utils.js';
|
||||
// Matches a negative filter surrounded by '(!' and ')'.
|
||||
const OMISSION = /\(!([^)]+)\)/;
|
||||
/**
|
||||
* Finds wildcards in 'npm/yarn/pnpm/bun run', 'node --run' and 'deno task'
|
||||
* commands and replaces them with all matching scripts in the NodeJS and Deno
|
||||
* configuration files of the current directory.
|
||||
*/
|
||||
export class ExpandWildcard {
|
||||
readDeno;
|
||||
readPackage;
|
||||
static readDeno() {
|
||||
try {
|
||||
let json = '{}';
|
||||
if (fs.existsSync('deno.json')) {
|
||||
json = fs.readFileSync('deno.json', { encoding: 'utf-8' });
|
||||
}
|
||||
else if (fs.existsSync('deno.jsonc')) {
|
||||
json = fs.readFileSync('deno.jsonc', { encoding: 'utf-8' });
|
||||
}
|
||||
return JSONC.parse(json);
|
||||
}
|
||||
catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
static readPackage() {
|
||||
try {
|
||||
let json = '{}';
|
||||
if (fs.existsSync('package.json')) {
|
||||
json = fs.readFileSync('package.json', { encoding: 'utf-8' });
|
||||
}
|
||||
else if (fs.existsSync('package.json5')) {
|
||||
json = fs.readFileSync('package.json5', { encoding: 'utf-8' });
|
||||
}
|
||||
return JSONC.parse(json);
|
||||
}
|
||||
catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
packageScripts;
|
||||
denoTasks;
|
||||
constructor(readDeno = ExpandWildcard.readDeno, readPackage = ExpandWildcard.readPackage) {
|
||||
this.readDeno = readDeno;
|
||||
this.readPackage = readPackage;
|
||||
}
|
||||
relevantScripts(command) {
|
||||
if (!this.packageScripts) {
|
||||
this.packageScripts = Object.keys(this.readPackage().scripts || {});
|
||||
}
|
||||
if (command === 'deno task') {
|
||||
if (!this.denoTasks) {
|
||||
// If Deno tries to run a task that doesn't exist,
|
||||
// it can fall back to running a script with the same name.
|
||||
// Therefore, the actual list of tasks is the union of the tasks and scripts.
|
||||
this.denoTasks = [
|
||||
...Object.keys(this.readDeno().tasks || {}),
|
||||
...this.packageScripts,
|
||||
];
|
||||
}
|
||||
return this.denoTasks;
|
||||
}
|
||||
return this.packageScripts;
|
||||
}
|
||||
parse(commandInfo) {
|
||||
// We expect one of the following patterns:
|
||||
// - <npm|yarn|pnpm|bun> run <script> [args]
|
||||
// - node --run <script> [args]
|
||||
// - deno task <script> [args]
|
||||
const [, command, scriptGlob, args] = /((?:npm|yarn|pnpm|bun) run|node --run|deno task) (\S+)([^&]*)/.exec(commandInfo.command) || [];
|
||||
const wildcardPosition = (scriptGlob || '').indexOf('*');
|
||||
// If the regex didn't match an npm script, or it has no wildcard,
|
||||
// then we have nothing to do here
|
||||
if (wildcardPosition === -1) {
|
||||
return commandInfo;
|
||||
}
|
||||
const [, omission] = OMISSION.exec(scriptGlob) || [];
|
||||
const scriptGlobSansOmission = scriptGlob.replace(OMISSION, '');
|
||||
const preWildcard = escapeRegExp(scriptGlobSansOmission.slice(0, wildcardPosition));
|
||||
const postWildcard = escapeRegExp(scriptGlobSansOmission.slice(wildcardPosition + 1));
|
||||
const wildcardRegex = new RegExp(`^${preWildcard}(.*?)${postWildcard}$`);
|
||||
// If 'commandInfo.name' doesn't match 'scriptGlob', this means a custom name
|
||||
// has been specified and thus becomes the prefix (as described in the README).
|
||||
const prefix = commandInfo.name !== scriptGlob ? commandInfo.name : '';
|
||||
const commands = [];
|
||||
for (const script of this.relevantScripts(command)) {
|
||||
if (omission && new RegExp(omission).test(script)) {
|
||||
continue;
|
||||
}
|
||||
const result = wildcardRegex.exec(script);
|
||||
const match = result?.[1];
|
||||
if (match !== undefined) {
|
||||
commands.push({
|
||||
...commandInfo,
|
||||
command: `${command} ${script}${args}`,
|
||||
// Will use an empty command name if no prefix has been specified and
|
||||
// the wildcard match is empty, e.g. if `npm:watch-*` matches `npm run watch-`.
|
||||
name: prefix + match,
|
||||
});
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { ChildProcess as BaseChildProcess, MessageOptions, SendHandle, SpawnOptions } from 'node:child_process';
|
||||
import { EventEmitter, Writable } from 'node:stream';
|
||||
import Rx from 'rxjs';
|
||||
/**
|
||||
* Identifier for a command; if string, it's the command's name, if number, it's the index.
|
||||
*/
|
||||
export type CommandIdentifier = string | number;
|
||||
export interface CommandInfo {
|
||||
/**
|
||||
* Command's name.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Which command line the command has.
|
||||
*/
|
||||
command: string;
|
||||
/**
|
||||
* Which environment variables should the spawned process have.
|
||||
*/
|
||||
env?: Record<string, unknown>;
|
||||
/**
|
||||
* The current working directory of the process when spawned.
|
||||
*/
|
||||
cwd?: string;
|
||||
/**
|
||||
* Color to use on prefix of the command.
|
||||
*/
|
||||
prefixColor?: string;
|
||||
/**
|
||||
* Whether sending of messages to/from this command (also known as "inter-process communication")
|
||||
* should be enabled, and using which file descriptor number.
|
||||
*
|
||||
* If set, must be > 2.
|
||||
*/
|
||||
ipc?: number;
|
||||
/**
|
||||
* Output command in raw format.
|
||||
*/
|
||||
raw?: boolean;
|
||||
}
|
||||
export interface CloseEvent {
|
||||
command: CommandInfo;
|
||||
/**
|
||||
* The command's index among all commands ran.
|
||||
*/
|
||||
index: number;
|
||||
/**
|
||||
* Whether the command exited because it was killed.
|
||||
*/
|
||||
killed: boolean;
|
||||
/**
|
||||
* The exit code or signal for the command.
|
||||
*/
|
||||
exitCode: string | number;
|
||||
timings: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
durationSeconds: number;
|
||||
};
|
||||
}
|
||||
export interface TimerEvent {
|
||||
startDate: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
export interface MessageEvent {
|
||||
message: object;
|
||||
handle?: SendHandle;
|
||||
}
|
||||
interface OutgoingMessageEvent extends MessageEvent {
|
||||
options?: MessageOptions;
|
||||
onSent: (error?: unknown) => void;
|
||||
}
|
||||
/**
|
||||
* Subtype of NodeJS's child_process including only what's actually needed for a command to work.
|
||||
*/
|
||||
export type ChildProcess = EventEmitter & Pick<BaseChildProcess, 'pid' | 'stdin' | 'stdout' | 'stderr' | 'send'>;
|
||||
/**
|
||||
* Interface for a function that must kill the process with `pid`, optionally sending `signal` to it.
|
||||
*/
|
||||
export type KillProcess = (pid: number, signal?: string) => void;
|
||||
/**
|
||||
* Interface for a function that spawns a command and returns its child process instance.
|
||||
*/
|
||||
export type SpawnCommand = (command: string, options: SpawnOptions) => ChildProcess;
|
||||
/**
|
||||
* The state of a command.
|
||||
*
|
||||
* - `stopped`: command was never started
|
||||
* - `started`: command is currently running
|
||||
* - `errored`: command failed spawning
|
||||
* - `exited`: command is not running anymore, e.g. it received a close event
|
||||
*/
|
||||
type CommandState = 'stopped' | 'started' | 'errored' | 'exited';
|
||||
export declare class Command implements CommandInfo {
|
||||
private readonly killProcess;
|
||||
private readonly spawn;
|
||||
private readonly spawnOpts;
|
||||
readonly index: number;
|
||||
/** @inheritdoc */
|
||||
readonly name: string;
|
||||
/** @inheritdoc */
|
||||
readonly command: string;
|
||||
/** @inheritdoc */
|
||||
readonly prefixColor?: string;
|
||||
/** @inheritdoc */
|
||||
readonly env: Record<string, unknown>;
|
||||
/** @inheritdoc */
|
||||
readonly cwd?: string;
|
||||
/** @inheritdoc */
|
||||
readonly ipc?: number;
|
||||
readonly close: Rx.Subject<CloseEvent>;
|
||||
readonly error: Rx.Subject<unknown>;
|
||||
readonly stdout: Rx.Subject<Buffer<ArrayBufferLike>>;
|
||||
readonly stderr: Rx.Subject<Buffer<ArrayBufferLike>>;
|
||||
readonly timer: Rx.Subject<TimerEvent>;
|
||||
/**
|
||||
* A stream of changes to the `#state` property.
|
||||
*
|
||||
* Note that the command never goes back to the `stopped` state, therefore it's not a value
|
||||
* that's emitted by this stream.
|
||||
*/
|
||||
readonly stateChange: Rx.Subject<"started" | "errored" | "exited">;
|
||||
readonly messages: {
|
||||
incoming: Rx.Subject<MessageEvent>;
|
||||
outgoing: Rx.ReplaySubject<OutgoingMessageEvent>;
|
||||
};
|
||||
process?: ChildProcess;
|
||||
private subscriptions;
|
||||
stdin?: Writable;
|
||||
pid?: number;
|
||||
killed: boolean;
|
||||
exited: boolean;
|
||||
state: CommandState;
|
||||
constructor({ index, name, command, prefixColor, env, cwd, ipc }: CommandInfo & {
|
||||
index: number;
|
||||
}, spawnOpts: SpawnOptions, spawn: SpawnCommand, killProcess: KillProcess);
|
||||
/**
|
||||
* Starts this command, piping output, error and close events onto the corresponding observables.
|
||||
*/
|
||||
start(): void;
|
||||
private changeState;
|
||||
private maybeSetupIPC;
|
||||
/**
|
||||
* Sends a message to the underlying process once it starts.
|
||||
*
|
||||
* @throws If the command doesn't have an IPC channel enabled
|
||||
* @returns Promise that resolves when the message is sent,
|
||||
* or rejects if it fails to deliver the message.
|
||||
*/
|
||||
send(message: object, handle?: SendHandle, options?: MessageOptions): Promise<void>;
|
||||
/**
|
||||
* Kills this command, optionally specifying a signal to send to it.
|
||||
*/
|
||||
kill(code?: string): void;
|
||||
private cleanUp;
|
||||
/**
|
||||
* Detects whether a command can be killed.
|
||||
*
|
||||
* Also works as a type guard on the input `command`.
|
||||
*/
|
||||
static canKill(command: Command): command is Command & {
|
||||
pid: number;
|
||||
process: ChildProcess;
|
||||
};
|
||||
}
|
||||
export {};
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
import process from 'node:process';
|
||||
import Rx from 'rxjs';
|
||||
export class Command {
|
||||
killProcess;
|
||||
spawn;
|
||||
spawnOpts;
|
||||
index;
|
||||
/** @inheritdoc */
|
||||
name;
|
||||
/** @inheritdoc */
|
||||
command;
|
||||
/** @inheritdoc */
|
||||
prefixColor;
|
||||
/** @inheritdoc */
|
||||
env;
|
||||
/** @inheritdoc */
|
||||
cwd;
|
||||
/** @inheritdoc */
|
||||
ipc;
|
||||
close = new Rx.Subject();
|
||||
error = new Rx.Subject();
|
||||
stdout = new Rx.Subject();
|
||||
stderr = new Rx.Subject();
|
||||
timer = new Rx.Subject();
|
||||
/**
|
||||
* A stream of changes to the `#state` property.
|
||||
*
|
||||
* Note that the command never goes back to the `stopped` state, therefore it's not a value
|
||||
* that's emitted by this stream.
|
||||
*/
|
||||
stateChange = new Rx.Subject();
|
||||
messages = {
|
||||
incoming: new Rx.Subject(),
|
||||
outgoing: new Rx.ReplaySubject(),
|
||||
};
|
||||
process;
|
||||
// TODO: Should exit/error/stdio subscriptions be added here?
|
||||
subscriptions = [];
|
||||
stdin;
|
||||
pid;
|
||||
killed = false;
|
||||
exited = false;
|
||||
state = 'stopped';
|
||||
constructor({ index, name, command, prefixColor, env, cwd, ipc }, spawnOpts, spawn, killProcess) {
|
||||
this.index = index;
|
||||
this.name = name;
|
||||
this.command = command;
|
||||
this.prefixColor = prefixColor;
|
||||
this.env = env || {};
|
||||
this.cwd = cwd;
|
||||
this.ipc = ipc;
|
||||
this.killProcess = killProcess;
|
||||
this.spawn = spawn;
|
||||
this.spawnOpts = spawnOpts;
|
||||
}
|
||||
/**
|
||||
* Starts this command, piping output, error and close events onto the corresponding observables.
|
||||
*/
|
||||
start() {
|
||||
const child = this.spawn(this.command, this.spawnOpts);
|
||||
this.changeState('started');
|
||||
this.process = child;
|
||||
this.pid = child.pid;
|
||||
const startDate = new Date(Date.now());
|
||||
const highResStartTime = process.hrtime();
|
||||
this.timer.next({ startDate });
|
||||
this.subscriptions = [...this.maybeSetupIPC(child)];
|
||||
Rx.fromEvent(child, 'error').subscribe((event) => {
|
||||
this.cleanUp();
|
||||
const endDate = new Date(Date.now());
|
||||
this.timer.next({ startDate, endDate });
|
||||
this.error.next(event);
|
||||
this.changeState('errored');
|
||||
});
|
||||
Rx.fromEvent(child, 'close')
|
||||
.pipe(Rx.map((event) => event))
|
||||
.subscribe(([exitCode, signal]) => {
|
||||
this.cleanUp();
|
||||
// Don't override error event
|
||||
if (this.state !== 'errored') {
|
||||
this.changeState('exited');
|
||||
}
|
||||
const endDate = new Date(Date.now());
|
||||
this.timer.next({ startDate, endDate });
|
||||
const [durationSeconds, durationNanoSeconds] = process.hrtime(highResStartTime);
|
||||
this.close.next({
|
||||
command: this,
|
||||
index: this.index,
|
||||
exitCode: exitCode ?? String(signal),
|
||||
killed: this.killed,
|
||||
timings: {
|
||||
startDate,
|
||||
endDate,
|
||||
durationSeconds: durationSeconds + durationNanoSeconds / 1e9,
|
||||
},
|
||||
});
|
||||
});
|
||||
if (child.stdout) {
|
||||
pipeTo(Rx.fromEvent(child.stdout, 'data').pipe(Rx.map((event) => event)), this.stdout);
|
||||
}
|
||||
if (child.stderr) {
|
||||
pipeTo(Rx.fromEvent(child.stderr, 'data').pipe(Rx.map((event) => event)), this.stderr);
|
||||
}
|
||||
this.stdin = child.stdin || undefined;
|
||||
}
|
||||
changeState(state) {
|
||||
this.state = state;
|
||||
this.stateChange.next(state);
|
||||
}
|
||||
maybeSetupIPC(child) {
|
||||
if (!this.ipc) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
pipeTo(Rx.fromEvent(child, 'message').pipe(Rx.map((event) => {
|
||||
const [message, handle] = event;
|
||||
return { message, handle };
|
||||
})), this.messages.incoming),
|
||||
this.messages.outgoing.subscribe((message) => {
|
||||
if (!child.send) {
|
||||
return message.onSent(new Error('Command does not have an IPC channel'));
|
||||
}
|
||||
child.send(message.message, message.handle, message.options, (error) => {
|
||||
message.onSent(error);
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Sends a message to the underlying process once it starts.
|
||||
*
|
||||
* @throws If the command doesn't have an IPC channel enabled
|
||||
* @returns Promise that resolves when the message is sent,
|
||||
* or rejects if it fails to deliver the message.
|
||||
*/
|
||||
send(message, handle, options) {
|
||||
if (this.ipc == null) {
|
||||
throw new Error('Command IPC is disabled');
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.messages.outgoing.next({
|
||||
message,
|
||||
handle,
|
||||
options,
|
||||
onSent(error) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Kills this command, optionally specifying a signal to send to it.
|
||||
*/
|
||||
kill(code) {
|
||||
if (Command.canKill(this)) {
|
||||
this.killed = true;
|
||||
this.killProcess(this.pid, code);
|
||||
}
|
||||
}
|
||||
cleanUp() {
|
||||
this.subscriptions?.forEach((sub) => sub.unsubscribe());
|
||||
this.messages.outgoing = new Rx.ReplaySubject();
|
||||
this.process = undefined;
|
||||
}
|
||||
/**
|
||||
* Detects whether a command can be killed.
|
||||
*
|
||||
* Also works as a type guard on the input `command`.
|
||||
*/
|
||||
static canKill(command) {
|
||||
return !!command.pid && !!command.process;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Pipes all events emitted by `stream` into `subject`.
|
||||
*/
|
||||
function pipeTo(stream, subject) {
|
||||
return stream.subscribe((event) => subject.next(event));
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import Rx from 'rxjs';
|
||||
import { CloseEvent, Command } from './command.js';
|
||||
/**
|
||||
* Defines which command(s) in a list must exit successfully (with an exit code of `0`):
|
||||
*
|
||||
* - `first`: only the first specified command;
|
||||
* - `last`: only the last specified command;
|
||||
* - `all`: all commands.
|
||||
* - `command-{name|index}`: only the commands with the specified names or index.
|
||||
* - `!command-{name|index}`: all commands but the ones with the specified names or index.
|
||||
*/
|
||||
export type SuccessCondition = 'first' | 'last' | 'all' | `command-${string | number}` | `!command-${string | number}`;
|
||||
/**
|
||||
* Provides logic to determine whether lists of commands ran successfully.
|
||||
*/
|
||||
export declare class CompletionListener {
|
||||
private readonly successCondition;
|
||||
private readonly scheduler?;
|
||||
constructor({ successCondition, scheduler, }: {
|
||||
/**
|
||||
* How this instance will define that a list of commands ran successfully.
|
||||
* Defaults to `all`.
|
||||
*
|
||||
* @see {SuccessCondition}
|
||||
*/
|
||||
successCondition?: SuccessCondition;
|
||||
/**
|
||||
* For testing only.
|
||||
*/
|
||||
scheduler?: Rx.SchedulerLike;
|
||||
});
|
||||
private isSuccess;
|
||||
/**
|
||||
* Given a list of commands, wait for all of them to exit and then evaluate their exit codes.
|
||||
*
|
||||
* @returns A Promise that resolves if the success condition is met, or rejects otherwise.
|
||||
* In either case, the value is a list of close events for commands that spawned.
|
||||
* Commands that didn't spawn are filtered out.
|
||||
*/
|
||||
listen(commands: Command[], abortSignal?: AbortSignal): Promise<CloseEvent[]>;
|
||||
private emitWithScheduler;
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import Rx from 'rxjs';
|
||||
import { delay, filter, map, share, switchMap, take } from 'rxjs/operators';
|
||||
/**
|
||||
* Provides logic to determine whether lists of commands ran successfully.
|
||||
*/
|
||||
export class CompletionListener {
|
||||
successCondition;
|
||||
scheduler;
|
||||
constructor({ successCondition = 'all', scheduler, }) {
|
||||
this.successCondition = successCondition;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
isSuccess(events) {
|
||||
if (!events.length) {
|
||||
// When every command was aborted, consider a success.
|
||||
return true;
|
||||
}
|
||||
if (this.successCondition === 'first') {
|
||||
return events[0].exitCode === 0;
|
||||
}
|
||||
else if (this.successCondition === 'last') {
|
||||
return events[events.length - 1].exitCode === 0;
|
||||
}
|
||||
const commandSyntaxMatch = this.successCondition.match(/^!?command-(.+)$/);
|
||||
if (commandSyntaxMatch == null) {
|
||||
// If not a `command-` syntax, then it's an 'all' condition or it's treated as such.
|
||||
return events.every(({ exitCode }) => exitCode === 0);
|
||||
}
|
||||
// Check `command-` syntax condition.
|
||||
// Note that a command's `name` is not necessarily unique,
|
||||
// in which case all of them must meet the success condition.
|
||||
const nameOrIndex = commandSyntaxMatch[1];
|
||||
const targetCommandsEvents = events.filter(({ command, index }) => command.name === nameOrIndex || index === Number(nameOrIndex));
|
||||
if (this.successCondition.startsWith('!')) {
|
||||
// All commands except the specified ones must exit successfully
|
||||
return events.every((event) => targetCommandsEvents.includes(event) || event.exitCode === 0);
|
||||
}
|
||||
// Only the specified commands must exit successfully
|
||||
return (targetCommandsEvents.length > 0 &&
|
||||
targetCommandsEvents.every((event) => event.exitCode === 0));
|
||||
}
|
||||
/**
|
||||
* Given a list of commands, wait for all of them to exit and then evaluate their exit codes.
|
||||
*
|
||||
* @returns A Promise that resolves if the success condition is met, or rejects otherwise.
|
||||
* In either case, the value is a list of close events for commands that spawned.
|
||||
* Commands that didn't spawn are filtered out.
|
||||
*/
|
||||
listen(commands, abortSignal) {
|
||||
if (!commands.length) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const abort = abortSignal &&
|
||||
Rx.fromEvent(abortSignal, 'abort', { once: true }).pipe(
|
||||
// The abort signal must happen before commands are killed, otherwise new commands
|
||||
// might spawn. Because of this, it's not be possible to capture the close events
|
||||
// without an immediate delay
|
||||
delay(0, this.scheduler), map(() => undefined),
|
||||
// #502 - node might warn of too many active listeners on this object if it isn't shared,
|
||||
// as each command subscribes to abort event over and over
|
||||
share());
|
||||
const closeStreams = commands.map((command) => abort
|
||||
? // Commands that have been started must close.
|
||||
Rx.race(command.close, abort.pipe(filter(() => command.state === 'stopped')))
|
||||
: command.close);
|
||||
return Rx.lastValueFrom(Rx.combineLatest(closeStreams).pipe(filter(() => commands.every((command) => command.state !== 'started')), map((events) => events
|
||||
// Filter out aborts, since they cannot be sorted and are considered success condition anyways
|
||||
.filter((event) => event != null)
|
||||
// Sort according to exit time
|
||||
.sort((first, second) => first.timings.endDate.getTime() - second.timings.endDate.getTime())), switchMap((events) => this.isSuccess(events)
|
||||
? this.emitWithScheduler(Rx.of(events))
|
||||
: this.emitWithScheduler(Rx.throwError(() => events))), take(1)));
|
||||
}
|
||||
emitWithScheduler(input) {
|
||||
return this.scheduler ? input.pipe(Rx.observeOn(this.scheduler)) : input;
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import { Writable } from 'node:stream';
|
||||
import { CloseEvent, Command, CommandIdentifier, CommandInfo, KillProcess, SpawnCommand } from './command.js';
|
||||
import { SuccessCondition } from './completion-listener.js';
|
||||
import { FlowController } from './flow-control/flow-controller.js';
|
||||
import { Logger } from './logger.js';
|
||||
/**
|
||||
* A command that is to be passed into `concurrently()`.
|
||||
* If value is a string, then that's the command's command line.
|
||||
* Fine grained options can be defined by using the object format.
|
||||
*/
|
||||
export type ConcurrentlyCommandInput = string | ({
|
||||
command: string;
|
||||
} & Partial<CommandInfo>);
|
||||
export interface ConcurrentlyResult {
|
||||
/**
|
||||
* All commands created and ran by concurrently.
|
||||
*/
|
||||
commands: Command[];
|
||||
/**
|
||||
* A promise that resolves when concurrently ran successfully according to the specified
|
||||
* success condition, or reject otherwise.
|
||||
*
|
||||
* Both the resolved and rejected value is a list of all the close events for commands that
|
||||
* spawned; commands that didn't spawn are filtered out.
|
||||
*/
|
||||
result: Promise<CloseEvent[]>;
|
||||
}
|
||||
export interface ConcurrentlyOptions {
|
||||
logger?: Logger;
|
||||
/**
|
||||
* Which stream should the commands output be written to.
|
||||
*/
|
||||
outputStream?: Writable;
|
||||
/**
|
||||
* Whether the output should be ordered as if the commands were run sequentially.
|
||||
*/
|
||||
group?: boolean;
|
||||
/**
|
||||
* A comma-separated list of Chalk colors or a string for available styles listed below to use on prefixes.
|
||||
* If there are more commands than colors, the last color will be repeated.
|
||||
*
|
||||
* Available modifiers:
|
||||
* - `reset`, `bold`, `dim`, `italic`, `underline`, `inverse`, `hidden`, `strikethrough`
|
||||
*
|
||||
* Available colors:
|
||||
* - `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`,
|
||||
* any hex values for colors (e.g. `#23de43`) or `auto` for an automatically picked color
|
||||
*
|
||||
* Available background colors:
|
||||
* - `bgBlack`, `bgRed`, `bgGreen`, `bgYellow`, `bgBlue`, `bgMagenta`, `bgCyan`, `bgWhite`
|
||||
*
|
||||
* Set to `false` to disable colors.
|
||||
*
|
||||
* @see {@link https://www.npmjs.com/package/chalk} for more information.
|
||||
*/
|
||||
prefixColors?: string | string[] | false;
|
||||
/**
|
||||
* Maximum number of commands to run at once.
|
||||
* Exact number or a percent of CPUs available (for example "50%").
|
||||
*
|
||||
* If undefined, then all processes will start in parallel.
|
||||
* Setting this value to 1 will achieve sequential running.
|
||||
*/
|
||||
maxProcesses?: number | string;
|
||||
/**
|
||||
* Whether commands should be spawned in raw mode.
|
||||
* Defaults to false.
|
||||
*/
|
||||
raw?: boolean;
|
||||
/**
|
||||
* Which commands should have their output hidden.
|
||||
*/
|
||||
hide?: CommandIdentifier[];
|
||||
/**
|
||||
* The current working directory of commands which didn't specify one.
|
||||
* Defaults to `process.cwd()`.
|
||||
*/
|
||||
cwd?: string;
|
||||
/**
|
||||
* @see CompletionListener
|
||||
*/
|
||||
successCondition?: SuccessCondition;
|
||||
/**
|
||||
* A signal to stop spawning further processes.
|
||||
*/
|
||||
abortSignal?: AbortSignal;
|
||||
/**
|
||||
* Which flow controllers should be applied on commands spawned by concurrently.
|
||||
* Defaults to an empty array.
|
||||
*/
|
||||
controllers: FlowController[];
|
||||
/**
|
||||
* A function that will spawn commands.
|
||||
* Defaults to a function that spawns using either `cmd.exe` or `/bin/sh`.
|
||||
*/
|
||||
spawn: SpawnCommand;
|
||||
/**
|
||||
* A function that will kill processes.
|
||||
* Defaults to the `tree-kill` module.
|
||||
*/
|
||||
kill: KillProcess;
|
||||
/**
|
||||
* List of additional arguments passed that will get replaced in each command.
|
||||
* If not defined, no argument replacing will happen.
|
||||
*
|
||||
* @see ExpandArguments
|
||||
*/
|
||||
additionalArguments?: string[];
|
||||
}
|
||||
/**
|
||||
* Core concurrently functionality -- spawns the given commands concurrently and
|
||||
* returns the commands themselves + the result according to the specified success condition.
|
||||
*
|
||||
* @see CompletionListener
|
||||
*/
|
||||
export declare function concurrently(baseCommands: ConcurrentlyCommandInput[], baseOptions?: Partial<ConcurrentlyOptions>): ConcurrentlyResult;
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import assert from 'node:assert';
|
||||
import os from 'node:os';
|
||||
import { takeUntil } from 'rxjs';
|
||||
import treeKill from 'tree-kill';
|
||||
import { Command, } from './command.js';
|
||||
import { ExpandArguments } from './command-parser/expand-arguments.js';
|
||||
import { ExpandShortcut } from './command-parser/expand-shortcut.js';
|
||||
import { ExpandWildcard } from './command-parser/expand-wildcard.js';
|
||||
import { CompletionListener } from './completion-listener.js';
|
||||
import { OutputWriter } from './output-writer.js';
|
||||
import { PrefixColorSelector } from './prefix-color-selector.js';
|
||||
import { createSpawn, getSpawnOpts } from './spawn.js';
|
||||
import { castArray } from './utils.js';
|
||||
const defaults = {
|
||||
spawn: createSpawn(),
|
||||
kill: treeKill,
|
||||
raw: false,
|
||||
controllers: [],
|
||||
cwd: undefined,
|
||||
};
|
||||
/**
|
||||
* Core concurrently functionality -- spawns the given commands concurrently and
|
||||
* returns the commands themselves + the result according to the specified success condition.
|
||||
*
|
||||
* @see CompletionListener
|
||||
*/
|
||||
export function concurrently(baseCommands, baseOptions) {
|
||||
assert.ok(Array.isArray(baseCommands), '[concurrently] commands should be an array');
|
||||
assert.notStrictEqual(baseCommands.length, 0, '[concurrently] no commands provided');
|
||||
const options = { ...defaults, ...baseOptions };
|
||||
const prefixColorSelector = new PrefixColorSelector(options.prefixColors || []);
|
||||
const commandParsers = [new ExpandShortcut(), new ExpandWildcard()];
|
||||
if (options.additionalArguments) {
|
||||
commandParsers.push(new ExpandArguments(options.additionalArguments));
|
||||
}
|
||||
const hide = (options.hide || []).map(String);
|
||||
let commands = baseCommands
|
||||
.map(mapToCommandInfo)
|
||||
.flatMap((command) => parseCommand(command, commandParsers))
|
||||
.map((command, index) => {
|
||||
const hidden = hide.includes(command.name) || hide.includes(String(index));
|
||||
return new Command({
|
||||
index,
|
||||
prefixColor: prefixColorSelector.getNextColor(),
|
||||
...command,
|
||||
}, getSpawnOpts({
|
||||
ipc: command.ipc,
|
||||
stdio: hidden ? 'hidden' : (command.raw ?? options.raw) ? 'raw' : 'normal',
|
||||
env: command.env,
|
||||
cwd: command.cwd || options.cwd,
|
||||
}), options.spawn, options.kill);
|
||||
});
|
||||
const handleResult = options.controllers.reduce(({ commands: prevCommands, onFinishCallbacks }, controller) => {
|
||||
const { commands, onFinish } = controller.handle(prevCommands);
|
||||
return {
|
||||
commands,
|
||||
onFinishCallbacks: onFinishCallbacks.concat(onFinish ? [onFinish] : []),
|
||||
};
|
||||
}, { commands, onFinishCallbacks: [] });
|
||||
commands = handleResult.commands;
|
||||
if (options.logger && options.outputStream) {
|
||||
const outputWriter = new OutputWriter({
|
||||
outputStream: options.outputStream,
|
||||
group: !!options.group,
|
||||
commands,
|
||||
});
|
||||
options.logger.output
|
||||
// Stop trying to write after there's been an error.
|
||||
.pipe(takeUntil(outputWriter.error))
|
||||
.subscribe(({ command, text }) => outputWriter.write(command, text));
|
||||
}
|
||||
const commandsLeft = commands.slice();
|
||||
const maxProcesses = Math.max(1, (typeof options.maxProcesses === 'string' && options.maxProcesses.endsWith('%')
|
||||
? Math.round((os.cpus().length * Number(options.maxProcesses.slice(0, -1))) / 100)
|
||||
: Number(options.maxProcesses)) || commandsLeft.length);
|
||||
for (let i = 0; i < maxProcesses; i++) {
|
||||
maybeRunMore(commandsLeft, options.abortSignal);
|
||||
}
|
||||
const result = new CompletionListener({ successCondition: options.successCondition })
|
||||
.listen(commands, options.abortSignal)
|
||||
.finally(() => Promise.all(handleResult.onFinishCallbacks.map((onFinish) => onFinish())));
|
||||
return {
|
||||
result,
|
||||
commands,
|
||||
};
|
||||
}
|
||||
function mapToCommandInfo(command) {
|
||||
if (typeof command === 'string') {
|
||||
return mapToCommandInfo({ command });
|
||||
}
|
||||
assert.ok(command.command, '[concurrently] command cannot be empty');
|
||||
return {
|
||||
command: command.command,
|
||||
name: command.name || '',
|
||||
env: command.env || {},
|
||||
cwd: command.cwd || '',
|
||||
ipc: command.ipc,
|
||||
...(command.prefixColor
|
||||
? {
|
||||
prefixColor: command.prefixColor,
|
||||
}
|
||||
: {}),
|
||||
...(command.raw !== undefined
|
||||
? {
|
||||
raw: command.raw,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
function parseCommand(command, parsers) {
|
||||
return parsers.reduce((commands, parser) => commands.flatMap((command) => parser.parse(command)), castArray(command));
|
||||
}
|
||||
function maybeRunMore(commandsLeft, abortSignal) {
|
||||
const command = commandsLeft.shift();
|
||||
if (!command || abortSignal?.aborted) {
|
||||
return;
|
||||
}
|
||||
command.start();
|
||||
command.close.subscribe(() => {
|
||||
maybeRunMore(commandsLeft, abortSignal);
|
||||
});
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
export interface FormatterOptions {
|
||||
locale?: string;
|
||||
calendar?: string;
|
||||
}
|
||||
/**
|
||||
* Unicode-compliant date/time formatter.
|
||||
*
|
||||
* @see https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
|
||||
*/
|
||||
export declare class DateFormatter {
|
||||
private readonly options;
|
||||
private static tokenRegex;
|
||||
private readonly parts;
|
||||
constructor(pattern: string, options?: FormatterOptions);
|
||||
private compileLiteral;
|
||||
private compileOther;
|
||||
private compileToken;
|
||||
format(date: Date): string;
|
||||
}
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* A map of token to its implementations by length.
|
||||
* If an index is undefined, then that token length is unsupported.
|
||||
*/
|
||||
const tokens = new Map()
|
||||
// era
|
||||
.set('G', [
|
||||
makeTokenFn({ era: 'short' }, 'era'),
|
||||
makeTokenFn({ era: 'short' }, 'era'),
|
||||
makeTokenFn({ era: 'short' }, 'era'),
|
||||
makeTokenFn({ era: 'long' }, 'era'),
|
||||
makeTokenFn({ era: 'narrow' }, 'era'),
|
||||
])
|
||||
// year
|
||||
.set('y', [
|
||||
// TODO: does not support BC years.
|
||||
// https://stackoverflow.com/a/41345095/2083599
|
||||
(date) => date.getFullYear(),
|
||||
(date) => pad(2, date.getFullYear()).slice(-2),
|
||||
(date) => pad(3, date.getFullYear()),
|
||||
(date) => pad(4, date.getFullYear()),
|
||||
(date) => pad(5, date.getFullYear()),
|
||||
])
|
||||
.set('Y', [
|
||||
getWeekYear,
|
||||
(date, options) => pad(2, getWeekYear(date, options)).slice(-2),
|
||||
(date, options) => pad(3, getWeekYear(date, options)),
|
||||
(date, options) => pad(4, getWeekYear(date, options)),
|
||||
(date, options) => pad(5, getWeekYear(date, options)),
|
||||
])
|
||||
.set('u', [])
|
||||
.set('U', [
|
||||
// Fallback implemented as yearName is not available in gregorian calendars, for instance.
|
||||
makeTokenFn({ dateStyle: 'full' }, 'yearName', (date) => String(date.getFullYear())),
|
||||
])
|
||||
.set('r', [
|
||||
// Fallback implemented as relatedYear is not available in gregorian calendars, for instance.
|
||||
makeTokenFn({ dateStyle: 'full' }, 'relatedYear', (date) => String(date.getFullYear())),
|
||||
])
|
||||
// quarter
|
||||
.set('Q', [
|
||||
(date) => Math.floor(date.getMonth() / 3) + 1,
|
||||
(date) => pad(2, Math.floor(date.getMonth() / 3) + 1),
|
||||
// these aren't localized in Intl.DateTimeFormat.
|
||||
undefined,
|
||||
undefined,
|
||||
(date) => Math.floor(date.getMonth() / 3) + 1,
|
||||
])
|
||||
.set('q', [
|
||||
(date) => Math.floor(date.getMonth() / 3) + 1,
|
||||
(date) => pad(2, Math.floor(date.getMonth() / 3) + 1),
|
||||
// these aren't localized in Intl.DateTimeFormat.
|
||||
undefined,
|
||||
undefined,
|
||||
(date) => Math.floor(date.getMonth() / 3) + 1,
|
||||
])
|
||||
// month
|
||||
.set('M', [
|
||||
(date) => date.getMonth() + 1,
|
||||
(date) => pad(2, date.getMonth() + 1),
|
||||
// these include the day so that it forces non-stand-alone month part
|
||||
makeTokenFn({ day: 'numeric', month: 'short' }, 'month'),
|
||||
makeTokenFn({ day: 'numeric', month: 'long' }, 'month'),
|
||||
makeTokenFn({ day: 'numeric', month: 'narrow' }, 'month'),
|
||||
])
|
||||
.set('L', [
|
||||
(date) => date.getMonth() + 1,
|
||||
(date) => pad(2, date.getMonth() + 1),
|
||||
makeTokenFn({ month: 'short' }, 'month'),
|
||||
makeTokenFn({ month: 'long' }, 'month'),
|
||||
makeTokenFn({ month: 'narrow' }, 'month'),
|
||||
])
|
||||
.set('l', [() => ''])
|
||||
// week
|
||||
.set('w', [getWeek, (date, options) => pad(2, getWeek(date, options))])
|
||||
.set('W', [getWeekOfMonth])
|
||||
// day
|
||||
.set('d', [(date) => date.getDate(), (date) => pad(2, date.getDate())])
|
||||
.set('D', [
|
||||
getDayOfYear,
|
||||
(date) => pad(2, getDayOfYear(date)),
|
||||
(date) => pad(3, getDayOfYear(date)),
|
||||
])
|
||||
.set('F', [(date) => Math.ceil(date.getDate() / 7)])
|
||||
.set('g', [])
|
||||
// week day
|
||||
.set('E', [
|
||||
makeTokenFn({ weekday: 'short' }, 'weekday'),
|
||||
makeTokenFn({ weekday: 'short' }, 'weekday'),
|
||||
makeTokenFn({ weekday: 'short' }, 'weekday'),
|
||||
makeTokenFn({ weekday: 'long' }, 'weekday'),
|
||||
])
|
||||
.set('e', [
|
||||
undefined,
|
||||
undefined,
|
||||
makeTokenFn({ weekday: 'short' }, 'weekday'),
|
||||
makeTokenFn({ weekday: 'long' }, 'weekday'),
|
||||
])
|
||||
.set('c', [])
|
||||
// period
|
||||
.set('a', [
|
||||
makeTokenFn({ hour12: true, timeStyle: 'full' }, 'dayPeriod'),
|
||||
makeTokenFn({ hour12: true, timeStyle: 'full' }, 'dayPeriod'),
|
||||
makeTokenFn({ hour12: true, timeStyle: 'full' }, 'dayPeriod'),
|
||||
])
|
||||
.set('b', [])
|
||||
.set('B', [
|
||||
makeTokenFn({ dayPeriod: 'short' }, 'dayPeriod'),
|
||||
makeTokenFn({ dayPeriod: 'short' }, 'dayPeriod'),
|
||||
makeTokenFn({ dayPeriod: 'short' }, 'dayPeriod'),
|
||||
makeTokenFn({ dayPeriod: 'long' }, 'dayPeriod'),
|
||||
])
|
||||
// hour
|
||||
.set('h', [(date) => date.getHours() % 12 || 12, (date) => pad(2, date.getHours() % 12 || 12)])
|
||||
.set('H', [(date) => date.getHours(), (date) => pad(2, date.getHours())])
|
||||
.set('K', [(date) => date.getHours() % 12, (date) => pad(2, date.getHours() % 12)])
|
||||
.set('k', [(date) => date.getHours() % 24 || 24, (date) => pad(2, date.getHours() % 24 || 24)])
|
||||
.set('j', [])
|
||||
.set('J', [])
|
||||
.set('C', [])
|
||||
// minute
|
||||
.set('m', [(date) => date.getMinutes(), (date) => pad(2, date.getMinutes())])
|
||||
// second
|
||||
.set('s', [(date) => date.getSeconds(), (date) => pad(2, date.getSeconds())])
|
||||
.set('S', [
|
||||
(date) => Math.trunc(date.getMilliseconds() / 100),
|
||||
(date) => pad(2, Math.trunc(date.getMilliseconds() / 10)),
|
||||
(date) => pad(3, Math.trunc(date.getMilliseconds())),
|
||||
])
|
||||
.set('A', [])
|
||||
// zone
|
||||
// none of these have tests
|
||||
.set('z', [
|
||||
makeTokenFn({ timeZoneName: 'short' }, 'timeZoneName'),
|
||||
makeTokenFn({ timeZoneName: 'short' }, 'timeZoneName'),
|
||||
makeTokenFn({ timeZoneName: 'short' }, 'timeZoneName'),
|
||||
makeTokenFn({ timeZoneName: 'long' }, 'timeZoneName'),
|
||||
])
|
||||
.set('Z', [
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
// equivalent to `OOOO`.
|
||||
makeTokenFn({ timeZoneName: 'longOffset' }, 'timeZoneName'),
|
||||
])
|
||||
.set('O', [
|
||||
makeTokenFn({ timeZoneName: 'shortOffset' }, 'timeZoneName'),
|
||||
undefined,
|
||||
undefined,
|
||||
// equivalent to `ZZZZ`.
|
||||
makeTokenFn({ timeZoneName: 'longOffset' }, 'timeZoneName'),
|
||||
])
|
||||
.set('v', [
|
||||
makeTokenFn({ timeZoneName: 'shortGeneric' }, 'timeZoneName'),
|
||||
undefined,
|
||||
undefined,
|
||||
makeTokenFn({ timeZoneName: 'longGeneric' }, 'timeZoneName'),
|
||||
])
|
||||
.set('V', [])
|
||||
.set('X', [])
|
||||
.set('x', []);
|
||||
let locale;
|
||||
function getLocale(options) {
|
||||
if (!locale || locale.baseName !== options.locale) {
|
||||
locale = new Intl.Locale(
|
||||
/* v8 ignore next - fallback value only for safety */
|
||||
options.locale || new Intl.DateTimeFormat().resolvedOptions().locale);
|
||||
}
|
||||
return locale;
|
||||
}
|
||||
/**
|
||||
* Unicode-compliant date/time formatter.
|
||||
*
|
||||
* @see https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
|
||||
*/
|
||||
export class DateFormatter {
|
||||
options;
|
||||
static tokenRegex = /[A-Z]/i;
|
||||
parts = [];
|
||||
constructor(pattern, options = {}) {
|
||||
this.options = options;
|
||||
let i = 0;
|
||||
while (i < pattern.length) {
|
||||
const char = pattern[i];
|
||||
const { fn, length } = char === "'"
|
||||
? this.compileLiteral(pattern, i)
|
||||
: DateFormatter.tokenRegex.test(char)
|
||||
? this.compileToken(pattern, i)
|
||||
: this.compileOther(pattern, i);
|
||||
this.parts.push(fn);
|
||||
i += length;
|
||||
}
|
||||
}
|
||||
compileLiteral(pattern, offset) {
|
||||
let length = 1;
|
||||
let value = '';
|
||||
for (; length < pattern.length; length++) {
|
||||
const i = offset + length;
|
||||
const char = pattern[i];
|
||||
if (char === "'") {
|
||||
const nextChar = pattern[i + 1];
|
||||
length++;
|
||||
// if the next character is another single quote, it's been escaped.
|
||||
// if not, then the literal has been closed
|
||||
if (nextChar !== "'") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
value += char;
|
||||
}
|
||||
return { fn: () => value || "'", length };
|
||||
}
|
||||
compileOther(pattern, offset) {
|
||||
let value = '';
|
||||
while (!DateFormatter.tokenRegex.test(pattern[offset]) && pattern[offset] !== "'") {
|
||||
value += pattern[offset++];
|
||||
}
|
||||
return { fn: () => value, length: value.length };
|
||||
}
|
||||
compileToken(pattern, offset) {
|
||||
const type = pattern[offset];
|
||||
const token = tokens.get(type);
|
||||
if (!token) {
|
||||
throw new SyntaxError(`Formatting token "${type}" is invalid`);
|
||||
}
|
||||
let length = 0;
|
||||
while (pattern[offset + length] === type) {
|
||||
length++;
|
||||
}
|
||||
const tokenFn = token[length - 1];
|
||||
if (!tokenFn) {
|
||||
throw new RangeError(`Formatting token "${type.repeat(length)}" is unsupported`);
|
||||
}
|
||||
return { fn: tokenFn, length };
|
||||
}
|
||||
format(date) {
|
||||
return this.parts.reduce((output, part) => output + String(part(date, this.options)), '');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* See https://github.com/moment/luxon/issues/1693
|
||||
*
|
||||
* @todo Replace once there's a suitable alternative implementation
|
||||
*/
|
||||
const fallbackWeekInfo = {
|
||||
minimalDays: 4,
|
||||
};
|
||||
const weekInfoCache = new Map();
|
||||
function getWeekInfo(locale) {
|
||||
let data = weekInfoCache.get(locale.baseName);
|
||||
if (!data) {
|
||||
// The specs now envisage a method for this,
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo
|
||||
data = locale.getWeekInfo?.() ?? locale.weekInfo;
|
||||
// The specs have dropped minimalDays,
|
||||
// see https://github.com/tc39/proposal-intl-locale-info/issues/86
|
||||
if (!data.minimalDays) {
|
||||
data = { ...fallbackWeekInfo, ...data };
|
||||
}
|
||||
weekInfoCache.set(locale.baseName, data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
/**
|
||||
* Creates a token formatting function that returns the value of the chosen part type,
|
||||
* using the current locale's settings.
|
||||
*
|
||||
* If the date/formatter settings doesn't include the requested part type,
|
||||
* the `fallback` function is invoked, if specified. If none has been specified, returns an
|
||||
* empty string.
|
||||
*/
|
||||
function makeTokenFn(options, type, fallback) {
|
||||
let formatter;
|
||||
return (date, formatterOptions) => {
|
||||
// Allow tests to set a different locale and have that cause the formatter to be recreated
|
||||
if (!formatter ||
|
||||
formatter.resolvedOptions().locale !== formatterOptions.locale ||
|
||||
formatter.resolvedOptions().calendar !== formatterOptions.calendar) {
|
||||
formatter = new Intl.DateTimeFormat(formatterOptions.locale, {
|
||||
...options,
|
||||
calendar: options.calendar ?? formatterOptions.calendar,
|
||||
});
|
||||
}
|
||||
const parts = formatter.formatToParts(date);
|
||||
const part = parts.find((p) => p.type === type);
|
||||
/* v8 ignore next - fallback value '' only for safety */
|
||||
return part?.value ?? (fallback ? fallback(date, formatterOptions) : '');
|
||||
};
|
||||
}
|
||||
function startOfWeek(date, options) {
|
||||
const locale = getLocale(options);
|
||||
const weekInfo = getWeekInfo(locale);
|
||||
const firstDay = weekInfo.firstDay === 7 ? 0 : weekInfo.firstDay;
|
||||
const day = date.getDay();
|
||||
const diff = (day < firstDay ? 7 : 0) + day - firstDay;
|
||||
date.setDate(date.getDate() - diff);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return date;
|
||||
}
|
||||
function getWeekYear(date, options) {
|
||||
const locale = getLocale(options);
|
||||
const minimalDays = getWeekInfo(locale).minimalDays;
|
||||
const year = date.getFullYear();
|
||||
const thisYear = startOfWeek(new Date(year, 0, minimalDays), options);
|
||||
const nextYear = startOfWeek(new Date(year + 1, 0, minimalDays), options);
|
||||
if (date.getTime() >= nextYear.getTime()) {
|
||||
return year + 1;
|
||||
}
|
||||
else if (date.getTime() >= thisYear.getTime()) {
|
||||
return year;
|
||||
}
|
||||
else {
|
||||
return year - 1;
|
||||
}
|
||||
}
|
||||
function getWeek(date, options) {
|
||||
const locale = getLocale(options);
|
||||
const weekMs = 7 * 24 * 3600 * 1000;
|
||||
const temp = startOfWeek(new Date(date), options);
|
||||
const thisYear = new Date(getWeekYear(date, options), 0, getWeekInfo(locale).minimalDays);
|
||||
startOfWeek(thisYear, options);
|
||||
const diff = temp.getTime() - thisYear.getTime();
|
||||
return Math.round(diff / weekMs) + 1;
|
||||
}
|
||||
function getWeekOfMonth(date, options) {
|
||||
const current = new Date(date);
|
||||
current.setHours(0, 0, 0, 0);
|
||||
const monthWeekStart = startOfWeek(new Date(date.getFullYear(), date.getMonth(), 1), options);
|
||||
const weekMs = 7 * 24 * 3600 * 1000;
|
||||
return Math.floor((date.getTime() - monthWeekStart.getTime()) / weekMs) + 1;
|
||||
}
|
||||
function getDayOfYear(date) {
|
||||
let days = 0;
|
||||
for (let i = 0; i <= date.getMonth() - 1; i++) {
|
||||
const temp = new Date(date.getFullYear(), i + 1, 0, 0, 0, 0);
|
||||
days += temp.getDate();
|
||||
}
|
||||
return days + date.getDate();
|
||||
}
|
||||
function pad(length, val) {
|
||||
return String(val).padStart(length, '0');
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { SuccessCondition } from './completion-listener.js';
|
||||
export declare const defaultInputTarget = 0;
|
||||
/**
|
||||
* Whether process.stdin should be forwarded to child processes.
|
||||
*/
|
||||
export declare const handleInput = false;
|
||||
/**
|
||||
* How many processes to run at once.
|
||||
*/
|
||||
export declare const maxProcesses = 0;
|
||||
/**
|
||||
* Indices and names of commands whose output are not to be logged.
|
||||
*/
|
||||
export declare const hide = "";
|
||||
/**
|
||||
* Which prefix style to use when logging processes output.
|
||||
*/
|
||||
export declare const prefix = "";
|
||||
/**
|
||||
* Default prefix color.
|
||||
* @see https://www.npmjs.com/package/chalk
|
||||
*/
|
||||
export declare const prefixColors = "auto";
|
||||
/**
|
||||
* How many bytes we'll show on the command prefix.
|
||||
*/
|
||||
export declare const prefixLength = 10;
|
||||
export declare const raw = false;
|
||||
/**
|
||||
* Number of attempts of restarting a process, if it exits with non-0 code.
|
||||
*/
|
||||
export declare const restartTries = 0;
|
||||
/**
|
||||
* How many milliseconds concurrently should wait before restarting a process.
|
||||
*/
|
||||
export declare const restartDelay = 0;
|
||||
/**
|
||||
* Condition of success for concurrently itself.
|
||||
*/
|
||||
export declare const success: SuccessCondition;
|
||||
/**
|
||||
* Date format used when logging date/time.
|
||||
* @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
|
||||
*/
|
||||
export declare const timestampFormat = "yyyy-MM-dd HH:mm:ss.SSS";
|
||||
/**
|
||||
* Current working dir passed as option to spawn command.
|
||||
* Defaults to process.cwd()
|
||||
*/
|
||||
export declare const cwd: string | undefined;
|
||||
/**
|
||||
* Whether to show timing information for processes in console output.
|
||||
*/
|
||||
export declare const timings = false;
|
||||
/**
|
||||
* Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands.
|
||||
*/
|
||||
export declare const passthroughArguments = false;
|
||||
/**
|
||||
* Signal to send to other processes if one exits or dies.
|
||||
*
|
||||
* Defaults to OS specific signal. (SIGTERM on Linux/MacOS)
|
||||
*/
|
||||
export declare const killSignal: string | undefined;
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// This file is meant to be a shared place for default configs.
|
||||
// It's read by the flow controllers, the executable, etc.
|
||||
// Refer to tests for the meaning of the different possible values.
|
||||
export const defaultInputTarget = 0;
|
||||
/**
|
||||
* Whether process.stdin should be forwarded to child processes.
|
||||
*/
|
||||
export const handleInput = false;
|
||||
/**
|
||||
* How many processes to run at once.
|
||||
*/
|
||||
export const maxProcesses = 0;
|
||||
/**
|
||||
* Indices and names of commands whose output are not to be logged.
|
||||
*/
|
||||
export const hide = '';
|
||||
/**
|
||||
* Which prefix style to use when logging processes output.
|
||||
*/
|
||||
export const prefix = '';
|
||||
/**
|
||||
* Default prefix color.
|
||||
* @see https://www.npmjs.com/package/chalk
|
||||
*/
|
||||
export const prefixColors = 'auto';
|
||||
/**
|
||||
* How many bytes we'll show on the command prefix.
|
||||
*/
|
||||
export const prefixLength = 10;
|
||||
export const raw = false;
|
||||
/**
|
||||
* Number of attempts of restarting a process, if it exits with non-0 code.
|
||||
*/
|
||||
export const restartTries = 0;
|
||||
/**
|
||||
* How many milliseconds concurrently should wait before restarting a process.
|
||||
*/
|
||||
export const restartDelay = 0;
|
||||
/**
|
||||
* Condition of success for concurrently itself.
|
||||
*/
|
||||
export const success = 'all';
|
||||
/**
|
||||
* Date format used when logging date/time.
|
||||
* @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
|
||||
*/
|
||||
export const timestampFormat = 'yyyy-MM-dd HH:mm:ss.SSS';
|
||||
/**
|
||||
* Current working dir passed as option to spawn command.
|
||||
* Defaults to process.cwd()
|
||||
*/
|
||||
export const cwd = undefined;
|
||||
/**
|
||||
* Whether to show timing information for processes in console output.
|
||||
*/
|
||||
export const timings = false;
|
||||
/**
|
||||
* Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands.
|
||||
*/
|
||||
export const passthroughArguments = false;
|
||||
/**
|
||||
* Signal to send to other processes if one exits or dies.
|
||||
*
|
||||
* Defaults to OS specific signal. (SIGTERM on Linux/MacOS)
|
||||
*/
|
||||
export const killSignal = undefined;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import type { Command } from '../command.js';
|
||||
/**
|
||||
* Interface for a class that controls and/or watches the behavior of commands.
|
||||
*
|
||||
* This may include logging their output, creating interactions between them, or changing when they
|
||||
* actually finish.
|
||||
*/
|
||||
export interface FlowController {
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
onFinish?: () => void | Promise<void>;
|
||||
};
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { Readable } from 'node:stream';
|
||||
import { Command, CommandIdentifier } from '../command.js';
|
||||
import { Logger } from '../logger.js';
|
||||
import { FlowController } from './flow-controller.js';
|
||||
/**
|
||||
* Sends input from concurrently through to commands.
|
||||
*
|
||||
* Input can start with a command identifier, in which case it will be sent to that specific command.
|
||||
* For instance, `0:bla` will send `bla` to command at index `0`, and `server:stop` will send `stop`
|
||||
* to command with name `server`.
|
||||
*
|
||||
* If the input doesn't start with a command identifier, it is then always sent to the default target.
|
||||
*/
|
||||
export declare class InputHandler implements FlowController {
|
||||
private readonly logger;
|
||||
private readonly defaultInputTarget;
|
||||
private readonly inputStream?;
|
||||
private readonly pauseInputStreamOnFinish;
|
||||
constructor({ defaultInputTarget, inputStream, pauseInputStreamOnFinish, logger, }: {
|
||||
inputStream?: Readable;
|
||||
logger: Logger;
|
||||
defaultInputTarget?: CommandIdentifier;
|
||||
pauseInputStreamOnFinish?: boolean;
|
||||
});
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
onFinish?: () => void | undefined;
|
||||
};
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import Rx from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import * as defaults from '../defaults.js';
|
||||
/**
|
||||
* Sends input from concurrently through to commands.
|
||||
*
|
||||
* Input can start with a command identifier, in which case it will be sent to that specific command.
|
||||
* For instance, `0:bla` will send `bla` to command at index `0`, and `server:stop` will send `stop`
|
||||
* to command with name `server`.
|
||||
*
|
||||
* If the input doesn't start with a command identifier, it is then always sent to the default target.
|
||||
*/
|
||||
export class InputHandler {
|
||||
logger;
|
||||
defaultInputTarget;
|
||||
inputStream;
|
||||
pauseInputStreamOnFinish;
|
||||
constructor({ defaultInputTarget, inputStream, pauseInputStreamOnFinish, logger, }) {
|
||||
this.logger = logger;
|
||||
this.defaultInputTarget = defaultInputTarget || defaults.defaultInputTarget;
|
||||
this.inputStream = inputStream;
|
||||
this.pauseInputStreamOnFinish = pauseInputStreamOnFinish !== false;
|
||||
}
|
||||
handle(commands) {
|
||||
const { inputStream } = this;
|
||||
if (!inputStream) {
|
||||
return { commands };
|
||||
}
|
||||
const commandsMap = new Map();
|
||||
for (const command of commands) {
|
||||
commandsMap.set(command.index.toString(), command);
|
||||
commandsMap.set(command.name, command);
|
||||
}
|
||||
Rx.fromEvent(inputStream, 'data')
|
||||
.pipe(map((data) => String(data)))
|
||||
.subscribe((data) => {
|
||||
const dataParts = data.split(/:(.+)/s);
|
||||
let target = dataParts[0];
|
||||
let command = commandsMap.get(target);
|
||||
let input;
|
||||
if (dataParts.length > 1 && command) {
|
||||
input = dataParts[1];
|
||||
}
|
||||
else {
|
||||
// If `target` does not match a registered command,
|
||||
// fallback to `defaultInputTarget` and forward the whole input data
|
||||
target = this.defaultInputTarget.toString();
|
||||
command = commandsMap.get(target);
|
||||
input = data;
|
||||
}
|
||||
if (command?.stdin) {
|
||||
command.stdin.write(input);
|
||||
}
|
||||
else {
|
||||
this.logger.logGlobalEvent(`Unable to find command "${target}", or it has no stdin open\n`);
|
||||
}
|
||||
});
|
||||
return {
|
||||
commands,
|
||||
onFinish: () => {
|
||||
if (this.pauseInputStreamOnFinish) {
|
||||
// https://github.com/kimmobrunfeldt/concurrently/issues/252
|
||||
inputStream.pause();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import EventEmitter from 'node:events';
|
||||
import { Command } from '../command.js';
|
||||
import { FlowController } from './flow-controller.js';
|
||||
/**
|
||||
* Watches the main concurrently process for signals and sends the same signal down to each spawned
|
||||
* command.
|
||||
*/
|
||||
export declare class KillOnSignal implements FlowController {
|
||||
private readonly process;
|
||||
private readonly abortController?;
|
||||
constructor({ process, abortController, }: {
|
||||
process: EventEmitter;
|
||||
abortController?: AbortController;
|
||||
});
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
onFinish: () => void;
|
||||
};
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
const SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
||||
/**
|
||||
* Watches the main concurrently process for signals and sends the same signal down to each spawned
|
||||
* command.
|
||||
*/
|
||||
export class KillOnSignal {
|
||||
process;
|
||||
abortController;
|
||||
constructor({ process, abortController, }) {
|
||||
this.process = process;
|
||||
this.abortController = abortController;
|
||||
}
|
||||
handle(commands) {
|
||||
let caughtSignal;
|
||||
const signalListener = (signal) => {
|
||||
caughtSignal = signal;
|
||||
this.abortController?.abort();
|
||||
commands.forEach((command) => command.kill(signal));
|
||||
};
|
||||
SIGNALS.forEach((signal) => this.process.on(signal, signalListener));
|
||||
return {
|
||||
commands: commands.map((command) => {
|
||||
const closeStream = command.close.pipe(map((exitInfo) => {
|
||||
const exitCode = caughtSignal === 'SIGINT' ? 0 : exitInfo.exitCode;
|
||||
return { ...exitInfo, exitCode };
|
||||
}));
|
||||
// Return a proxy so that mutations happen on the original Command object.
|
||||
// If either `Object.assign()` or `Object.create()` were used, it'd be hard to
|
||||
// reflect the mutations on Command objects referenced by previous flow controllers.
|
||||
return new Proxy(command, {
|
||||
get(target, prop) {
|
||||
return prop === 'close' ? closeStream : target[prop];
|
||||
},
|
||||
});
|
||||
}),
|
||||
onFinish: () => {
|
||||
// Avoids MaxListenersExceededWarning when running programmatically
|
||||
SIGNALS.forEach((signal) => this.process.off(signal, signalListener));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Command } from '../command.js';
|
||||
import { Logger } from '../logger.js';
|
||||
import { FlowController } from './flow-controller.js';
|
||||
export type ProcessCloseCondition = 'failure' | 'success';
|
||||
/**
|
||||
* Sends a SIGTERM signal to all commands when one of the commands exits with a matching condition.
|
||||
*/
|
||||
export declare class KillOthers implements FlowController {
|
||||
private readonly logger;
|
||||
private readonly abortController?;
|
||||
private readonly conditions;
|
||||
private readonly killSignal;
|
||||
private readonly timeoutMs?;
|
||||
constructor({ logger, abortController, conditions, killSignal, timeoutMs, }: {
|
||||
logger: Logger;
|
||||
abortController?: AbortController;
|
||||
conditions: ProcessCloseCondition | ProcessCloseCondition[];
|
||||
killSignal: string | undefined;
|
||||
timeoutMs?: number;
|
||||
});
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
};
|
||||
private maybeForceKill;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { Command } from '../command.js';
|
||||
import { castArray } from '../utils.js';
|
||||
/**
|
||||
* Sends a SIGTERM signal to all commands when one of the commands exits with a matching condition.
|
||||
*/
|
||||
export class KillOthers {
|
||||
logger;
|
||||
abortController;
|
||||
conditions;
|
||||
killSignal;
|
||||
timeoutMs;
|
||||
constructor({ logger, abortController, conditions, killSignal, timeoutMs, }) {
|
||||
this.logger = logger;
|
||||
this.abortController = abortController;
|
||||
this.conditions = castArray(conditions);
|
||||
this.killSignal = killSignal;
|
||||
this.timeoutMs = timeoutMs;
|
||||
}
|
||||
handle(commands) {
|
||||
const conditions = this.conditions.filter((condition) => condition === 'failure' || condition === 'success');
|
||||
if (!conditions.length) {
|
||||
return { commands };
|
||||
}
|
||||
const closeStates = commands.map((command) => command.close.pipe(map(({ exitCode }) => exitCode === 0 ? 'success' : 'failure'), filter((state) => conditions.includes(state))));
|
||||
closeStates.forEach((closeState) => closeState.subscribe(() => {
|
||||
this.abortController?.abort();
|
||||
const killableCommands = commands.filter((command) => Command.canKill(command));
|
||||
if (killableCommands.length) {
|
||||
this.logger.logGlobalEvent(`Sending ${this.killSignal || 'SIGTERM'} to other processes..`);
|
||||
killableCommands.forEach((command) => command.kill(this.killSignal));
|
||||
this.maybeForceKill(killableCommands);
|
||||
}
|
||||
}));
|
||||
return { commands };
|
||||
}
|
||||
maybeForceKill(commands) {
|
||||
// No need to force kill when the signal already is SIGKILL.
|
||||
if (!this.timeoutMs || this.killSignal === 'SIGKILL') {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
const killableCommands = commands.filter((command) => Command.canKill(command));
|
||||
if (killableCommands) {
|
||||
this.logger.logGlobalEvent(`Sending SIGKILL to ${killableCommands.length} processes..`);
|
||||
killableCommands.forEach((command) => command.kill('SIGKILL'));
|
||||
}
|
||||
}, this.timeoutMs).unref();
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { Command } from '../command.js';
|
||||
import { Logger } from '../logger.js';
|
||||
import { FlowController } from './flow-controller.js';
|
||||
/**
|
||||
* Logs when commands failed executing, e.g. due to the executable not existing in the system.
|
||||
*/
|
||||
export declare class LogError implements FlowController {
|
||||
private readonly logger;
|
||||
constructor({ logger }: {
|
||||
logger: Logger;
|
||||
});
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
};
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Logs when commands failed executing, e.g. due to the executable not existing in the system.
|
||||
*/
|
||||
export class LogError {
|
||||
logger;
|
||||
constructor({ logger }) {
|
||||
this.logger = logger;
|
||||
}
|
||||
handle(commands) {
|
||||
commands.forEach((command) => command.error.subscribe((event) => {
|
||||
this.logger.logCommandEvent(`Error occurred when executing command: ${command.command}`, command);
|
||||
const errorText = String(event instanceof Error ? event.stack || event : event);
|
||||
this.logger.logCommandEvent(errorText, command);
|
||||
}));
|
||||
return { commands };
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { Command } from '../command.js';
|
||||
import { Logger } from '../logger.js';
|
||||
import { FlowController } from './flow-controller.js';
|
||||
/**
|
||||
* Logs the exit code/signal of commands.
|
||||
*/
|
||||
export declare class LogExit implements FlowController {
|
||||
private readonly logger;
|
||||
constructor({ logger }: {
|
||||
logger: Logger;
|
||||
});
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
};
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Logs the exit code/signal of commands.
|
||||
*/
|
||||
export class LogExit {
|
||||
logger;
|
||||
constructor({ logger }) {
|
||||
this.logger = logger;
|
||||
}
|
||||
handle(commands) {
|
||||
commands.forEach((command) => command.close.subscribe(({ exitCode }) => {
|
||||
this.logger.logCommandEvent(`${command.command} exited with code ${exitCode}`, command);
|
||||
}));
|
||||
return { commands };
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { Command } from '../command.js';
|
||||
import { Logger } from '../logger.js';
|
||||
import { FlowController } from './flow-controller.js';
|
||||
/**
|
||||
* Logs the stdout and stderr output of commands.
|
||||
*/
|
||||
export declare class LogOutput implements FlowController {
|
||||
private readonly logger;
|
||||
constructor({ logger }: {
|
||||
logger: Logger;
|
||||
});
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
};
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Logs the stdout and stderr output of commands.
|
||||
*/
|
||||
export class LogOutput {
|
||||
logger;
|
||||
constructor({ logger }) {
|
||||
this.logger = logger;
|
||||
}
|
||||
handle(commands) {
|
||||
commands.forEach((command) => {
|
||||
command.stdout.subscribe((text) => this.logger.logCommandText(text.toString(), command));
|
||||
command.stderr.subscribe((text) => this.logger.logCommandText(text.toString(), command));
|
||||
});
|
||||
return { commands };
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { CloseEvent, Command } from '../command.js';
|
||||
import { Logger } from '../logger.js';
|
||||
import { FlowController } from './flow-controller.js';
|
||||
type TimingInfo = {
|
||||
name: string;
|
||||
duration: string;
|
||||
'exit code': string | number;
|
||||
killed: boolean;
|
||||
command: string;
|
||||
};
|
||||
/**
|
||||
* Logs timing information about commands as they start/stop and then a summary when all commands finish.
|
||||
*/
|
||||
export declare class LogTimings implements FlowController {
|
||||
static mapCloseEventToTimingInfo({ command, timings, killed, exitCode, }: CloseEvent): TimingInfo;
|
||||
private readonly logger?;
|
||||
private readonly dateFormatter;
|
||||
constructor({ logger, timestampFormat, }: {
|
||||
logger?: Logger;
|
||||
timestampFormat?: string;
|
||||
});
|
||||
private printExitInfoTimingTable;
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
onFinish?: undefined;
|
||||
} | {
|
||||
commands: Command[];
|
||||
onFinish: () => void;
|
||||
};
|
||||
}
|
||||
export {};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import assert from 'node:assert';
|
||||
import Rx from 'rxjs';
|
||||
import { bufferCount, combineLatestWith, take } from 'rxjs/operators';
|
||||
import { DateFormatter } from '../date-format.js';
|
||||
import * as defaults from '../defaults.js';
|
||||
/**
|
||||
* Logs timing information about commands as they start/stop and then a summary when all commands finish.
|
||||
*/
|
||||
export class LogTimings {
|
||||
static mapCloseEventToTimingInfo({ command, timings, killed, exitCode, }) {
|
||||
const readableDurationMs = (timings.endDate.getTime() - timings.startDate.getTime()).toLocaleString();
|
||||
return {
|
||||
name: command.name,
|
||||
duration: readableDurationMs,
|
||||
'exit code': exitCode,
|
||||
killed,
|
||||
command: command.command,
|
||||
};
|
||||
}
|
||||
logger;
|
||||
dateFormatter;
|
||||
constructor({ logger, timestampFormat = defaults.timestampFormat, }) {
|
||||
this.logger = logger;
|
||||
this.dateFormatter = new DateFormatter(timestampFormat);
|
||||
}
|
||||
printExitInfoTimingTable(exitInfos) {
|
||||
assert.ok(this.logger);
|
||||
const exitInfoTable = exitInfos
|
||||
.sort((a, b) => b.timings.durationSeconds - a.timings.durationSeconds)
|
||||
.map(LogTimings.mapCloseEventToTimingInfo);
|
||||
this.logger.logGlobalEvent('Timings:');
|
||||
this.logger.logTable(exitInfoTable);
|
||||
return exitInfos;
|
||||
}
|
||||
handle(commands) {
|
||||
const { logger } = this;
|
||||
if (!logger) {
|
||||
return { commands };
|
||||
}
|
||||
// individual process timings
|
||||
commands.forEach((command) => {
|
||||
command.timer.subscribe(({ startDate, endDate }) => {
|
||||
if (!endDate) {
|
||||
const formattedStartDate = this.dateFormatter.format(startDate);
|
||||
logger.logCommandEvent(`${command.command} started at ${formattedStartDate}`, command);
|
||||
}
|
||||
else {
|
||||
const durationMs = endDate.getTime() - startDate.getTime();
|
||||
const formattedEndDate = this.dateFormatter.format(endDate);
|
||||
logger.logCommandEvent(`${command.command} stopped at ${formattedEndDate} after ${durationMs.toLocaleString()}ms`, command);
|
||||
}
|
||||
});
|
||||
});
|
||||
// overall summary timings
|
||||
const closeStreams = commands.map((command) => command.close);
|
||||
const finished = new Rx.Subject();
|
||||
const allProcessesClosed = Rx.merge(...closeStreams).pipe(bufferCount(closeStreams.length), take(1), combineLatestWith(finished));
|
||||
allProcessesClosed.subscribe(([exitInfos]) => this.printExitInfoTimingTable(exitInfos));
|
||||
return { commands, onFinish: () => finished.next() };
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { Command } from '../command.js';
|
||||
import { Logger } from '../logger.js';
|
||||
import { FlowController } from './flow-controller.js';
|
||||
export declare class LoggerPadding implements FlowController {
|
||||
private readonly logger;
|
||||
constructor({ logger }: {
|
||||
logger: Logger;
|
||||
});
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
onFinish: () => void;
|
||||
};
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { COLOR_MARKER_RE } from '../logger.js';
|
||||
function visibleLength(value) {
|
||||
return value ? value.replace(COLOR_MARKER_RE, '').length : 0;
|
||||
}
|
||||
export class LoggerPadding {
|
||||
logger;
|
||||
constructor({ logger }) {
|
||||
this.logger = logger;
|
||||
}
|
||||
handle(commands) {
|
||||
// Sometimes there's limited concurrency, so not all commands will spawn straight away.
|
||||
// Compute the prefix length now, which works for all styles but those with a PID.
|
||||
let length = commands.reduce((length, command) => {
|
||||
const content = this.logger.getPrefixContent(command);
|
||||
return Math.max(length, visibleLength(content?.value));
|
||||
}, 0);
|
||||
this.logger.setPrefixLength(length);
|
||||
// The length of prefixes is somewhat stable, except for PIDs, which might change when a
|
||||
// process spawns (e.g. PIDs might look like 1, 10 or 100), therefore listen to command starts
|
||||
// and update the prefix length when this happens.
|
||||
const subs = commands.map((command) => command.timer.subscribe((event) => {
|
||||
if (!event.endDate) {
|
||||
const content = this.logger.getPrefixContent(command);
|
||||
length = Math.max(length, visibleLength(content?.value));
|
||||
this.logger.setPrefixLength(length);
|
||||
}
|
||||
}));
|
||||
return {
|
||||
commands,
|
||||
onFinish() {
|
||||
subs.forEach((sub) => sub.unsubscribe());
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { Writable } from 'node:stream';
|
||||
import { Command } from '../command.js';
|
||||
import { FlowController } from './flow-controller.js';
|
||||
/**
|
||||
* Kills processes and aborts further command spawning on output stream error (namely, SIGPIPE).
|
||||
*/
|
||||
export declare class OutputErrorHandler implements FlowController {
|
||||
private readonly outputStream;
|
||||
private readonly abortController;
|
||||
constructor({ abortController, outputStream, }: {
|
||||
abortController: AbortController;
|
||||
outputStream: Writable;
|
||||
});
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
onFinish: () => void;
|
||||
};
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { fromSharedEvent } from '../observables.js';
|
||||
/**
|
||||
* Kills processes and aborts further command spawning on output stream error (namely, SIGPIPE).
|
||||
*/
|
||||
export class OutputErrorHandler {
|
||||
outputStream;
|
||||
abortController;
|
||||
constructor({ abortController, outputStream, }) {
|
||||
this.abortController = abortController;
|
||||
this.outputStream = outputStream;
|
||||
}
|
||||
handle(commands) {
|
||||
const subscription = fromSharedEvent(this.outputStream, 'error').subscribe(() => {
|
||||
commands.forEach((command) => command.kill());
|
||||
// Avoid further commands from spawning, e.g. if `RestartProcess` is used.
|
||||
this.abortController.abort();
|
||||
});
|
||||
return {
|
||||
commands,
|
||||
onFinish: () => subscription.unsubscribe(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import Rx from 'rxjs';
|
||||
import { Command } from '../command.js';
|
||||
import { Logger } from '../logger.js';
|
||||
import { FlowController } from './flow-controller.js';
|
||||
export type RestartDelay = number | 'exponential';
|
||||
/**
|
||||
* Restarts commands that fail up to a defined number of times.
|
||||
*/
|
||||
export declare class RestartProcess implements FlowController {
|
||||
private readonly logger;
|
||||
private readonly scheduler?;
|
||||
private readonly delay;
|
||||
readonly tries: number;
|
||||
constructor({ delay, tries, logger, scheduler, }: {
|
||||
delay?: RestartDelay;
|
||||
tries?: number;
|
||||
logger: Logger;
|
||||
scheduler?: Rx.SchedulerLike;
|
||||
});
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
};
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import Rx from 'rxjs';
|
||||
import { defaultIfEmpty, delayWhen, filter, map, skip, take, takeWhile } from 'rxjs/operators';
|
||||
import * as defaults from '../defaults.js';
|
||||
/**
|
||||
* Restarts commands that fail up to a defined number of times.
|
||||
*/
|
||||
export class RestartProcess {
|
||||
logger;
|
||||
scheduler;
|
||||
delay;
|
||||
tries;
|
||||
constructor({ delay, tries, logger, scheduler, }) {
|
||||
this.logger = logger;
|
||||
this.delay = delay ?? 0;
|
||||
this.tries = tries != null ? +tries : defaults.restartTries;
|
||||
this.tries = this.tries < 0 ? Infinity : this.tries;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
handle(commands) {
|
||||
if (this.tries === 0) {
|
||||
return { commands };
|
||||
}
|
||||
const delayOperator = delayWhen((_, index) => {
|
||||
const { delay } = this;
|
||||
const value = delay === 'exponential' ? 2 ** index * 1000 : delay;
|
||||
return Rx.timer(value, this.scheduler);
|
||||
});
|
||||
commands
|
||||
.map((command) => command.close.pipe(take(this.tries), takeWhile(({ exitCode }) => exitCode !== 0)))
|
||||
.forEach((failure, index) => Rx.merge(
|
||||
// Delay the emission (so that the restarts happen on time),
|
||||
// explicitly telling the subscriber that a restart is needed
|
||||
failure.pipe(delayOperator, map(() => true)),
|
||||
// Skip the first N emissions (as these would be duplicates of the above),
|
||||
// meaning it will be empty because of success, or failed all N times,
|
||||
// and no more restarts should be attempted.
|
||||
failure.pipe(skip(this.tries), map(() => false), defaultIfEmpty(false))).subscribe((restart) => {
|
||||
const command = commands[index];
|
||||
if (restart) {
|
||||
this.logger.logCommandEvent(`${command.command} restarted`, command);
|
||||
command.start();
|
||||
}
|
||||
}));
|
||||
return {
|
||||
commands: commands.map((command) => {
|
||||
const closeStream = command.close.pipe(filter(({ exitCode }, emission) => {
|
||||
// We let all success codes pass, and failures only after restarting won't happen again
|
||||
return exitCode === 0 || emission >= this.tries;
|
||||
}));
|
||||
// Return a proxy so that mutations happen on the original Command object.
|
||||
// If either `Object.assign()` or `Object.create()` were used, it'd be hard to
|
||||
// reflect the mutations on Command objects referenced by previous flow controllers.
|
||||
return new Proxy(command, {
|
||||
get(target, prop) {
|
||||
return prop === 'close' ? closeStream : target[prop];
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { Command, SpawnCommand } from '../command.js';
|
||||
import { Logger } from '../logger.js';
|
||||
import { FlowController } from './flow-controller.js';
|
||||
export declare class Teardown implements FlowController {
|
||||
private readonly logger;
|
||||
private readonly spawn;
|
||||
private readonly teardown;
|
||||
constructor({ logger, spawn, commands, }: {
|
||||
logger: Logger;
|
||||
/**
|
||||
* Which function to use to spawn commands.
|
||||
*/
|
||||
spawn: SpawnCommand;
|
||||
commands: readonly string[];
|
||||
});
|
||||
handle(commands: Command[]): {
|
||||
commands: Command[];
|
||||
onFinish: () => Promise<void>;
|
||||
};
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import Rx from 'rxjs';
|
||||
import { getSpawnOpts } from '../spawn.js';
|
||||
export class Teardown {
|
||||
logger;
|
||||
spawn;
|
||||
teardown;
|
||||
constructor({ logger, spawn, commands, }) {
|
||||
this.logger = logger;
|
||||
this.spawn = spawn;
|
||||
this.teardown = commands;
|
||||
}
|
||||
handle(commands) {
|
||||
const { logger, teardown, spawn } = this;
|
||||
const onFinish = async () => {
|
||||
if (!teardown.length) {
|
||||
return;
|
||||
}
|
||||
for (const command of teardown) {
|
||||
logger.logGlobalEvent(`Running teardown command "${command}"`);
|
||||
const child = spawn(command, getSpawnOpts({ stdio: 'raw' }));
|
||||
const error = Rx.fromEvent(child, 'error');
|
||||
const close = Rx.fromEvent(child, 'close');
|
||||
try {
|
||||
const [exitCode, signal] = await Promise.race([
|
||||
Rx.firstValueFrom(error).then((event) => {
|
||||
throw event;
|
||||
}),
|
||||
Rx.firstValueFrom(close).then((event) => event),
|
||||
]);
|
||||
logger.logGlobalEvent(`Teardown command "${command}" exited with code ${exitCode ?? signal}`);
|
||||
if (signal === 'SIGINT') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const errorText = String(error instanceof Error ? error.stack || error : error);
|
||||
logger.logGlobalEvent(`Teardown command "${command}" errored:`);
|
||||
logger.logGlobalEvent(errorText);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
return { commands, onFinish };
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { Readable } from 'node:stream';
|
||||
import { CloseEvent, Command, CommandIdentifier, TimerEvent } from './command.js';
|
||||
import { concurrently as createConcurrently, ConcurrentlyCommandInput, ConcurrentlyOptions as BaseConcurrentlyOptions, ConcurrentlyResult } from './concurrently.js';
|
||||
import type { FlowController } from './flow-control/flow-controller.js';
|
||||
import { InputHandler } from './flow-control/input-handler.js';
|
||||
import { KillOnSignal } from './flow-control/kill-on-signal.js';
|
||||
import { KillOthers, ProcessCloseCondition } from './flow-control/kill-others.js';
|
||||
import { LogError } from './flow-control/log-error.js';
|
||||
import { LogExit } from './flow-control/log-exit.js';
|
||||
import { LogOutput } from './flow-control/log-output.js';
|
||||
import { LogTimings } from './flow-control/log-timings.js';
|
||||
import { RestartDelay, RestartProcess } from './flow-control/restart-process.js';
|
||||
import { Logger } from './logger.js';
|
||||
export type ConcurrentlyOptions = Omit<BaseConcurrentlyOptions, 'abortSignal' | 'hide' | 'spawn'> & {
|
||||
/**
|
||||
* Which command(s) should have their output hidden.
|
||||
*/
|
||||
hide?: CommandIdentifier | CommandIdentifier[];
|
||||
/**
|
||||
* The prefix format to use when logging a command's output.
|
||||
* Defaults to the command's index.
|
||||
*/
|
||||
prefix?: string;
|
||||
/**
|
||||
* How many characters should a prefix have at most, used when the prefix format is `command`.
|
||||
*/
|
||||
prefixLength?: number;
|
||||
/**
|
||||
* Pads short prefixes with spaces so that all prefixes have the same length.
|
||||
*/
|
||||
padPrefix?: boolean;
|
||||
/**
|
||||
* Whether output should be formatted to include prefixes and whether "event" logs will be logged.
|
||||
*/
|
||||
raw?: boolean;
|
||||
/**
|
||||
* Date format used when logging date/time.
|
||||
* @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
|
||||
*/
|
||||
timestampFormat?: string;
|
||||
defaultInputTarget?: CommandIdentifier;
|
||||
inputStream?: Readable;
|
||||
handleInput?: boolean;
|
||||
pauseInputStreamOnFinish?: boolean;
|
||||
/**
|
||||
* How much time in milliseconds to wait before restarting a command.
|
||||
*
|
||||
* @see RestartProcess
|
||||
*/
|
||||
restartDelay?: RestartDelay;
|
||||
/**
|
||||
* How many times commands should be restarted when they exit with a failure.
|
||||
*
|
||||
* @see RestartProcess
|
||||
*/
|
||||
restartTries?: number;
|
||||
/**
|
||||
* Once the first command exits with one of these statuses, kill other commands.
|
||||
* @see KillOthers
|
||||
*/
|
||||
killOthersOn?: ProcessCloseCondition | ProcessCloseCondition[];
|
||||
/**
|
||||
* Signal to send to killed processes.
|
||||
*/
|
||||
killSignal?: string;
|
||||
/**
|
||||
* How many milliseconds to wait before killing processes.
|
||||
*/
|
||||
killTimeout?: number;
|
||||
/**
|
||||
* Whether to output timing information for processes.
|
||||
*
|
||||
* @see LogTimings
|
||||
*/
|
||||
timings?: boolean;
|
||||
/**
|
||||
* Clean up command(s) to execute before exiting concurrently.
|
||||
* These won't be prefixed and don't affect concurrently's exit code.
|
||||
*/
|
||||
teardown?: readonly string[];
|
||||
/**
|
||||
* List of additional arguments passed that will get replaced in each command.
|
||||
* If not defined, no argument replacing will happen.
|
||||
*/
|
||||
additionalArguments?: string[];
|
||||
/**
|
||||
* Shell executable used to run command strings.
|
||||
* When unset, uses the `npm_config_script_shell` env variable if present. Otherwise, falls back
|
||||
* to `cmd.exe` on Windows, and `/bin/sh` elsewhere.
|
||||
*/
|
||||
shell?: string;
|
||||
};
|
||||
export declare function concurrently(commands: ConcurrentlyCommandInput[], options?: Partial<ConcurrentlyOptions>): ConcurrentlyResult;
|
||||
export default concurrently;
|
||||
export { ConcurrentlyCommandInput, ConcurrentlyResult, createConcurrently, Logger };
|
||||
export { CloseEvent, Command, CommandIdentifier, TimerEvent };
|
||||
export { FlowController, InputHandler, KillOnSignal, KillOthers, LogError, LogExit, LogOutput, LogTimings, RestartProcess, };
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import process from 'node:process';
|
||||
import { assertNotRuntime } from './assert.js';
|
||||
import { Command } from './command.js';
|
||||
import { concurrently as createConcurrently, } from './concurrently.js';
|
||||
import { InputHandler } from './flow-control/input-handler.js';
|
||||
import { KillOnSignal } from './flow-control/kill-on-signal.js';
|
||||
import { KillOthers } from './flow-control/kill-others.js';
|
||||
import { LogError } from './flow-control/log-error.js';
|
||||
import { LogExit } from './flow-control/log-exit.js';
|
||||
import { LogOutput } from './flow-control/log-output.js';
|
||||
import { LogTimings } from './flow-control/log-timings.js';
|
||||
import { LoggerPadding } from './flow-control/logger-padding.js';
|
||||
import { OutputErrorHandler } from './flow-control/output-error-handler.js';
|
||||
import { RestartProcess } from './flow-control/restart-process.js';
|
||||
import { Teardown } from './flow-control/teardown.js';
|
||||
import { Logger } from './logger.js';
|
||||
import { createSpawn } from './spawn.js';
|
||||
import { castArray } from './utils.js';
|
||||
export function concurrently(commands, options = {}) {
|
||||
assertNotRuntime(
|
||||
// When run via /snap/bin/node, process.execPath maps to the actual snap path, but it also sets
|
||||
// several SNAP_* env variables. If the snap is run directly via e.g. /snap/node/current/bin/node,
|
||||
// the issues don't happen and no env variables are set.
|
||||
!String(process.env.SNAP).startsWith('/snap'), 'Snap', 'Snap confinement can interfere with spawning child processes. See issue #584');
|
||||
// To avoid empty strings from hiding the output of commands that don't have a name,
|
||||
// keep in the list of commands to hide only strings with some length.
|
||||
// This might happen through the CLI when no `--hide` argument is specified, for example.
|
||||
const hide = castArray(options.hide).filter((id) => id || id === 0);
|
||||
const logger = options.logger ||
|
||||
new Logger({
|
||||
hide,
|
||||
prefixFormat: options.prefix,
|
||||
commandLength: options.prefixLength,
|
||||
raw: options.raw,
|
||||
timestampFormat: options.timestampFormat,
|
||||
});
|
||||
if (options.prefixColors === false) {
|
||||
logger.toggleColors(false);
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
const outputStream = options.outputStream || process.stdout;
|
||||
const spawn = createSpawn(options.shell);
|
||||
return createConcurrently(commands, {
|
||||
maxProcesses: options.maxProcesses,
|
||||
raw: options.raw,
|
||||
successCondition: options.successCondition,
|
||||
cwd: options.cwd,
|
||||
spawn,
|
||||
hide,
|
||||
logger,
|
||||
outputStream,
|
||||
group: options.group,
|
||||
abortSignal: abortController.signal,
|
||||
controllers: [
|
||||
// LoggerPadding needs to run before any other controllers that might output something
|
||||
...(options.padPrefix ? [new LoggerPadding({ logger })] : []),
|
||||
new LogError({ logger }),
|
||||
new LogOutput({ logger }),
|
||||
new LogExit({ logger }),
|
||||
new InputHandler({
|
||||
logger,
|
||||
defaultInputTarget: options.defaultInputTarget,
|
||||
inputStream: options.inputStream || (options.handleInput ? process.stdin : undefined),
|
||||
pauseInputStreamOnFinish: options.pauseInputStreamOnFinish,
|
||||
}),
|
||||
new KillOnSignal({ process, abortController }),
|
||||
new RestartProcess({
|
||||
logger,
|
||||
delay: options.restartDelay,
|
||||
tries: options.restartTries,
|
||||
}),
|
||||
new KillOthers({
|
||||
logger,
|
||||
conditions: options.killOthersOn || [],
|
||||
timeoutMs: options.killTimeout,
|
||||
killSignal: options.killSignal,
|
||||
abortController,
|
||||
}),
|
||||
new OutputErrorHandler({ abortController, outputStream }),
|
||||
new LogTimings({
|
||||
logger: options.timings ? logger : undefined,
|
||||
timestampFormat: options.timestampFormat,
|
||||
}),
|
||||
new Teardown({ logger, spawn, commands: options.teardown || [] }),
|
||||
],
|
||||
prefixColors: options.prefixColors || [],
|
||||
additionalArguments: options.additionalArguments,
|
||||
});
|
||||
}
|
||||
// Export all flow controllers, types, and the main concurrently function,
|
||||
// so that 3rd-parties can use them however they want
|
||||
// Main
|
||||
export default concurrently;
|
||||
export { createConcurrently, Logger };
|
||||
// Command specific
|
||||
export { Command };
|
||||
// Flow controllers
|
||||
export { InputHandler, KillOnSignal, KillOthers, LogError, LogExit, LogOutput, LogTimings, RestartProcess, };
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
declare const JSONC: {
|
||||
parse: (text: string) => any;
|
||||
stringify: {
|
||||
(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string;
|
||||
(value: any, replacer?: (number | string)[] | null, space?: string | number): string;
|
||||
};
|
||||
};
|
||||
export default JSONC;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
ORIGINAL https://www.npmjs.com/package/tiny-jsonc
|
||||
BY Fabio Spampinato
|
||||
MIT license
|
||||
|
||||
Copied due to the dependency not being compatible with CommonJS
|
||||
*/
|
||||
const stringOrCommentRe = /("(?:\\?[\s\S])*?")|(\/\/.*)|(\/\*[\s\S]*?\*\/)/g;
|
||||
const stringOrTrailingCommaRe = /("(?:\\?[\s\S])*?")|(,\s*)(?=\]|\})/g;
|
||||
const JSONC = {
|
||||
parse: (text) => {
|
||||
text = String(text); // To be extra safe
|
||||
try {
|
||||
// Fast path for valid JSON
|
||||
return JSON.parse(text);
|
||||
}
|
||||
catch {
|
||||
// Slow path for JSONC and invalid inputs
|
||||
return JSON.parse(text.replace(stringOrCommentRe, '$1').replace(stringOrTrailingCommaRe, '$1'));
|
||||
}
|
||||
},
|
||||
stringify: JSON.stringify,
|
||||
};
|
||||
export default JSONC;
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import Rx from 'rxjs';
|
||||
import { Command, CommandIdentifier } from './command.js';
|
||||
export declare const COLOR_MARKER_RE: RegExp;
|
||||
export declare class Logger {
|
||||
private readonly hide;
|
||||
private readonly raw;
|
||||
private readonly prefixFormat?;
|
||||
private readonly commandLength;
|
||||
private readonly dateFormatter;
|
||||
private chalk;
|
||||
/**
|
||||
* How many characters should a prefix have.
|
||||
* Prefixes shorter than this will be padded with spaces to the right.
|
||||
*/
|
||||
private prefixLength;
|
||||
/**
|
||||
* Last character emitted, and from which command.
|
||||
* If `undefined`, then nothing has been logged yet.
|
||||
*/
|
||||
private lastWrite?;
|
||||
/**
|
||||
* Observable that emits when there's been output logged.
|
||||
* If `command` is is `undefined`, then the log is for a global event.
|
||||
*/
|
||||
readonly output: Rx.Subject<{
|
||||
command: Command | undefined;
|
||||
text: string;
|
||||
}>;
|
||||
constructor({ hide, prefixFormat, commandLength, raw, timestampFormat, }: {
|
||||
/**
|
||||
* Which commands should have their output hidden.
|
||||
*/
|
||||
hide?: CommandIdentifier[];
|
||||
/**
|
||||
* Whether output should be formatted to include prefixes and whether "event" logs will be
|
||||
* logged.
|
||||
*/
|
||||
raw?: boolean;
|
||||
/**
|
||||
* The prefix format to use when logging a command's output.
|
||||
* Defaults to the command's index.
|
||||
*/
|
||||
prefixFormat?: string;
|
||||
/**
|
||||
* How many characters should a prefix have at most when the format is `command`.
|
||||
*/
|
||||
commandLength?: number;
|
||||
/**
|
||||
* Date format used when logging date/time.
|
||||
* @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
|
||||
*/
|
||||
timestampFormat?: string;
|
||||
});
|
||||
/**
|
||||
* Toggles colors on/off globally.
|
||||
*/
|
||||
toggleColors(on: boolean): void;
|
||||
private shortenText;
|
||||
private getPrefixesFor;
|
||||
getPrefixContent(command: Command): {
|
||||
type: 'default' | 'template';
|
||||
value: string;
|
||||
} | undefined;
|
||||
getPrefix(command: Command): string;
|
||||
setPrefixLength(length: number): void;
|
||||
colorText(command: Command, text: string): string;
|
||||
/**
|
||||
* Logs an event for a command (e.g. start, stop).
|
||||
*
|
||||
* If raw mode is on, then nothing is logged.
|
||||
*/
|
||||
logCommandEvent(text: string, command: Command): void;
|
||||
logCommandText(text: string, command: Command): void;
|
||||
/**
|
||||
* Logs a global event (e.g. sending signals to processes).
|
||||
*
|
||||
* If raw mode is on, then nothing is logged.
|
||||
*/
|
||||
logGlobalEvent(text: string): void;
|
||||
/**
|
||||
* Logs a table from an input object array, like `console.table`.
|
||||
*
|
||||
* Each row is a single input item, and they are presented in the input order.
|
||||
*/
|
||||
logTable(tableContents: Record<string, unknown>[]): void;
|
||||
log(prefix: string, text: string, command?: Command): void;
|
||||
emit(command: Command | undefined, text: string): void;
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
import chalk, { Chalk } from 'chalk';
|
||||
import Rx from 'rxjs';
|
||||
import { DateFormatter } from './date-format.js';
|
||||
import * as defaults from './defaults.js';
|
||||
import { escapeRegExp, splitOutsideParens } from './utils.js';
|
||||
const defaultChalk = chalk;
|
||||
const noColorChalk = new Chalk({ level: 0 });
|
||||
const HEX_PATTERN = /^#[0-9A-Fa-f]{3,6}$/;
|
||||
const COLOR_OPEN = '{color}';
|
||||
const COLOR_CLOSE = '{/color}';
|
||||
export const COLOR_MARKER_RE = /\{\/?color\}/g;
|
||||
/**
|
||||
* Applies a single color segment to a chalk instance.
|
||||
* Handles: function calls (hex, bgHex, rgb, bgRgb, ansi256, bgAnsi256, etc.),
|
||||
* shorthands (#HEX, bg#HEX), and named colors/modifiers.
|
||||
*/
|
||||
function applySegment(color, segment) {
|
||||
// Function call: name(args) - handles chalk color functions
|
||||
const fnMatch = segment.match(/^(\w+)\((.+)\)$/);
|
||||
if (fnMatch) {
|
||||
const [, fnName, argsStr] = fnMatch;
|
||||
const args = argsStr.split(',').map((a) => {
|
||||
const t = a.trim();
|
||||
return /^\d+$/.test(t) ? parseInt(t, 10) : t;
|
||||
});
|
||||
// Explicit function calls for known chalk color functions
|
||||
switch (fnName) {
|
||||
case 'rgb':
|
||||
return color.rgb(args[0], args[1], args[2]);
|
||||
case 'bgRgb':
|
||||
return color.bgRgb(args[0], args[1], args[2]);
|
||||
case 'hex':
|
||||
if (!HEX_PATTERN.test(args[0]))
|
||||
return undefined;
|
||||
return color.hex(args[0]);
|
||||
case 'bgHex':
|
||||
if (!HEX_PATTERN.test(args[0]))
|
||||
return undefined;
|
||||
return color.bgHex(args[0]);
|
||||
case 'ansi256':
|
||||
return color.ansi256(args[0]);
|
||||
case 'bgAnsi256':
|
||||
return color.bgAnsi256(args[0]);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
// Shorthands
|
||||
if (segment.startsWith('bg#'))
|
||||
return color.bgHex(segment.slice(2));
|
||||
if (segment.startsWith('#'))
|
||||
return color.hex(segment);
|
||||
// Property: black, bold, dim, etc.
|
||||
return color[segment] ?? undefined;
|
||||
}
|
||||
/**
|
||||
* Applies a color string to chalk, supporting chained colors and modifiers.
|
||||
* Returns undefined if any segment is invalid (triggers fallback to default).
|
||||
*/
|
||||
function applyColor(chalkInstance, colorString) {
|
||||
const segments = splitOutsideParens(colorString, '.');
|
||||
if (segments.length === 0)
|
||||
return undefined;
|
||||
let color = chalkInstance;
|
||||
for (const segment of segments) {
|
||||
const next = applySegment(color, segment);
|
||||
if (!next)
|
||||
return undefined;
|
||||
color = next;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
export class Logger {
|
||||
hide;
|
||||
raw;
|
||||
prefixFormat;
|
||||
commandLength;
|
||||
dateFormatter;
|
||||
chalk = defaultChalk;
|
||||
/**
|
||||
* How many characters should a prefix have.
|
||||
* Prefixes shorter than this will be padded with spaces to the right.
|
||||
*/
|
||||
prefixLength = 0;
|
||||
/**
|
||||
* Last character emitted, and from which command.
|
||||
* If `undefined`, then nothing has been logged yet.
|
||||
*/
|
||||
lastWrite;
|
||||
/**
|
||||
* Observable that emits when there's been output logged.
|
||||
* If `command` is is `undefined`, then the log is for a global event.
|
||||
*/
|
||||
output = new Rx.Subject();
|
||||
constructor({ hide, prefixFormat, commandLength, raw = false, timestampFormat, }) {
|
||||
this.hide = (hide || []).map(String);
|
||||
this.raw = raw;
|
||||
this.prefixFormat = prefixFormat;
|
||||
this.commandLength = commandLength || defaults.prefixLength;
|
||||
this.dateFormatter = new DateFormatter(timestampFormat || defaults.timestampFormat);
|
||||
}
|
||||
/**
|
||||
* Toggles colors on/off globally.
|
||||
*/
|
||||
toggleColors(on) {
|
||||
this.chalk = on ? defaultChalk : noColorChalk;
|
||||
}
|
||||
shortenText(text) {
|
||||
if (!text || text.length <= this.commandLength) {
|
||||
return text;
|
||||
}
|
||||
const ellipsis = '..';
|
||||
const prefixLength = this.commandLength - ellipsis.length;
|
||||
const endLength = Math.floor(prefixLength / 2);
|
||||
const beginningLength = prefixLength - endLength;
|
||||
const beginning = text.slice(0, beginningLength);
|
||||
const end = text.slice(text.length - endLength, text.length);
|
||||
return beginning + ellipsis + end;
|
||||
}
|
||||
getPrefixesFor(command) {
|
||||
return {
|
||||
// When there's limited concurrency, the PID might not be immediately available,
|
||||
// so avoid the string 'undefined' from becoming a prefix
|
||||
pid: command.pid != null ? String(command.pid) : '',
|
||||
index: String(command.index),
|
||||
name: command.name,
|
||||
command: this.shortenText(command.command),
|
||||
time: this.dateFormatter.format(new Date()),
|
||||
};
|
||||
}
|
||||
getPrefixContent(command) {
|
||||
const prefix = this.prefixFormat || (command.name ? 'name' : 'index');
|
||||
if (prefix === 'none') {
|
||||
return;
|
||||
}
|
||||
const prefixes = this.getPrefixesFor(command);
|
||||
if (Object.keys(prefixes).includes(prefix)) {
|
||||
return { type: 'default', value: prefixes[prefix] };
|
||||
}
|
||||
const value = Object.entries(prefixes).reduce((prev, [key, val]) => {
|
||||
const keyRegex = new RegExp(escapeRegExp(`{${key}}`), 'g');
|
||||
return prev.replace(keyRegex, String(val));
|
||||
}, prefix);
|
||||
return { type: 'template', value };
|
||||
}
|
||||
getPrefix(command) {
|
||||
const content = this.getPrefixContent(command);
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
const visibleLength = content.value.replace(COLOR_MARKER_RE, '').length;
|
||||
const padding = ' '.repeat(Math.max(0, this.prefixLength - visibleLength));
|
||||
return content.type === 'template'
|
||||
? content.value + padding
|
||||
: `[${content.value}${padding}]`;
|
||||
}
|
||||
setPrefixLength(length) {
|
||||
this.prefixLength = length;
|
||||
}
|
||||
colorText(command, text) {
|
||||
const prefixColor = command.prefixColor ?? '';
|
||||
const defaultColor = applyColor(this.chalk, defaults.prefixColors) ?? this.chalk.reset;
|
||||
const color = applyColor(this.chalk, prefixColor) ?? defaultColor;
|
||||
// Segment the text around `{color}` / `{/color}` markers and only apply `color`
|
||||
// inside opened regions. If either marker is missing, it's implicitly added to
|
||||
// the start or end respectively — so a marker-free input stays fully colored,
|
||||
// preserving backward compatibility.
|
||||
let normalized = text;
|
||||
if (!normalized.includes(COLOR_OPEN))
|
||||
normalized = COLOR_OPEN + normalized;
|
||||
if (!normalized.includes(COLOR_CLOSE))
|
||||
normalized = normalized + COLOR_CLOSE;
|
||||
let output = '';
|
||||
let rest = normalized;
|
||||
let inColorRegion = false;
|
||||
while (rest.length > 0) {
|
||||
const marker = inColorRegion ? COLOR_CLOSE : COLOR_OPEN;
|
||||
const idx = rest.indexOf(marker);
|
||||
if (idx === -1) {
|
||||
// Tail after the last closing marker: normalization guarantees a
|
||||
// `{/color}` exists, so once opened a region always finds its close —
|
||||
// reaching here implies `inColorRegion` is false and the tail is plain.
|
||||
output += rest;
|
||||
break;
|
||||
}
|
||||
const segment = rest.slice(0, idx);
|
||||
output += inColorRegion ? color(segment) : segment;
|
||||
rest = rest.slice(idx + marker.length);
|
||||
inColorRegion = !inColorRegion;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
/**
|
||||
* Logs an event for a command (e.g. start, stop).
|
||||
*
|
||||
* If raw mode is on, then nothing is logged.
|
||||
*/
|
||||
logCommandEvent(text, command) {
|
||||
if (this.raw) {
|
||||
return;
|
||||
}
|
||||
// Last write was from this command, but it didn't end with a line feed.
|
||||
// Prepend one, otherwise the event's text will be concatenated to that write.
|
||||
// A line feed is otherwise inserted anyway.
|
||||
let prefix = '';
|
||||
if (this.lastWrite?.command === command && this.lastWrite.char !== '\n') {
|
||||
prefix = '\n';
|
||||
}
|
||||
this.logCommandText(`${prefix}${this.chalk.reset(text)}\n`, command);
|
||||
}
|
||||
logCommandText(text, command) {
|
||||
if (this.hide.includes(String(command.index)) || this.hide.includes(command.name)) {
|
||||
return;
|
||||
}
|
||||
const prefix = this.colorText(command, this.getPrefix(command));
|
||||
return this.log(prefix + (prefix ? ' ' : ''), text, command);
|
||||
}
|
||||
/**
|
||||
* Logs a global event (e.g. sending signals to processes).
|
||||
*
|
||||
* If raw mode is on, then nothing is logged.
|
||||
*/
|
||||
logGlobalEvent(text) {
|
||||
if (this.raw) {
|
||||
return;
|
||||
}
|
||||
this.log(`${this.chalk.reset('-->')} `, `${this.chalk.reset(text)}\n`);
|
||||
}
|
||||
/**
|
||||
* Logs a table from an input object array, like `console.table`.
|
||||
*
|
||||
* Each row is a single input item, and they are presented in the input order.
|
||||
*/
|
||||
logTable(tableContents) {
|
||||
// For now, can only print array tables with some content.
|
||||
if (this.raw || !Array.isArray(tableContents) || !tableContents.length) {
|
||||
return;
|
||||
}
|
||||
let nextColIndex = 0;
|
||||
const headers = {};
|
||||
const contentRows = tableContents.map((row) => {
|
||||
const rowContents = [];
|
||||
Object.keys(row).forEach((col) => {
|
||||
if (!headers[col]) {
|
||||
headers[col] = {
|
||||
index: nextColIndex++,
|
||||
length: col.length,
|
||||
};
|
||||
}
|
||||
const colIndex = headers[col].index;
|
||||
const formattedValue = String(row[col] == null ? '' : row[col]);
|
||||
// Update the column length in case this rows value is longer than the previous length for the column.
|
||||
headers[col].length = Math.max(formattedValue.length, headers[col].length);
|
||||
rowContents[colIndex] = formattedValue;
|
||||
return rowContents;
|
||||
});
|
||||
return rowContents;
|
||||
});
|
||||
const headersFormatted = Object.keys(headers).map((header) => header.padEnd(headers[header].length, ' '));
|
||||
if (!headersFormatted.length) {
|
||||
// No columns exist.
|
||||
return;
|
||||
}
|
||||
const borderRowFormatted = headersFormatted.map((header) => '─'.padEnd(header.length, '─'));
|
||||
this.logGlobalEvent(`┌─${borderRowFormatted.join('─┬─')}─┐`);
|
||||
this.logGlobalEvent(`│ ${headersFormatted.join(' │ ')} │`);
|
||||
this.logGlobalEvent(`├─${borderRowFormatted.join('─┼─')}─┤`);
|
||||
contentRows.forEach((contentRow) => {
|
||||
const contentRowFormatted = headersFormatted.map((header, colIndex) => {
|
||||
// If the table was expanded after this row was processed, it won't have this column.
|
||||
// Use an empty string in this case.
|
||||
const col = contentRow[colIndex] || '';
|
||||
return col.padEnd(header.length, ' ');
|
||||
});
|
||||
this.logGlobalEvent(`│ ${contentRowFormatted.join(' │ ')} │`);
|
||||
});
|
||||
this.logGlobalEvent(`└─${borderRowFormatted.join('─┴─')}─┘`);
|
||||
}
|
||||
log(prefix, text, command) {
|
||||
if (this.raw) {
|
||||
return this.emit(command, text);
|
||||
}
|
||||
// #70 - replace some ANSI code that would impact clearing lines
|
||||
text = text.replace(/\u2026/g, '...');
|
||||
// This write's interrupting another command, emit a line feed to start clean.
|
||||
if (this.lastWrite && this.lastWrite.command !== command && this.lastWrite.char !== '\n') {
|
||||
this.emit(this.lastWrite.command, '\n');
|
||||
}
|
||||
// Clean lines should emit a prefix
|
||||
if (!this.lastWrite || this.lastWrite.char === '\n') {
|
||||
this.emit(command, prefix);
|
||||
}
|
||||
const textToWrite = text.replaceAll('\n', (lf, i) => lf + (text[i + 1] ? prefix : ''));
|
||||
this.emit(command, textToWrite);
|
||||
}
|
||||
emit(command, text) {
|
||||
this.lastWrite = { command, char: text[text.length - 1] };
|
||||
this.output.next({ command, text });
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import EventEmitter from 'node:events';
|
||||
import { Observable } from 'rxjs';
|
||||
/**
|
||||
* Creates an observable for a specific event of an `EventEmitter` instance.
|
||||
*
|
||||
* The underlying event listener is set up only once across the application for that event emitter/name pair.
|
||||
*/
|
||||
export declare function fromSharedEvent(emitter: EventEmitter, event: string): Observable<unknown>;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { fromEvent, share } from 'rxjs';
|
||||
const sharedEvents = new WeakMap();
|
||||
/**
|
||||
* Creates an observable for a specific event of an `EventEmitter` instance.
|
||||
*
|
||||
* The underlying event listener is set up only once across the application for that event emitter/name pair.
|
||||
*/
|
||||
export function fromSharedEvent(emitter, event) {
|
||||
let emitterEvents = sharedEvents.get(emitter);
|
||||
if (!emitterEvents) {
|
||||
emitterEvents = new Map();
|
||||
sharedEvents.set(emitter, emitterEvents);
|
||||
}
|
||||
let observable = emitterEvents.get(event);
|
||||
if (!observable) {
|
||||
observable = fromEvent(emitter, event).pipe(share());
|
||||
emitterEvents.set(event, observable);
|
||||
}
|
||||
return observable;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { Writable } from 'node:stream';
|
||||
import Rx from 'rxjs';
|
||||
import { Command } from './command.js';
|
||||
/**
|
||||
* Class responsible for actually writing output onto a writable stream.
|
||||
*/
|
||||
export declare class OutputWriter {
|
||||
private readonly outputStream;
|
||||
private readonly group;
|
||||
readonly buffers: string[][];
|
||||
activeCommandIndex: number;
|
||||
readonly error: Rx.Observable<unknown>;
|
||||
private get errored();
|
||||
constructor({ outputStream, group, commands, }: {
|
||||
outputStream: Writable;
|
||||
group: boolean;
|
||||
commands: Command[];
|
||||
});
|
||||
private ensureWritable;
|
||||
write(command: Command | undefined, text: string): void;
|
||||
private flushBuffer;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import Rx from 'rxjs';
|
||||
import { fromSharedEvent } from './observables.js';
|
||||
/**
|
||||
* Class responsible for actually writing output onto a writable stream.
|
||||
*/
|
||||
export class OutputWriter {
|
||||
outputStream;
|
||||
group;
|
||||
buffers;
|
||||
activeCommandIndex = 0;
|
||||
error;
|
||||
get errored() {
|
||||
return this.outputStream.errored;
|
||||
}
|
||||
constructor({ outputStream, group, commands, }) {
|
||||
this.outputStream = outputStream;
|
||||
this.ensureWritable();
|
||||
this.error = fromSharedEvent(this.outputStream, 'error');
|
||||
this.group = group;
|
||||
this.buffers = commands.map(() => []);
|
||||
if (this.group) {
|
||||
Rx.merge(...commands.map((c) => c.close)).subscribe((command) => {
|
||||
if (command.index !== this.activeCommandIndex) {
|
||||
return;
|
||||
}
|
||||
for (let i = command.index + 1; i < commands.length; i++) {
|
||||
this.activeCommandIndex = i;
|
||||
this.flushBuffer(i);
|
||||
// TODO: Should errored commands also flush buffer?
|
||||
if (commands[i].state !== 'exited') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
ensureWritable() {
|
||||
if (this.errored) {
|
||||
throw new TypeError('outputStream is in errored state', { cause: this.errored });
|
||||
}
|
||||
}
|
||||
write(command, text) {
|
||||
this.ensureWritable();
|
||||
if (this.group && command) {
|
||||
if (command.index <= this.activeCommandIndex) {
|
||||
this.outputStream.write(text);
|
||||
}
|
||||
else {
|
||||
this.buffers[command.index].push(text);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// "global" logs (command=null) are output out of order
|
||||
this.outputStream.write(text);
|
||||
}
|
||||
}
|
||||
flushBuffer(index) {
|
||||
if (!this.errored) {
|
||||
this.buffers[index].forEach((t) => this.outputStream.write(t));
|
||||
}
|
||||
this.buffers[index] = [];
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { ChalkInstance } from 'chalk';
|
||||
export declare class PrefixColorSelector {
|
||||
private colorGenerator;
|
||||
constructor(customColors?: string | string[]);
|
||||
/**
|
||||
* Colors used by `auto` selection and default cycling.
|
||||
*
|
||||
* Each color is chosen to be visually distinct on both dark and light
|
||||
* terminal backgrounds, without carrying semantic meaning (e.g. red
|
||||
* implies errors) or blending into default text (e.g. white/grey).
|
||||
* Background colors are excluded to keep output lightweight.
|
||||
*
|
||||
* This list does NOT restrict manually specified colors — any valid Chalk
|
||||
* color name, hex value, or modifier can be passed via `--prefix-colors`.
|
||||
*/
|
||||
static get ACCEPTABLE_CONSOLE_COLORS(): (keyof ChalkInstance)[];
|
||||
/**
|
||||
* @returns The given custom colors then a set of acceptable console colors indefinitely.
|
||||
*/
|
||||
getNextColor(): string;
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
function getConsoleColorsWithoutCustomColors(customColors) {
|
||||
return PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS.filter(
|
||||
// Consider the "Bright" variants of colors to be the same as the plain color to avoid similar colors
|
||||
(color) => !customColors.includes(color.replace(/Bright$/, '')));
|
||||
}
|
||||
/**
|
||||
* Creates a generator that yields an infinite stream of colors.
|
||||
*/
|
||||
function* createColorGenerator(customColors) {
|
||||
// Custom colors should be used as is, except for "auto"
|
||||
const nextAutoColors = getConsoleColorsWithoutCustomColors(customColors);
|
||||
let lastColor;
|
||||
for (const customColor of customColors) {
|
||||
let currentColor = customColor;
|
||||
if (currentColor !== 'auto') {
|
||||
yield currentColor; // Manual color
|
||||
}
|
||||
else {
|
||||
// Find the first auto color that is not the same as the last color
|
||||
while (currentColor === 'auto' || lastColor === currentColor) {
|
||||
if (!nextAutoColors.length) {
|
||||
// There could be more "auto" values than auto colors so this needs to be able to refill
|
||||
nextAutoColors.push(...PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS);
|
||||
}
|
||||
currentColor = String(nextAutoColors.shift());
|
||||
}
|
||||
yield currentColor; // Auto color
|
||||
}
|
||||
lastColor = currentColor;
|
||||
}
|
||||
const lastCustomColor = customColors[customColors.length - 1] || '';
|
||||
if (lastCustomColor !== 'auto') {
|
||||
while (true) {
|
||||
yield lastCustomColor; // If last custom color was not "auto" then return same color forever, to maintain existing behavior
|
||||
}
|
||||
}
|
||||
// Finish the initial set(s) of auto colors to avoid repetition
|
||||
for (const color of nextAutoColors) {
|
||||
yield color;
|
||||
}
|
||||
// Yield an infinite stream of acceptable console colors
|
||||
//
|
||||
// If the given custom colors use every ACCEPTABLE_CONSOLE_COLORS except one then there is a chance a color will be repeated,
|
||||
// however its highly unlikely and low consequence so not worth the extra complexity to account for it
|
||||
while (true) {
|
||||
for (const color of PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS) {
|
||||
yield color; // Repeat colors forever
|
||||
}
|
||||
}
|
||||
}
|
||||
export class PrefixColorSelector {
|
||||
colorGenerator;
|
||||
constructor(customColors = []) {
|
||||
const normalizedColors = typeof customColors === 'string' ? [customColors] : customColors;
|
||||
this.colorGenerator = createColorGenerator(normalizedColors);
|
||||
}
|
||||
/**
|
||||
* Colors used by `auto` selection and default cycling.
|
||||
*
|
||||
* Each color is chosen to be visually distinct on both dark and light
|
||||
* terminal backgrounds, without carrying semantic meaning (e.g. red
|
||||
* implies errors) or blending into default text (e.g. white/grey).
|
||||
* Background colors are excluded to keep output lightweight.
|
||||
*
|
||||
* This list does NOT restrict manually specified colors — any valid Chalk
|
||||
* color name, hex value, or modifier can be passed via `--prefix-colors`.
|
||||
*/
|
||||
static get ACCEPTABLE_CONSOLE_COLORS() {
|
||||
return [...new Set(['cyan', 'magenta', 'green', 'yellow', 'blue'])];
|
||||
}
|
||||
/**
|
||||
* @returns The given custom colors then a set of acceptable console colors indefinitely.
|
||||
*/
|
||||
getNextColor() {
|
||||
return this.colorGenerator.next().value.trim();
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { ChildProcess, SpawnOptions } from 'node:child_process';
|
||||
import { ColorSupport } from 'supports-color';
|
||||
import { SpawnCommand } from './command.js';
|
||||
/**
|
||||
* Creates a spawn function that uses the given shell executable.
|
||||
*
|
||||
* The shell is resolved in the following priority order:
|
||||
* 1. explicit shell option
|
||||
* 2. `npm_config_script_shell` env variable
|
||||
* 3. platform default (`cmd.exe` on Windows, `/bin/sh` elsewhere)
|
||||
*
|
||||
* @see https://docs.npmjs.com/cli/v6/using-npm/config#script-shell
|
||||
*/
|
||||
export declare function createSpawn(shell?: string, spawn?: (command: string, args: string[], options: SpawnOptions) => ChildProcess, process?: Pick<NodeJS.Process, 'platform' | 'env'>): SpawnCommand;
|
||||
export type ShellKind = 'cmd' | 'posix' | 'powershell';
|
||||
export declare const getSpawnOpts: ({ colorSupport, cwd, process, ipc, stdio, env, }: {
|
||||
/**
|
||||
* What the color support of the spawned processes should be.
|
||||
* If set to `false`, then no colors should be output.
|
||||
*
|
||||
* Defaults to whatever the terminal's stdout support is.
|
||||
*/
|
||||
colorSupport?: Pick<ColorSupport, "level"> | false;
|
||||
/**
|
||||
* The NodeJS process.
|
||||
*/
|
||||
process?: Pick<NodeJS.Process, "cwd" | "platform" | "env">;
|
||||
/**
|
||||
* A custom working directory to spawn processes in.
|
||||
* Defaults to `process.cwd()`.
|
||||
*/
|
||||
cwd?: string;
|
||||
/**
|
||||
* The file descriptor number at which the channel for inter-process communication
|
||||
* should be set up.
|
||||
*/
|
||||
ipc?: number;
|
||||
/**
|
||||
* Which stdio mode to use. Raw implies inheriting the parent process' stdio.
|
||||
*
|
||||
* - `normal`: all of stdout, stderr and stdin are piped
|
||||
* - `hidden`: stdin is piped, stdout/stderr outputs are ignored
|
||||
* - `raw`: all of stdout, stderr and stdin are inherited from the main process
|
||||
*
|
||||
* Defaults to `normal`.
|
||||
*/
|
||||
stdio?: "normal" | "hidden" | "raw";
|
||||
/**
|
||||
* Map of custom environment variables to include in the spawn options.
|
||||
*/
|
||||
env?: Record<string, unknown>;
|
||||
}) => SpawnOptions;
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert';
|
||||
import { spawn as baseSpawn } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import nodeProcess from 'node:process';
|
||||
import supportsColor from 'supports-color';
|
||||
import { UnreachableError } from './utils.js';
|
||||
/**
|
||||
* Creates a spawn function that uses the given shell executable.
|
||||
*
|
||||
* The shell is resolved in the following priority order:
|
||||
* 1. explicit shell option
|
||||
* 2. `npm_config_script_shell` env variable
|
||||
* 3. platform default (`cmd.exe` on Windows, `/bin/sh` elsewhere)
|
||||
*
|
||||
* @see https://docs.npmjs.com/cli/v6/using-npm/config#script-shell
|
||||
*/
|
||||
export function createSpawn(shell,
|
||||
// For testing
|
||||
spawn = baseSpawn, process = nodeProcess) {
|
||||
const resolved = resolveShell(shell, process);
|
||||
return (command, spawnOpts) => {
|
||||
const { file, args, shellOptions } = getShellSpawnArgs(resolved, command);
|
||||
return spawn(file, args, { ...spawnOpts, ...shellOptions });
|
||||
};
|
||||
}
|
||||
const NPM_SCRIPT_SHELL_ENV = 'npm_config_script_shell';
|
||||
/**
|
||||
* Resolves which shell executable to use when spawning commands.
|
||||
* @see {@link createSpawn()}
|
||||
*/
|
||||
function resolveShell(shell, process = nodeProcess) {
|
||||
if (shell) {
|
||||
return shell;
|
||||
}
|
||||
const npmScriptShell = process.env[NPM_SCRIPT_SHELL_ENV];
|
||||
if (npmScriptShell) {
|
||||
return npmScriptShell;
|
||||
}
|
||||
return process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
|
||||
}
|
||||
/**
|
||||
* Builds spawn file/args for the given shell and command string.
|
||||
*/
|
||||
function getShellSpawnArgs(shellPath, command) {
|
||||
const kind = detectShellKind(shellPath);
|
||||
switch (kind) {
|
||||
case 'cmd':
|
||||
return {
|
||||
file: shellPath,
|
||||
args: ['/s', '/c', `"${command}"`],
|
||||
shellOptions: { windowsVerbatimArguments: true },
|
||||
};
|
||||
case 'powershell':
|
||||
return {
|
||||
file: shellPath,
|
||||
args: ['-NoProfile', '-Command', command],
|
||||
};
|
||||
case 'posix':
|
||||
return {
|
||||
file: shellPath,
|
||||
args: ['-c', command],
|
||||
};
|
||||
default:
|
||||
throw new UnreachableError(kind);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Detects which argument style to use when spawning the given shell executable.
|
||||
*/
|
||||
function detectShellKind(shellPath) {
|
||||
const normalized = shellPath.replace(/\\/g, '/');
|
||||
const base = path
|
||||
.basename(normalized)
|
||||
.toLowerCase()
|
||||
.replace(/\.exe$/i, '');
|
||||
if (base === 'cmd') {
|
||||
return 'cmd';
|
||||
}
|
||||
if (base === 'powershell' || base === 'pwsh') {
|
||||
return 'powershell';
|
||||
}
|
||||
return 'posix';
|
||||
}
|
||||
export const getSpawnOpts = ({ colorSupport = supportsColor.stdout, cwd, process = nodeProcess, ipc, stdio = 'normal', env = {}, }) => {
|
||||
const stdioValues = stdio === 'normal'
|
||||
? ['pipe', 'pipe', 'pipe']
|
||||
: stdio === 'raw'
|
||||
? ['inherit', 'inherit', 'inherit']
|
||||
: ['pipe', 'ignore', 'ignore'];
|
||||
if (ipc != null) {
|
||||
// Avoid overriding the stdout/stderr/stdin
|
||||
assert.ok(ipc > 2, '[concurrently] the IPC channel number should be > 2');
|
||||
stdioValues[ipc] = 'ipc';
|
||||
}
|
||||
return {
|
||||
cwd: cwd || process.cwd(),
|
||||
stdio: stdioValues,
|
||||
...(process.platform.startsWith('win') && { detached: false }),
|
||||
env: {
|
||||
...(colorSupport ? { FORCE_COLOR: colorSupport.level.toString() } : {}),
|
||||
...process.env,
|
||||
...env,
|
||||
},
|
||||
};
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Escapes a string for use in a regular expression.
|
||||
*/
|
||||
export declare function escapeRegExp(str: string): string;
|
||||
type CastArrayResult<T> = T extends undefined | null ? never[] : T extends unknown[] ? T : T[];
|
||||
/**
|
||||
* Casts a value to an array if it's not one.
|
||||
*/
|
||||
export declare function castArray<T = never[]>(value?: T): CastArrayResult<T>;
|
||||
/**
|
||||
* Splits a string on `delimiter`, ignoring delimiters inside parentheses.
|
||||
* Trims each segment and discards empty ones.
|
||||
*
|
||||
* Examples:
|
||||
* splitOutsideParens('red,rgb(255,0,0),blue', ',') → ['red', 'rgb(255,0,0)', 'blue']
|
||||
* splitOutsideParens('black.bgHex(#533AFD).dim', '.') → ['black', 'bgHex(#533AFD)', 'dim']
|
||||
*/
|
||||
export declare function splitOutsideParens(input: string, delimiter: string): string[];
|
||||
/**
|
||||
* Error thrown when a condition is reached that should be impossible.
|
||||
*/
|
||||
export declare class UnreachableError extends Error {
|
||||
constructor(value: never);
|
||||
}
|
||||
export {};
|
||||
Loaded 100 of 2571 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user